UNPKG

@opensearch-project/opensearch

Version:

The official OpenSearch client for Node.js

342 lines (298 loc) 9.85 kB
/* * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * */ /* * 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. */ 'use strict'; const assert = require('assert'); const { inspect } = require('util'); const hpagent = require('hpagent'); const http = require('http'); const https = require('https'); const debug = require('debug')('opensearch'); const { pipeline } = require('stream'); const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/; const { ConnectionError, RequestAbortedError, TimeoutError, ConfigurationError, } = require('./errors'); class Connection { constructor(opts) { this.url = opts.url; this.ssl = opts.ssl || null; this.id = opts.id || stripAuth(opts.url.href); this.headers = prepareHeaders(opts.headers, opts.auth); this.deadCount = 0; this.resurrectTimeout = 0; this._openRequests = 0; this._status = opts.status || Connection.statuses.ALIVE; this.roles = Object.assign({}, defaultRoles, opts.roles); if (!['http:', 'https:'].includes(this.url.protocol)) { throw new ConfigurationError(`Invalid protocol: '${this.url.protocol}'`); } if (typeof opts.agent === 'function') { this.agent = opts.agent(opts); } else if (opts.agent === false) { this.agent = undefined; } else { const agentOptions = Object.assign( {}, { keepAlive: true, keepAliveMsecs: 1000, maxSockets: 256, maxFreeSockets: 256, scheduling: 'lifo', }, opts.agent ); if (opts.proxy) { agentOptions.proxy = opts.proxy; this.agent = this.url.protocol === 'http:' ? new hpagent.HttpProxyAgent(agentOptions) : new hpagent.HttpsProxyAgent(Object.assign({}, agentOptions, this.ssl)); } else { this.agent = this.url.protocol === 'http:' ? new http.Agent(agentOptions) : new https.Agent(Object.assign({}, agentOptions, this.ssl)); } } this.makeRequest = this.url.protocol === 'http:' ? http.request : https.request; } request(params, callback) { this._openRequests++; let cleanedListeners = false; const requestParams = this.buildRequestObject(params); // https://github.com/nodejs/node/commit/b961d9fd83 if (INVALID_PATH_REGEX.test(requestParams.path) === true) { callback(new TypeError(`ERR_UNESCAPED_CHARACTERS: ${requestParams.path}`), null); /* istanbul ignore next */ return { abort: () => {} }; } debug('Starting a new request', params); const request = this.makeRequest(requestParams); const onResponse = (response) => { cleanListeners(); this._openRequests--; callback(null, response); }; const onTimeout = () => { cleanListeners(); this._openRequests--; request.once('error', () => {}); // we need to catch the request aborted error request.abort(); callback(new TimeoutError('Request timed out', params), null); }; const onError = (err) => { cleanListeners(); this._openRequests--; callback(new ConnectionError(err.message), null); }; const onAbort = () => { cleanListeners(); request.once('error', () => {}); // we need to catch the request aborted error debug('Request aborted', params); this._openRequests--; callback(new RequestAbortedError('Request aborted'), null); }; request.on('response', onResponse); request.on('timeout', onTimeout); request.on('error', onError); request.on('abort', onAbort); // Disables the Nagle algorithm request.setNoDelay(true); // starts the request if (isStream(params.body) === true) { pipeline(params.body, request, (err) => { /* istanbul ignore if */ if (err != null && cleanedListeners === false) { cleanListeners(); this._openRequests--; callback(err, null); } }); } 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); cleanedListeners = true; } } // TODO: write a better closing logic close(callback = () => {}) { debug('Closing connection', this.id); if (this._openRequests > 0) { setTimeout(() => this.close(callback), 1000); } else { if (this.agent !== undefined) { this.agent.destroy(); } callback(); } } setRole(role, enabled) { if (validRoles.indexOf(role) === -1) { throw new ConfigurationError(`Unsupported role: '${role}'`); } if (typeof enabled !== 'boolean') { throw new ConfigurationError('enabled should be a boolean'); } this.roles[role] = enabled; return this; } get status() { return this._status; } set status(status) { assert(~validStatuses.indexOf(status), `Unsupported status: '${status}'`); this._status = status; } buildRequestObject(params) { const url = this.url; const request = { protocol: url.protocol, hostname: url.hostname[0] === '[' ? url.hostname.slice(1, -1) : url.hostname, hash: url.hash, search: url.search, pathname: url.pathname, path: '', href: url.href, origin: url.origin, // https://github.com/elastic/elasticsearch-js/issues/843 port: url.port !== '' ? url.port : undefined, headers: Object.assign({}, this.headers), agent: this.agent, }; const paramsKeys = Object.keys(params); for (let i = 0, len = paramsKeys.length; i < len; i++) { const key = paramsKeys[i]; if (key === 'path') { request.pathname = resolve(request.pathname, params[key]); } else if (key === 'querystring' && !!params[key] === true) { if (request.search === '') { request.search = '?' + params[key]; } else { request.search += '&' + params[key]; } } else if (key === 'headers') { request.headers = Object.assign({}, request.headers, params.headers); } else { request[key] = params[key]; } } request.path = request.pathname + request.search; return request; } // Handles console.log and utils.inspect invocations. // We want to hide `auth`, `agent` and `ssl` since they made // the logs very hard to read. The user can still // access them with `instance.agent` and `instance.ssl`. [inspect.custom]() { // eslint-disable-next-line no-unused-vars const { authorization, ...headers } = this.headers; return { url: stripAuth(this.url.toString()), id: this.id, headers, deadCount: this.deadCount, resurrectTimeout: this.resurrectTimeout, _openRequests: this._openRequests, status: this.status, roles: this.roles, }; } toJSON() { // eslint-disable-next-line no-unused-vars const { authorization, ...headers } = this.headers; return { url: stripAuth(this.url.toString()), id: this.id, headers, deadCount: this.deadCount, resurrectTimeout: this.resurrectTimeout, _openRequests: this._openRequests, status: this.status, roles: this.roles, }; } } Connection.statuses = { ALIVE: 'alive', DEAD: 'dead', }; Connection.roles = { CLUSTER_MANAGER: 'cluster_manager', /** * @deprecated use CLUSTER_MANAGER instead */ MASTER: 'master', DATA: 'data', INGEST: 'ingest', }; const defaultRoles = { [Connection.roles.DATA]: true, [Connection.roles.INGEST]: true, }; const validStatuses = Object.keys(Connection.statuses).map((k) => Connection.statuses[k]); const validRoles = Object.keys(Connection.roles).map((k) => Connection.roles[k]); function stripAuth(url) { if (url.indexOf('@') === -1) return url; return url.slice(0, url.indexOf('//') + 2) + url.slice(url.indexOf('@') + 1); } 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 === true && pathStartsWithSlash === true) { return host + path.slice(1); } else if (hostEndWithSlash !== pathStartsWithSlash) { return host + path; } else { return host + '/' + path; } } function prepareHeaders(headers = {}, auth) { if (auth != null && headers.authorization == null) { /* istanbul ignore else */ if (auth.username && auth.password) { headers.authorization = 'Basic ' + Buffer.from(`${auth.username}:${auth.password}`).toString('base64'); } } return headers; } module.exports = Connection; module.exports.internals = { prepareHeaders };