378 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			378 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| "use strict";
 | |
| /*
 | |
|  * Licensed to Elasticsearch B.V. under one or more contributor
 | |
|  * license agreements. See the NOTICE file distributed with
 | |
|  * this work for additional information regarding copyright
 | |
|  * ownership. Elasticsearch B.V. licenses this file to you under
 | |
|  * the Apache License, Version 2.0 (the "License"); you may
 | |
|  * not use this file except in compliance with the License.
 | |
|  * You may obtain a copy of the License at
 | |
|  *
 | |
|  *    http://www.apache.org/licenses/LICENSE-2.0
 | |
|  *
 | |
|  * Unless required by applicable law or agreed to in writing,
 | |
|  * software distributed under the License is distributed on an
 | |
|  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 | |
|  * KIND, either express or implied.  See the License for the
 | |
|  * specific language governing permissions and limitations
 | |
|  * under the License.
 | |
|  */
 | |
| Object.defineProperty(exports, "__esModule", { value: true });
 | |
| const tslib_1 = require("tslib");
 | |
| /* eslint-disable @typescript-eslint/restrict-template-expressions */
 | |
| const hpagent_1 = tslib_1.__importDefault(require("hpagent"));
 | |
| const http_1 = tslib_1.__importDefault(require("http"));
 | |
| const https_1 = tslib_1.__importDefault(require("https"));
 | |
| const debug_1 = tslib_1.__importDefault(require("debug"));
 | |
| const buffer_1 = tslib_1.__importDefault(require("buffer"));
 | |
| const BaseConnection_1 = tslib_1.__importStar(require("./BaseConnection"));
 | |
| const symbols_1 = require("../symbols");
 | |
| const stream_1 = require("stream");
 | |
| const errors_1 = require("../errors");
 | |
| const debug = (0, debug_1.default)('elasticsearch');
 | |
| const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
 | |
| const MAX_BUFFER_LENGTH = buffer_1.default.constants.MAX_LENGTH;
 | |
| const MAX_STRING_LENGTH = buffer_1.default.constants.MAX_STRING_LENGTH;
 | |
| const noop = () => { };
 | |
| class HttpConnection extends BaseConnection_1.default {
 | |
|     constructor(opts) {
 | |
|         super(opts);
 | |
|         Object.defineProperty(this, "agent", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: void 0
 | |
|         });
 | |
|         Object.defineProperty(this, "makeRequest", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: void 0
 | |
|         });
 | |
|         if (typeof opts.agent === 'function') {
 | |
|             this.agent = opts.agent(opts);
 | |
|         }
 | |
|         else if (typeof opts.agent === 'boolean') {
 | |
|             this.agent = undefined;
 | |
|         }
 | |
|         else {
 | |
|             if (opts.agent != null && !isHttpAgentOptions(opts.agent)) {
 | |
|                 throw new errors_1.ConfigurationError('Bad agent configuration for Http agent');
 | |
|             }
 | |
|             const agentOptions = Object.assign({}, {
 | |
|                 keepAlive: true,
 | |
|                 keepAliveMsecs: 1000,
 | |
|                 maxSockets: 256,
 | |
|                 maxFreeSockets: 256,
 | |
|                 scheduling: 'lifo'
 | |
|             }, opts.agent);
 | |
|             if (opts.proxy != null) {
 | |
|                 const proxyAgentOptions = {
 | |
|                     ...agentOptions,
 | |
|                     proxy: opts.proxy
 | |
|                 };
 | |
|                 this.agent = this.url.protocol === 'http:'
 | |
|                     ? new hpagent_1.default.HttpProxyAgent(proxyAgentOptions)
 | |
|                     : new hpagent_1.default.HttpsProxyAgent(Object.assign({}, proxyAgentOptions, this.tls));
 | |
|             }
 | |
|             else {
 | |
|                 this.agent = this.url.protocol === 'http:'
 | |
|                     ? new http_1.default.Agent(agentOptions)
 | |
|                     : new https_1.default.Agent(Object.assign({}, agentOptions, this.tls));
 | |
|             }
 | |
|         }
 | |
|         this.makeRequest = this.url.protocol === 'http:'
 | |
|             ? http_1.default.request
 | |
|             : https_1.default.request;
 | |
|     }
 | |
|     async request(params, options) {
 | |
|         return await new Promise((resolve, reject) => {
 | |
|             var _a, _b;
 | |
|             let cleanedListeners = false;
 | |
|             const maxResponseSize = (_a = options.maxResponseSize) !== null && _a !== void 0 ? _a : MAX_STRING_LENGTH;
 | |
|             const maxCompressedResponseSize = (_b = options.maxCompressedResponseSize) !== null && _b !== void 0 ? _b : MAX_BUFFER_LENGTH;
 | |
|             const requestParams = this.buildRequestObject(params, options);
 | |
|             // https://github.com/nodejs/node/commit/b961d9fd83
 | |
|             if (INVALID_PATH_REGEX.test(requestParams.path)) {
 | |
|                 return reject(new TypeError(`ERR_UNESCAPED_CHARACTERS: ${requestParams.path}`));
 | |
|             }
 | |
|             debug('Starting a new request', params);
 | |
|             let request;
 | |
|             try {
 | |
|                 request = this.makeRequest(requestParams);
 | |
|             }
 | |
|             catch (err) {
 | |
|                 return reject(err);
 | |
|             }
 | |
|             const abortListener = () => {
 | |
|                 request.abort();
 | |
|             };
 | |
|             this._openRequests++;
 | |
|             if (options.signal != null) {
 | |
|                 options.signal.addEventListener('abort', abortListener, { once: true });
 | |
|             }
 | |
|             const onResponse = (response) => {
 | |
|                 var _a, _b;
 | |
|                 cleanListeners();
 | |
|                 request.on('error', noop); // There are some edge cases where the request emits an error while processing the response.
 | |
|                 this._openRequests--;
 | |
|                 if (options.asStream === true) {
 | |
|                     return resolve({
 | |
|                         body: response,
 | |
|                         statusCode: response.statusCode,
 | |
|                         headers: response.headers
 | |
|                     });
 | |
|                 }
 | |
|                 const contentEncoding = ((_a = response.headers['content-encoding']) !== null && _a !== void 0 ? _a : '').toLowerCase();
 | |
|                 const isCompressed = contentEncoding.includes('gzip') || contentEncoding.includes('deflate');
 | |
|                 const isVectorTile = ((_b = response.headers['content-type']) !== null && _b !== void 0 ? _b : '').includes('application/vnd.mapbox-vector-tile');
 | |
|                 /* istanbul ignore else */
 | |
|                 if (response.headers['content-length'] !== undefined) {
 | |
|                     const contentLength = Number(response.headers['content-length']);
 | |
|                     if (isCompressed && contentLength > maxCompressedResponseSize) {
 | |
|                         response.destroy();
 | |
|                         return reject(new errors_1.RequestAbortedError(`The content length (${contentLength}) is bigger than the maximum allowed buffer (${maxCompressedResponseSize})`));
 | |
|                     }
 | |
|                     else if (contentLength > maxResponseSize) {
 | |
|                         response.destroy();
 | |
|                         return reject(new errors_1.RequestAbortedError(`The content length (${contentLength}) is bigger than the maximum allowed string (${maxResponseSize})`));
 | |
|                     }
 | |
|                 }
 | |
|                 // if the response is compressed, we must handle it
 | |
|                 // as buffer for allowing decompression later
 | |
|                 let payload = isCompressed || isVectorTile ? new Array() : '';
 | |
|                 const onData = isCompressed || isVectorTile ? onDataAsBuffer : onDataAsString;
 | |
|                 let currentLength = 0;
 | |
|                 function onDataAsBuffer(chunk) {
 | |
|                     currentLength += Buffer.byteLength(chunk);
 | |
|                     if (currentLength > maxCompressedResponseSize) {
 | |
|                         // TODO: hacky solution, refactor to avoid using the deprecated aborted event
 | |
|                         response.removeListener('aborted', onAbort);
 | |
|                         response.destroy();
 | |
|                         onEnd(new errors_1.RequestAbortedError(`The content length (${currentLength}) is bigger than the maximum allowed buffer (${maxCompressedResponseSize})`));
 | |
|                     }
 | |
|                     else {
 | |
|                         payload.push(chunk);
 | |
|                     }
 | |
|                 }
 | |
|                 function onDataAsString(chunk) {
 | |
|                     currentLength += Buffer.byteLength(chunk);
 | |
|                     if (currentLength > maxResponseSize) {
 | |
|                         // TODO: hacky solution, refactor to avoid using the deprecated aborted event
 | |
|                         response.removeListener('aborted', onAbort);
 | |
|                         response.destroy();
 | |
|                         onEnd(new errors_1.RequestAbortedError(`The content length (${currentLength}) is bigger than the maximum allowed string (${maxResponseSize})`));
 | |
|                     }
 | |
|                     else {
 | |
|                         payload = `${payload}${chunk}`;
 | |
|                     }
 | |
|                 }
 | |
|                 const onEnd = (err) => {
 | |
|                     response.removeListener('data', onData);
 | |
|                     response.removeListener('end', onEnd);
 | |
|                     response.removeListener('error', onEnd);
 | |
|                     response.removeListener('aborted', onAbort);
 | |
|                     request.removeListener('error', noop);
 | |
|                     if (err != null) {
 | |
|                         if (err.name === 'RequestAbortedError') {
 | |
|                             return reject(err);
 | |
|                         }
 | |
|                         return reject(new errors_1.ConnectionError(err.message));
 | |
|                     }
 | |
|                     resolve({
 | |
|                         body: isCompressed || isVectorTile ? Buffer.concat(payload) : payload,
 | |
|                         statusCode: response.statusCode,
 | |
|                         headers: response.headers
 | |
|                     });
 | |
|                 };
 | |
|                 const onAbort = () => {
 | |
|                     response.destroy();
 | |
|                     onEnd(new Error('Response aborted while reading the body'));
 | |
|                 };
 | |
|                 if (!isCompressed && !isVectorTile) {
 | |
|                     response.setEncoding('utf8');
 | |
|                 }
 | |
|                 this.diagnostic.emit('deserialization', null, options);
 | |
|                 response.on('data', onData);
 | |
|                 response.on('error', onEnd);
 | |
|                 response.on('end', onEnd);
 | |
|                 response.on('aborted', onAbort);
 | |
|             };
 | |
|             const onTimeout = () => {
 | |
|                 cleanListeners();
 | |
|                 this._openRequests--;
 | |
|                 request.once('error', () => { }); // we need to catch the request aborted error
 | |
|                 request.abort();
 | |
|                 reject(new errors_1.TimeoutError('Request timed out'));
 | |
|             };
 | |
|             const onError = (err) => {
 | |
|                 var _a, _b, _c, _d, _e, _f, _g, _h;
 | |
|                 cleanListeners();
 | |
|                 this._openRequests--;
 | |
|                 let message = err.message;
 | |
|                 // @ts-expect-error
 | |
|                 if (err.code === 'ECONNRESET') {
 | |
|                     message += ` - Local: ${(_b = (_a = request.socket) === null || _a === void 0 ? void 0 : _a.localAddress) !== null && _b !== void 0 ? _b : 'unknown'}:${(_d = (_c = request.socket) === null || _c === void 0 ? void 0 : _c.localPort) !== null && _d !== void 0 ? _d : 'unknown'}, Remote: ${(_f = (_e = request.socket) === null || _e === void 0 ? void 0 : _e.remoteAddress) !== null && _f !== void 0 ? _f : 'unknown'}:${(_h = (_g = request.socket) === null || _g === void 0 ? void 0 : _g.remotePort) !== null && _h !== void 0 ? _h : 'unknown'}`;
 | |
|                 }
 | |
|                 reject(new errors_1.ConnectionError(message));
 | |
|             };
 | |
|             const onAbort = () => {
 | |
|                 cleanListeners();
 | |
|                 request.once('error', () => { }); // we need to catch the request aborted error
 | |
|                 debug('Request aborted', params);
 | |
|                 this._openRequests--;
 | |
|                 reject(new errors_1.RequestAbortedError('Request aborted'));
 | |
|             };
 | |
|             const onSocket = (socket) => {
 | |
|                 /* istanbul ignore else */
 | |
|                 if (!socket.isSessionReused()) {
 | |
|                     socket.once('secureConnect', () => {
 | |
|                         const issuerCertificate = (0, BaseConnection_1.getIssuerCertificate)(socket);
 | |
|                         /* istanbul ignore next */
 | |
|                         if (issuerCertificate == null) {
 | |
|                             onError(new Error('Invalid or malformed certificate'));
 | |
|                             request.once('error', () => { }); // we need to catch the request aborted error
 | |
|                             return request.abort();
 | |
|                         }
 | |
|                         // Check if fingerprint matches
 | |
|                         /* istanbul ignore else */
 | |
|                         if (this[symbols_1.kCaFingerprint] !== issuerCertificate.fingerprint256) {
 | |
|                             onError(new Error('Server certificate CA fingerprint does not match the value configured in caFingerprint'));
 | |
|                             request.once('error', () => { }); // we need to catch the request aborted error
 | |
|                             return request.abort();
 | |
|                         }
 | |
|                     });
 | |
|                 }
 | |
|             };
 | |
|             request.on('response', onResponse);
 | |
|             request.on('timeout', onTimeout);
 | |
|             request.on('error', onError);
 | |
|             request.on('abort', onAbort);
 | |
|             if (this[symbols_1.kCaFingerprint] != null && requestParams.protocol === 'https:') {
 | |
|                 request.on('socket', onSocket);
 | |
|             }
 | |
|             // Disables the Nagle algorithm
 | |
|             request.setNoDelay(true);
 | |
|             // starts the request
 | |
|             if (isStream(params.body)) {
 | |
|                 (0, stream_1.pipeline)(params.body, request, err => {
 | |
|                     /* istanbul ignore if  */
 | |
|                     if (err != null && !cleanedListeners) {
 | |
|                         cleanListeners();
 | |
|                         this._openRequests--;
 | |
|                         reject(err);
 | |
|                     }
 | |
|                 });
 | |
|             }
 | |
|             else {
 | |
|                 request.end(params.body);
 | |
|             }
 | |
|             return request;
 | |
|             function cleanListeners() {
 | |
|                 request.removeListener('response', onResponse);
 | |
|                 request.removeListener('timeout', onTimeout);
 | |
|                 request.removeListener('error', onError);
 | |
|                 request.removeListener('abort', onAbort);
 | |
|                 request.removeListener('socket', onSocket);
 | |
|                 if (options.signal != null) {
 | |
|                     if ('removeEventListener' in options.signal) {
 | |
|                         options.signal.removeEventListener('abort', abortListener);
 | |
|                     }
 | |
|                     else {
 | |
|                         options.signal.removeListener('abort', abortListener);
 | |
|                     }
 | |
|                 }
 | |
|                 cleanedListeners = true;
 | |
|             }
 | |
|         });
 | |
|     }
 | |
|     async close() {
 | |
|         debug('Closing connection', this.id);
 | |
|         while (this._openRequests > 0) {
 | |
|             await sleep(1000);
 | |
|         }
 | |
|         /* istanbul ignore else */
 | |
|         if (this.agent !== undefined) {
 | |
|             this.agent.destroy();
 | |
|         }
 | |
|     }
 | |
|     buildRequestObject(params, options) {
 | |
|         var _a;
 | |
|         const url = this.url;
 | |
|         let search = url.search;
 | |
|         let pathname = url.pathname;
 | |
|         const request = {
 | |
|             protocol: url.protocol,
 | |
|             hostname: url.hostname[0] === '['
 | |
|                 ? url.hostname.slice(1, -1)
 | |
|                 : url.hostname,
 | |
|             path: '',
 | |
|             // https://github.com/elastic/elasticsearch-js/issues/843
 | |
|             port: url.port !== '' ? url.port : undefined,
 | |
|             headers: this.headers,
 | |
|             agent: this.agent,
 | |
|             timeout: (_a = options.timeout) !== null && _a !== void 0 ? _a : this.timeout
 | |
|         };
 | |
|         const paramsKeys = Object.keys(params);
 | |
|         for (let i = 0, len = paramsKeys.length; i < len; i++) {
 | |
|             const key = paramsKeys[i];
 | |
|             if (key === 'path') {
 | |
|                 pathname = resolve(pathname, params[key]);
 | |
|             }
 | |
|             else if (key === 'querystring' && Boolean(params[key])) {
 | |
|                 if (search === '') {
 | |
|                     search = `?${params[key]}`;
 | |
|                 }
 | |
|                 else {
 | |
|                     search += `&${params[key]}`;
 | |
|                 }
 | |
|             }
 | |
|             else if (key === 'headers') {
 | |
|                 request.headers = Object.assign({}, request.headers, params.headers);
 | |
|             }
 | |
|             else {
 | |
|                 // @ts-expect-error
 | |
|                 request[key] = params[key];
 | |
|             }
 | |
|         }
 | |
|         request.path = pathname + search;
 | |
|         return request;
 | |
|     }
 | |
| }
 | |
| exports.default = HttpConnection;
 | |
| function isStream(obj) {
 | |
|     return obj != null && typeof obj.pipe === 'function';
 | |
| }
 | |
| function resolve(host, path) {
 | |
|     const hostEndWithSlash = host[host.length - 1] === '/';
 | |
|     const pathStartsWithSlash = path[0] === '/';
 | |
|     if (hostEndWithSlash && pathStartsWithSlash) {
 | |
|         return host + path.slice(1);
 | |
|     }
 | |
|     else if (hostEndWithSlash !== pathStartsWithSlash) {
 | |
|         return host + path;
 | |
|     }
 | |
|     else {
 | |
|         return host + '/' + path;
 | |
|     }
 | |
| }
 | |
| /* istanbul ignore next */
 | |
| function isHttpAgentOptions(opts) {
 | |
|     if (opts.keepAliveTimeout != null)
 | |
|         return false;
 | |
|     if (opts.keepAliveMaxTimeout != null)
 | |
|         return false;
 | |
|     if (opts.keepAliveTimeoutThreshold != null)
 | |
|         return false;
 | |
|     if (opts.pipelining != null)
 | |
|         return false;
 | |
|     if (opts.maxHeaderSize != null)
 | |
|         return false;
 | |
|     if (opts.connections != null)
 | |
|         return false;
 | |
|     return true;
 | |
| }
 | |
| async function sleep(ms) {
 | |
|     return await new Promise((resolve) => setTimeout(resolve, ms));
 | |
| }
 | |
| //# sourceMappingURL=HttpConnection.js.map
 |