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
|