From f62d59aa6d99ca26bbc39838c19eae01ab5a3dc7 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 27 Nov 2019 20:25:47 +0100 Subject: [PATCH] [Console] Proxy fallback (#50185) (#51814) * First iteration of liveness manager for Console * First iteration of PoC working * Updated console proxy fallback behaviour after feedback * remove @types/node-fetch * If all hosts failed due to connection refused errors 502 * Remove unnecessary existence check --- src/legacy/core_plugins/console/index.ts | 2 +- .../server/__tests__/proxy_route/body.js | 2 +- .../server/__tests__/proxy_route/headers.js | 2 +- .../server/__tests__/proxy_route/params.js | 22 +---- .../__tests__/proxy_route/query_string.js | 2 +- .../server/{proxy_route.js => proxy_route.ts} | 94 ++++++++++++------- .../console/server/request.test.ts | 4 +- .../core_plugins/console/server/request.ts | 4 +- 8 files changed, 71 insertions(+), 61 deletions(-) rename src/legacy/core_plugins/console/server/{proxy_route.js => proxy_route.ts} (65%) diff --git a/src/legacy/core_plugins/console/index.ts b/src/legacy/core_plugins/console/index.ts index caef3ff6f99f3..c4e6a77b7d859 100644 --- a/src/legacy/core_plugins/console/index.ts +++ b/src/legacy/core_plugins/console/index.ts @@ -141,7 +141,7 @@ export default function(kibana: any) { server.route( createProxyRoute({ - baseUrl: head(legacyEsConfig.hosts), + hosts: legacyEsConfig.hosts, pathFilters: proxyPathFilters, getConfigForReq(req: any, uri: any) { const filteredHeaders = filterHeaders( diff --git a/src/legacy/core_plugins/console/server/__tests__/proxy_route/body.js b/src/legacy/core_plugins/console/server/__tests__/proxy_route/body.js index c9ad09cb017c4..a7fd8df1b10f4 100644 --- a/src/legacy/core_plugins/console/server/__tests__/proxy_route/body.js +++ b/src/legacy/core_plugins/console/server/__tests__/proxy_route/body.js @@ -36,7 +36,7 @@ describe('Console Proxy Route', () => { const server = new Server(); server.route( createProxyRoute({ - baseUrl: 'http://localhost:9200', + hosts: ['http://localhost:9200'], }) ); diff --git a/src/legacy/core_plugins/console/server/__tests__/proxy_route/headers.js b/src/legacy/core_plugins/console/server/__tests__/proxy_route/headers.js index 2e78201f9990e..347b8dae80e29 100644 --- a/src/legacy/core_plugins/console/server/__tests__/proxy_route/headers.js +++ b/src/legacy/core_plugins/console/server/__tests__/proxy_route/headers.js @@ -40,7 +40,7 @@ describe('Console Proxy Route', () => { const server = new Server(); server.route( createProxyRoute({ - baseUrl: 'http://localhost:9200', + hosts: ['http://localhost:9200'], }) ); diff --git a/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js b/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js index 7c6e221d9ce81..0069a08400c57 100644 --- a/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js +++ b/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js @@ -72,7 +72,7 @@ describe('Console Proxy Route', () => { const { server } = setup(); server.route( createProxyRoute({ - baseUrl: 'http://localhost:9200', + hosts: ['http://localhost:9200'], pathFilters: [/^\/foo\//, /^\/bar\//], }) ); @@ -91,7 +91,7 @@ describe('Console Proxy Route', () => { const { server } = setup(); server.route( createProxyRoute({ - baseUrl: 'http://localhost:9200', + hosts: ['http://localhost:9200'], pathFilters: [/^\/foo\//, /^\/bar\//], }) ); @@ -113,7 +113,7 @@ describe('Console Proxy Route', () => { const getConfigForReq = sinon.stub().returns({}); - server.route(createProxyRoute({ baseUrl: 'http://localhost:9200', getConfigForReq })); + server.route(createProxyRoute({ hosts: ['http://localhost:9200'], getConfigForReq })); await server.inject({ method: 'POST', url: '/api/console/proxy?method=HEAD&path=/index/type/id', @@ -142,7 +142,7 @@ describe('Console Proxy Route', () => { server.route( createProxyRoute({ - baseUrl: 'http://localhost:9200', + hosts: ['http://localhost:9200'], getConfigForReq: () => ({ timeout, agent, @@ -166,19 +166,5 @@ describe('Console Proxy Route', () => { expect(opts.headers).to.have.property('baz', 'bop'); }); }); - - describe('baseUrl', () => { - describe('default', () => { - it('ensures that the path starts with a /'); - }); - describe('url ends with a slash', () => { - it('combines clean with paths that start with a slash'); - it(`combines clean with paths that don't start with a slash`); - }); - describe(`url doesn't end with a slash`, () => { - it('combines clean with paths that start with a slash'); - it(`combines clean with paths that don't start with a slash`); - }); - }); }); }); diff --git a/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js b/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js index 7cabe89177164..4f96b032d9cc6 100644 --- a/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js +++ b/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js @@ -38,7 +38,7 @@ describe('Console Proxy Route', () => { const server = new Server(); server.route( createProxyRoute({ - baseUrl: 'http://localhost:9200', + hosts: ['http://localhost:9200'], }) ); diff --git a/src/legacy/core_plugins/console/server/proxy_route.js b/src/legacy/core_plugins/console/server/proxy_route.ts similarity index 65% rename from src/legacy/core_plugins/console/server/proxy_route.js rename to src/legacy/core_plugins/console/server/proxy_route.ts index 856128f3d4c03..f67c97443ba07 100644 --- a/src/legacy/core_plugins/console/server/proxy_route.js +++ b/src/legacy/core_plugins/console/server/proxy_route.ts @@ -18,12 +18,13 @@ */ import Joi from 'joi'; +import * as url from 'url'; +import { IncomingMessage } from 'http'; import Boom from 'boom'; import { trimLeft, trimRight } from 'lodash'; import { sendRequest } from './request'; -import * as url from 'url'; -function toURL(base, path) { +function toURL(base: string, path: string) { const urlResult = new url.URL(`${trimRight(base, '/')}/${trimLeft(path, '/')}`); // Appending pretty here to have Elasticsearch do the JSON formatting, as doing // in JS can lead to data loss (7.0 will get munged into 7, thus losing indication of @@ -34,11 +35,11 @@ function toURL(base, path) { return urlResult; } -function getProxyHeaders(req) { +function getProxyHeaders(req: any) { const headers = Object.create(null); // Scope this proto-unsafe functionality to where it is being used. - function extendCommaList(obj, property, value) { + function extendCommaList(obj: Record, property: string, value: any) { obj[property] = (obj[property] ? obj[property] + ',' : '') + value; } @@ -58,9 +59,13 @@ function getProxyHeaders(req) { } export const createProxyRoute = ({ - baseUrl = '/', + hosts, pathFilters = [/.*/], getConfigForReq = () => ({}), +}: { + hosts: string[]; + pathFilters: RegExp[]; + getConfigForReq: (...args: any[]) => any; }) => ({ path: '/api/console/proxy', method: 'POST', @@ -84,7 +89,7 @@ export const createProxyRoute = ({ }, pre: [ - function filterPath(req) { + function filterPath(req: any) { const { path } = req.query; if (pathFilters.some(re => re.test(path))) { @@ -92,55 +97,74 @@ export const createProxyRoute = ({ } const err = Boom.forbidden(); - err.output.payload = `Error connecting to '${path}':\n\nUnable to send requests to that path.`; + err.output.payload = `Error connecting to '${path}':\n\nUnable to send requests to that path.` as any; err.output.headers['content-type'] = 'text/plain'; throw err; }, ], - handler: async (req, h) => { + handler: async (req: any, h: any) => { const { payload, query } = req; const { path, method } = query; - const uri = toURL(baseUrl, path); - - // Because this can technically be provided by a settings-defined proxy config, we need to - // preserve these property names to maintain BWC. - const { timeout, agent, headers, rejectUnauthorized } = getConfigForReq(req, uri.toString()); - - const requestHeaders = { - ...headers, - ...getProxyHeaders(req), - }; - - const esIncomingMessage = await sendRequest({ - method, - headers: requestHeaders, - uri, - timeout, - payload, - rejectUnauthorized, - agent, - }); + + let esIncomingMessage: IncomingMessage; + + for (let idx = 0; idx < hosts.length; ++idx) { + const host = hosts[idx]; + try { + const uri = toURL(host, path); + + // Because this can technically be provided by a settings-defined proxy config, we need to + // preserve these property names to maintain BWC. + const { timeout, agent, headers, rejectUnauthorized } = getConfigForReq( + req, + uri.toString() + ); + + const requestHeaders = { + ...headers, + ...getProxyHeaders(req), + }; + + esIncomingMessage = await sendRequest({ + method, + headers: requestHeaders, + uri, + timeout, + payload, + rejectUnauthorized, + agent, + }); + + break; + } catch (e) { + if (e.code !== 'ECONNREFUSED') { + throw Boom.boomify(e); + } + if (idx === hosts.length - 1) { + throw Boom.badGateway('Could not reach any configured nodes.'); + } + // Otherwise, try the next host... + } + } const { statusCode, statusMessage, - headers: responseHeaders, - } = esIncomingMessage; - - const { warning } = responseHeaders; + headers: { warning }, + } = esIncomingMessage!; if (method.toUpperCase() !== 'HEAD') { return h - .response(esIncomingMessage) + .response(esIncomingMessage!) .code(statusCode) - .header('warning', warning); + .header('warning', warning!); } else { return h .response(`${statusCode} - ${statusMessage}`) .code(statusCode) .type('text/plain') - .header('warning', warning); + .header('warning', warning!); } }, }, diff --git a/src/legacy/core_plugins/console/server/request.test.ts b/src/legacy/core_plugins/console/server/request.test.ts index d5504c0f3a3c2..2cbde5b3b39b8 100644 --- a/src/legacy/core_plugins/console/server/request.test.ts +++ b/src/legacy/core_plugins/console/server/request.test.ts @@ -24,7 +24,7 @@ import { fail } from 'assert'; describe(`Console's send request`, () => { let sandbox: sinon.SinonSandbox; - let stub: sinon.SinonStub, ClientRequest>; + let stub: sinon.SinonStub, ClientRequest>; let fakeRequest: http.ClientRequest; beforeEach(() => { @@ -52,7 +52,7 @@ describe(`Console's send request`, () => { method: 'get', payload: null as any, timeout: 0, // immediately timeout - uri: new URL('http://noone.nowhere.com'), + uri: new URL('http://noone.nowhere.none'), }); fail('Should not reach here!'); } catch (e) { diff --git a/src/legacy/core_plugins/console/server/request.ts b/src/legacy/core_plugins/console/server/request.ts index 0082f3591a132..0f6b78b484adf 100644 --- a/src/legacy/core_plugins/console/server/request.ts +++ b/src/legacy/core_plugins/console/server/request.ts @@ -89,7 +89,7 @@ export const sendRequest = ({ } }); - const onError = () => reject(); + const onError = (e: Error) => reject(e); req.once('error', onError); const timeoutPromise = new Promise((timeoutResolve, timeoutReject) => { @@ -103,5 +103,5 @@ export const sendRequest = ({ }, timeout); }); - return Promise.race([reqPromise, timeoutPromise]); + return Promise.race([reqPromise, timeoutPromise]); };