diff options
author | Minteck <nekostarfan@gmail.com> | 2021-08-24 14:41:48 +0200 |
---|---|---|
committer | Minteck <nekostarfan@gmail.com> | 2021-08-24 14:41:48 +0200 |
commit | d25e11bee6ca5ca523884da132d18e1400e077b9 (patch) | |
tree | 8af39fde19f7ed640a60fb397c7edd647dff1c4c /node_modules/http2-wrapper | |
download | kartik-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/LICENSE | 21 | ||||
-rw-r--r-- | node_modules/http2-wrapper/README.md | 470 | ||||
-rw-r--r-- | node_modules/http2-wrapper/package.json | 54 | ||||
-rw-r--r-- | node_modules/http2-wrapper/source/agent.js | 670 | ||||
-rw-r--r-- | node_modules/http2-wrapper/source/auto.js | 149 | ||||
-rw-r--r-- | node_modules/http2-wrapper/source/client-request.js | 445 | ||||
-rw-r--r-- | node_modules/http2-wrapper/source/incoming-message.js | 58 | ||||
-rw-r--r-- | node_modules/http2-wrapper/source/index.js | 28 | ||||
-rw-r--r-- | node_modules/http2-wrapper/source/utils/calculate-server-name.js | 27 | ||||
-rw-r--r-- | node_modules/http2-wrapper/source/utils/errors.js | 45 | ||||
-rw-r--r-- | node_modules/http2-wrapper/source/utils/is-request-pseudo-header.js | 13 | ||||
-rw-r--r-- | node_modules/http2-wrapper/source/utils/proxy-events.js | 7 | ||||
-rw-r--r-- | node_modules/http2-wrapper/source/utils/url-to-options.js | 25 |
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; +}; |