summaryrefslogtreecommitdiff
path: root/includes/external/addressbook/node_modules/http2-wrapper/source/agent.js
diff options
context:
space:
mode:
Diffstat (limited to 'includes/external/addressbook/node_modules/http2-wrapper/source/agent.js')
-rw-r--r--includes/external/addressbook/node_modules/http2-wrapper/source/agent.js796
1 files changed, 796 insertions, 0 deletions
diff --git a/includes/external/addressbook/node_modules/http2-wrapper/source/agent.js b/includes/external/addressbook/node_modules/http2-wrapper/source/agent.js
new file mode 100644
index 0000000..922d202
--- /dev/null
+++ b/includes/external/addressbook/node_modules/http2-wrapper/source/agent.js
@@ -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()
+};