diff --git a/package.json b/package.json index e35b83e690a1a..59493914d3907 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,7 @@ "glob-all": "3.0.1", "good-squeeze": "2.1.0", "h2o2": "5.1.1", + "h2o2-latest": "npm:h2o2@8.1.2", "handlebars": "4.0.5", "hapi": "14.2.0", "hapi-latest": "npm:hapi@17.5.0", diff --git a/src/cli/cluster/base_path_proxy.js b/src/cli/cluster/base_path_proxy.js deleted file mode 100644 index b6cd3f93b2249..0000000000000 --- a/src/cli/cluster/base_path_proxy.js +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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. - */ - -import { Server } from 'hapi'; -import { notFound } from 'boom'; -import { map, sample } from 'lodash'; -import { map as promiseMap, fromNode } from 'bluebird'; -import { Agent as HttpsAgent } from 'https'; -import { readFileSync } from 'fs'; - -import { setupConnection } from '../../server/http/setup_connection'; -import { registerHapiPlugins } from '../../server/http/register_hapi_plugins'; -import { setupLogging } from '../../server/logging'; - -const alphabet = 'abcdefghijklmnopqrztuvwxyz'.split(''); - -export default class BasePathProxy { - constructor(clusterManager, config) { - this.clusterManager = clusterManager; - this.server = new Server(); - - this.targetPort = config.get('dev.basePathProxyTarget'); - this.basePath = config.get('server.basePath'); - - const sslEnabled = config.get('server.ssl.enabled'); - if (sslEnabled) { - this.proxyAgent = new HttpsAgent({ - key: readFileSync(config.get('server.ssl.key')), - passphrase: config.get('server.ssl.keyPassphrase'), - cert: readFileSync(config.get('server.ssl.certificate')), - ca: map(config.get('server.ssl.certificateAuthorities'), readFileSync), - rejectUnauthorized: false - }); - } - - if (!this.basePath) { - this.basePath = `/${sample(alphabet, 3).join('')}`; - config.set('server.basePath', this.basePath); - } - - const ONE_GIGABYTE = 1024 * 1024 * 1024; - config.set('server.maxPayloadBytes', ONE_GIGABYTE); - - setupLogging(this.server, config); - setupConnection(this.server, config); - registerHapiPlugins(this.server, config); - - this.setupRoutes(); - } - - setupRoutes() { - const { clusterManager, server, basePath, targetPort } = this; - - server.route({ - method: 'GET', - path: '/', - handler(req, reply) { - return reply.redirect(basePath); - } - }); - - server.route({ - method: '*', - path: `${basePath}/{kbnPath*}`, - config: { - pre: [ - (req, reply) => { - promiseMap(clusterManager.workers, worker => { - if (worker.type === 'server' && !worker.listening && !worker.crashed) { - return fromNode(cb => { - const done = () => { - worker.removeListener('listening', done); - worker.removeListener('crashed', done); - cb(); - }; - - worker.on('listening', done); - worker.on('crashed', done); - }); - } - }) - .return(undefined) - .nodeify(reply); - } - ], - }, - handler: { - proxy: { - passThrough: true, - xforward: true, - agent: this.proxyAgent, - protocol: server.info.protocol, - host: server.info.host, - port: targetPort, - } - } - }); - - server.route({ - method: '*', - path: `/{oldBasePath}/{kbnPath*}`, - handler(req, reply) { - const { oldBasePath, kbnPath = '' } = req.params; - - const isGet = req.method === 'get'; - const isBasePath = oldBasePath.length === 3; - const isApp = kbnPath.startsWith('app/'); - const isKnownShortPath = ['login', 'logout', 'status'].includes(kbnPath); - - if (isGet && isBasePath && (isApp || isKnownShortPath)) { - return reply.redirect(`${basePath}/${kbnPath}`); - } - - return reply(notFound()); - } - }); - } - - async listen() { - await fromNode(cb => this.server.start(cb)); - this.server.log(['listening', 'info'], `basePath Proxy running at ${this.server.info.uri}${this.basePath}`); - } - -} diff --git a/src/cli/cluster/cluster_manager.js b/src/cli/cluster/cluster_manager.js index 5ae5ca2bfadc6..0543c1030d714 100644 --- a/src/cli/cluster/cluster_manager.js +++ b/src/cli/cluster/cluster_manager.js @@ -22,9 +22,9 @@ import { debounce, invoke, bindAll, once, uniq } from 'lodash'; import Log from '../log'; import Worker from './worker'; -import BasePathProxy from './base_path_proxy'; import { Config } from '../../server/config/config'; import { transformDeprecations } from '../../server/config/transform_deprecations'; +import { configureBasePathProxy } from './configure_base_path_proxy'; process.env.kbnWorkerType = 'managr'; @@ -33,10 +33,14 @@ export default class ClusterManager { const transformedSettings = transformDeprecations(settings); const config = await Config.withDefaultSchema(transformedSettings); - return new ClusterManager(opts, config); + const basePathProxy = opts.basePath + ? await configureBasePathProxy(config) + : undefined; + + return new ClusterManager(opts, config, basePathProxy); } - constructor(opts, config) { + constructor(opts, config, basePathProxy) { this.log = new Log(opts.quiet, opts.silent); this.addedCount = 0; this.inReplMode = !!opts.repl; @@ -47,17 +51,17 @@ export default class ClusterManager { '--server.autoListen=false', ]; - if (opts.basePath) { - this.basePathProxy = new BasePathProxy(this, config); + if (basePathProxy) { + this.basePathProxy = basePathProxy; optimizerArgv.push( - `--server.basePath=${this.basePathProxy.basePath}`, + `--server.basePath=${this.basePathProxy.getBasePath()}`, '--server.rewriteBasePath=true', ); serverArgv.push( - `--server.port=${this.basePathProxy.targetPort}`, - `--server.basePath=${this.basePathProxy.basePath}`, + `--server.port=${this.basePathProxy.getTargetPort()}`, + `--server.basePath=${this.basePathProxy.getBasePath()}`, '--server.rewriteBasePath=true', ); } @@ -78,6 +82,12 @@ export default class ClusterManager { }) ]; + if (basePathProxy) { + // Pass server worker to the basepath proxy so that it can hold off the + // proxying until server worker is ready. + this.basePathProxy.serverWorker = this.server; + } + // broker messages between workers this.workers.forEach((worker) => { worker.on('broadcast', (msg) => { @@ -120,7 +130,7 @@ export default class ClusterManager { this.setupManualRestart(); invoke(this.workers, 'start'); if (this.basePathProxy) { - this.basePathProxy.listen(); + this.basePathProxy.start(); } } diff --git a/src/cli/cluster/configure_base_path_proxy.js b/src/cli/cluster/configure_base_path_proxy.js new file mode 100644 index 0000000000000..477b10053d1e6 --- /dev/null +++ b/src/cli/cluster/configure_base_path_proxy.js @@ -0,0 +1,64 @@ +/* + * 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. + */ + +import { Server } from 'hapi'; +import { createBasePathProxy } from '../../core'; +import { setupLogging } from '../../server/logging'; + +export async function configureBasePathProxy(config) { + // New platform forwards all logs to the legacy platform so we need HapiJS server + // here just for logging purposes and nothing else. + const server = new Server(); + setupLogging(server, config); + + const basePathProxy = createBasePathProxy({ server, config }); + + await basePathProxy.configure({ + shouldRedirectFromOldBasePath: path => { + const isApp = path.startsWith('app/'); + const isKnownShortPath = ['login', 'logout', 'status'].includes(path); + + return isApp || isKnownShortPath; + }, + + blockUntil: () => { + // Wait until `serverWorker either crashes or starts to listen. + // The `serverWorker` property should be set by the ClusterManager + // once it creates the worker. + const serverWorker = basePathProxy.serverWorker; + if (serverWorker.listening || serverWorker.crashed) { + return Promise.resolve(); + } + + return new Promise(resolve => { + const done = () => { + serverWorker.removeListener('listening', done); + serverWorker.removeListener('crashed', done); + + resolve(); + }; + + serverWorker.on('listening', done); + serverWorker.on('crashed', done); + }); + }, + }); + + return basePathProxy; +} diff --git a/src/cli/cluster/configure_base_path_proxy.test.js b/src/cli/cluster/configure_base_path_proxy.test.js new file mode 100644 index 0000000000000..01cbaf0bcc900 --- /dev/null +++ b/src/cli/cluster/configure_base_path_proxy.test.js @@ -0,0 +1,163 @@ +/* + * 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. + */ + +jest.mock('../../core', () => ({ + createBasePathProxy: jest.fn(), +})); + +jest.mock('../../server/logging', () => ({ + setupLogging: jest.fn(), +})); + +import { Server } from 'hapi'; +import { createBasePathProxy as createBasePathProxyMock } from '../../core'; +import { setupLogging as setupLoggingMock } from '../../server/logging'; +import { configureBasePathProxy } from './configure_base_path_proxy'; + +describe('configureBasePathProxy()', () => { + it('returns `BasePathProxy` instance.', async () => { + const basePathProxyMock = { configure: jest.fn() }; + createBasePathProxyMock.mockReturnValue(basePathProxyMock); + + const basePathProxy = await configureBasePathProxy({}); + + expect(basePathProxy).toBe(basePathProxyMock); + }); + + it('correctly configures `BasePathProxy`.', async () => { + const configMock = {}; + const basePathProxyMock = { configure: jest.fn() }; + createBasePathProxyMock.mockReturnValue(basePathProxyMock); + + await configureBasePathProxy(configMock); + + // Check that logging is configured with the right parameters. + expect(setupLoggingMock).toHaveBeenCalledWith( + expect.any(Server), + configMock + ); + + const [[server]] = setupLoggingMock.mock.calls; + expect(createBasePathProxyMock).toHaveBeenCalledWith({ + config: configMock, + server, + }); + + expect(basePathProxyMock.configure).toHaveBeenCalledWith({ + shouldRedirectFromOldBasePath: expect.any(Function), + blockUntil: expect.any(Function), + }); + }); + + describe('configured with the correct `shouldRedirectFromOldBasePath` and `blockUntil` functions.', async () => { + let serverWorkerMock; + let shouldRedirectFromOldBasePath; + let blockUntil; + beforeEach(async () => { + serverWorkerMock = { + listening: false, + crashed: false, + on: jest.fn(), + removeListener: jest.fn(), + }; + + const basePathProxyMock = { + configure: jest.fn(), + serverWorker: serverWorkerMock, + }; + + createBasePathProxyMock.mockReturnValue(basePathProxyMock); + + await configureBasePathProxy({}); + + [[{ blockUntil, shouldRedirectFromOldBasePath }]] = basePathProxyMock.configure.mock.calls; + }); + + it('`shouldRedirectFromOldBasePath()` returns `false` for unknown paths.', async () => { + expect(shouldRedirectFromOldBasePath('')).toBe(false); + expect(shouldRedirectFromOldBasePath('some-path/')).toBe(false); + expect(shouldRedirectFromOldBasePath('some-other-path')).toBe(false); + }); + + it('`shouldRedirectFromOldBasePath()` returns `true` for `app` and other known paths.', async () => { + expect(shouldRedirectFromOldBasePath('app/')).toBe(true); + expect(shouldRedirectFromOldBasePath('login')).toBe(true); + expect(shouldRedirectFromOldBasePath('logout')).toBe(true); + expect(shouldRedirectFromOldBasePath('status')).toBe(true); + }); + + it('`blockUntil()` resolves immediately if worker has already crashed.', async () => { + serverWorkerMock.crashed = true; + + await expect(blockUntil()).resolves.not.toBeDefined(); + expect(serverWorkerMock.on).not.toHaveBeenCalled(); + expect(serverWorkerMock.removeListener).not.toHaveBeenCalled(); + }); + + it('`blockUntil()` resolves immediately if worker is already listening.', async () => { + serverWorkerMock.listening = true; + + await expect(blockUntil()).resolves.not.toBeDefined(); + expect(serverWorkerMock.on).not.toHaveBeenCalled(); + expect(serverWorkerMock.removeListener).not.toHaveBeenCalled(); + }); + + it('`blockUntil()` resolves when worker crashes.', async () => { + const blockUntilPromise = blockUntil(); + + expect(serverWorkerMock.on).toHaveBeenCalledTimes(2); + expect(serverWorkerMock.on).toHaveBeenCalledWith( + 'crashed', + expect.any(Function) + ); + + const [, [eventName, onCrashed]] = serverWorkerMock.on.mock.calls; + // Check event name to make sure we call the right callback, + // in Jest 23 we could use `toHaveBeenNthCalledWith` instead. + expect(eventName).toBe('crashed'); + expect(serverWorkerMock.removeListener).not.toHaveBeenCalled(); + + onCrashed(); + await expect(blockUntilPromise).resolves.not.toBeDefined(); + + expect(serverWorkerMock.removeListener).toHaveBeenCalledTimes(2); + }); + + it('`blockUntil()` resolves when worker starts listening.', async () => { + const blockUntilPromise = blockUntil(); + + expect(serverWorkerMock.on).toHaveBeenCalledTimes(2); + expect(serverWorkerMock.on).toHaveBeenCalledWith( + 'listening', + expect.any(Function) + ); + + const [[eventName, onListening]] = serverWorkerMock.on.mock.calls; + // Check event name to make sure we call the right callback, + // in Jest 23 we could use `toHaveBeenNthCalledWith` instead. + expect(eventName).toBe('listening'); + expect(serverWorkerMock.removeListener).not.toHaveBeenCalled(); + + onListening(); + await expect(blockUntilPromise).resolves.not.toBeDefined(); + + expect(serverWorkerMock.removeListener).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/src/cli/serve/integration_tests/__snapshots__/reload_logging_config.test.js.snap b/src/cli/serve/integration_tests/__snapshots__/reload_logging_config.test.js.snap index 948c6a30607b1..04014e02fbb05 100644 --- a/src/cli/serve/integration_tests/__snapshots__/reload_logging_config.test.js.snap +++ b/src/cli/serve/integration_tests/__snapshots__/reload_logging_config.test.js.snap @@ -13,16 +13,6 @@ Object { ], "type": "log", }, - Object { - "@timestamp": "## @timestamp ##", - "message": "starting the server", - "pid": "## PID ##", - "tags": Array [ - "info", - "root", - ], - "type": "log", - }, Object { "@timestamp": "## @timestamp ##", "message": "starting server :tada:", diff --git a/src/core/index.ts b/src/core/index.ts index 3aeff811a2232..b95300d29a761 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -17,4 +17,7 @@ * under the License. */ -export { injectIntoKbnServer } from './server/legacy_compat'; +export { + injectIntoKbnServer, + createBasePathProxy, +} from './server/legacy_compat'; diff --git a/src/core/server/dev/dev_config.ts b/src/core/server/dev/dev_config.ts new file mode 100644 index 0000000000000..5c8aca3ce3c51 --- /dev/null +++ b/src/core/server/dev/dev_config.ts @@ -0,0 +1,44 @@ +/* + * 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. + */ + +import { schema, TypeOf } from '../config/schema'; + +const createDevSchema = schema.object({ + basePathProxyTarget: schema.number({ + defaultValue: 5603, + }), +}); + +type DevConfigType = TypeOf; + +export class DevConfig { + /** + * @internal + */ + public static schema = createDevSchema; + + public basePathProxyTargetPort: number; + + /** + * @internal + */ + constructor(config: DevConfigType) { + this.basePathProxyTargetPort = config.basePathProxyTarget; + } +} diff --git a/src/core/server/dev/index.ts b/src/core/server/dev/index.ts new file mode 100644 index 0000000000000..b3fa85892330e --- /dev/null +++ b/src/core/server/dev/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { DevConfig } from './dev_config'; diff --git a/src/core/server/http/__tests__/__snapshots__/http_service.test.ts.snap b/src/core/server/http/__tests__/__snapshots__/http_service.test.ts.snap index e5217758742b5..86fce993d6da2 100644 --- a/src/core/server/http/__tests__/__snapshots__/http_service.test.ts.snap +++ b/src/core/server/http/__tests__/__snapshots__/http_service.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`logs error is already started 1`] = ` +exports[`logs error if already started 1`] = ` Object { "debug": Array [], "error": Array [], diff --git a/src/core/server/http/__tests__/__snapshots__/https_redirect_server.test.ts.snap b/src/core/server/http/__tests__/__snapshots__/https_redirect_server.test.ts.snap new file mode 100644 index 0000000000000..a4b87fdabd6c2 --- /dev/null +++ b/src/core/server/http/__tests__/__snapshots__/https_redirect_server.test.ts.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`throws if [redirectHttpFromPort] is in use 1`] = `[Error: Redirect server cannot be started when [ssl.enabled] is set to \`false\` or [ssl.redirectHttpFromPort] is not specified.]`; + +exports[`throws if [redirectHttpFromPort] is not specified 1`] = `[Error: Redirect server cannot be started when [ssl.enabled] is set to \`false\` or [ssl.redirectHttpFromPort] is not specified.]`; + +exports[`throws if SSL is not enabled 1`] = `[Error: Redirect server cannot be started when [ssl.enabled] is set to \`false\` or [ssl.redirectHttpFromPort] is not specified.]`; diff --git a/src/core/server/http/__tests__/http_server.test.ts b/src/core/server/http/__tests__/http_server.test.ts index 236c8bb690354..deebd5f21483b 100644 --- a/src/core/server/http/__tests__/http_server.test.ts +++ b/src/core/server/http/__tests__/http_server.test.ts @@ -41,10 +41,6 @@ function getServerListener(httpServer: HttpServer) { return (httpServer as any).server.listener; } -function getRedirectServerListener(httpServer: HttpServer) { - return (httpServer as any).redirectServer.listener; -} - beforeEach(() => { config = { host: '127.0.0.1', @@ -569,17 +565,6 @@ describe('with defined `redirectHttpFromPort`', () => { await server.start(configWithSSL); }); - - test('http requests are forwarded to https', async () => { - await supertest(getRedirectServerListener(server)) - .get('/') - .expect(302) - .then(res => { - expect(res.header.location).toEqual( - `https://${configWithSSL.host}:${configWithSSL.port}/` - ); - }); - }); }); describe('when run within legacy platform', () => { diff --git a/src/core/server/http/__tests__/http_service.test.ts b/src/core/server/http/__tests__/http_service.test.ts index 6084bd93ebc46..4e30b74c183e5 100644 --- a/src/core/server/http/__tests__/http_service.test.ts +++ b/src/core/server/http/__tests__/http_service.test.ts @@ -41,6 +41,7 @@ test('creates and starts http server', async () => { const config = { host: 'example.org', port: 1234, + ssl: {}, } as HttpConfig; const config$ = new BehaviorSubject(config); @@ -66,8 +67,8 @@ test('creates and starts http server', async () => { expect(httpServer.start).toHaveBeenCalledTimes(1); }); -test('logs error is already started', async () => { - const config = {} as HttpConfig; +test('logs error if already started', async () => { + const config = { ssl: {} } as HttpConfig; const config$ = new BehaviorSubject(config); @@ -90,7 +91,7 @@ test('logs error is already started', async () => { }); test('stops http server', async () => { - const config = {} as HttpConfig; + const config = { ssl: {} } as HttpConfig; const config$ = new BehaviorSubject(config); diff --git a/src/core/server/http/__tests__/https_redirect_server.test.ts b/src/core/server/http/__tests__/https_redirect_server.test.ts new file mode 100644 index 0000000000000..74498d41509c5 --- /dev/null +++ b/src/core/server/http/__tests__/https_redirect_server.test.ts @@ -0,0 +1,111 @@ +/* + * 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. + */ + +jest.mock('fs', () => ({ + readFileSync: jest.fn(), +})); + +import Chance from 'chance'; +import { Server } from 'http'; +import supertest from 'supertest'; + +import { ByteSizeValue } from '../../config/schema'; +import { logger } from '../../logging/__mocks__'; +import { HttpConfig } from '../http_config'; +import { HttpsRedirectServer } from '../https_redirect_server'; + +const chance = new Chance(); + +let server: HttpsRedirectServer; +let config: HttpConfig; + +function getServerListener(httpServer: HttpsRedirectServer) { + return (httpServer as any).server.listener; +} + +beforeEach(() => { + config = { + host: '127.0.0.1', + maxPayload: new ByteSizeValue(1024), + port: chance.integer({ min: 10000, max: 15000 }), + ssl: { + enabled: true, + redirectHttpFromPort: chance.integer({ min: 20000, max: 30000 }), + }, + } as HttpConfig; + + server = new HttpsRedirectServer(logger.get()); +}); + +afterEach(async () => { + await server.stop(); + logger.mockClear(); +}); + +test('throws if SSL is not enabled', async () => { + await expect( + server.start({ + ...config, + ssl: { + enabled: false, + redirectHttpFromPort: chance.integer({ min: 20000, max: 30000 }), + }, + } as HttpConfig) + ).rejects.toMatchSnapshot(); +}); + +test('throws if [redirectHttpFromPort] is not specified', async () => { + await expect( + server.start({ + ...config, + ssl: { enabled: true }, + } as HttpConfig) + ).rejects.toMatchSnapshot(); +}); + +test('throws if [redirectHttpFromPort] is in use', async () => { + const mockListen = jest + .spyOn(Server.prototype, 'listen') + .mockImplementation(() => { + throw { code: 'EADDRINUSE' }; + }); + + await expect( + server.start({ + ...config, + ssl: { enabled: true }, + } as HttpConfig) + ).rejects.toMatchSnapshot(); + + // Workaround for https://github.com/DefinitelyTyped/DefinitelyTyped/issues/17605. + (mockListen as any).mockRestore(); +}); + +test('forwards http requests to https', async () => { + await server.start(config); + + await supertest(getServerListener(server)) + .get('/') + .expect(302) + .then(res => { + expect(res.header.location).toEqual( + `https://${config.host}:${config.port}/` + ); + }); +}); diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts new file mode 100644 index 0000000000000..12906d5a9b672 --- /dev/null +++ b/src/core/server/http/base_path_proxy_server.ts @@ -0,0 +1,174 @@ +/* + * 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. + */ + +import { Server } from 'hapi-latest'; +import { Agent as HttpsAgent, ServerOptions as TlsOptions } from 'https'; +import { sample } from 'lodash'; +import { ByteSizeValue } from '../config/schema'; +import { DevConfig } from '../dev'; +import { Logger } from '../logging'; +import { HttpConfig } from './http_config'; +import { createServer, getServerOptions } from './http_tools'; + +const alphabet = 'abcdefghijklmnopqrztuvwxyz'.split(''); + +export interface BasePathProxyServerOptions { + httpConfig: HttpConfig; + devConfig: DevConfig; + shouldRedirectFromOldBasePath: (path: string) => boolean; + blockUntil: () => Promise; +} + +export class BasePathProxyServer { + private server?: Server; + private httpsAgent?: HttpsAgent; + + get basePath() { + return this.options.httpConfig.basePath; + } + + get targetPort() { + return this.options.devConfig.basePathProxyTargetPort; + } + + constructor( + private readonly log: Logger, + private readonly options: BasePathProxyServerOptions + ) { + const ONE_GIGABYTE = 1024 * 1024 * 1024; + options.httpConfig.maxPayload = new ByteSizeValue(ONE_GIGABYTE); + + if (!options.httpConfig.basePath) { + options.httpConfig.basePath = `/${sample(alphabet, 3).join('')}`; + } + } + + public async start() { + const { httpConfig } = this.options; + + const options = getServerOptions(httpConfig); + this.server = createServer(options); + + // Register hapi plugin that adds proxying functionality. It can be configured + // through the route configuration object (see { handler: { proxy: ... } }). + await this.server.register({ plugin: require('h2o2-latest') }); + + if (httpConfig.ssl.enabled) { + const tlsOptions = options.tls as TlsOptions; + this.httpsAgent = new HttpsAgent({ + ca: tlsOptions.ca, + cert: tlsOptions.cert, + key: tlsOptions.key, + passphrase: tlsOptions.passphrase, + rejectUnauthorized: false, + }); + } + + this.setupRoutes(); + + this.log.info( + `starting basepath proxy server at ${this.server.info.uri}${ + httpConfig.basePath + }` + ); + + await this.server.start(); + } + + public async stop() { + this.log.info('stopping basepath proxy server'); + + if (this.server !== undefined) { + await this.server.stop(); + this.server = undefined; + } + + if (this.httpsAgent !== undefined) { + this.httpsAgent.destroy(); + this.httpsAgent = undefined; + } + } + + private setupRoutes() { + if (this.server === undefined) { + throw new Error( + `Routes cannot be set up since server is not initialized.` + ); + } + + const { + httpConfig, + devConfig, + blockUntil, + shouldRedirectFromOldBasePath, + } = this.options; + + // Always redirect from root URL to the URL with basepath. + this.server.route({ + handler: (request, responseToolkit) => { + return responseToolkit.redirect(httpConfig.basePath); + }, + method: 'GET', + path: '/', + }); + + this.server.route({ + handler: { + proxy: { + agent: this.httpsAgent, + host: this.server.info.host, + passThrough: true, + port: devConfig.basePathProxyTargetPort, + protocol: this.server.info.protocol, + xforward: true, + }, + }, + method: '*', + options: { + pre: [ + // Before we proxy request to a target port we may want to wait until some + // condition is met (e.g. until target listener is ready). + async (request, responseToolkit) => { + await blockUntil(); + return responseToolkit.continue; + }, + ], + }, + path: `${httpConfig.basePath}/{kbnPath*}`, + }); + + // It may happen that basepath has changed, but user still uses the old one, + // so we can try to check if that's the case and just redirect user to the + // same URL, but with valid basepath. + this.server.route({ + handler: (request, responseToolkit) => { + const { oldBasePath, kbnPath = '' } = request.params; + + const isGet = request.method === 'get'; + const isBasepathLike = oldBasePath.length === 3; + + return isGet && isBasepathLike && shouldRedirectFromOldBasePath(kbnPath) + ? responseToolkit.redirect(`${httpConfig.basePath}/${kbnPath}`) + : responseToolkit.response('Not Found').code(404); + }, + method: '*', + path: `/{oldBasePath}/{kbnPath*}`, + }); + } +} diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 934c00f01ac74..82ca75169bf65 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -17,20 +17,17 @@ * under the License. */ -import { readFileSync } from 'fs'; -import { Request, ResponseToolkit, Server, ServerOptions } from 'hapi-latest'; -import { ServerOptions as TLSOptions } from 'https'; -import { format as formatUrl } from 'url'; +import { Server } from 'hapi-latest'; import { modifyUrl } from '../../utils'; import { Env } from '../config'; import { Logger } from '../logging'; import { HttpConfig } from './http_config'; +import { createServer, getServerOptions } from './http_tools'; import { Router } from './router'; export class HttpServer { private server?: Server; - private redirectServer?: Server; private registeredRouters: Set = new Set(); constructor(private readonly log: Logger, private readonly env: Env) {} @@ -50,13 +47,7 @@ export class HttpServer { } public async start(config: HttpConfig) { - this.server = this.initializeServer(config); - - // If a redirect port is specified, we start an http server at this port and - // redirect all requests to the ssl port. - if (config.ssl.enabled && config.ssl.redirectHttpFromPort !== undefined) { - await this.setupRedirectServer(config); - } + this.server = createServer(getServerOptions(config)); this.setupBasePathRewrite(this.server, config); @@ -94,14 +85,6 @@ export class HttpServer { }); } - this.server.listener.on('clientError', (err, socket) => { - if (socket.writable) { - socket.end(new Buffer('HTTP/1.1 400 Bad Request\r\n\r\n', 'ascii')); - } else { - socket.destroy(err); - } - }); - this.log.info(`starting http server [${config.host}:${config.port}]`); await this.server.start(); @@ -114,58 +97,6 @@ export class HttpServer { await this.server.stop(); this.server = undefined; } - - if (this.redirectServer !== undefined) { - await this.redirectServer.stop(); - this.redirectServer = undefined; - } - } - - private initializeServer(config: HttpConfig) { - const options: ServerOptions = { - host: config.host, - port: config.port, - routes: { - cors: config.cors, - payload: { - maxBytes: config.maxPayload.getValueInBytes(), - }, - validate: { - options: { - abortEarly: false, - }, - }, - }, - state: { - strictHeader: false, - }, - }; - - const ssl = config.ssl; - if (ssl.enabled) { - const tlsOptions: TLSOptions = { - ca: - config.ssl.certificateAuthorities && - config.ssl.certificateAuthorities.map(caFilePath => - readFileSync(caFilePath) - ), - - cert: readFileSync(ssl.certificate!), - ciphers: config.ssl.cipherSuites.join(':'), - // We use the server's cipher order rather than the client's to prevent the BEAST attack. - honorCipherOrder: true, - - key: readFileSync(ssl.key!), - passphrase: ssl.keyPassphrase, - secureOptions: ssl.getSecureOptions(), - }; - - // TODO: Hapi types have a typo in `tls` property type definition: `https.RequestOptions` is used instead of - // `https.ServerOptions`, and `honorCipherOrder` isn't presented in `https.RequestOptions`. - options.tls = tlsOptions as any; - } - - return new Server(options); } private setupBasePathRewrite(server: Server, config: HttpConfig) { @@ -174,81 +105,32 @@ export class HttpServer { } const basePath = config.basePath; - server.ext( - 'onRequest', - (request: Request, responseToolkit: ResponseToolkit) => { - const newURL = modifyUrl(request.url.href!, urlParts => { - if ( - urlParts.pathname != null && - urlParts.pathname.startsWith(basePath) - ) { - urlParts.pathname = urlParts.pathname.replace(basePath, '') || '/'; - } else { - return {}; - } - }); - - if (!newURL) { - return responseToolkit - .response('Not Found') - .code(404) - .takeover(); + server.ext('onRequest', (request, responseToolkit) => { + const newURL = modifyUrl(request.url.href!, urlParts => { + if ( + urlParts.pathname != null && + urlParts.pathname.startsWith(basePath) + ) { + urlParts.pathname = urlParts.pathname.replace(basePath, '') || '/'; + } else { + return {}; } + }); - request.setUrl(newURL); - // We should update raw request as well since it can be proxied to the old platform - // where base path isn't expected. - request.raw.req.url = request.url.href; - - return responseToolkit.continue; - } - ); - } - - private async setupRedirectServer(config: HttpConfig) { - this.log.info( - `starting HTTP --> HTTPS redirect server [${config.host}:${ - config.ssl.redirectHttpFromPort - }]` - ); - - this.redirectServer = new Server({ - host: config.host, - port: config.ssl.redirectHttpFromPort, - }); - - this.redirectServer.ext( - 'onRequest', - (request: Request, responseToolkit: ResponseToolkit) => { + if (!newURL) { return responseToolkit - .redirect( - formatUrl({ - hostname: config.host, - pathname: request.url.pathname, - port: config.port, - protocol: 'https', - search: request.url.search, - }) - ) + .response('Not Found') + .code(404) .takeover(); } - ); - try { - await this.redirectServer.start(); - } catch (err) { - if (err.code === 'EADDRINUSE') { - throw new Error( - 'The redirect server failed to start up because port ' + - `${ - config.ssl.redirectHttpFromPort - } is already in use. Ensure the port specified ` + - 'in `server.ssl.redirectHttpFromPort` is available.' - ); - } else { - throw err; - } - } + request.setUrl(newURL); + // We should update raw request as well since it can be proxied to the old platform + // where base path isn't expected. + request.raw.req.url = request.url.href; + + return responseToolkit.continue; + }); } private getRouteFullPath(routerPath: string, routePath: string) { diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index 141f1ea0ab028..8eba002f7194f 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -30,10 +30,12 @@ import { Env } from '../config'; import { Logger, LoggerFactory } from '../logging'; import { HttpConfig } from './http_config'; import { HttpServer } from './http_server'; +import { HttpsRedirectServer } from './https_redirect_server'; import { Router } from './router'; export class HttpService implements CoreService { private readonly httpServer: HttpServer; + private readonly httpsRedirectServer: HttpsRedirectServer; private configSubscription?: Subscription; private readonly log: Logger; @@ -44,7 +46,11 @@ export class HttpService implements CoreService { env: Env ) { this.log = logger.get('http'); + this.httpServer = new HttpServer(logger.get('http', 'server'), env); + this.httpsRedirectServer = new HttpsRedirectServer( + logger.get('http', 'redirect', 'server') + ); } public async start() { @@ -60,6 +66,13 @@ export class HttpService implements CoreService { }); const config = await k$(this.config$)(first(), toPromise()); + + // If a redirect port is specified, we start an HTTP server at this port and + // redirect all requests to the SSL port. + if (config.ssl.enabled && config.ssl.redirectHttpFromPort !== undefined) { + await this.httpsRedirectServer.start(config); + } + await this.httpServer.start(config); } @@ -72,6 +85,7 @@ export class HttpService implements CoreService { this.configSubscription = undefined; await this.httpServer.stop(); + await this.httpsRedirectServer.stop(); } public registerRouter(router: Router): void { diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts new file mode 100644 index 0000000000000..0da8cff821249 --- /dev/null +++ b/src/core/server/http/http_tools.ts @@ -0,0 +1,95 @@ +/* + * 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. + */ + +import { readFileSync } from 'fs'; +import { Server, ServerOptions } from 'hapi-latest'; +import { ServerOptions as TLSOptions } from 'https'; +import { HttpConfig } from './http_config'; + +/** + * Converts Kibana `HttpConfig` into `ServerOptions` that are accepted by the Hapi server. + */ +export function getServerOptions( + config: HttpConfig, + { configureTLS = true } = {} +) { + // Note that all connection options configured here should be exactly the same + // as in the legacy platform server (see `src/server/http/index`). Any change + // SHOULD BE applied in both places. The only exception is TLS-specific options, + // that are configured only here. + const options: ServerOptions = { + host: config.host, + port: config.port, + routes: { + cors: config.cors, + payload: { + maxBytes: config.maxPayload.getValueInBytes(), + }, + validate: { + options: { + abortEarly: false, + }, + }, + }, + state: { + strictHeader: false, + }, + }; + + if (configureTLS && config.ssl.enabled) { + const ssl = config.ssl; + + // TODO: Hapi types have a typo in `tls` property type definition: `https.RequestOptions` is used instead of + // `https.ServerOptions`, and `honorCipherOrder` isn't presented in `https.RequestOptions`. + const tlsOptions: TLSOptions = { + ca: + config.ssl.certificateAuthorities && + config.ssl.certificateAuthorities.map(caFilePath => + readFileSync(caFilePath) + ), + cert: readFileSync(ssl.certificate!), + ciphers: config.ssl.cipherSuites.join(':'), + // We use the server's cipher order rather than the client's to prevent the BEAST attack. + honorCipherOrder: true, + key: readFileSync(ssl.key!), + passphrase: ssl.keyPassphrase, + secureOptions: ssl.getSecureOptions(), + }; + + options.tls = tlsOptions; + } + + return options; +} + +export function createServer(options: ServerOptions) { + const server = new Server(options); + + // Revert to previous 120 seconds keep-alive timeout in Node < 8. + server.listener.keepAliveTimeout = 120e3; + server.listener.on('clientError', (err, socket) => { + if (socket.writable) { + socket.end(new Buffer('HTTP/1.1 400 Bad Request\r\n\r\n', 'ascii')); + } else { + socket.destroy(err); + } + }); + + return server; +} diff --git a/src/core/server/http/https_redirect_server.ts b/src/core/server/http/https_redirect_server.ts new file mode 100644 index 0000000000000..967de7ffab510 --- /dev/null +++ b/src/core/server/http/https_redirect_server.ts @@ -0,0 +1,96 @@ +/* + * 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. + */ + +import { Request, ResponseToolkit, Server } from 'hapi-latest'; +import { format as formatUrl } from 'url'; + +import { Logger } from '../logging'; +import { HttpConfig } from './http_config'; +import { createServer, getServerOptions } from './http_tools'; + +export class HttpsRedirectServer { + private server?: Server; + + constructor(private readonly log: Logger) {} + + public async start(config: HttpConfig) { + if (!config.ssl.enabled || config.ssl.redirectHttpFromPort === undefined) { + throw new Error( + 'Redirect server cannot be started when [ssl.enabled] is set to `false`' + + ' or [ssl.redirectHttpFromPort] is not specified.' + ); + } + + this.log.info( + `starting HTTP --> HTTPS redirect server [${config.host}:${ + config.ssl.redirectHttpFromPort + }]` + ); + + // Redirect server is configured in the same way as any other HTTP server + // within the platform with the only exception that it should always be a + // plain HTTP server, so we just ignore `tls` part of options. + this.server = createServer({ + ...getServerOptions(config, { configureTLS: false }), + port: config.ssl.redirectHttpFromPort, + }); + + this.server.ext( + 'onRequest', + (request: Request, responseToolkit: ResponseToolkit) => { + return responseToolkit + .redirect( + formatUrl({ + hostname: config.host, + pathname: request.url.pathname, + port: config.port, + protocol: 'https', + search: request.url.search, + }) + ) + .takeover(); + } + ); + + try { + await this.server.start(); + } catch (err) { + if (err.code === 'EADDRINUSE') { + throw new Error( + 'The redirect server failed to start up because port ' + + `${ + config.ssl.redirectHttpFromPort + } is already in use. Ensure the port specified ` + + 'in `server.ssl.redirectHttpFromPort` is available.' + ); + } else { + throw err; + } + } + } + + public async stop() { + this.log.info('stopping HTTPS redirect server'); + + if (this.server !== undefined) { + await this.server.stop(); + this.server = undefined; + } + } +} diff --git a/src/core/server/legacy_compat/index.ts b/src/core/server/legacy_compat/index.ts index 5e6930e65347f..566ae50ea2a88 100644 --- a/src/core/server/legacy_compat/index.ts +++ b/src/core/server/legacy_compat/index.ts @@ -36,16 +36,23 @@ import { import { BehaviorSubject, k$, map } from '../../lib/kbn_observable'; import { Env } from '../config'; import { Root } from '../root'; +import { BasePathProxyRoot } from '../root/base_path_proxy_root'; -/** - * @internal - */ -export const injectIntoKbnServer = (rawKbnServer: any) => { +function getConfigs(rawKbnServer: any) { const legacyConfig$ = new BehaviorSubject(rawKbnServer.config); const config$ = k$(legacyConfig$)( map(legacyConfig => new LegacyConfigToRawConfigAdapter(legacyConfig)) ); + return { legacyConfig$, config$ }; +} + +/** + * @internal + */ +export const injectIntoKbnServer = (rawKbnServer: any) => { + const { legacyConfig$, config$ } = getConfigs(rawKbnServer); + rawKbnServer.newPlatform = { // Custom HTTP Listener that will be used within legacy platform by HapiJS server. proxyListener: new LegacyPlatformProxifier( @@ -61,3 +68,12 @@ export const injectIntoKbnServer = (rawKbnServer: any) => { }, }; }; + +export const createBasePathProxy = (rawKbnServer: any) => { + const { config$ } = getConfigs(rawKbnServer); + + return new BasePathProxyRoot( + config$, + Env.createDefault({ kbnServer: new LegacyKbnServer(rawKbnServer) }) + ); +}; diff --git a/src/core/server/root/base_path_proxy_root.ts b/src/core/server/root/base_path_proxy_root.ts new file mode 100644 index 0000000000000..22d3c805e2b54 --- /dev/null +++ b/src/core/server/root/base_path_proxy_root.ts @@ -0,0 +1,80 @@ +/* + * 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. + */ + +import { first, k$, toPromise } from '../../lib/kbn_observable'; + +import { Root } from '.'; +import { DevConfig } from '../dev'; +import { HttpConfig } from '../http'; +import { + BasePathProxyServer, + BasePathProxyServerOptions, +} from '../http/base_path_proxy_server'; + +/** + * Top-level entry point to start BasePathProxy server. + */ +export class BasePathProxyRoot extends Root { + private basePathProxy?: BasePathProxyServer; + + public async configure({ + blockUntil, + shouldRedirectFromOldBasePath, + }: Pick< + BasePathProxyServerOptions, + 'blockUntil' | 'shouldRedirectFromOldBasePath' + >) { + const [devConfig, httpConfig] = await Promise.all([ + k$(this.configService.atPath('dev', DevConfig))(first(), toPromise()), + k$(this.configService.atPath('server', HttpConfig))(first(), toPromise()), + ]); + + this.basePathProxy = new BasePathProxyServer(this.logger.get('server'), { + blockUntil, + devConfig, + httpConfig, + shouldRedirectFromOldBasePath, + }); + } + + public getBasePath() { + return this.getBasePathProxy().basePath; + } + + public getTargetPort() { + return this.getBasePathProxy().targetPort; + } + + protected async startServer() { + return this.getBasePathProxy().start(); + } + + protected async stopServer() { + await this.getBasePathProxy().stop(); + this.basePathProxy = undefined; + } + + private getBasePathProxy() { + if (this.basePathProxy === undefined) { + throw new Error('BasePathProxyRoot is not configured!'); + } + + return this.basePathProxy; + } +} diff --git a/src/core/server/root/index.ts b/src/core/server/root/index.ts index ad80076bc08f4..0e42c71ee860a 100644 --- a/src/core/server/root/index.ts +++ b/src/core/server/root/index.ts @@ -34,9 +34,9 @@ export type OnShutdown = (reason?: Error) => void; */ export class Root { public configService: ConfigService; - public server?: Server; public readonly log: Logger; public readonly logger: LoggerFactory; + private server?: Server; private readonly loggingService: LoggingService; constructor( @@ -71,12 +71,8 @@ export class Root { throw e; } - this.log.info('starting the server'); - - this.server = new Server(this.configService, this.logger, this.env); - try { - await this.server.start(); + await this.startServer(); } catch (e) { this.log.error(e); @@ -86,13 +82,24 @@ export class Root { } public async shutdown(reason?: Error) { - this.log.info('stopping Kibana'); - if (this.server !== undefined) { - await this.server.stop(); - } + await this.stopServer(); await this.loggingService.stop(); this.onShutdown(reason); } + + protected async startServer() { + this.server = new Server(this.configService, this.logger, this.env); + return this.server.start(); + } + + protected async stopServer() { + if (this.server === undefined) { + return; + } + + await this.server.stop(); + this.server = undefined; + } } diff --git a/src/server/http/index.js b/src/server/http/index.js index 78f8b41f145ec..690451bb2cbca 100644 --- a/src/server/http/index.js +++ b/src/server/http/index.js @@ -35,10 +35,27 @@ export default async function (kbnServer, server, config) { const shortUrlLookup = shortUrlLookupProvider(server); + // Note that all connection options configured here should be exactly the same + // as in `getServerOptions()` in the new platform (see `src/core/server/http/http_tools`). + // Any change SHOULD BE applied in both places. server.connection({ host: config.get('server.host'), port: config.get('server.port'), - listener: kbnServer.newPlatform.proxyListener + listener: kbnServer.newPlatform.proxyListener, + state: { + strictHeader: false, + }, + routes: { + cors: config.get('server.cors'), + payload: { + maxBytes: config.get('server.maxPayloadBytes'), + }, + validate: { + options: { + abortEarly: false, + }, + }, + }, }); registerHapiPlugins(server); diff --git a/src/server/http/setup_connection.js b/src/server/http/setup_connection.js index 4b02ee10e3221..e69de29bb2d1d 100644 --- a/src/server/http/setup_connection.js +++ b/src/server/http/setup_connection.js @@ -1,85 +0,0 @@ -/* - * 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. - */ - -import { readFileSync } from 'fs'; -import secureOptions from './secure_options'; - -export function setupConnection(server, config) { - const host = config.get('server.host'); - const port = config.get('server.port'); - - const connectionOptions = { - host, - port, - state: { - strictHeader: false - }, - routes: { - cors: config.get('server.cors'), - payload: { - maxBytes: config.get('server.maxPayloadBytes') - }, - validate: { - options: { - abortEarly: false - } - } - } - }; - - const useSsl = config.get('server.ssl.enabled'); - - // not using https? well that's easy! - if (!useSsl) { - const connection = server.connection(connectionOptions); - - // revert to previous 5m keepalive timeout in Node < 8 - connection.listener.keepAliveTimeout = 120e3; - - return; - } - - const connection = server.connection({ - ...connectionOptions, - tls: { - key: readFileSync(config.get('server.ssl.key')), - cert: readFileSync(config.get('server.ssl.certificate')), - ca: config.get('server.ssl.certificateAuthorities').map(ca => readFileSync(ca, 'utf8')), - passphrase: config.get('server.ssl.keyPassphrase'), - - ciphers: config.get('server.ssl.cipherSuites').join(':'), - // We use the server's cipher order rather than the client's to prevent the BEAST attack - honorCipherOrder: true, - secureOptions: secureOptions(config.get('server.ssl.supportedProtocols')) - } - }); - - // revert to previous 5m keepalive timeout in Node < 8 - connection.listener.keepAliveTimeout = 120e3; - - const badRequestResponse = new Buffer('HTTP/1.1 400 Bad Request\r\n\r\n', 'ascii'); - connection.listener.on('clientError', (err, socket) => { - if (socket.writable) { - socket.end(badRequestResponse); - } - else { - socket.destroy(err); - } - }); -} diff --git a/yarn.lock b/yarn.lock index a95889266ba09..67da12cfaa8ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5958,6 +5958,15 @@ gulp-sourcemaps@1.7.3: through2 "2.X" vinyl "1.X" +"h2o2-latest@npm:h2o2@8.1.2": + version "8.1.2" + resolved "https://registry.yarnpkg.com/h2o2/-/h2o2-8.1.2.tgz#25e6f69f453175c9ca1e3618741c5ebe1b5000c1" + dependencies: + boom "7.x.x" + hoek "5.x.x" + joi "13.x.x" + wreck "14.x.x" + h2o2@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/h2o2/-/h2o2-5.1.1.tgz#dc09d59e8771d0ffc9f3bdba2e6b72ef6151c1e3"