diff options
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.js | 563 |
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; |