aboutsummaryrefslogtreecommitdiff
path: root/node_modules/http2-wrapper
diff options
context:
space:
mode:
authorMinteck <nekostarfan@gmail.com>2021-08-24 14:41:48 +0200
committerMinteck <nekostarfan@gmail.com>2021-08-24 14:41:48 +0200
commitd25e11bee6ca5ca523884da132d18e1400e077b9 (patch)
tree8af39fde19f7ed640a60fb397c7edd647dff1c4c /node_modules/http2-wrapper
downloadkartik-iridium-d25e11bee6ca5ca523884da132d18e1400e077b9.tar.gz
kartik-iridium-d25e11bee6ca5ca523884da132d18e1400e077b9.tar.bz2
kartik-iridium-d25e11bee6ca5ca523884da132d18e1400e077b9.zip
Initial commit
Diffstat (limited to 'node_modules/http2-wrapper')
-rw-r--r--node_modules/http2-wrapper/LICENSE21
-rw-r--r--node_modules/http2-wrapper/README.md470
-rw-r--r--node_modules/http2-wrapper/package.json54
-rw-r--r--node_modules/http2-wrapper/source/agent.js670
-rw-r--r--node_modules/http2-wrapper/source/auto.js149
-rw-r--r--node_modules/http2-wrapper/source/client-request.js445
-rw-r--r--node_modules/http2-wrapper/source/incoming-message.js58
-rw-r--r--node_modules/http2-wrapper/source/index.js28
-rw-r--r--node_modules/http2-wrapper/source/utils/calculate-server-name.js27
-rw-r--r--node_modules/http2-wrapper/source/utils/errors.js45
-rw-r--r--node_modules/http2-wrapper/source/utils/is-request-pseudo-header.js13
-rw-r--r--node_modules/http2-wrapper/source/utils/proxy-events.js7
-rw-r--r--node_modules/http2-wrapper/source/utils/url-to-options.js25
13 files changed, 2012 insertions, 0 deletions
diff --git a/node_modules/http2-wrapper/LICENSE b/node_modules/http2-wrapper/LICENSE
new file mode 100644
index 0000000..15ad2e8
--- /dev/null
+++ b/node_modules/http2-wrapper/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Szymon Marczak
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/node_modules/http2-wrapper/README.md b/node_modules/http2-wrapper/README.md
new file mode 100644
index 0000000..09b5f48
--- /dev/null
+++ b/node_modules/http2-wrapper/README.md
@@ -0,0 +1,470 @@
+# http2-wrapper
+> HTTP/2 client, just with the familiar `https` API
+
+[![Node CI](https://github.com/szmarczak/http2-wrapper/workflows/Node%20CI/badge.svg)](https://github.com/szmarczak/http2-wrapper/actions)
+[![codecov](https://codecov.io/gh/szmarczak/http2-wrapper/branch/master/graph/badge.svg)](https://codecov.io/gh/szmarczak/http2-wrapper)
+[![npm](https://img.shields.io/npm/dm/http2-wrapper.svg)](https://www.npmjs.com/package/http2-wrapper)
+[![install size](https://packagephobia.now.sh/badge?p=http2-wrapper)](https://packagephobia.now.sh/result?p=http2-wrapper)
+
+This package was created to support HTTP/2 without the need to rewrite your code.<br>
+I recommend adapting to the [`http2`](https://nodejs.org/api/http2.html) module if possible - it's much simpler to use and has many cool features!
+
+**Tip**: `http2-wrapper` is very useful when you rely on other modules that use the HTTP/1 API and you want to support HTTP/2.
+
+**Pro Tip**: While the native `http2` doesn't have agents yet, you can use `http2-wrapper` Agents and still operate on the native HTTP/2 streams.
+
+## Installation
+
+> `$ npm install http2-wrapper`<br>
+> `$ yarn add http2-wrapper`
+
+## Usage
+```js
+const http2 = require('http2-wrapper');
+
+const options = {
+ hostname: 'nghttp2.org',
+ protocol: 'https:',
+ path: '/httpbin/post',
+ method: 'POST',
+ headers: {
+ 'content-length': 6
+ }
+};
+
+const request = http2.request(options, response => {
+ console.log('statusCode:', response.statusCode);
+ console.log('headers:', response.headers);
+
+ const body = [];
+ response.on('data', chunk => {
+ body.push(chunk);
+ });
+ response.on('end', () => {
+ console.log('body:', Buffer.concat(body).toString());
+ });
+});
+
+request.on('error', console.error);
+
+request.write('123');
+request.end('456');
+
+// statusCode: 200
+// headers: [Object: null prototype] {
+// ':status': 200,
+// date: 'Fri, 27 Sep 2019 19:45:46 GMT',
+// 'content-type': 'application/json',
+// 'access-control-allow-origin': '*',
+// 'access-control-allow-credentials': 'true',
+// 'content-length': '239',
+// 'x-backend-header-rtt': '0.002516',
+// 'strict-transport-security': 'max-age=31536000',
+// server: 'nghttpx',
+// via: '1.1 nghttpx',
+// 'alt-svc': 'h3-23=":4433"; ma=3600',
+// 'x-frame-options': 'SAMEORIGIN',
+// 'x-xss-protection': '1; mode=block',
+// 'x-content-type-options': 'nosniff'
+// }
+// body: {
+// "args": {},
+// "data": "123456",
+// "files": {},
+// "form": {},
+// "headers": {
+// "Content-Length": "6",
+// "Host": "nghttp2.org"
+// },
+// "json": 123456,
+// "origin": "xxx.xxx.xxx.xxx",
+// "url": "https://nghttp2.org/httpbin/post"
+// }
+```
+
+## API
+
+**Note:** The `session` option was renamed to `tlsSession` for better readability.
+
+### http2.auto(url, options, callback)
+
+Performs [ALPN](https://nodejs.org/api/tls.html#tls_alpn_and_sni) negotiation.
+Returns a Promise giving proper `ClientRequest` instance (depending on the ALPN).
+
+**Note**: The `agent` option represents an object with `http`, `https` and `http2` properties.
+
+```js
+const http2 = require('http2-wrapper');
+
+const options = {
+ hostname: 'httpbin.org',
+ protocol: 'http:', // Note the `http:` protocol here
+ path: '/post',
+ method: 'POST',
+ headers: {
+ 'content-length': 6
+ }
+};
+
+(async () => {
+ try {
+ const request = await http2.auto(options, response => {
+ console.log('statusCode:', response.statusCode);
+ console.log('headers:', response.headers);
+
+ const body = [];
+ response.on('data', chunk => body.push(chunk));
+ response.on('end', () => {
+ console.log('body:', Buffer.concat(body).toString());
+ });
+ });
+
+ request.on('error', console.error);
+
+ request.write('123');
+ request.end('456');
+ } catch (error) {
+ console.error(error);
+ }
+})();
+
+// statusCode: 200
+// headers: { connection: 'close',
+// server: 'gunicorn/19.9.0',
+// date: 'Sat, 15 Dec 2018 18:19:32 GMT',
+// 'content-type': 'application/json',
+// 'content-length': '259',
+// 'access-control-allow-origin': '*',
+// 'access-control-allow-credentials': 'true',
+// via: '1.1 vegur' }
+// body: {
+// "args": {},
+// "data": "123456",
+// "files": {},
+// "form": {},
+// "headers": {
+// "Connection": "close",
+// "Content-Length": "6",
+// "Host": "httpbin.org"
+// },
+// "json": 123456,
+// "origin": "xxx.xxx.xxx.xxx",
+// "url": "http://httpbin.org/post"
+// }
+```
+
+### http2.auto.protocolCache
+
+An instance of [`quick-lru`](https://github.com/sindresorhus/quick-lru) used for ALPN cache.
+
+There is a maximum of 100 entries. You can modify the limit through `protocolCache.maxSize` - note that the change will be visible globally.
+
+### http2.request(url, options, callback)
+
+Same as [`https.request`](https://nodejs.org/api/https.html#https_https_request_options_callback).
+
+##### options.h2session
+
+Type: `Http2Session`<br>
+
+The session used to make the actual request. If none provided, it will use `options.agent`.
+
+### http2.get(url, options, callback)
+
+Same as [`https.get`](https://nodejs.org/api/https.html#https_https_get_options_callback).
+
+### new http2.ClientRequest(url, options, callback)
+
+Same as [`https.ClientRequest`](https://nodejs.org/api/https.html#https_class_https_clientrequest).
+
+### new http2.IncomingMessage(socket)
+
+Same as [`https.IncomingMessage`](https://nodejs.org/api/https.html#https_class_https_incomingmessage).
+
+### new http2.Agent(options)
+
+**Note:** this is **not** compatible with the classic `http.Agent`.
+
+Usage example:
+
+```js
+const http2 = require('http2-wrapper');
+
+class MyAgent extends http2.Agent {
+ createConnection(origin, options) {
+ console.log(`Connecting to ${http2.Agent.normalizeOrigin(origin)}`);
+ return http2.Agent.connect(origin, options);
+ }
+}
+
+http2.get({
+ hostname: 'google.com',
+ agent: new MyAgent()
+}, res => {
+ res.on('data', chunk => console.log(`Received chunk of ${chunk.length} bytes`));
+});
+```
+
+#### options
+
+Each option is assigned to each `Agent` instance and can be changed later.
+
+##### timeout
+
+Type: `number`<br>
+Default: `60000`
+
+If there's no activity after `timeout` milliseconds, the session will be closed.
+
+##### maxSessions
+
+Type: `number`<br>
+Default: `Infinity`
+
+The maximum amount of sessions in total.
+
+##### maxFreeSessions
+
+Type: `number`<br>
+Default: `10`
+
+The maximum amount of free sessions in total. This only applies to sessions with no pending requests.
+
+**Note:** It is possible that the amount will be exceeded when sessions have at least 1 pending request.
+
+##### maxCachedTlsSessions
+
+Type: `number`<br>
+Default: `100`
+
+The maximum amount of cached TLS sessions.
+
+#### Agent.normalizeOrigin(url)
+
+Returns a string representing the origin of the URL.
+
+#### agent.settings
+
+Type: `object`<br>
+Default: `{enablePush: false}`
+
+[Settings](https://nodejs.org/api/http2.html#http2_settings_object) used by the current agent instance.
+
+#### agent.normalizeOptions([options](https://github.com/szmarczak/http2-wrapper/blob/master/source/agent.js))
+
+Returns a string representing normalized options.
+
+```js
+Agent.normalizeOptions({servername: 'example.com'});
+// => ':example.com'
+```
+
+#### agent.getSession(origin, options)
+
+##### [origin](https://nodejs.org/api/http2.html#http2_http2_connect_authority_options_listener)
+
+Type: `string` `URL` `object`
+
+An origin used to create new session.
+
+##### [options](https://nodejs.org/api/http2.html#http2_http2_connect_authority_options_listener)
+
+Type: `object`
+
+The options used to create new session.
+
+Returns a Promise giving free `Http2Session`. If no free sessions are found, a new one is created.
+
+#### agent.getSession([origin](#origin), [options](options-1), listener)
+
+##### listener
+
+Type: `object`
+
+```
+{
+ reject: error => void,
+ resolve: session => void
+}
+```
+
+If the `listener` argument is present, the Promise will resolve immediately. It will use the `resolve` function to pass the session.
+
+#### agent.request([origin](#origin), [options](#options-1), [headers](https://nodejs.org/api/http2.html#http2_headers_object), [streamOptions](https://nodejs.org/api/http2.html#http2_clienthttp2session_request_headers_options))
+
+Returns a Promise giving `Http2Stream`.
+
+#### agent.createConnection([origin](#origin), [options](#options-1))
+
+Returns a new `TLSSocket`. It defaults to `Agent.connect(origin, options)`.
+
+#### agent.closeFreeSessions()
+
+Makes an attempt to close free sessions. Only sessions with 0 concurrent streams will be closed.
+
+#### agent.destroy(reason)
+
+Destroys **all** sessions.
+
+#### Event: 'session'
+
+```js
+agent.on('session', session => {
+ // A new session has been created by the Agent.
+});
+```
+
+## Proxy support
+
+An example of a full-featured proxy server can be found [here](examples/proxy/server.js). It supports **mirroring, custom authorities and the CONNECT protocol**.
+
+### Mirroring
+
+To mirror another server we need to use only [`http2-proxy`](https://github.com/nxtedition/node-http2-proxy). We don't need the CONNECT protocol or custom authorities.
+
+To see the result, just navigate to the server's address.
+
+### HTTP/1 over HTTP/2
+
+Since we don't care about mirroring, the server needs to support the CONNECT protocol in this case.
+
+The client looks like this:
+
+```js
+const https = require('https');
+const http2 = require('http2');
+
+const session = http2.connect('https://localhost:8000', {
+ // For demo purposes only!
+ rejectUnauthorized: false
+});
+
+session.ref();
+
+https.request('https://httpbin.org/anything', {
+ createConnection: options => {
+ return session.request({
+ ':method': 'CONNECT',
+ ':authority': `${options.host}:${options.port}`
+ });
+ }
+}, response => {
+ console.log('statusCode:', response.statusCode);
+ console.log('headers:', response.headers);
+
+ const body = [];
+ response.on('data', chunk => {
+ body.push(chunk);
+ });
+ response.on('end', () => {
+ console.log('body:', Buffer.concat(body).toString());
+
+ session.unref();
+ });
+}).end();
+```
+
+### HTTP/2 over HTTP/2
+
+It's a tricky one! We cannot create an HTTP/2 session on top of an HTTP/2 stream. But... we can still specify the `:authority` header, no need to use the CONNECT protocol here.
+
+The client looks like this:
+
+```js
+const http2 = require('../../source');
+const {Agent} = http2;
+
+class ProxyAgent extends Agent {
+ constructor(url, options) {
+ super(options);
+
+ this.origin = url;
+ }
+
+ request(origin, sessionOptions, headers, streamOptions) {
+ return super.request(this.origin, sessionOptions, {
+ ...headers,
+ ':authority': (new URL(origin)).host
+ }, streamOptions);
+ }
+}
+
+const request = http2.request({
+ hostname: 'httpbin.org',
+ protocol: 'https:',
+ path: '/anything',
+ agent: new ProxyAgent('https://localhost:8000'),
+ // For demo purposes only!
+ rejectUnauthorized: false
+}, response => {
+ console.log('statusCode:', response.statusCode);
+ console.log('headers:', response.headers);
+
+ const body = [];
+ response.on('data', chunk => {
+ body.push(chunk);
+ });
+ response.on('end', () => {
+ console.log('body:', Buffer.concat(body).toString());
+ });
+});
+
+request.on('error', console.error);
+
+request.end();
+```
+
+## Notes
+
+ - If you're interested in [WebSockets over HTTP/2](https://tools.ietf.org/html/rfc8441), then [check out this discussion](https://github.com/websockets/ws/issues/1458).
+ - [HTTP/2 sockets cannot be malformed](https://github.com/nodejs/node/blob/cc8250fab86486632fdeb63892be735d7628cd13/lib/internal/http2/core.js#L725), therefore modifying the socket will have no effect.
+ - You can make [a custom Agent](examples/push-stream/index.js) to support push streams.
+
+## Benchmarks
+
+CPU: Intel i7-7700k (governor: performance)<br>
+Server: H2O v2.2.5 [`h2o.conf`](h2o.conf)<br>
+Node: v14.5.0
+Linux: 5.6.18-156.current
+
+`auto` means `http2wrapper.auto`.
+
+```
+http2-wrapper x 12,181 ops/sec ±3.39% (75 runs sampled)
+http2-wrapper - preconfigured session x 13,140 ops/sec ±2.51% (79 runs sampled)
+http2-wrapper - auto x 11,412 ops/sec ±2.55% (78 runs sampled)
+http2 x 16,050 ops/sec ±1.39% (86 runs sampled)
+https - auto - keepalive x 12,288 ops/sec ±2.69% (79 runs sampled)
+https - keepalive x 12,155 ops/sec ±3.32% (78 runs sampled)
+https x 1,604 ops/sec ±2.03% (77 runs sampled)
+http x 6,041 ops/sec ±3.82% (76 runs sampled)
+Fastest is http2
+```
+
+`http2-wrapper`:
+- 32% **less** performant than `http2`
+- as performant as `https - keepalive`
+- 100% **more** performant than `http`
+
+`http2-wrapper - preconfigured session`:
+- 22% **less** performant than `http2`
+- 8% **more** performant than `https - keepalive`
+- 118% **more** performant than `http`
+
+`http2-wrapper - auto`:
+- 41% **less** performant than `http2`
+- 8% **less** performant than `https - keepalive`
+- 89% **more** performant than `http`
+
+`https - auto - keepalive`:
+- 31% **less** performant than `http2`
+- as performant as `https - keepalive`
+- 103% **more** performant than `http`
+
+## Related
+
+ - [`got`](https://github.com/sindresorhus/got) - Simplified HTTP requests
+
+## License
+
+MIT
diff --git a/node_modules/http2-wrapper/package.json b/node_modules/http2-wrapper/package.json
new file mode 100644
index 0000000..d47894f
--- /dev/null
+++ b/node_modules/http2-wrapper/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "http2-wrapper",
+ "version": "1.0.3",
+ "description": "HTTP2 client, just with the familiar `https` API",
+ "main": "source",
+ "engines": {
+ "node": ">=10.19.0"
+ },
+ "scripts": {
+ "test": "xo && nyc --reporter=lcovonly --reporter=text --reporter=html ava"
+ },
+ "files": [
+ "source"
+ ],
+ "keywords": [
+ "http2",
+ "https",
+ "http",
+ "request"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/szmarczak/http2-wrapper.git"
+ },
+ "author": "Szymon Marczak",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/szmarczak/http2-wrapper/issues"
+ },
+ "homepage": "https://github.com/szmarczak/http2-wrapper#readme",
+ "dependencies": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.0.0"
+ },
+ "devDependencies": {
+ "@sindresorhus/is": "^3.0.0",
+ "ava": "^3.10.1",
+ "benchmark": "^2.1.4",
+ "get-stream": "^5.1.0",
+ "got": "^11.5.0",
+ "http2-proxy": "^5.0.51",
+ "lolex": "^6.0.0",
+ "many-keys-map": "^1.0.2",
+ "nyc": "^15.1.0",
+ "p-event": "^4.2.0",
+ "tempy": "^0.5.0",
+ "to-readable-stream": "^2.1.0",
+ "tsd": "^0.13.1",
+ "xo": "^0.32.1"
+ },
+ "ava": {
+ "timeout": "2m"
+ }
+}
diff --git a/node_modules/http2-wrapper/source/agent.js b/node_modules/http2-wrapper/source/agent.js
new file mode 100644
index 0000000..b2b1cff
--- /dev/null
+++ b/node_modules/http2-wrapper/source/agent.js
@@ -0,0 +1,670 @@
+'use strict';
+const EventEmitter = require('events');
+const tls = require('tls');
+const http2 = require('http2');
+const QuickLRU = require('quick-lru');
+
+const kCurrentStreamsCount = Symbol('currentStreamsCount');
+const kRequest = Symbol('request');
+const kOriginSet = Symbol('cachedOriginSet');
+const kGracefullyClosing = Symbol('gracefullyClosing');
+
+const nameKeys = [
+ // `http2.connect()` options
+ 'maxDeflateDynamicTableSize',
+ 'maxSessionMemory',
+ 'maxHeaderListPairs',
+ 'maxOutstandingPings',
+ 'maxReservedRemoteStreams',
+ 'maxSendHeaderBlockLength',
+ 'paddingStrategy',
+
+ // `tls.connect()` options
+ 'localAddress',
+ 'path',
+ 'rejectUnauthorized',
+ 'minDHSize',
+
+ // `tls.createSecureContext()` options
+ 'ca',
+ 'cert',
+ 'clientCertEngine',
+ 'ciphers',
+ 'key',
+ 'pfx',
+ 'servername',
+ 'minVersion',
+ 'maxVersion',
+ 'secureProtocol',
+ 'crl',
+ 'honorCipherOrder',
+ 'ecdhCurve',
+ 'dhparam',
+ 'secureOptions',
+ 'sessionIdContext'
+];
+
+const getSortedIndex = (array, value, compare) => {
+ let low = 0;
+ let high = array.length;
+
+ while (low < high) {
+ const mid = (low + high) >>> 1;
+
+ /* istanbul ignore next */
+ if (compare(array[mid], value)) {
+ // This never gets called because we use descending sort. Better to have this anyway.
+ low = mid + 1;
+ } else {
+ high = mid;
+ }
+ }
+
+ return low;
+};
+
+const compareSessions = (a, b) => {
+ return a.remoteSettings.maxConcurrentStreams > b.remoteSettings.maxConcurrentStreams;
+};
+
+// See https://tools.ietf.org/html/rfc8336
+const closeCoveredSessions = (where, session) => {
+ // Clients SHOULD NOT emit new requests on any connection whose Origin
+ // Set is a proper subset of another connection's Origin Set, and they
+ // SHOULD close it once all outstanding requests are satisfied.
+ for (const coveredSession of where) {
+ if (
+ // The set is a proper subset when its length is less than the other set.
+ coveredSession[kOriginSet].length < session[kOriginSet].length &&
+
+ // And the other set includes all elements of the subset.
+ coveredSession[kOriginSet].every(origin => session[kOriginSet].includes(origin)) &&
+
+ // Makes sure that the session can handle all requests from the covered session.
+ coveredSession[kCurrentStreamsCount] + session[kCurrentStreamsCount] <= session.remoteSettings.maxConcurrentStreams
+ ) {
+ // This allows pending requests to finish and prevents making new requests.
+ gracefullyClose(coveredSession);
+ }
+ }
+};
+
+// This is basically inverted `closeCoveredSessions(...)`.
+const closeSessionIfCovered = (where, coveredSession) => {
+ for (const session of where) {
+ if (
+ coveredSession[kOriginSet].length < session[kOriginSet].length &&
+ coveredSession[kOriginSet].every(origin => session[kOriginSet].includes(origin)) &&
+ coveredSession[kCurrentStreamsCount] + session[kCurrentStreamsCount] <= session.remoteSettings.maxConcurrentStreams
+ ) {
+ gracefullyClose(coveredSession);
+ }
+ }
+};
+
+const getSessions = ({agent, isFree}) => {
+ const result = {};
+
+ // eslint-disable-next-line guard-for-in
+ for (const normalizedOptions in agent.sessions) {
+ const sessions = agent.sessions[normalizedOptions];
+
+ const filtered = sessions.filter(session => {
+ const result = session[Agent.kCurrentStreamsCount] < session.remoteSettings.maxConcurrentStreams;
+
+ return isFree ? result : !result;
+ });
+
+ if (filtered.length !== 0) {
+ result[normalizedOptions] = filtered;
+ }
+ }
+
+ return result;
+};
+
+const gracefullyClose = session => {
+ session[kGracefullyClosing] = true;
+
+ if (session[kCurrentStreamsCount] === 0) {
+ session.close();
+ }
+};
+
+class Agent extends EventEmitter {
+ constructor({timeout = 60000, maxSessions = Infinity, maxFreeSessions = 10, maxCachedTlsSessions = 100} = {}) {
+ super();
+
+ // A session is considered busy when its current streams count
+ // is equal to or greater than the `maxConcurrentStreams` value.
+
+ // A session is considered free when its current streams count
+ // is less than the `maxConcurrentStreams` value.
+
+ // SESSIONS[NORMALIZED_OPTIONS] = [];
+ this.sessions = {};
+
+ // The queue for creating new sessions. It looks like this:
+ // QUEUE[NORMALIZED_OPTIONS][NORMALIZED_ORIGIN] = ENTRY_FUNCTION
+ //
+ // The entry function has `listeners`, `completed` and `destroyed` properties.
+ // `listeners` is an array of objects containing `resolve` and `reject` functions.
+ // `completed` is a boolean. It's set to true after ENTRY_FUNCTION is executed.
+ // `destroyed` is a boolean. If it's set to true, the session will be destroyed if hasn't connected yet.
+ this.queue = {};
+
+ // Each session will use this timeout value.
+ this.timeout = timeout;
+
+ // Max sessions in total
+ this.maxSessions = maxSessions;
+
+ // Max free sessions in total
+ // TODO: decreasing `maxFreeSessions` should close some sessions
+ this.maxFreeSessions = maxFreeSessions;
+
+ this._freeSessionsCount = 0;
+ this._sessionsCount = 0;
+
+ // We don't support push streams by default.
+ this.settings = {
+ enablePush: false
+ };
+
+ // Reusing TLS sessions increases performance.
+ this.tlsSessionCache = new QuickLRU({maxSize: maxCachedTlsSessions});
+ }
+
+ static normalizeOrigin(url, servername) {
+ if (typeof url === 'string') {
+ url = new URL(url);
+ }
+
+ if (servername && url.hostname !== servername) {
+ url.hostname = servername;
+ }
+
+ return url.origin;
+ }
+
+ normalizeOptions(options) {
+ let normalized = '';
+
+ if (options) {
+ for (const key of nameKeys) {
+ if (options[key]) {
+ normalized += `:${options[key]}`;
+ }
+ }
+ }
+
+ return normalized;
+ }
+
+ _tryToCreateNewSession(normalizedOptions, normalizedOrigin) {
+ if (!(normalizedOptions in this.queue) || !(normalizedOrigin in this.queue[normalizedOptions])) {
+ return;
+ }
+
+ const item = this.queue[normalizedOptions][normalizedOrigin];
+
+ // The entry function can be run only once.
+ // BUG: The session may be never created when:
+ // - the first condition is false AND
+ // - this function is never called with the same arguments in the future.
+ if (this._sessionsCount < this.maxSessions && !item.completed) {
+ item.completed = true;
+
+ item();
+ }
+ }
+
+ getSession(origin, options, listeners) {
+ return new Promise((resolve, reject) => {
+ if (Array.isArray(listeners)) {
+ listeners = [...listeners];
+
+ // Resolve the current promise ASAP, we're just moving the listeners.
+ // They will be executed at a different time.
+ resolve();
+ } else {
+ listeners = [{resolve, reject}];
+ }
+
+ const normalizedOptions = this.normalizeOptions(options);
+ const normalizedOrigin = Agent.normalizeOrigin(origin, options && options.servername);
+
+ if (normalizedOrigin === undefined) {
+ for (const {reject} of listeners) {
+ reject(new TypeError('The `origin` argument needs to be a string or an URL object'));
+ }
+
+ return;
+ }
+
+ if (normalizedOptions in this.sessions) {
+ const sessions = this.sessions[normalizedOptions];
+
+ let maxConcurrentStreams = -1;
+ let currentStreamsCount = -1;
+ let optimalSession;
+
+ // We could just do this.sessions[normalizedOptions].find(...) but that isn't optimal.
+ // Additionally, we are looking for session which has biggest current pending streams count.
+ for (const session of sessions) {
+ const sessionMaxConcurrentStreams = session.remoteSettings.maxConcurrentStreams;
+
+ if (sessionMaxConcurrentStreams < maxConcurrentStreams) {
+ break;
+ }
+
+ if (session[kOriginSet].includes(normalizedOrigin)) {
+ const sessionCurrentStreamsCount = session[kCurrentStreamsCount];
+
+ if (
+ sessionCurrentStreamsCount >= sessionMaxConcurrentStreams ||
+ session[kGracefullyClosing] ||
+ // Unfortunately the `close` event isn't called immediately,
+ // so `session.destroyed` is `true`, but `session.closed` is `false`.
+ session.destroyed
+ ) {
+ continue;
+ }
+
+ // We only need set this once.
+ if (!optimalSession) {
+ maxConcurrentStreams = sessionMaxConcurrentStreams;
+ }
+
+ // We're looking for the session which has biggest current pending stream count,
+ // in order to minimalize the amount of active sessions.
+ if (sessionCurrentStreamsCount > currentStreamsCount) {
+ optimalSession = session;
+ currentStreamsCount = sessionCurrentStreamsCount;
+ }
+ }
+ }
+
+ if (optimalSession) {
+ /* istanbul ignore next: safety check */
+ if (listeners.length !== 1) {
+ for (const {reject} of listeners) {
+ const error = new Error(
+ `Expected the length of listeners to be 1, got ${listeners.length}.\n` +
+ 'Please report this to https://github.com/szmarczak/http2-wrapper/'
+ );
+
+ reject(error);
+ }
+
+ return;
+ }
+
+ listeners[0].resolve(optimalSession);
+ return;
+ }
+ }
+
+ if (normalizedOptions in this.queue) {
+ if (normalizedOrigin in this.queue[normalizedOptions]) {
+ // There's already an item in the queue, just attach ourselves to it.
+ this.queue[normalizedOptions][normalizedOrigin].listeners.push(...listeners);
+
+ // This shouldn't be executed here.
+ // See the comment inside _tryToCreateNewSession.
+ this._tryToCreateNewSession(normalizedOptions, normalizedOrigin);
+ return;
+ }
+ } else {
+ this.queue[normalizedOptions] = {};
+ }
+
+ // The entry must be removed from the queue IMMEDIATELY when:
+ // 1. the session connects successfully,
+ // 2. an error occurs.
+ const removeFromQueue = () => {
+ // Our entry can be replaced. We cannot remove the new one.
+ if (normalizedOptions in this.queue && this.queue[normalizedOptions][normalizedOrigin] === entry) {
+ delete this.queue[normalizedOptions][normalizedOrigin];
+
+ if (Object.keys(this.queue[normalizedOptions]).length === 0) {
+ delete this.queue[normalizedOptions];
+ }
+ }
+ };
+
+ // The main logic is here
+ const entry = () => {
+ const name = `${normalizedOrigin}:${normalizedOptions}`;
+ let receivedSettings = false;
+
+ try {
+ const session = http2.connect(origin, {
+ createConnection: this.createConnection,
+ settings: this.settings,
+ session: this.tlsSessionCache.get(name),
+ ...options
+ });
+ session[kCurrentStreamsCount] = 0;
+ session[kGracefullyClosing] = false;
+
+ const isFree = () => session[kCurrentStreamsCount] < session.remoteSettings.maxConcurrentStreams;
+ let wasFree = true;
+
+ session.socket.once('session', tlsSession => {
+ this.tlsSessionCache.set(name, tlsSession);
+ });
+
+ session.once('error', error => {
+ // Listeners are empty when the session successfully connected.
+ for (const {reject} of listeners) {
+ reject(error);
+ }
+
+ // The connection got broken, purge the cache.
+ this.tlsSessionCache.delete(name);
+ });
+
+ session.setTimeout(this.timeout, () => {
+ // Terminates all streams owned by this session.
+ // TODO: Maybe the streams should have a "Session timed out" error?
+ session.destroy();
+ });
+
+ session.once('close', () => {
+ if (receivedSettings) {
+ // 1. If it wasn't free then no need to decrease because
+ // it has been decreased already in session.request().
+ // 2. `stream.once('close')` won't increment the count
+ // because the session is already closed.
+ if (wasFree) {
+ this._freeSessionsCount--;
+ }
+
+ this._sessionsCount--;
+
+ // This cannot be moved to the stream logic,
+ // because there may be a session that hadn't made a single request.
+ const where = this.sessions[normalizedOptions];
+ where.splice(where.indexOf(session), 1);
+
+ if (where.length === 0) {
+ delete this.sessions[normalizedOptions];
+ }
+ } else {
+ // Broken connection
+ const error = new Error('Session closed without receiving a SETTINGS frame');
+ error.code = 'HTTP2WRAPPER_NOSETTINGS';
+
+ for (const {reject} of listeners) {
+ reject(error);
+ }
+
+ removeFromQueue();
+ }
+
+ // There may be another session awaiting.
+ this._tryToCreateNewSession(normalizedOptions, normalizedOrigin);
+ });
+
+ // Iterates over the queue and processes listeners.
+ const processListeners = () => {
+ if (!(normalizedOptions in this.queue) || !isFree()) {
+ return;
+ }
+
+ for (const origin of session[kOriginSet]) {
+ if (origin in this.queue[normalizedOptions]) {
+ const {listeners} = this.queue[normalizedOptions][origin];
+
+ // Prevents session overloading.
+ while (listeners.length !== 0 && isFree()) {
+ // We assume `resolve(...)` calls `request(...)` *directly*,
+ // otherwise the session will get overloaded.
+ listeners.shift().resolve(session);
+ }
+
+ const where = this.queue[normalizedOptions];
+ if (where[origin].listeners.length === 0) {
+ delete where[origin];
+
+ if (Object.keys(where).length === 0) {
+ delete this.queue[normalizedOptions];
+ break;
+ }
+ }
+
+ // We're no longer free, no point in continuing.
+ if (!isFree()) {
+ break;
+ }
+ }
+ }
+ };
+
+ // The Origin Set cannot shrink. No need to check if it suddenly became covered by another one.
+ session.on('origin', () => {
+ session[kOriginSet] = session.originSet;
+
+ if (!isFree()) {
+ // The session is full.
+ return;
+ }
+
+ processListeners();
+
+ // Close covered sessions (if possible).
+ closeCoveredSessions(this.sessions[normalizedOptions], session);
+ });
+
+ session.once('remoteSettings', () => {
+ // Fix Node.js bug preventing the process from exiting
+ session.ref();
+ session.unref();
+
+ this._sessionsCount++;
+
+ // The Agent could have been destroyed already.
+ if (entry.destroyed) {
+ const error = new Error('Agent has been destroyed');
+
+ for (const listener of listeners) {
+ listener.reject(error);
+ }
+
+ session.destroy();
+ return;
+ }
+
+ session[kOriginSet] = session.originSet;
+
+ {
+ const where = this.sessions;
+
+ if (normalizedOptions in where) {
+ const sessions = where[normalizedOptions];
+ sessions.splice(getSortedIndex(sessions, session, compareSessions), 0, session);
+ } else {
+ where[normalizedOptions] = [session];
+ }
+ }
+
+ this._freeSessionsCount += 1;
+ receivedSettings = true;
+
+ this.emit('session', session);
+
+ processListeners();
+ removeFromQueue();
+
+ // TODO: Close last recently used (or least used?) session
+ if (session[kCurrentStreamsCount] === 0 && this._freeSessionsCount > this.maxFreeSessions) {
+ session.close();
+ }
+
+ // Check if we haven't managed to execute all listeners.
+ if (listeners.length !== 0) {
+ // Request for a new session with predefined listeners.
+ this.getSession(normalizedOrigin, options, listeners);
+ listeners.length = 0;
+ }
+
+ // `session.remoteSettings.maxConcurrentStreams` might get increased
+ session.on('remoteSettings', () => {
+ processListeners();
+
+ // In case the Origin Set changes
+ closeCoveredSessions(this.sessions[normalizedOptions], session);
+ });
+ });
+
+ // Shim `session.request()` in order to catch all streams
+ session[kRequest] = session.request;
+ session.request = (headers, streamOptions) => {
+ if (session[kGracefullyClosing]) {
+ throw new Error('The session is gracefully closing. No new streams are allowed.');
+ }
+
+ const stream = session[kRequest](headers, streamOptions);
+
+ // The process won't exit until the session is closed or all requests are gone.
+ session.ref();
+
+ ++session[kCurrentStreamsCount];
+
+ if (session[kCurrentStreamsCount] === session.remoteSettings.maxConcurrentStreams) {
+ this._freeSessionsCount--;
+ }
+
+ stream.once('close', () => {
+ wasFree = isFree();
+
+ --session[kCurrentStreamsCount];
+
+ if (!session.destroyed && !session.closed) {
+ closeSessionIfCovered(this.sessions[normalizedOptions], session);
+
+ if (isFree() && !session.closed) {
+ if (!wasFree) {
+ this._freeSessionsCount++;
+
+ wasFree = true;
+ }
+
+ const isEmpty = session[kCurrentStreamsCount] === 0;
+
+ if (isEmpty) {
+ session.unref();
+ }
+
+ if (
+ isEmpty &&
+ (
+ this._freeSessionsCount > this.maxFreeSessions ||
+ session[kGracefullyClosing]
+ )
+ ) {
+ session.close();
+ } else {
+ closeCoveredSessions(this.sessions[normalizedOptions], session);
+ processListeners();
+ }
+ }
+ }
+ });
+
+ return stream;
+ };
+ } catch (error) {
+ for (const listener of listeners) {
+ listener.reject(error);
+ }
+
+ removeFromQueue();
+ }
+ };
+
+ entry.listeners = listeners;
+ entry.completed = false;
+ entry.destroyed = false;
+
+ this.queue[normalizedOptions][normalizedOrigin] = entry;
+ this._tryToCreateNewSession(normalizedOptions, normalizedOrigin);
+ });
+ }
+
+ request(origin, options, headers, streamOptions) {
+ return new Promise((resolve, reject) => {
+ this.getSession(origin, options, [{
+ reject,
+ resolve: session => {
+ try {
+ resolve(session.request(headers, streamOptions));
+ } catch (error) {
+ reject(error);
+ }
+ }
+ }]);
+ });
+ }
+
+ createConnection(origin, options) {
+ return Agent.connect(origin, options);
+ }
+
+ static connect(origin, options) {
+ options.ALPNProtocols = ['h2'];
+
+ const port = origin.port || 443;
+ const host = origin.hostname || origin.host;
+
+ if (typeof options.servername === 'undefined') {
+ options.servername = host;
+ }
+
+ return tls.connect(port, host, options);
+ }
+
+ closeFreeSessions() {
+ for (const sessions of Object.values(this.sessions)) {
+ for (const session of sessions) {
+ if (session[kCurrentStreamsCount] === 0) {
+ session.close();
+ }
+ }
+ }
+ }
+
+ destroy(reason) {
+ for (const sessions of Object.values(this.sessions)) {
+ for (const session of sessions) {
+ session.destroy(reason);
+ }
+ }
+
+ for (const entriesOfAuthority of Object.values(this.queue)) {
+ for (const entry of Object.values(entriesOfAuthority)) {
+ entry.destroyed = true;
+ }
+ }
+
+ // New requests should NOT attach to destroyed sessions
+ this.queue = {};
+ }
+
+ get freeSessions() {
+ return getSessions({agent: this, isFree: true});
+ }
+
+ get busySessions() {
+ return getSessions({agent: this, isFree: false});
+ }
+}
+
+Agent.kCurrentStreamsCount = kCurrentStreamsCount;
+Agent.kGracefullyClosing = kGracefullyClosing;
+
+module.exports = {
+ Agent,
+ globalAgent: new Agent()
+};
diff --git a/node_modules/http2-wrapper/source/auto.js b/node_modules/http2-wrapper/source/auto.js
new file mode 100644
index 0000000..af4e9ba
--- /dev/null
+++ b/node_modules/http2-wrapper/source/auto.js
@@ -0,0 +1,149 @@
+'use strict';
+const http = require('http');
+const https = require('https');
+const resolveALPN = require('resolve-alpn');
+const QuickLRU = require('quick-lru');
+const Http2ClientRequest = require('./client-request');
+const calculateServerName = require('./utils/calculate-server-name');
+const urlToOptions = require('./utils/url-to-options');
+
+const cache = new QuickLRU({maxSize: 100});
+const queue = new Map();
+
+const installSocket = (agent, socket, options) => {
+ socket._httpMessage = {shouldKeepAlive: true};
+
+ const onFree = () => {
+ agent.emit('free', socket, options);
+ };
+
+ socket.on('free', onFree);
+
+ const onClose = () => {
+ agent.removeSocket(socket, options);
+ };
+
+ socket.on('close', onClose);
+
+ const onRemove = () => {
+ agent.removeSocket(socket, options);
+ socket.off('close', onClose);
+ socket.off('free', onFree);
+ socket.off('agentRemove', onRemove);
+ };
+
+ socket.on('agentRemove', onRemove);
+
+ agent.emit('free', socket, options);
+};
+
+const resolveProtocol = async options => {
+ const name = `${options.host}:${options.port}:${options.ALPNProtocols.sort()}`;
+
+ if (!cache.has(name)) {
+ if (queue.has(name)) {
+ const result = await queue.get(name);
+ return result.alpnProtocol;
+ }
+
+ const {path, agent} = options;
+ options.path = options.socketPath;
+
+ const resultPromise = resolveALPN(options);
+ queue.set(name, resultPromise);
+
+ try {
+ const {socket, alpnProtocol} = await resultPromise;
+ cache.set(name, alpnProtocol);
+
+ options.path = path;
+
+ if (alpnProtocol === 'h2') {
+ // https://github.com/nodejs/node/issues/33343
+ socket.destroy();
+ } else {
+ const {globalAgent} = https;
+ const defaultCreateConnection = https.Agent.prototype.createConnection;
+
+ if (agent) {
+ if (agent.createConnection === defaultCreateConnection) {
+ installSocket(agent, socket, options);
+ } else {
+ socket.destroy();
+ }
+ } else if (globalAgent.createConnection === defaultCreateConnection) {
+ installSocket(globalAgent, socket, options);
+ } else {
+ socket.destroy();
+ }
+ }
+
+ queue.delete(name);
+
+ return alpnProtocol;
+ } catch (error) {
+ queue.delete(name);
+
+ throw error;
+ }
+ }
+
+ return cache.get(name);
+};
+
+module.exports = async (input, options, callback) => {
+ if (typeof input === 'string' || input instanceof URL) {
+ input = urlToOptions(new URL(input));
+ }
+
+ if (typeof options === 'function') {
+ callback = options;
+ options = undefined;
+ }
+
+ options = {
+ ALPNProtocols: ['h2', 'http/1.1'],
+ ...input,
+ ...options,
+ resolveSocket: true
+ };
+
+ if (!Array.isArray(options.ALPNProtocols) || options.ALPNProtocols.length === 0) {
+ throw new Error('The `ALPNProtocols` option must be an Array with at least one entry');
+ }
+
+ options.protocol = options.protocol || 'https:';
+ const isHttps = options.protocol === 'https:';
+
+ options.host = options.hostname || options.host || 'localhost';
+ options.session = options.tlsSession;
+ options.servername = options.servername || calculateServerName(options);
+ options.port = options.port || (isHttps ? 443 : 80);
+ options._defaultAgent = isHttps ? https.globalAgent : http.globalAgent;
+
+ const agents = options.agent;
+
+ if (agents) {
+ if (agents.addRequest) {
+ throw new Error('The `options.agent` object can contain only `http`, `https` or `http2` properties');
+ }
+
+ options.agent = agents[isHttps ? 'https' : 'http'];
+ }
+
+ if (isHttps) {
+ const protocol = await resolveProtocol(options);
+
+ if (protocol === 'h2') {
+ if (agents) {
+ options.agent = agents.http2;
+ }
+
+ return new Http2ClientRequest(options, callback);
+ }
+ }
+
+ return http.request(options, callback);
+};
+
+module.exports.protocolCache = cache;
diff --git a/node_modules/http2-wrapper/source/client-request.js b/node_modules/http2-wrapper/source/client-request.js
new file mode 100644
index 0000000..b712967
--- /dev/null
+++ b/node_modules/http2-wrapper/source/client-request.js
@@ -0,0 +1,445 @@
+'use strict';
+const http2 = require('http2');
+const {Writable} = require('stream');
+const {Agent, globalAgent} = require('./agent');
+const IncomingMessage = require('./incoming-message');
+const urlToOptions = require('./utils/url-to-options');
+const proxyEvents = require('./utils/proxy-events');
+const isRequestPseudoHeader = require('./utils/is-request-pseudo-header');
+const {
+ ERR_INVALID_ARG_TYPE,
+ ERR_INVALID_PROTOCOL,
+ ERR_HTTP_HEADERS_SENT,
+ ERR_INVALID_HTTP_TOKEN,
+ ERR_HTTP_INVALID_HEADER_VALUE,
+ ERR_INVALID_CHAR
+} = require('./utils/errors');
+
+const {
+ HTTP2_HEADER_STATUS,
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_PATH,
+ HTTP2_METHOD_CONNECT
+} = http2.constants;
+
+const kHeaders = Symbol('headers');
+const kOrigin = Symbol('origin');
+const kSession = Symbol('session');
+const kOptions = Symbol('options');
+const kFlushedHeaders = Symbol('flushedHeaders');
+const kJobs = Symbol('jobs');
+
+const isValidHttpToken = /^[\^`\-\w!#$%&*+.|~]+$/;
+const isInvalidHeaderValue = /[^\t\u0020-\u007E\u0080-\u00FF]/;
+
+class ClientRequest extends Writable {
+ constructor(input, options, callback) {
+ super({
+ autoDestroy: false
+ });
+
+ const hasInput = typeof input === 'string' || input instanceof URL;
+ if (hasInput) {
+ input = urlToOptions(input instanceof URL ? input : new URL(input));
+ }
+
+ if (typeof options === 'function' || options === undefined) {
+ // (options, callback)
+ callback = options;
+ options = hasInput ? input : {...input};
+ } else {
+ // (input, options, callback)
+ options = {...input, ...options};
+ }
+
+ if (options.h2session) {
+ this[kSession] = options.h2session;
+ } else if (options.agent === false) {
+ this.agent = new Agent({maxFreeSessions: 0});
+ } else if (typeof options.agent === 'undefined' || options.agent === null) {
+ if (typeof options.createConnection === 'function') {
+ // This is a workaround - we don't have to create the session on our own.
+ this.agent = new Agent({maxFreeSessions: 0});
+ this.agent.createConnection = options.createConnection;
+ } else {
+ this.agent = globalAgent;
+ }
+ } else if (typeof options.agent.request === 'function') {
+ this.agent = options.agent;
+ } else {
+ throw new ERR_INVALID_ARG_TYPE('options.agent', ['Agent-like Object', 'undefined', 'false'], options.agent);
+ }
+
+ if (options.protocol && options.protocol !== 'https:') {
+ throw new ERR_INVALID_PROTOCOL(options.protocol, 'https:');
+ }
+
+ const port = options.port || options.defaultPort || (this.agent && this.agent.defaultPort) || 443;
+ const host = options.hostname || options.host || 'localhost';
+
+ // Don't enforce the origin via options. It may be changed in an Agent.
+ delete options.hostname;
+ delete options.host;
+ delete options.port;
+
+ const {timeout} = options;
+ options.timeout = undefined;
+
+ this[kHeaders] = Object.create(null);
+ this[kJobs] = [];
+
+ this.socket = null;
+ this.connection = null;
+
+ this.method = options.method || 'GET';
+ this.path = options.path;
+
+ this.res = null;
+ this.aborted = false;
+ this.reusedSocket = false;
+
+ if (options.headers) {
+ for (const [header, value] of Object.entries(options.headers)) {
+ this.setHeader(header, value);
+ }
+ }
+
+ if (options.auth && !('authorization' in this[kHeaders])) {
+ this[kHeaders].authorization = 'Basic ' + Buffer.from(options.auth).toString('base64');
+ }
+
+ options.session = options.tlsSession;
+ options.path = options.socketPath;
+
+ this[kOptions] = options;
+
+ // Clients that generate HTTP/2 requests directly SHOULD use the :authority pseudo-header field instead of the Host header field.
+ if (port === 443) {
+ this[kOrigin] = `https://${host}`;
+
+ if (!(':authority' in this[kHeaders])) {
+ this[kHeaders][':authority'] = host;
+ }
+ } else {
+ this[kOrigin] = `https://${host}:${port}`;
+
+ if (!(':authority' in this[kHeaders])) {
+ this[kHeaders][':authority'] = `${host}:${port}`;
+ }
+ }
+
+ if (timeout) {
+ this.setTimeout(timeout);
+ }
+
+ if (callback) {
+ this.once('response', callback);
+ }
+
+ this[kFlushedHeaders] = false;
+ }
+
+ get method() {
+ return this[kHeaders][HTTP2_HEADER_METHOD];
+ }
+
+ set method(value) {
+ if (value) {
+ this[kHeaders][HTTP2_HEADER_METHOD] = value.toUpperCase();
+ }
+ }
+
+ get path() {
+ return this[kHeaders][HTTP2_HEADER_PATH];
+ }
+
+ set path(value) {
+ if (value) {
+ this[kHeaders][HTTP2_HEADER_PATH] = value;
+ }
+ }
+
+ get _mustNotHaveABody() {
+ return this.method === 'GET' || this.method === 'HEAD' || this.method === 'DELETE';
+ }
+
+ _write(chunk, encoding, callback) {
+ // https://github.com/nodejs/node/blob/654df09ae0c5e17d1b52a900a545f0664d8c7627/lib/internal/http2/util.js#L148-L156
+ if (this._mustNotHaveABody) {
+ callback(new Error('The GET, HEAD and DELETE methods must NOT have a body'));
+ /* istanbul ignore next: Node.js 12 throws directly */
+ return;
+ }
+
+ this.flushHeaders();
+
+ const callWrite = () => this._request.write(chunk, encoding, callback);
+ if (this._request) {
+ callWrite();
+ } else {
+ this[kJobs].push(callWrite);
+ }
+ }
+
+ _final(callback) {
+ if (this.destroyed) {
+ return;
+ }
+
+ this.flushHeaders();
+
+ const callEnd = () => {
+ // For GET, HEAD and DELETE
+ if (this._mustNotHaveABody) {
+ callback();
+ return;
+ }
+
+ this._request.end(callback);
+ };
+
+ if (this._request) {
+ callEnd();
+ } else {
+ this[kJobs].push(callEnd);
+ }
+ }
+
+ abort() {
+ if (this.res && this.res.complete) {
+ return;
+ }
+
+ if (!this.aborted) {
+ process.nextTick(() => this.emit('abort'));
+ }
+
+ this.aborted = true;
+
+ this.destroy();
+ }
+
+ _destroy(error, callback) {
+ if (this.res) {
+ this.res._dump();
+ }
+
+ if (this._request) {
+ this._request.destroy();
+ }
+
+ callback(error);
+ }
+
+ async flushHeaders() {
+ if (this[kFlushedHeaders] || this.destroyed) {
+ return;
+ }
+
+ this[kFlushedHeaders] = true;
+
+ const isConnectMethod = this.method === HTTP2_METHOD_CONNECT;
+
+ // The real magic is here
+ const onStream = stream => {
+ this._request = stream;
+
+ if (this.destroyed) {
+ stream.destroy();
+ return;
+ }
+
+ // Forwards `timeout`, `continue`, `close` and `error` events to this instance.
+ if (!isConnectMethod) {
+ proxyEvents(stream, this, ['timeout', 'continue', 'close', 'error']);
+ }
+
+ // Wait for the `finish` event. We don't want to emit the `response` event
+ // before `request.end()` is called.
+ const waitForEnd = fn => {
+ return (...args) => {
+ if (!this.writable && !this.destroyed) {
+ fn(...args);
+ } else {
+ this.once('finish', () => {
+ fn(...args);
+ });
+ }
+ };
+ };
+
+ // This event tells we are ready to listen for the data.
+ stream.once('response', waitForEnd((headers, flags, rawHeaders) => {
+ // If we were to emit raw request stream, it would be as fast as the native approach.
+ // Note that wrapping the raw stream in a Proxy instance won't improve the performance (already tested it).
+ const response = new IncomingMessage(this.socket, stream.readableHighWaterMark);
+ this.res = response;
+
+ response.req = this;
+ response.statusCode = headers[HTTP2_HEADER_STATUS];
+ response.headers = headers;
+ response.rawHeaders = rawHeaders;
+
+ response.once('end', () => {
+ if (this.aborted) {
+ response.aborted = true;
+ response.emit('aborted');
+ } else {
+ response.complete = true;
+
+ // Has no effect, just be consistent with the Node.js behavior
+ response.socket = null;
+ response.connection = null;
+ }
+ });
+
+ if (isConnectMethod) {
+ response.upgrade = true;
+
+ // The HTTP1 API says the socket is detached here,
+ // but we can't do that so we pass the original HTTP2 request.
+ if (this.emit('connect', response, stream, Buffer.alloc(0))) {
+ this.emit('close');
+ } else {
+ // No listeners attached, destroy the original request.
+ stream.destroy();
+ }
+ } else {
+ // Forwards data
+ stream.on('data', chunk => {
+ if (!response._dumped && !response.push(chunk)) {
+ stream.pause();
+ }
+ });
+
+ stream.once('end', () => {
+ response.push(null);
+ });
+
+ if (!this.emit('response', response)) {
+ // No listeners attached, dump the response.
+ response._dump();
+ }
+ }
+ }));
+
+ // Emits `information` event
+ stream.once('headers', waitForEnd(
+ headers => this.emit('information', {statusCode: headers[HTTP2_HEADER_STATUS]})
+ ));
+
+ stream.once('trailers', waitForEnd((trailers, flags, rawTrailers) => {
+ const {res} = this;
+
+ // Assigns trailers to the response object.
+ res.trailers = trailers;
+ res.rawTrailers = rawTrailers;
+ }));
+
+ const {socket} = stream.session;
+ this.socket = socket;
+ this.connection = socket;
+
+ for (const job of this[kJobs]) {
+ job();
+ }
+
+ this.emit('socket', this.socket);
+ };
+
+ // Makes a HTTP2 request
+ if (this[kSession]) {
+ try {
+ onStream(this[kSession].request(this[kHeaders]));
+ } catch (error) {
+ this.emit('error', error);
+ }
+ } else {
+ this.reusedSocket = true;
+
+ try {
+ onStream(await this.agent.request(this[kOrigin], this[kOptions], this[kHeaders]));
+ } catch (error) {
+ this.emit('error', error);
+ }
+ }
+ }
+
+ getHeader(name) {
+ if (typeof name !== 'string') {
+ throw new ERR_INVALID_ARG_TYPE('name', 'string', name);
+ }
+
+ return this[kHeaders][name.toLowerCase()];
+ }
+
+ get headersSent() {
+ return this[kFlushedHeaders];
+ }
+
+ removeHeader(name) {
+ if (typeof name !== 'string') {
+ throw new ERR_INVALID_ARG_TYPE('name', 'string', name);
+ }
+
+ if (this.headersSent) {
+ throw new ERR_HTTP_HEADERS_SENT('remove');
+ }
+
+ delete this[kHeaders][name.toLowerCase()];
+ }
+
+ setHeader(name, value) {
+ if (this.headersSent) {
+ throw new ERR_HTTP_HEADERS_SENT('set');
+ }
+
+ if (typeof name !== 'string' || (!isValidHttpToken.test(name) && !isRequestPseudoHeader(name))) {
+ throw new ERR_INVALID_HTTP_TOKEN('Header name', name);
+ }
+
+ if (typeof value === 'undefined') {
+ throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name);
+ }
+
+ if (isInvalidHeaderValue.test(value)) {
+ throw new ERR_INVALID_CHAR('header content', name);
+ }
+
+ this[kHeaders][name.toLowerCase()] = value;
+ }
+
+ setNoDelay() {
+ // HTTP2 sockets cannot be malformed, do nothing.
+ }
+
+ setSocketKeepAlive() {
+ // HTTP2 sockets cannot be malformed, do nothing.
+ }
+
+ setTimeout(ms, callback) {
+ const applyTimeout = () => this._request.setTimeout(ms, callback);
+
+ if (this._request) {
+ applyTimeout();
+ } else {
+ this[kJobs].push(applyTimeout);
+ }
+
+ return this;
+ }
+
+ get maxHeadersCount() {
+ if (!this.destroyed && this._request) {
+ return this._request.session.localSettings.maxHeaderListSize;
+ }
+
+ return undefined;
+ }
+
+ set maxHeadersCount(_value) {
+ // Updating HTTP2 settings would affect all requests, do nothing.
+ }
+}
+
+module.exports = ClientRequest;
diff --git a/node_modules/http2-wrapper/source/incoming-message.js b/node_modules/http2-wrapper/source/incoming-message.js
new file mode 100644
index 0000000..7d5c0d3
--- /dev/null
+++ b/node_modules/http2-wrapper/source/incoming-message.js
@@ -0,0 +1,58 @@
+'use strict';
+const {Readable} = require('stream');
+
+class IncomingMessage extends Readable {
+ constructor(socket, highWaterMark) {
+ super({
+ highWaterMark,
+ autoDestroy: false
+ });
+
+ this.statusCode = null;
+ this.statusMessage = '';
+ this.httpVersion = '2.0';
+ this.httpVersionMajor = 2;
+ this.httpVersionMinor = 0;
+ this.headers = {};
+ this.trailers = {};
+ this.req = null;
+
+ this.aborted = false;
+ this.complete = false;
+ this.upgrade = null;
+
+ this.rawHeaders = [];
+ this.rawTrailers = [];
+
+ this.socket = socket;
+ this.connection = socket;
+
+ this._dumped = false;
+ }
+
+ _destroy(error) {
+ this.req._request.destroy(error);
+ }
+
+ setTimeout(ms, callback) {
+ this.req.setTimeout(ms, callback);
+ return this;
+ }
+
+ _dump() {
+ if (!this._dumped) {
+ this._dumped = true;
+
+ this.removeAllListeners('data');
+ this.resume();
+ }
+ }
+
+ _read() {
+ if (this.req) {
+ this.req._request.resume();
+ }
+ }
+}
+
+module.exports = IncomingMessage;
diff --git a/node_modules/http2-wrapper/source/index.js b/node_modules/http2-wrapper/source/index.js
new file mode 100644
index 0000000..fb83349
--- /dev/null
+++ b/node_modules/http2-wrapper/source/index.js
@@ -0,0 +1,28 @@
+'use strict';
+const http2 = require('http2');
+const agent = require('./agent');
+const ClientRequest = require('./client-request');
+const IncomingMessage = require('./incoming-message');
+const auto = require('./auto');
+
+const request = (url, options, callback) => {
+ return new ClientRequest(url, options, callback);
+};
+
+const get = (url, options, callback) => {
+ // eslint-disable-next-line unicorn/prevent-abbreviations
+ const req = new ClientRequest(url, options, callback);
+ req.end();
+
+ return req;
+};
+
+module.exports = {
+ ...http2,
+ ClientRequest,
+ IncomingMessage,
+ ...agent,
+ request,
+ get,
+ auto
+};
diff --git a/node_modules/http2-wrapper/source/utils/calculate-server-name.js b/node_modules/http2-wrapper/source/utils/calculate-server-name.js
new file mode 100644
index 0000000..b05c099
--- /dev/null
+++ b/node_modules/http2-wrapper/source/utils/calculate-server-name.js
@@ -0,0 +1,27 @@
+'use strict';
+const net = require('net');
+/* istanbul ignore file: https://github.com/nodejs/node/blob/v13.0.1/lib/_http_agent.js */
+
+module.exports = options => {
+ let servername = options.host;
+ const hostHeader = options.headers && options.headers.host;
+
+ if (hostHeader) {
+ if (hostHeader.startsWith('[')) {
+ const index = hostHeader.indexOf(']');
+ if (index === -1) {
+ servername = hostHeader;
+ } else {
+ servername = hostHeader.slice(1, -1);
+ }
+ } else {
+ servername = hostHeader.split(':', 1)[0];
+ }
+ }
+
+ if (net.isIP(servername)) {
+ return '';
+ }
+
+ return servername;
+};
diff --git a/node_modules/http2-wrapper/source/utils/errors.js b/node_modules/http2-wrapper/source/utils/errors.js
new file mode 100644
index 0000000..5018283
--- /dev/null
+++ b/node_modules/http2-wrapper/source/utils/errors.js
@@ -0,0 +1,45 @@
+'use strict';
+/* istanbul ignore file: https://github.com/nodejs/node/blob/master/lib/internal/errors.js */
+
+const makeError = (Base, key, getMessage) => {
+ module.exports[key] = class NodeError extends Base {
+ constructor(...args) {
+ super(typeof getMessage === 'string' ? getMessage : getMessage(args));
+ this.name = `${super.name} [${key}]`;
+ this.code = key;
+ }
+ };
+};
+
+makeError(TypeError, 'ERR_INVALID_ARG_TYPE', args => {
+ const type = args[0].includes('.') ? 'property' : 'argument';
+
+ let valid = args[1];
+ const isManyTypes = Array.isArray(valid);
+
+ if (isManyTypes) {
+ valid = `${valid.slice(0, -1).join(', ')} or ${valid.slice(-1)}`;
+ }
+
+ return `The "${args[0]}" ${type} must be ${isManyTypes ? 'one of' : 'of'} type ${valid}. Received ${typeof args[2]}`;
+});
+
+makeError(TypeError, 'ERR_INVALID_PROTOCOL', args => {
+ return `Protocol "${args[0]}" not supported. Expected "${args[1]}"`;
+});
+
+makeError(Error, 'ERR_HTTP_HEADERS_SENT', args => {
+ return `Cannot ${args[0]} headers after they are sent to the client`;
+});
+
+makeError(TypeError, 'ERR_INVALID_HTTP_TOKEN', args => {
+ return `${args[0]} must be a valid HTTP token [${args[1]}]`;
+});
+
+makeError(TypeError, 'ERR_HTTP_INVALID_HEADER_VALUE', args => {
+ return `Invalid value "${args[0]} for header "${args[1]}"`;
+});
+
+makeError(TypeError, 'ERR_INVALID_CHAR', args => {
+ return `Invalid character in ${args[0]} [${args[1]}]`;
+});
diff --git a/node_modules/http2-wrapper/source/utils/is-request-pseudo-header.js b/node_modules/http2-wrapper/source/utils/is-request-pseudo-header.js
new file mode 100644
index 0000000..bed31cd
--- /dev/null
+++ b/node_modules/http2-wrapper/source/utils/is-request-pseudo-header.js
@@ -0,0 +1,13 @@
+'use strict';
+
+module.exports = header => {
+ switch (header) {
+ case ':method':
+ case ':scheme':
+ case ':authority':
+ case ':path':
+ return true;
+ default:
+ return false;
+ }
+};
diff --git a/node_modules/http2-wrapper/source/utils/proxy-events.js b/node_modules/http2-wrapper/source/utils/proxy-events.js
new file mode 100644
index 0000000..35e2ae0
--- /dev/null
+++ b/node_modules/http2-wrapper/source/utils/proxy-events.js
@@ -0,0 +1,7 @@
+'use strict';
+
+module.exports = (from, to, events) => {
+ for (const event of events) {
+ from.on(event, (...args) => to.emit(event, ...args));
+ }
+};
diff --git a/node_modules/http2-wrapper/source/utils/url-to-options.js b/node_modules/http2-wrapper/source/utils/url-to-options.js
new file mode 100644
index 0000000..36d1580
--- /dev/null
+++ b/node_modules/http2-wrapper/source/utils/url-to-options.js
@@ -0,0 +1,25 @@
+'use strict';
+/* istanbul ignore file: https://github.com/nodejs/node/blob/a91293d4d9ab403046ab5eb022332e4e3d249bd3/lib/internal/url.js#L1257 */
+
+module.exports = url => {
+ const options = {
+ protocol: url.protocol,
+ hostname: typeof url.hostname === 'string' && url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname,
+ host: url.host,
+ hash: url.hash,
+ search: url.search,
+ pathname: url.pathname,
+ href: url.href,
+ path: `${url.pathname || ''}${url.search || ''}`
+ };
+
+ if (typeof url.port === 'string' && url.port.length !== 0) {
+ options.port = Number(url.port);
+ }
+
+ if (url.username || url.password) {
+ options.auth = `${url.username || ''}:${url.password || ''}`;
+ }
+
+ return options;
+};