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