This commit is contained in:
2025-05-12 05:38:44 +09:00
parent dced21c3f8
commit 6d78bfa46e
8120 changed files with 1161564 additions and 0 deletions

21
book/node_modules/http2-wrapper/LICENSE generated vendored Normal file
View File

@ -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.

459
book/node_modules/http2-wrapper/README.md generated vendored Normal file
View File

@ -0,0 +1,459 @@
# 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.
**Note:** The `timeout` option applies to HTTP/2 streams only. In order to set session timeout, pass an Agent with custom `timeout` option set.
### 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:', // Try changing this to https:
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.auto.createResolveProtocol(cache, queue, connect)
#### cache
Type: `Map<string, string>`
This is the store where cached ALPN protocols are put into.
#### queue
Type: `Map<string, Promise>`
This is the store that contains pending ALPN negotiation promises.
#### connect
Type: `(options, callback) => TLSSocket | Promise<TLSSocket>`
See https://github.com/szmarczak/resolve-alpn#connect
### http2.auto.resolveProtocol(options)
Returns a `Promise<{alpnProtocol: string}>`.
### 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` to get one.
### 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()
}, response => {
response.on('data', chunk => console.log(`Received chunk of ${chunk.length} bytes`));
});
```
#### options
Each option is an `Agent` property and can be changed later.
##### timeout
Type: `number`<br>
Default: `0`
If there's no activity after `timeout` milliseconds, the session will be closed. If `0`, no timeout is applied.
##### maxSessions
Type: `number`<br>
Default: `Infinity`
The maximum amount of sessions in total.
##### maxEmptySessions
Type: `number`<br>
Default: `10`
The maximum amount of empty sessions in total. An empty session is a session with no pending requests.
##### maxCachedTlsSessions
Type: `number`<br>
Default: `100`
The maximum amount of cached TLS sessions.
#### agent.protocol
Type: `string`<br>
Default: `https:`
#### 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'});
// => ':::::::::::::::::::::::::::::::::::::'
```
#### agent.getSession(origin, options)
##### [origin](https://nodejs.org/api/http2.html#http2_http2_connect_authority_options_listener)
Type: `string` `URL` `object`
Origin used to create new session.
##### [options](https://nodejs.org/api/http2.html#http2_http2_connect_authority_options_listener)
Type: `object`
Options used to create new session.
Returns a Promise giving free `Http2Session`. If no free sessions are found, a new one is created.
A session is considered free when pending streams count is less than max concurrent streams settings.
#### 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.closeEmptySessions(count)
##### count
Type: `number`
Default: `Number.POSITIVE_INFINITY`
Makes an attempt to close empty sessions. Only sessions with 0 concurrent streams will be closed.
#### agent.destroy(reason)
Destroys **all** sessions.
#### agent.emptySessionCount
Type: `number`
A number of empty sessions.
#### agent.pendingSessionCount
Type: `number`
A number of pending sessions.
#### agent.sessionCount
Type: `number`
A number of all sessions held by the Agent.
#### Event: 'session'
```js
agent.on('session', session => {
// A new session has been created by the Agent.
});
```
## Proxy support
Currently `http2-wrapper` provides support for these proxies:
- `HttpOverHttp2`
- `HttpsOverHttp2`
- `Http2OverHttp2`
- `Http2OverHttp`
- `Http2OverHttps`
Any of the above can be accessed via `http2wrapper.proxies`. Check out the [`examples/proxies`](examples/proxies) directory to learn more.
**Note:** If you use the `http2.auto` function, the real IP address will leak. `http2wrapper` is not aware of the context. It will create a connection to the end server using your real IP address to get the ALPN protocol. Then it will create another connection using proxy. To migitate this, you need to pass a custom `resolveProtocol` function as an option:
```js
const resolveAlpnProxy = new URL('https://username:password@localhost:8000');
const connect = async (options, callback) => new Promise((resolve, reject) => {
const host = `${options.host}:${options.port}`;
(async () => {
try {
const request = await http2.auto(resolveAlpnProxy, {
method: 'CONNECT',
headers: {
host
},
path: host,
// For demo purposes only!
rejectUnauthorized: false,
});
request.end();
request.once('error', reject);
request.once('connect', (response, socket, head) => {
if (head.length > 0) {
reject(new Error(`Unexpected data before CONNECT tunnel: ${head.length} bytes`));
socket.destroy();
return;
}
const tlsSocket = tls.connect({
...options,
socket
}, callback);
resolve(tlsSocket);
});
} catch (error) {
reject(error);
}
})();
});
// This is required to prevent leaking real IP address on ALPN negotiation
const resolveProtocol = http2.auto.createResolveProtocol(new Map(), new Map(), connect);
const request = await http2.auto('https://httpbin.org/anything', {
agent: {},
resolveProtocol
}, response => {
// Read the response here
});
request.end();
```
See [`unknown-over-unknown.js`](examples/proxies/unknown-over-unknown.js) to learn more.
## Mirroring another server
See [`examples/proxies/mirror.js`](examples/proxies/mirror.js) for an example.
## [WebSockets over HTTP/2](https://tools.ietf.org/html/rfc8441)
See [`examples/ws`](examples/ws) for an example.
## Push streams
See [`examples/push-stream`](examples/push-stream) for an example.
## Related
- [`got`](https://github.com/sindresorhus/got) - Simplified HTTP requests
- [`http2-proxy`](https://github.com/nxtedition/node-http2-proxy) - A simple http/2 & http/1.1 spec compliant proxy helper for Node.
## License
MIT

141
book/node_modules/http2-wrapper/index.d.ts generated vendored Normal file
View File

@ -0,0 +1,141 @@
// See https://github.com/facebook/jest/issues/2549
// eslint-disable-next-line node/prefer-global/url
import {URL} from 'url';
import {EventEmitter} from 'events';
import tls = require('tls');
import http = require('http');
import https = require('https');
import http2 = require('http2');
// Note: do not convert this to import from.
import QuickLRU = require('quick-lru');
export interface RequestOptions extends Omit<https.RequestOptions, 'session' | 'agent'> {
tlsSession?: tls.ConnectionOptions['session'];
h2session?: http2.ClientHttp2Session;
agent?: Agent | false;
// Required because @types/node is missing types
ALPNProtocols?: string[];
}
export interface AutoRequestOptions extends Omit<RequestOptions, 'agent' | 'h2session'> {
agent?: {
http?: http.Agent | false;
https?: https.Agent | false;
http2?: Agent | false;
};
resolveProtocol?: ResolveProtocolFunction;
}
export interface EntryFunction {
(): Promise<void>;
completed: boolean;
destroyed: boolean;
listeners: PromiseListeners;
}
export interface AgentOptions {
timeout?: number;
maxSessions?: number;
maxEmptySessions?: number;
maxCachedTlsSessions?: number;
}
export interface PromiseListeners {
resolve: (value: unknown) => void;
reject: (error: Error) => void;
}
export class Agent extends EventEmitter {
sessions: Record<string, http2.ClientHttp2Session[]>;
queue: Record<string, Record<string, EntryFunction>>;
timeout: number;
maxSessions: number;
maxEmptySessions: number;
protocol: string;
settings: http2.Settings;
tlsSessionCache: QuickLRU<string, string>;
emptySessionCount: number;
pendingSessionCount: number;
sessionCount: number;
constructor(options?: AgentOptions);
static connect(origin: URL, options: http2.SecureClientSessionOptions): tls.TLSSocket;
normalizeOptions(options: http2.ClientSessionRequestOptions): string;
getSession(origin: string | URL, options?: http2.SecureClientSessionOptions, listeners?: PromiseListeners): Promise<http2.ClientHttp2Session>;
request(origin: string | URL, options?: http2.SecureClientSessionOptions, headers?: http2.OutgoingHttpHeaders, streamOptions?: http2.ClientSessionRequestOptions): Promise<http2.ClientHttp2Stream>;
createConnection(origin: URL, options: http2.SecureClientSessionOptions): Promise<tls.TLSSocket>;
closeEmptySessions(count?: number): void;
destroy(reason?: Error): void;
}
export interface ProxyOptions {
headers?: http2.OutgoingHttpHeaders;
raw?: boolean;
url: URL | string;
}
export namespace proxies {
class HttpOverHttp2 extends http.Agent {
constructor(options: http.AgentOptions & {proxyOptions: ProxyOptions});
}
class HttpsOverHttp2 extends https.Agent {
constructor(options: https.AgentOptions & {proxyOptions: ProxyOptions});
}
class Http2OverHttp2 extends Agent {
constructor(options: AgentOptions & {proxyOptions: ProxyOptions});
}
class Http2OverHttp extends Agent {
constructor(options: AgentOptions & {proxyOptions: ProxyOptions});
}
class Http2OverHttps extends Agent {
constructor(options: AgentOptions & {proxyOptions: ProxyOptions});
}
}
export type RequestFunction<T, O = RequestOptions> =
((url: string | URL, options?: O, callback?: (response: http.IncomingMessage) => void) => T) &
((url: string | URL, callback?: (response: http.IncomingMessage) => void) => T) &
((options: O, callback?: (response: http.IncomingMessage) => void) => T);
export const globalAgent: Agent;
export type ResolveProtocolResult = {
alpnProtocol: string;
socket?: tls.TLSSocket;
timeout?: boolean;
};
export type ResolveProtocolFunction = (options: AutoRequestOptions) => Promise<ResolveProtocolResult>;
type Promisable<T> = T | Promise<T>;
export type ResolveProtocolConnectFunction = (options: tls.ConnectionOptions, callback: () => void) => Promisable<tls.TLSSocket>;
export const request: RequestFunction<http.ClientRequest>;
export const get: RequestFunction<http.ClientRequest>;
export const auto: RequestFunction<Promise<http.ClientRequest>, AutoRequestOptions> & {
protocolCache: QuickLRU<string, string>;
resolveProtocol: ResolveProtocolFunction;
createResolveProtocol: (cache: Map<string, string>, queue: Map<string, Promise<ResolveProtocolResult>>, connect?: ResolveProtocolConnectFunction) => ResolveProtocolFunction;
};
export {
ClientRequest,
IncomingMessage
} from 'http';
export * from 'http2';

81
book/node_modules/http2-wrapper/package.json generated vendored Normal file
View File

@ -0,0 +1,81 @@
{
"name": "http2-wrapper",
"version": "2.2.1",
"description": "HTTP2 client, just with the familiar `https` API",
"main": "source",
"types": "index.d.ts",
"engines": {
"node": ">=10.19.0"
},
"scripts": {
"test": "xo && nyc --reporter=lcovonly --reporter=text --reporter=html ava && tsd"
},
"files": [
"source",
"index.d.ts"
],
"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.2.0"
},
"devDependencies": {
"@sindresorhus/is": "^4.0.1",
"ava": "^3.15.0",
"benchmark": "^2.1.4",
"get-stream": "^6.0.1",
"got": "^11.8.2",
"http2-proxy": "^5.0.53",
"https-proxy-agent": "^5.0.0",
"lolex": "^6.0.0",
"many-keys-map": "^1.0.3",
"nyc": "^15.1.0",
"p-event": "^4.2.0",
"tempy": "^1.0.1",
"to-readable-stream": "^2.1.0",
"tsd": "^0.17.0",
"websocket-stream": "^5.5.2",
"ws": "^7.5.3",
"xo": "0.39.1"
},
"ava": {
"timeout": "10s"
},
"nyc": {
"include": [
"source"
]
},
"xo": {
"rules": {
"unicorn/no-for-loop": "off",
"unicorn/prefer-module": "off",
"comma-dangle": "off",
"@typescript-eslint/comma-dangle": "off",
"quotes": [
"error",
"single",
{
"avoidEscape": true,
"allowTemplateLiterals": true
}
],
"operator-linebreak": ["error", "before"]
}
}
}

796
book/node_modules/http2-wrapper/source/agent.js generated vendored Normal file
View File

@ -0,0 +1,796 @@
'use strict';
// See https://github.com/facebook/jest/issues/2549
// eslint-disable-next-line node/prefer-global/url
const {URL} = require('url');
const EventEmitter = require('events');
const tls = require('tls');
const http2 = require('http2');
const QuickLRU = require('quick-lru');
const delayAsyncDestroy = require('./utils/delay-async-destroy.js');
const kCurrentStreamCount = Symbol('currentStreamCount');
const kRequest = Symbol('request');
const kOriginSet = Symbol('cachedOriginSet');
const kGracefullyClosing = Symbol('gracefullyClosing');
const kLength = Symbol('length');
const nameKeys = [
// Not an Agent option actually
'createConnection',
// `http2.connect()` options
'maxDeflateDynamicTableSize',
'maxSettings',
'maxSessionMemory',
'maxHeaderListPairs',
'maxOutstandingPings',
'maxReservedRemoteStreams',
'maxSendHeaderBlockLength',
'paddingStrategy',
'peerMaxConcurrentStreams',
'settings',
// `tls.connect()` source options
'family',
'localAddress',
'rejectUnauthorized',
// `tls.connect()` secure context options
'pskCallback',
'minDHSize',
// `tls.connect()` destination options
// - `servername` is automatically validated, skip it
// - `host` and `port` just describe the destination server,
'path',
'socket',
// `tls.createSecureContext()` options
'ca',
'cert',
'sigalgs',
'ciphers',
'clientCertEngine',
'crl',
'dhparam',
'ecdhCurve',
'honorCipherOrder',
'key',
'privateKeyEngine',
'privateKeyIdentifier',
'maxVersion',
'minVersion',
'pfx',
'secureOptions',
'secureProtocol',
'sessionIdContext',
'ticketKeys'
];
const getSortedIndex = (array, value, compare) => {
let low = 0;
let high = array.length;
while (low < high) {
const mid = (low + high) >>> 1;
if (compare(array[mid], value)) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
};
const compareSessions = (a, b) => 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 (let index = 0; index < where.length; index++) {
const coveredSession = where[index];
if (
// Unfortunately `.every()` returns true for an empty array
coveredSession[kOriginSet].length > 0
// 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[kCurrentStreamCount] + session[kCurrentStreamCount]) <= 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 (let index = 0; index < where.length; index++) {
const session = where[index];
if (
coveredSession[kOriginSet].length > 0
&& coveredSession[kOriginSet].length < session[kOriginSet].length
&& coveredSession[kOriginSet].every(origin => session[kOriginSet].includes(origin))
&& (coveredSession[kCurrentStreamCount] + session[kCurrentStreamCount]) <= session.remoteSettings.maxConcurrentStreams
) {
gracefullyClose(coveredSession);
return true;
}
}
return false;
};
const gracefullyClose = session => {
session[kGracefullyClosing] = true;
if (session[kCurrentStreamCount] === 0) {
session.close();
}
};
class Agent extends EventEmitter {
constructor({timeout = 0, maxSessions = Number.POSITIVE_INFINITY, maxEmptySessions = 10, maxCachedTlsSessions = 100} = {}) {
super();
// SESSIONS[NORMALIZED_OPTIONS] = [];
this.sessions = {};
// The queue for creating new sessions. It looks like this:
// QUEUE[NORMALIZED_OPTIONS][NORMALIZED_ORIGIN] = ENTRY_FUNCTION
//
// It's faster when there are many origins. If there's only one, then QUEUE[`${options}:${origin}`] is faster.
// I guess object creation / deletion is causing the slowdown.
//
// 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 empty sessions in total
this.maxEmptySessions = maxEmptySessions;
this._emptySessionCount = 0;
this._sessionCount = 0;
// We don't support push streams by default.
this.settings = {
enablePush: false,
initialWindowSize: 1024 * 1024 * 32 // 32MB, see https://github.com/nodejs/node/issues/38426
};
// Reusing TLS sessions increases performance.
this.tlsSessionCache = new QuickLRU({maxSize: maxCachedTlsSessions});
}
get protocol() {
return 'https:';
}
normalizeOptions(options) {
let normalized = '';
for (let index = 0; index < nameKeys.length; index++) {
const key = nameKeys[index];
normalized += ':';
if (options && options[key] !== undefined) {
normalized += options[key];
}
}
return normalized;
}
_processQueue() {
if (this._sessionCount >= this.maxSessions) {
this.closeEmptySessions(this.maxSessions - this._sessionCount + 1);
return;
}
// eslint-disable-next-line guard-for-in
for (const normalizedOptions in this.queue) {
// eslint-disable-next-line guard-for-in
for (const normalizedOrigin in this.queue[normalizedOptions]) {
const item = this.queue[normalizedOptions][normalizedOrigin];
// The entry function can be run only once.
if (!item.completed) {
item.completed = true;
item();
}
}
}
}
_isBetterSession(thisStreamCount, thatStreamCount) {
return thisStreamCount > thatStreamCount;
}
_accept(session, listeners, normalizedOrigin, options) {
let index = 0;
while (index < listeners.length && session[kCurrentStreamCount] < session.remoteSettings.maxConcurrentStreams) {
// We assume `resolve(...)` calls `request(...)` *directly*,
// otherwise the session will get overloaded.
listeners[index].resolve(session);
index++;
}
listeners.splice(0, index);
if (listeners.length > 0) {
this.getSession(normalizedOrigin, options, listeners);
listeners.length = 0;
}
}
getSession(origin, options, listeners) {
return new Promise((resolve, reject) => {
if (Array.isArray(listeners) && listeners.length > 0) {
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}];
}
try {
// Parse origin
if (typeof origin === 'string') {
origin = new URL(origin);
} else if (!(origin instanceof URL)) {
throw new TypeError('The `origin` argument needs to be a string or an URL object');
}
if (options) {
// Validate servername
const {servername} = options;
const {hostname} = origin;
if (servername && hostname !== servername) {
throw new Error(`Origin ${hostname} differs from servername ${servername}`);
}
}
} catch (error) {
for (let index = 0; index < listeners.length; index++) {
listeners[index].reject(error);
}
return;
}
const normalizedOptions = this.normalizeOptions(options);
const normalizedOrigin = origin.origin;
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.
//
// |------------| |------------| |------------| |------------|
// | Session: A | | Session: B | | Session: C | | Session: D |
// | Pending: 5 |-| Pending: 8 |-| Pending: 9 |-| Pending: 4 |
// | Max: 10 | | Max: 10 | | Max: 9 | | Max: 5 |
// |------------| |------------| |------------| |------------|
// ^
// |
// pick this one --
//
for (let index = 0; index < sessions.length; index++) {
const session = sessions[index];
const sessionMaxConcurrentStreams = session.remoteSettings.maxConcurrentStreams;
if (sessionMaxConcurrentStreams < maxConcurrentStreams) {
break;
}
if (!session[kOriginSet].includes(normalizedOrigin)) {
continue;
}
const sessionCurrentStreamsCount = session[kCurrentStreamCount];
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;
}
// Either get the session which has biggest current stream count or the lowest.
if (this._isBetterSession(sessionCurrentStreamsCount, currentStreamsCount)) {
optimalSession = session;
currentStreamsCount = sessionCurrentStreamsCount;
}
}
if (optimalSession) {
this._accept(optimalSession, listeners, normalizedOrigin, options);
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);
return;
}
} else {
this.queue[normalizedOptions] = {
[kLength]: 0
};
}
// 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 (--this.queue[normalizedOptions][kLength] === 0) {
delete this.queue[normalizedOptions];
}
}
};
// The main logic is here
const entry = async () => {
this._sessionCount++;
const name = `${normalizedOrigin}:${normalizedOptions}`;
let receivedSettings = false;
let socket;
try {
const computedOptions = {...options};
if (computedOptions.settings === undefined) {
computedOptions.settings = this.settings;
}
if (computedOptions.session === undefined) {
computedOptions.session = this.tlsSessionCache.get(name);
}
const createConnection = computedOptions.createConnection || this.createConnection;
// A hacky workaround to enable async `createConnection`
socket = await createConnection.call(this, origin, computedOptions);
computedOptions.createConnection = () => socket;
const session = http2.connect(origin, computedOptions);
session[kCurrentStreamCount] = 0;
session[kGracefullyClosing] = false;
// Node.js return https://false:443 instead of https://1.1.1.1:443
const getOriginSet = () => {
const {socket} = session;
let originSet;
if (socket.servername === false) {
socket.servername = socket.remoteAddress;
originSet = session.originSet;
socket.servername = false;
} else {
originSet = session.originSet;
}
return originSet;
};
const isFree = () => session[kCurrentStreamCount] < session.remoteSettings.maxConcurrentStreams;
session.socket.once('session', tlsSession => {
this.tlsSessionCache.set(name, tlsSession);
});
session.once('error', error => {
// Listeners are empty when the session successfully connected.
for (let index = 0; index < listeners.length; index++) {
listeners[index].reject(error);
}
// The connection got broken, purge the cache.
this.tlsSessionCache.delete(name);
});
session.setTimeout(this.timeout, () => {
// Terminates all streams owned by this session.
session.destroy();
});
session.once('close', () => {
this._sessionCount--;
if (receivedSettings) {
// Assumes session `close` is emitted after request `close`
this._emptySessionCount--;
// 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];
if (where.length === 1) {
delete this.sessions[normalizedOptions];
} else {
where.splice(where.indexOf(session), 1);
}
} else {
// Broken connection
removeFromQueue();
const error = new Error('Session closed without receiving a SETTINGS frame');
error.code = 'HTTP2WRAPPER_NOSETTINGS';
for (let index = 0; index < listeners.length; index++) {
listeners[index].reject(error);
}
}
// There may be another session awaiting.
this._processQueue();
});
// Iterates over the queue and processes listeners.
const processListeners = () => {
const queue = this.queue[normalizedOptions];
if (!queue) {
return;
}
const originSet = session[kOriginSet];
for (let index = 0; index < originSet.length; index++) {
const origin = originSet[index];
if (origin in queue) {
const {listeners, completed} = queue[origin];
let index = 0;
// Prevents session overloading.
while (index < listeners.length && isFree()) {
// We assume `resolve(...)` calls `request(...)` *directly*,
// otherwise the session will get overloaded.
listeners[index].resolve(session);
index++;
}
queue[origin].listeners.splice(0, index);
if (queue[origin].listeners.length === 0 && !completed) {
delete queue[origin];
if (--queue[kLength] === 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] = getOriginSet() || [];
session[kGracefullyClosing] = false;
closeSessionIfCovered(this.sessions[normalizedOptions], session);
if (session[kGracefullyClosing] || !isFree()) {
return;
}
processListeners();
if (!isFree()) {
return;
}
// Close covered sessions (if possible).
closeCoveredSessions(this.sessions[normalizedOptions], session);
});
session.once('remoteSettings', () => {
// The Agent could have been destroyed already.
if (entry.destroyed) {
const error = new Error('Agent has been destroyed');
for (let index = 0; index < listeners.length; index++) {
listeners[index].reject(error);
}
session.destroy();
return;
}
// See https://github.com/nodejs/node/issues/38426
if (session.setLocalWindowSize) {
session.setLocalWindowSize(1024 * 1024 * 4); // 4 MB
}
session[kOriginSet] = getOriginSet() || [];
if (session.socket.encrypted) {
const mainOrigin = session[kOriginSet][0];
if (mainOrigin !== normalizedOrigin) {
const error = new Error(`Requested origin ${normalizedOrigin} does not match server ${mainOrigin}`);
for (let index = 0; index < listeners.length; index++) {
listeners[index].reject(error);
}
session.destroy();
return;
}
}
removeFromQueue();
{
const where = this.sessions;
if (normalizedOptions in where) {
const sessions = where[normalizedOptions];
sessions.splice(getSortedIndex(sessions, session, compareSessions), 0, session);
} else {
where[normalizedOptions] = [session];
}
}
receivedSettings = true;
this._emptySessionCount++;
this.emit('session', session);
this._accept(session, listeners, normalizedOrigin, options);
if (session[kCurrentStreamCount] === 0 && this._emptySessionCount > this.maxEmptySessions) {
this.closeEmptySessions(this._emptySessionCount - this.maxEmptySessions);
}
// `session.remoteSettings.maxConcurrentStreams` might get increased
session.on('remoteSettings', () => {
if (!isFree()) {
return;
}
processListeners();
if (!isFree()) {
return;
}
// 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();
if (session[kCurrentStreamCount]++ === 0) {
this._emptySessionCount--;
}
stream.once('close', () => {
if (--session[kCurrentStreamCount] === 0) {
this._emptySessionCount++;
session.unref();
if (this._emptySessionCount > this.maxEmptySessions || session[kGracefullyClosing]) {
session.close();
return;
}
}
if (session.destroyed || session.closed) {
return;
}
if (isFree() && !closeSessionIfCovered(this.sessions[normalizedOptions], session)) {
closeCoveredSessions(this.sessions[normalizedOptions], session);
processListeners();
if (session[kCurrentStreamCount] === 0) {
this._processQueue();
}
}
});
return stream;
};
} catch (error) {
removeFromQueue();
this._sessionCount--;
for (let index = 0; index < listeners.length; index++) {
listeners[index].reject(error);
}
}
};
entry.listeners = listeners;
entry.completed = false;
entry.destroyed = false;
this.queue[normalizedOptions][normalizedOrigin] = entry;
this.queue[normalizedOptions][kLength]++;
this._processQueue();
});
}
request(origin, options, headers, streamOptions) {
return new Promise((resolve, reject) => {
this.getSession(origin, options, [{
reject,
resolve: session => {
try {
const stream = session.request(headers, streamOptions);
// Do not throw before `request(...)` has been awaited
delayAsyncDestroy(stream);
resolve(stream);
} catch (error) {
reject(error);
}
}
}]);
});
}
async createConnection(origin, options) {
return Agent.connect(origin, options);
}
static connect(origin, options) {
options.ALPNProtocols = ['h2'];
const port = origin.port || 443;
const host = origin.hostname;
if (typeof options.servername === 'undefined') {
options.servername = host;
}
const socket = tls.connect(port, host, options);
if (options.socket) {
socket._peername = {
family: undefined,
address: undefined,
port
};
}
return socket;
}
closeEmptySessions(maxCount = Number.POSITIVE_INFINITY) {
let closedCount = 0;
const {sessions} = this;
// eslint-disable-next-line guard-for-in
for (const key in sessions) {
const thisSessions = sessions[key];
for (let index = 0; index < thisSessions.length; index++) {
const session = thisSessions[index];
if (session[kCurrentStreamCount] === 0) {
closedCount++;
session.close();
if (closedCount >= maxCount) {
return closedCount;
}
}
}
}
return closedCount;
}
destroy(reason) {
const {sessions, queue} = this;
// eslint-disable-next-line guard-for-in
for (const key in sessions) {
const thisSessions = sessions[key];
for (let index = 0; index < thisSessions.length; index++) {
thisSessions[index].destroy(reason);
}
}
// eslint-disable-next-line guard-for-in
for (const normalizedOptions in queue) {
const entries = queue[normalizedOptions];
// eslint-disable-next-line guard-for-in
for (const normalizedOrigin in entries) {
entries[normalizedOrigin].destroyed = true;
}
}
// New requests should NOT attach to destroyed sessions
this.queue = {};
this.tlsSessionCache.clear();
}
get emptySessionCount() {
return this._emptySessionCount;
}
get pendingSessionCount() {
return this._sessionCount - this._emptySessionCount;
}
get sessionCount() {
return this._sessionCount;
}
}
Agent.kCurrentStreamCount = kCurrentStreamCount;
Agent.kGracefullyClosing = kGracefullyClosing;
module.exports = {
Agent,
globalAgent: new Agent()
};

225
book/node_modules/http2-wrapper/source/auto.js generated vendored Normal file
View File

@ -0,0 +1,225 @@
'use strict';
// See https://github.com/facebook/jest/issues/2549
// eslint-disable-next-line node/prefer-global/url
const {URL, urlToHttpOptions} = require('url');
const http = require('http');
const https = require('https');
const resolveALPN = require('resolve-alpn');
const QuickLRU = require('quick-lru');
const {Agent, globalAgent} = require('./agent.js');
const Http2ClientRequest = require('./client-request.js');
const calculateServerName = require('./utils/calculate-server-name.js');
const delayAsyncDestroy = require('./utils/delay-async-destroy.js');
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 onTimeout = () => {
const {freeSockets} = agent;
for (const sockets of Object.values(freeSockets)) {
if (sockets.includes(socket)) {
socket.destroy();
return;
}
}
};
socket.on('timeout', onTimeout);
const onRemove = () => {
agent.removeSocket(socket, options);
socket.off('close', onClose);
socket.off('free', onFree);
socket.off('timeout', onTimeout);
socket.off('agentRemove', onRemove);
};
socket.on('agentRemove', onRemove);
agent.emit('free', socket, options);
};
const createResolveProtocol = (cache, queue = new Map(), connect = undefined) => {
return 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 {alpnProtocol: result.alpnProtocol};
}
const {path} = options;
options.path = options.socketPath;
const resultPromise = resolveALPN(options, connect);
queue.set(name, resultPromise);
try {
const result = await resultPromise;
cache.set(name, result.alpnProtocol);
queue.delete(name);
options.path = path;
return result;
} catch (error) {
queue.delete(name);
options.path = path;
throw error;
}
}
return {alpnProtocol: cache.get(name)};
};
};
const defaultResolveProtocol = createResolveProtocol(cache, queue);
module.exports = async (input, options, callback) => {
if (typeof input === 'string') {
input = urlToHttpOptions(new URL(input));
} else if (input instanceof URL) {
input = urlToHttpOptions(input);
} else {
input = {...input};
}
if (typeof options === 'function' || options === undefined) {
// (options, callback)
callback = options;
options = input;
} else {
// (input, options, callback)
options = Object.assign(input, options);
}
options.ALPNProtocols = options.ALPNProtocols || ['h2', 'http/1.1'];
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.headers && options.headers.host) || options.host);
options.port = options.port || (isHttps ? 443 : 80);
options._defaultAgent = isHttps ? https.globalAgent : http.globalAgent;
const resolveProtocol = options.resolveProtocol || defaultResolveProtocol;
// Note: We don't support `h2session` here
let {agent} = options;
if (agent !== undefined && agent !== false && agent.constructor.name !== 'Object') {
throw new Error('The `options.agent` can be only an object `http`, `https` or `http2` properties');
}
if (isHttps) {
options.resolveSocket = true;
let {socket, alpnProtocol, timeout} = await resolveProtocol(options);
if (timeout) {
if (socket) {
socket.destroy();
}
const error = new Error(`Timed out resolving ALPN: ${options.timeout} ms`);
error.code = 'ETIMEDOUT';
error.ms = options.timeout;
throw error;
}
// We can't accept custom `createConnection` because the API is different for HTTP/2
if (socket && options.createConnection) {
socket.destroy();
socket = undefined;
}
delete options.resolveSocket;
const isHttp2 = alpnProtocol === 'h2';
if (agent) {
agent = isHttp2 ? agent.http2 : agent.https;
options.agent = agent;
}
if (agent === undefined) {
agent = isHttp2 ? globalAgent : https.globalAgent;
}
if (socket) {
if (agent === false) {
socket.destroy();
} else {
const defaultCreateConnection = (isHttp2 ? Agent : https.Agent).prototype.createConnection;
if (agent.createConnection === defaultCreateConnection) {
if (isHttp2) {
options._reuseSocket = socket;
} else {
installSocket(agent, socket, options);
}
} else {
socket.destroy();
}
}
}
if (isHttp2) {
return delayAsyncDestroy(new Http2ClientRequest(options, callback));
}
} else if (agent) {
options.agent = agent.http;
}
// If we're sending HTTP/1.1, handle any explicitly set H2 headers in the options:
if (options.headers) {
options.headers = {...options.headers};
// :authority is equivalent to the HTTP/1.1 host header
if (options.headers[':authority']) {
if (!options.headers.host) {
options.headers.host = options.headers[':authority'];
}
delete options.headers[':authority'];
}
// Remove other HTTP/2 headers as they have their counterparts in the options
delete options.headers[':method'];
delete options.headers[':scheme'];
delete options.headers[':path'];
}
return delayAsyncDestroy(http.request(options, callback));
};
module.exports.protocolCache = cache;
module.exports.resolveProtocol = defaultResolveProtocol;
module.exports.createResolveProtocol = createResolveProtocol;

View File

@ -0,0 +1,563 @@
'use strict';
// See https://github.com/facebook/jest/issues/2549
// eslint-disable-next-line node/prefer-global/url
const {URL, urlToHttpOptions} = require('url');
const http2 = require('http2');
const {Writable} = require('stream');
const {Agent, globalAgent} = require('./agent.js');
const IncomingMessage = require('./incoming-message.js');
const proxyEvents = require('./utils/proxy-events.js');
const {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_PROTOCOL,
ERR_HTTP_HEADERS_SENT
} = require('./utils/errors.js');
const validateHeaderName = require('./utils/validate-header-name.js');
const validateHeaderValue = require('./utils/validate-header-value.js');
const proxySocketHandler = require('./utils/proxy-socket-handler.js');
const {
HTTP2_HEADER_STATUS,
HTTP2_HEADER_METHOD,
HTTP2_HEADER_PATH,
HTTP2_HEADER_AUTHORITY,
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 kPendingAgentPromise = Symbol('pendingAgentPromise');
class ClientRequest extends Writable {
constructor(input, options, callback) {
super({
autoDestroy: false,
emitClose: false
});
if (typeof input === 'string') {
input = urlToHttpOptions(new URL(input));
} else if (input instanceof URL) {
input = urlToHttpOptions(input);
} else {
input = {...input};
}
if (typeof options === 'function' || options === undefined) {
// (options, callback)
callback = options;
options = input;
} else {
// (input, options, callback)
options = Object.assign(input, options);
}
if (options.h2session) {
this[kSession] = options.h2session;
if (this[kSession].destroyed) {
throw new Error('The session has been closed already');
}
this.protocol = this[kSession].socket.encrypted ? 'https:' : 'http:';
} else if (options.agent === false) {
this.agent = new Agent({maxEmptySessions: 0});
} else if (typeof options.agent === 'undefined' || options.agent === null) {
this.agent = globalAgent;
} else if (typeof options.agent.request === 'function') {
this.agent = options.agent;
} else {
throw new ERR_INVALID_ARG_TYPE('options.agent', ['http2wrapper.Agent-like Object', 'undefined', 'false'], options.agent);
}
if (this.agent) {
this.protocol = this.agent.protocol;
}
if (options.protocol && options.protocol !== this.protocol) {
throw new ERR_INVALID_PROTOCOL(options.protocol, this.protocol);
}
if (!options.port) {
options.port = options.defaultPort || (this.agent && this.agent.defaultPort) || 443;
}
options.host = options.hostname || options.host || 'localhost';
// Unused
delete options.hostname;
const {timeout} = options;
options.timeout = undefined;
this[kHeaders] = Object.create(null);
this[kJobs] = [];
this[kPendingAgentPromise] = undefined;
this.socket = null;
this.connection = null;
this.method = options.method || 'GET';
if (!(this.method === 'CONNECT' && (options.path === '/' || options.path === undefined))) {
this.path = options.path;
}
this.res = null;
this.aborted = false;
this.reusedSocket = false;
const {headers} = options;
if (headers) {
// eslint-disable-next-line guard-for-in
for (const header in headers) {
this.setHeader(header, headers[header]);
}
}
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.
this[kOrigin] = new URL(`${this.protocol}//${options.servername || options.host}:${options.port}`);
// A socket is being reused
const reuseSocket = options._reuseSocket;
if (reuseSocket) {
options.createConnection = (...args) => {
if (reuseSocket.destroyed) {
return this.agent.createConnection(...args);
}
return reuseSocket;
};
// eslint-disable-next-line promise/prefer-await-to-then
this.agent.getSession(this[kOrigin], this[kOptions]).catch(() => {});
}
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() {
const header = this.method === 'CONNECT' ? HTTP2_HEADER_AUTHORITY : HTTP2_HEADER_PATH;
return this[kHeaders][header];
}
set path(value) {
if (value) {
const header = this.method === 'CONNECT' ? HTTP2_HEADER_AUTHORITY : HTTP2_HEADER_PATH;
this[kHeaders][header] = value;
}
}
get host() {
return this[kOrigin].hostname;
}
set host(_value) {
// Do nothing as this is read only.
}
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) {
this.flushHeaders();
const callEnd = () => {
// For GET, HEAD and DELETE and CONNECT
if (this._mustNotHaveABody || this.method === 'CONNECT') {
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();
}
async _destroy(error, callback) {
if (this.res) {
this.res._dump();
}
if (this._request) {
this._request.destroy();
} else {
process.nextTick(() => {
this.emit('close');
});
}
try {
await this[kPendingAgentPromise];
} catch (internalError) {
if (this.aborted) {
error = internalError;
}
}
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) {
// TODO: Should we proxy `close` here?
proxyEvents(stream, this, ['timeout', 'continue']);
}
stream.once('error', error => {
this.destroy(error);
});
stream.once('aborted', () => {
const {res} = this;
if (res) {
res.aborted = true;
res.emit('aborted');
res.destroy();
} else {
this.destroy(new Error('The server aborted the HTTP/2 stream'));
}
});
const onResponse = (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;
// Undocumented, but it is used by `cacheable-request`
response.url = `${this[kOrigin].origin}${this.path}`;
response.req = this;
response.statusCode = headers[HTTP2_HEADER_STATUS];
response.headers = headers;
response.rawHeaders = rawHeaders;
response.once('end', () => {
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', () => {
if (!this.aborted) {
response.push(null);
}
});
if (!this.emit('response', response)) {
// No listeners attached, dump the response.
response._dump();
}
}
};
// This event tells we are ready to listen for the data.
stream.once('response', onResponse);
// Emits `information` event
stream.once('headers', headers => this.emit('information', {statusCode: headers[HTTP2_HEADER_STATUS]}));
stream.once('trailers', (trailers, flags, rawTrailers) => {
const {res} = this;
// https://github.com/nodejs/node/issues/41251
if (res === null) {
onResponse(trailers, flags, rawTrailers);
return;
}
// Assigns trailers to the response object.
res.trailers = trailers;
res.rawTrailers = rawTrailers;
});
stream.once('close', () => {
const {aborted, res} = this;
if (res) {
if (aborted) {
res.aborted = true;
res.emit('aborted');
res.destroy();
}
const finish = () => {
res.emit('close');
this.destroy();
this.emit('close');
};
if (res.readable) {
res.once('end', finish);
} else {
finish();
}
return;
}
if (!this.destroyed) {
this.destroy(new Error('The HTTP/2 stream has been early terminated'));
this.emit('close');
return;
}
this.destroy();
this.emit('close');
});
this.socket = new Proxy(stream, proxySocketHandler);
for (const job of this[kJobs]) {
job();
}
this[kJobs].length = 0;
this.emit('socket', this.socket);
};
if (!(HTTP2_HEADER_AUTHORITY in this[kHeaders]) && !isConnectMethod) {
this[kHeaders][HTTP2_HEADER_AUTHORITY] = this[kOrigin].host;
}
// Makes a HTTP2 request
if (this[kSession]) {
try {
onStream(this[kSession].request(this[kHeaders]));
} catch (error) {
this.destroy(error);
}
} else {
this.reusedSocket = true;
try {
const promise = this.agent.request(this[kOrigin], this[kOptions], this[kHeaders]);
this[kPendingAgentPromise] = promise;
onStream(await promise);
this[kPendingAgentPromise] = false;
} catch (error) {
this[kPendingAgentPromise] = false;
this.destroy(error);
}
}
}
get connection() {
return this.socket;
}
set connection(value) {
this.socket = value;
}
getHeaderNames() {
return Object.keys(this[kHeaders]);
}
hasHeader(name) {
if (typeof name !== 'string') {
throw new ERR_INVALID_ARG_TYPE('name', 'string', name);
}
return Boolean(this[kHeaders][name.toLowerCase()]);
}
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');
}
validateHeaderName(name);
validateHeaderValue(name, value);
const lowercased = name.toLowerCase();
if (lowercased === 'connection') {
if (value.toLowerCase() === 'keep-alive') {
return;
}
throw new Error(`Invalid 'connection' header: ${value}`);
}
if (lowercased === 'host' && this.method === 'CONNECT') {
this[kHeaders][HTTP2_HEADER_AUTHORITY] = value;
} else {
this[kHeaders][lowercased] = 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;

View File

@ -0,0 +1,73 @@
'use strict';
const {Readable} = require('stream');
class IncomingMessage extends Readable {
constructor(socket, highWaterMark) {
super({
emitClose: false,
autoDestroy: true,
highWaterMark
});
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._dumped = false;
}
get connection() {
return this.socket;
}
set connection(value) {
this.socket = value;
}
_destroy(error, callback) {
if (!this.readableEnded) {
this.aborted = true;
}
// See https://github.com/nodejs/node/issues/35303
callback();
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;

50
book/node_modules/http2-wrapper/source/index.js generated vendored Normal file
View File

@ -0,0 +1,50 @@
'use strict';
const http2 = require('http2');
const {
Agent,
globalAgent
} = require('./agent.js');
const ClientRequest = require('./client-request.js');
const IncomingMessage = require('./incoming-message.js');
const auto = require('./auto.js');
const {
HttpOverHttp2,
HttpsOverHttp2
} = require('./proxies/h1-over-h2.js');
const Http2OverHttp2 = require('./proxies/h2-over-h2.js');
const {
Http2OverHttp,
Http2OverHttps
} = require('./proxies/h2-over-h1.js');
const validateHeaderName = require('./utils/validate-header-name.js');
const validateHeaderValue = require('./utils/validate-header-value.js');
const request = (url, options, callback) => 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,
globalAgent,
request,
get,
auto,
proxies: {
HttpOverHttp2,
HttpsOverHttp2,
Http2OverHttp2,
Http2OverHttp,
Http2OverHttps
},
validateHeaderName,
validateHeaderValue
};

View File

@ -0,0 +1,17 @@
'use strict';
module.exports = self => {
const {username, password} = self.proxyOptions.url;
if (username || password) {
const data = `${username}:${password}`;
const authorization = `Basic ${Buffer.from(data).toString('base64')}`;
return {
'proxy-authorization': authorization,
authorization
};
}
return {};
};

View File

@ -0,0 +1,90 @@
'use strict';
const tls = require('tls');
const http = require('http');
const https = require('https');
const JSStreamSocket = require('../utils/js-stream-socket.js');
const {globalAgent} = require('../agent.js');
const UnexpectedStatusCodeError = require('./unexpected-status-code-error.js');
const initialize = require('./initialize.js');
const getAuthorizationHeaders = require('./get-auth-headers.js');
const createConnection = (self, options, callback) => {
(async () => {
try {
const {proxyOptions} = self;
const {url, headers, raw} = proxyOptions;
const stream = await globalAgent.request(url, proxyOptions, {
...getAuthorizationHeaders(self),
...headers,
':method': 'CONNECT',
':authority': `${options.host}:${options.port}`
});
stream.once('error', callback);
stream.once('response', headers => {
const statusCode = headers[':status'];
if (statusCode !== 200) {
callback(new UnexpectedStatusCodeError(statusCode, ''));
return;
}
const encrypted = self instanceof https.Agent;
if (raw && encrypted) {
options.socket = stream;
const secureStream = tls.connect(options);
secureStream.once('close', () => {
stream.destroy();
});
callback(null, secureStream);
return;
}
const socket = new JSStreamSocket(stream);
socket.encrypted = false;
socket._handle.getpeername = out => {
out.family = undefined;
out.address = undefined;
out.port = undefined;
};
callback(null, socket);
});
} catch (error) {
callback(error);
}
})();
};
class HttpOverHttp2 extends http.Agent {
constructor(options) {
super(options);
initialize(this, options.proxyOptions);
}
createConnection(options, callback) {
createConnection(this, options, callback);
}
}
class HttpsOverHttp2 extends https.Agent {
constructor(options) {
super(options);
initialize(this, options.proxyOptions);
}
createConnection(options, callback) {
createConnection(this, options, callback);
}
}
module.exports = {
HttpOverHttp2,
HttpsOverHttp2
};

View File

@ -0,0 +1,48 @@
'use strict';
const http = require('http');
const https = require('https');
const Http2OverHttpX = require('./h2-over-hx.js');
const getAuthorizationHeaders = require('./get-auth-headers.js');
const getStream = request => new Promise((resolve, reject) => {
const onConnect = (response, socket, head) => {
socket.unshift(head);
request.off('error', reject);
resolve([socket, response.statusCode, response.statusMessage]);
};
request.once('error', reject);
request.once('connect', onConnect);
});
class Http2OverHttp extends Http2OverHttpX {
async _getProxyStream(authority) {
const {proxyOptions} = this;
const {url, headers} = this.proxyOptions;
const network = url.protocol === 'https:' ? https : http;
// `new URL('https://localhost/httpbin.org:443')` results in
// a `/httpbin.org:443` path, which has an invalid leading slash.
const request = network.request({
...proxyOptions,
hostname: url.hostname,
port: url.port,
path: authority,
headers: {
...getAuthorizationHeaders(this),
...headers,
host: authority
},
method: 'CONNECT'
}).end();
return getStream(request);
}
}
module.exports = {
Http2OverHttp,
Http2OverHttps: Http2OverHttp
};

View File

@ -0,0 +1,32 @@
'use strict';
const {globalAgent} = require('../agent.js');
const Http2OverHttpX = require('./h2-over-hx.js');
const getAuthorizationHeaders = require('./get-auth-headers.js');
const getStatusCode = stream => new Promise((resolve, reject) => {
stream.once('error', reject);
stream.once('response', headers => {
stream.off('error', reject);
resolve(headers[':status']);
});
});
class Http2OverHttp2 extends Http2OverHttpX {
async _getProxyStream(authority) {
const {proxyOptions} = this;
const headers = {
...getAuthorizationHeaders(this),
...proxyOptions.headers,
':method': 'CONNECT',
':authority': authority
};
const stream = await globalAgent.request(proxyOptions.url, proxyOptions, headers);
const statusCode = await getStatusCode(stream);
return [stream, statusCode, ''];
}
}
module.exports = Http2OverHttp2;

View File

@ -0,0 +1,40 @@
'use strict';
const {Agent} = require('../agent.js');
const JSStreamSocket = require('../utils/js-stream-socket.js');
const UnexpectedStatusCodeError = require('./unexpected-status-code-error.js');
const initialize = require('./initialize.js');
class Http2OverHttpX extends Agent {
constructor(options) {
super(options);
initialize(this, options.proxyOptions);
}
async createConnection(origin, options) {
const authority = `${origin.hostname}:${origin.port || 443}`;
const [stream, statusCode, statusMessage] = await this._getProxyStream(authority);
if (statusCode !== 200) {
throw new UnexpectedStatusCodeError(statusCode, statusMessage);
}
if (this.proxyOptions.raw) {
options.socket = stream;
} else {
const socket = new JSStreamSocket(stream);
socket.encrypted = false;
socket._handle.getpeername = out => {
out.family = undefined;
out.address = undefined;
out.port = undefined;
};
return socket;
}
return super.createConnection(origin, options);
}
}
module.exports = Http2OverHttpX;

View File

@ -0,0 +1,21 @@
'use strict';
// See https://github.com/facebook/jest/issues/2549
// eslint-disable-next-line node/prefer-global/url
const {URL} = require('url');
const checkType = require('../utils/check-type.js');
module.exports = (self, proxyOptions) => {
checkType('proxyOptions', proxyOptions, ['object']);
checkType('proxyOptions.headers', proxyOptions.headers, ['object', 'undefined']);
checkType('proxyOptions.raw', proxyOptions.raw, ['boolean', 'undefined']);
checkType('proxyOptions.url', proxyOptions.url, [URL, 'string']);
const url = new URL(proxyOptions.url);
self.proxyOptions = {
raw: true,
...proxyOptions,
headers: {...proxyOptions.headers},
url
};
};

View File

@ -0,0 +1,11 @@
'use strict';
class UnexpectedStatusCodeError extends Error {
constructor(statusCode, statusMessage = '') {
super(`The proxy server rejected the request with status code ${statusCode} (${statusMessage || 'empty status message'})`);
this.statusCode = statusCode;
this.statusMessage = statusMessage;
}
}
module.exports = UnexpectedStatusCodeError;

View File

@ -0,0 +1,29 @@
'use strict';
const {isIP} = require('net');
const assert = require('assert');
const getHost = host => {
if (host[0] === '[') {
const idx = host.indexOf(']');
assert(idx !== -1);
return host.slice(1, idx);
}
const idx = host.indexOf(':');
if (idx === -1) {
return host;
}
return host.slice(0, idx);
};
module.exports = host => {
const servername = getHost(host);
if (isIP(servername)) {
return '';
}
return servername;
};

View File

@ -0,0 +1,20 @@
'use strict';
const checkType = (name, value, types) => {
const valid = types.some(type => {
const typeofType = typeof type;
if (typeofType === 'string') {
return typeof value === type;
}
return value instanceof type;
});
if (!valid) {
const names = types.map(type => typeof type === 'string' ? type : type.name);
throw new TypeError(`Expected '${name}' to be a type of ${names.join(' or ')}, got ${typeof value}`);
}
};
module.exports = checkType;

View File

@ -0,0 +1,33 @@
'use strict';
module.exports = stream => {
if (stream.listenerCount('error') !== 0) {
return stream;
}
stream.__destroy = stream._destroy;
stream._destroy = (...args) => {
const callback = args.pop();
stream.__destroy(...args, async error => {
await Promise.resolve();
callback(error);
});
};
const onError = error => {
// eslint-disable-next-line promise/prefer-await-to-then
Promise.resolve().then(() => {
stream.emit('error', error);
});
};
stream.once('error', onError);
// eslint-disable-next-line promise/prefer-await-to-then
Promise.resolve().then(() => {
stream.off('error', onError);
});
return stream;
};

51
book/node_modules/http2-wrapper/source/utils/errors.js generated vendored Normal file
View File

@ -0,0 +1,51 @@
'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 =>
`Protocol "${args[0]}" not supported. Expected "${args[1]}"`
);
makeError(Error, 'ERR_HTTP_HEADERS_SENT', args =>
`Cannot ${args[0]} headers after they are sent to the client`
);
makeError(TypeError, 'ERR_INVALID_HTTP_TOKEN', args =>
`${args[0]} must be a valid HTTP token [${args[1]}]`
);
makeError(TypeError, 'ERR_HTTP_INVALID_HEADER_VALUE', args =>
`Invalid value "${args[0]} for header "${args[1]}"`
);
makeError(TypeError, 'ERR_INVALID_CHAR', args =>
`Invalid character in ${args[0]} [${args[1]}]`
);
makeError(
Error,
'ERR_HTTP2_NO_SOCKET_MANIPULATION',
'HTTP/2 sockets should not be directly manipulated (e.g. read and written)'
);

View File

@ -0,0 +1,13 @@
'use strict';
module.exports = header => {
switch (header) {
case ':method':
case ':scheme':
case ':authority':
case ':path':
return true;
default:
return false;
}
};

View File

@ -0,0 +1,8 @@
'use strict';
const stream = require('stream');
const tls = require('tls');
// Really awesome hack.
const JSStreamSocket = (new tls.TLSSocket(new stream.PassThrough()))._handle._parentWrap.constructor;
module.exports = JSStreamSocket;

View File

@ -0,0 +1,7 @@
'use strict';
module.exports = (from, to, events) => {
for (const event of events) {
from.on(event, (...args) => to.emit(event, ...args));
}
};

View File

@ -0,0 +1,102 @@
'use strict';
const {ERR_HTTP2_NO_SOCKET_MANIPULATION} = require('./errors.js');
/* istanbul ignore file */
/* https://github.com/nodejs/node/blob/6eec858f34a40ffa489c1ec54bb24da72a28c781/lib/internal/http2/compat.js#L195-L272 */
const proxySocketHandler = {
has(stream, property) {
// Replaced [kSocket] with .socket
const reference = stream.session === undefined ? stream : stream.session.socket;
return (property in stream) || (property in reference);
},
get(stream, property) {
switch (property) {
case 'on':
case 'once':
case 'end':
case 'emit':
case 'destroy':
return stream[property].bind(stream);
case 'writable':
case 'destroyed':
return stream[property];
case 'readable':
if (stream.destroyed) {
return false;
}
return stream.readable;
case 'setTimeout': {
const {session} = stream;
if (session !== undefined) {
return session.setTimeout.bind(session);
}
return stream.setTimeout.bind(stream);
}
case 'write':
case 'read':
case 'pause':
case 'resume':
throw new ERR_HTTP2_NO_SOCKET_MANIPULATION();
default: {
// Replaced [kSocket] with .socket
const reference = stream.session === undefined ? stream : stream.session.socket;
const value = reference[property];
return typeof value === 'function' ? value.bind(reference) : value;
}
}
},
getPrototypeOf(stream) {
if (stream.session !== undefined) {
// Replaced [kSocket] with .socket
return Reflect.getPrototypeOf(stream.session.socket);
}
return Reflect.getPrototypeOf(stream);
},
set(stream, property, value) {
switch (property) {
case 'writable':
case 'readable':
case 'destroyed':
case 'on':
case 'once':
case 'end':
case 'emit':
case 'destroy':
stream[property] = value;
return true;
case 'setTimeout': {
const {session} = stream;
if (session === undefined) {
stream.setTimeout = value;
} else {
session.setTimeout = value;
}
return true;
}
case 'write':
case 'read':
case 'pause':
case 'resume':
throw new ERR_HTTP2_NO_SOCKET_MANIPULATION();
default: {
// Replaced [kSocket] with .socket
const reference = stream.session === undefined ? stream : stream.session.socket;
reference[property] = value;
return true;
}
}
}
};
module.exports = proxySocketHandler;

View File

@ -0,0 +1,11 @@
'use strict';
const {ERR_INVALID_HTTP_TOKEN} = require('./errors.js');
const isRequestPseudoHeader = require('./is-request-pseudo-header.js');
const isValidHttpToken = /^[\^`\-\w!#$%&*+.|~]+$/;
module.exports = name => {
if (typeof name !== 'string' || (!isValidHttpToken.test(name) && !isRequestPseudoHeader(name))) {
throw new ERR_INVALID_HTTP_TOKEN('Header name', name);
}
};

View File

@ -0,0 +1,17 @@
'use strict';
const {
ERR_HTTP_INVALID_HEADER_VALUE,
ERR_INVALID_CHAR
} = require('./errors.js');
const isInvalidHeaderValue = /[^\t\u0020-\u007E\u0080-\u00FF]/;
module.exports = (name, value) => {
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);
}
};