[Backport 7.x] Added proxy support (#1276)

Co-authored-by: Tomas Della Vedova <delvedor@users.noreply.github.com>
This commit is contained in:
github-actions[bot]
2020-08-03 11:41:20 +02:00
committed by GitHub
parent bb05668a44
commit 4c1095d805
15 changed files with 344 additions and 6 deletions

View File

@ -117,6 +117,24 @@ _Default:_ `false`
|`http.SecureContextOptions` - ssl https://nodejs.org/api/tls.html[configuraton]. + |`http.SecureContextOptions` - ssl https://nodejs.org/api/tls.html[configuraton]. +
_Default:_ `null` _Default:_ `null`
|`proxy`
a|`string, URL` - If you are using an http(s) proxy, you can put its url here.
The client will automatically handle the connection to it. +
_Default:_ `null`
[source,js]
----
const client = new Client({
node: 'http://localhost:9200',
proxy: 'http://localhost:8080'
})
// Proxy with basic authentication
const client = new Client({
node: 'http://localhost:9200',
proxy: 'http://user:pwd@localhost:8080'
})
----
|`agent` |`agent`
a|`http.AgentOptions, function` - http agent https://nodejs.org/api/http.html#http_new_agent_options[options], a|`http.AgentOptions, function` - http agent https://nodejs.org/api/http.html#http_new_agent_options[options],
or a function that returns an actual http agent instance. If you want to disable the http agent use entirely or a function that returns an actual http agent instance. If you want to disable the http agent use entirely

View File

@ -210,6 +210,45 @@ _Default:_ `null`
_Default:_ `null` _Default:_ `null`
|=== |===
=== Connecting through a proxy
~Added~ ~in~ ~`v7.10.0`~
If you need to pass through an http(s) proxy for connecting to Elasticsearch, the client offers
out of the box a handy configuration for helping you with it. Under the hood it
uses the https://github.com/delvedor/hpagent[`hpagent`] module.
[source,js]
----
const client = new Client({
node: 'http://localhost:9200',
proxy: 'http://localhost:8080'
})
----
Basic authentication is supported as well:
[source,js]
----
const client = new Client({
node: 'http://localhost:9200',
proxy: 'http:user:pwd@//localhost:8080'
})
----
If you are connecting through a not http(s) proxy, such as a `socks5` or `pac`,
you can use the `agent` option to configure it.
[source,js]
----
const SocksProxyAgent = require('socks-proxy-agent')
const client = new Client({
node: 'http://localhost:9200',
agent () {
return new SocksProxyAgent('socks://127.0.0.1:1080')
}
})
----
=== Error handling === Error handling

1
index.d.ts vendored
View File

@ -107,6 +107,7 @@ interface ClientOptions {
name?: string | symbol; name?: string | symbol;
auth?: BasicAuth | ApiKeyAuth; auth?: BasicAuth | ApiKeyAuth;
context?: Context; context?: Context;
proxy?: string | URL;
cloud?: { cloud?: {
id: string; id: string;
// TODO: remove username and password here in 8 // TODO: remove username and password here in 8

View File

@ -100,7 +100,8 @@ class Client {
name: 'elasticsearch-js', name: 'elasticsearch-js',
auth: null, auth: null,
opaqueIdPrefix: null, opaqueIdPrefix: null,
context: null context: null,
proxy: null
}, opts) }, opts)
this[kInitialOptions] = options this[kInitialOptions] = options
@ -119,6 +120,7 @@ class Client {
resurrectStrategy: options.resurrectStrategy, resurrectStrategy: options.resurrectStrategy,
ssl: options.ssl, ssl: options.ssl,
agent: options.agent, agent: options.agent,
proxy: options.proxy,
Connection: options.Connection, Connection: options.Connection,
auth: options.auth, auth: options.auth,
emit: this[kEventEmitter].emit.bind(this[kEventEmitter]), emit: this[kEventEmitter].emit.bind(this[kEventEmitter]),

5
lib/Connection.d.ts vendored
View File

@ -24,6 +24,8 @@ import { inspect, InspectOptions } from 'util'
import { Readable as ReadableStream } from 'stream'; import { Readable as ReadableStream } from 'stream';
import { ApiKeyAuth, BasicAuth } from './pool' import { ApiKeyAuth, BasicAuth } from './pool'
import * as http from 'http' import * as http from 'http'
import * as https from 'https'
import * as hpagent from 'hpagent'
import { ConnectionOptions as TlsConnectionOptions } from 'tls' import { ConnectionOptions as TlsConnectionOptions } from 'tls'
export declare type agentFn = () => any; export declare type agentFn = () => any;
@ -37,6 +39,7 @@ interface ConnectionOptions {
status?: string; status?: string;
roles?: ConnectionRoles; roles?: ConnectionRoles;
auth?: BasicAuth | ApiKeyAuth; auth?: BasicAuth | ApiKeyAuth;
proxy?: string | URL;
} }
interface ConnectionRoles { interface ConnectionRoles {
@ -81,7 +84,7 @@ export default class Connection {
makeRequest: any makeRequest: any
_openRequests: number _openRequests: number
_status: string _status: string
_agent: http.Agent _agent: http.Agent | https.Agent | hpagent.HttpProxyAgent | hpagent.HttpsProxyAgent
constructor(opts?: ConnectionOptions) constructor(opts?: ConnectionOptions)
request(params: RequestOptions, callback: (err: Error | null, response: http.IncomingMessage | null) => void): http.ClientRequest request(params: RequestOptions, callback: (err: Error | null, response: http.IncomingMessage | null) => void): http.ClientRequest
close(): Connection close(): Connection

View File

@ -21,6 +21,7 @@
const assert = require('assert') const assert = require('assert')
const { inspect } = require('util') const { inspect } = require('util')
const hpagent = require('hpagent')
const http = require('http') const http = require('http')
const https = require('https') const https = require('https')
const debug = require('debug')('elasticsearch') const debug = require('debug')('elasticsearch')
@ -63,10 +64,17 @@ class Connection {
maxFreeSockets: 256, maxFreeSockets: 256,
scheduling: 'lifo' scheduling: 'lifo'
}, opts.agent) }, 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:' this.agent = this.url.protocol === 'http:'
? new http.Agent(agentOptions) ? new http.Agent(agentOptions)
: new https.Agent(Object.assign({}, agentOptions, this.ssl)) : new https.Agent(Object.assign({}, agentOptions, this.ssl))
} }
}
this.makeRequest = this.url.protocol === 'http:' this.makeRequest = this.url.protocol === 'http:'
? http.request ? http.request

View File

@ -35,6 +35,7 @@ class BaseConnectionPool {
this.auth = opts.auth || null this.auth = opts.auth || null
this._ssl = opts.ssl this._ssl = opts.ssl
this._agent = opts.agent this._agent = opts.agent
this._proxy = opts.proxy || null
} }
getConnection () { getConnection () {
@ -69,6 +70,8 @@ class BaseConnectionPool {
if (opts.ssl == null) opts.ssl = this._ssl if (opts.ssl == null) opts.ssl = this._ssl
/* istanbul ignore else */ /* istanbul ignore else */
if (opts.agent == null) opts.agent = this._agent if (opts.agent == null) opts.agent = this._agent
/* istanbul ignore else */
if (opts.proxy == null) opts.proxy = this._proxy
const connection = new this.Connection(opts) const connection = new this.Connection(opts)

2
lib/pool/index.d.ts vendored
View File

@ -27,6 +27,7 @@ import { nodeFilterFn, nodeSelectorFn } from '../Transport';
interface BaseConnectionPoolOptions { interface BaseConnectionPoolOptions {
ssl?: SecureContextOptions; ssl?: SecureContextOptions;
agent?: AgentOptions; agent?: AgentOptions;
proxy?: string | URL;
auth?: BasicAuth | ApiKeyAuth; auth?: BasicAuth | ApiKeyAuth;
emit: (event: string | symbol, ...args: any[]) => boolean; emit: (event: string | symbol, ...args: any[]) => boolean;
Connection: typeof Connection; Connection: typeof Connection;
@ -83,6 +84,7 @@ declare class BaseConnectionPool {
emit: (event: string | symbol, ...args: any[]) => boolean; emit: (event: string | symbol, ...args: any[]) => boolean;
_ssl: SecureContextOptions | null; _ssl: SecureContextOptions | null;
_agent: AgentOptions | null; _agent: AgentOptions | null;
_proxy: string | URL;
auth: BasicAuth | ApiKeyAuth; auth: BasicAuth | ApiKeyAuth;
Connection: typeof Connection; Connection: typeof Connection;
constructor(opts?: BaseConnectionPoolOptions); constructor(opts?: BaseConnectionPoolOptions);

View File

@ -60,6 +60,7 @@
"minimist": "^1.2.0", "minimist": "^1.2.0",
"ora": "^3.4.0", "ora": "^3.4.0",
"pretty-hrtime": "^1.0.3", "pretty-hrtime": "^1.0.3",
"proxy": "^1.0.2",
"rimraf": "^2.6.3", "rimraf": "^2.6.3",
"semver": "^6.0.0", "semver": "^6.0.0",
"simple-git": "^1.110.0", "simple-git": "^1.110.0",
@ -75,6 +76,7 @@
"dependencies": { "dependencies": {
"debug": "^4.1.1", "debug": "^4.1.1",
"decompress-response": "^4.2.0", "decompress-response": "^4.2.0",
"hpagent": "^0.1.1",
"ms": "^2.1.1", "ms": "^2.1.1",
"pump": "^3.0.0", "pump": "^3.0.0",
"secure-json-parse": "^2.1.0" "secure-json-parse": "^2.1.0"

View File

@ -0,0 +1,149 @@
'use strict'
// We are using self-signed certificates
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0
const { test } = require('tap')
const { Client } = require('../../index')
const {
buildProxy: {
createProxy,
createSecureProxy,
createServer,
createSecureServer
}
} = require('../utils')
test('http-http proxy support', async t => {
const server = await createServer()
const proxy = await createProxy()
server.on('request', (req, res) => {
t.strictEqual(req.url, '/_cluster/health')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const client = new Client({
node: `http://${server.address().address}:${server.address().port}`,
proxy: `http://${proxy.address().address}:${proxy.address().port}`
})
const response = await client.cluster.health()
t.deepEqual(response.body, { hello: 'world' })
server.close()
proxy.close()
})
test('http-https proxy support', async t => {
const server = await createSecureServer()
const proxy = await createProxy()
server.on('request', (req, res) => {
t.strictEqual(req.url, '/_cluster/health')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const client = new Client({
node: `https://${server.address().address}:${server.address().port}`,
proxy: `http://${proxy.address().address}:${proxy.address().port}`
})
const response = await client.cluster.health()
t.deepEqual(response.body, { hello: 'world' })
server.close()
proxy.close()
})
test('https-http proxy support', async t => {
const server = await createServer()
const proxy = await createSecureProxy()
server.on('request', (req, res) => {
t.strictEqual(req.url, '/_cluster/health')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const client = new Client({
node: `http://${server.address().address}:${server.address().port}`,
proxy: `https://${proxy.address().address}:${proxy.address().port}`
})
const response = await client.cluster.health()
t.deepEqual(response.body, { hello: 'world' })
server.close()
proxy.close()
})
test('https-https proxy support', async t => {
const server = await createSecureServer()
const proxy = await createSecureProxy()
server.on('request', (req, res) => {
t.strictEqual(req.url, '/_cluster/health')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const client = new Client({
node: `https://${server.address().address}:${server.address().port}`,
proxy: `https://${proxy.address().address}:${proxy.address().port}`
})
const response = await client.cluster.health()
t.deepEqual(response.body, { hello: 'world' })
server.close()
proxy.close()
})
test('http basic authentication', async t => {
const server = await createServer()
const proxy = await createProxy()
server.on('request', (req, res) => {
t.strictEqual(req.url, '/_cluster/health')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
proxy.authenticate = function (req, fn) {
fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('hello:world').toString('base64')}`)
}
const client = new Client({
node: `http://${server.address().address}:${server.address().port}`,
proxy: `http://hello:world@${proxy.address().address}:${proxy.address().port}`
})
const response = await client.cluster.health()
t.deepEqual(response.body, { hello: 'world' })
server.close()
proxy.close()
})
test('https basic authentication', async t => {
const server = await createSecureServer()
const proxy = await createProxy()
server.on('request', (req, res) => {
t.strictEqual(req.url, '/_cluster/health')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
proxy.authenticate = function (req, fn) {
fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('hello:world').toString('base64')}`)
}
const client = new Client({
node: `https://${server.address().address}:${server.address().port}`,
proxy: `http://hello:world@${proxy.address().address}:${proxy.address().port}`
})
const response = await client.cluster.health()
t.deepEqual(response.body, { hello: 'world' })
server.close()
proxy.close()
})

View File

@ -84,7 +84,8 @@ test('Should update the connection pool', t => {
ml: false ml: false
}, },
ssl: null, ssl: null,
agent: null agent: null,
proxy: null
}) })
} }
} }

View File

@ -692,3 +692,28 @@ expectError<errors.ConfigurationError>(
context: 'hello world' context: 'hello world'
}) })
) )
/**
* `proxy` option
*/
expectType<Client>(
new Client({
node: 'http://localhost:9200',
proxy: 'http://localhost:8080'
})
)
expectType<Client>(
new Client({
node: 'http://localhost:9200',
proxy: new URL('http://localhost:8080')
})
)
expectError<errors.ConfigurationError>(
// @ts-expect-error
new Client({
node: 'http://localhost:9200',
proxy: 42
})
)

View File

@ -24,6 +24,7 @@ const { inspect } = require('util')
const { createGzip, createDeflate } = require('zlib') const { createGzip, createDeflate } = require('zlib')
const { URL } = require('url') const { URL } = require('url')
const { Agent } = require('http') const { Agent } = require('http')
const hpagent = require('hpagent')
const intoStream = require('into-stream') const intoStream = require('into-stream')
const { buildServer } = require('../utils') const { buildServer } = require('../utils')
const Connection = require('../../lib/Connection') const Connection = require('../../lib/Connection')
@ -975,3 +976,25 @@ test('Should correctly resolve request pathname', t => {
'/test/hello' '/test/hello'
) )
}) })
test('Proxy agent (http)', t => {
t.plan(1)
const connection = new Connection({
url: new URL('http://localhost:9200'),
proxy: 'http://localhost:8080'
})
t.true(connection.agent instanceof hpagent.HttpProxyAgent)
})
test('Proxy agent (https)', t => {
t.plan(1)
const connection = new Connection({
url: new URL('https://localhost:9200'),
proxy: 'http://localhost:8080'
})
t.true(connection.agent instanceof hpagent.HttpsProxyAgent)
})

60
test/utils/buildProxy.js Normal file
View File

@ -0,0 +1,60 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
'use strict'
const proxy = require('proxy')
const { readFileSync } = require('fs')
const { join } = require('path')
const http = require('http')
const https = require('https')
const ssl = {
key: readFileSync(join(__dirname, '..', 'fixtures', 'https.key')),
cert: readFileSync(join(__dirname, '..', 'fixtures', 'https.cert'))
}
function createProxy () {
return new Promise((resolve, reject) => {
const server = proxy(http.createServer())
server.listen(0, '127.0.0.1', () => {
resolve(server)
})
})
}
function createSecureProxy () {
return new Promise((resolve, reject) => {
const server = proxy(https.createServer(ssl))
server.listen(0, '127.0.0.1', () => {
resolve(server)
})
})
}
function createServer (handler, callback) {
return new Promise((resolve, reject) => {
const server = http.createServer()
server.listen(0, '127.0.0.1', () => {
resolve(server)
})
})
}
function createSecureServer (handler, callback) {
return new Promise((resolve, reject) => {
const server = https.createServer(ssl)
server.listen(0, '127.0.0.1', () => {
resolve(server)
})
})
}
module.exports = {
ssl,
createProxy,
createSecureProxy,
createServer,
createSecureServer
}

View File

@ -23,6 +23,7 @@ const { promisify } = require('util')
const sleep = promisify(setTimeout) const sleep = promisify(setTimeout)
const buildServer = require('./buildServer') const buildServer = require('./buildServer')
const buildCluster = require('./buildCluster') const buildCluster = require('./buildCluster')
const buildProxy = require('./buildProxy')
const connection = require('./MockConnection') const connection = require('./MockConnection')
async function waitCluster (client, waitForStatus = 'green', timeout = '50s', times = 0) { async function waitCluster (client, waitForStatus = 'green', timeout = '50s', times = 0) {
@ -43,6 +44,7 @@ async function waitCluster (client, waitForStatus = 'green', timeout = '50s', ti
module.exports = { module.exports = {
buildServer, buildServer,
buildCluster, buildCluster,
buildProxy,
connection, connection,
waitCluster waitCluster
} }