mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[http] explicitly create the server listener (#183591)
## Summary Related to https://github.com/elastic/kibana/issues/7104 Adapted from https://github.com/elastic/kibana/pull/183465 For `http2` support, we will have to change the way we configure the HAPI server to manually provide the listener instead of passing down the options for HAPI to create it. This PR prepares that work, by creating the `http` or `https` (`tls`) listener and passing it when creating the HAPI server instead of just passing the `tls` options. **Note:** no integration tests were added, because we already have the right coverage for both tls and non-tls mode, so any change of behavior introduced by the PR should be detectable by them.
This commit is contained in:
parent
2c9a89e921
commit
db316ad475
21 changed files with 463 additions and 170 deletions
|
@ -10,14 +10,7 @@ import { Server, Request } from '@hapi/hapi';
|
|||
import HapiStaticFiles from '@hapi/inert';
|
||||
import url from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
createServer,
|
||||
getListenerOptions,
|
||||
getServerOptions,
|
||||
setTlsConfig,
|
||||
getRequestId,
|
||||
} from '@kbn/server-http-tools';
|
||||
|
||||
import { createServer, getServerOptions, setTlsConfig, getRequestId } from '@kbn/server-http-tools';
|
||||
import type { Duration } from 'moment';
|
||||
import { Observable, Subscription, firstValueFrom, pairwise, take } from 'rxjs';
|
||||
import apm from 'elastic-apm-node';
|
||||
|
@ -235,9 +228,8 @@ export class HttpServer {
|
|||
this.config = config;
|
||||
|
||||
const serverOptions = getServerOptions(config);
|
||||
const listenerOptions = getListenerOptions(config);
|
||||
|
||||
this.server = createServer(serverOptions, listenerOptions);
|
||||
this.server = createServer(serverOptions);
|
||||
await this.server.register([HapiStaticFiles]);
|
||||
if (config.compression.brotli.enabled) {
|
||||
await this.server.register({
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { Request, ResponseToolkit, Server } from '@hapi/hapi';
|
||||
import { format as formatUrl } from 'url';
|
||||
import { createServer, getListenerOptions, getServerOptions } from '@kbn/server-http-tools';
|
||||
import { createServer, getServerOptions } from '@kbn/server-http-tools';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
|
||||
import { HttpConfig } from './http_config';
|
||||
|
@ -31,13 +31,10 @@ export class HttpsRedirectServer {
|
|||
// 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,
|
||||
},
|
||||
getListenerOptions(config)
|
||||
);
|
||||
this.server = createServer({
|
||||
...getServerOptions(config, { configureTLS: false }),
|
||||
port: config.ssl.redirectHttpFromPort,
|
||||
});
|
||||
|
||||
this.server.ext('onRequest', (request: Request, responseToolkit: ResponseToolkit) => {
|
||||
return responseToolkit
|
||||
|
|
|
@ -15,7 +15,7 @@ import { sampleSize } from 'lodash';
|
|||
import * as Rx from 'rxjs';
|
||||
import { take } from 'rxjs';
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
import { createServer, getListenerOptions, getServerOptions } from '@kbn/server-http-tools';
|
||||
import { createServer, getServerOptions } from '@kbn/server-http-tools';
|
||||
|
||||
import { DevConfig, HttpConfig } from './config';
|
||||
import { Log } from './log';
|
||||
|
@ -67,8 +67,7 @@ export class BasePathProxyServer {
|
|||
|
||||
public async start(options: BasePathProxyServerOptions) {
|
||||
const serverOptions = getServerOptions(this.httpConfig);
|
||||
const listenerOptions = getListenerOptions(this.httpConfig);
|
||||
this.server = createServer(serverOptions, listenerOptions);
|
||||
this.server = createServer(serverOptions);
|
||||
|
||||
// Register hapi plugin that adds proxying functionality. It can be configured
|
||||
// through the route configuration object (see { handler: { proxy: ... } }).
|
||||
|
|
|
@ -10,12 +10,7 @@ import { Server } from '@hapi/hapi';
|
|||
import { EMPTY } from 'rxjs';
|
||||
import moment from 'moment';
|
||||
import supertest from 'supertest';
|
||||
import {
|
||||
getServerOptions,
|
||||
getListenerOptions,
|
||||
createServer,
|
||||
IHttpConfig,
|
||||
} from '@kbn/server-http-tools';
|
||||
import { getServerOptions, createServer, type IHttpConfig } from '@kbn/server-http-tools';
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
|
||||
import { BasePathProxyServer, BasePathProxyServerOptions } from '../base_path_proxy_server';
|
||||
|
@ -51,8 +46,7 @@ describe('BasePathProxyServer', () => {
|
|||
};
|
||||
|
||||
const serverOptions = getServerOptions(config);
|
||||
const listenerOptions = getListenerOptions(config);
|
||||
server = createServer(serverOptions, listenerOptions);
|
||||
server = createServer(serverOptions);
|
||||
|
||||
// setup and start the proxy server
|
||||
const proxyConfig: IHttpConfig = { ...config, port: 10013 };
|
||||
|
@ -276,8 +270,7 @@ describe('BasePathProxyServer', () => {
|
|||
} as IHttpConfig;
|
||||
|
||||
const serverOptions = getServerOptions(configWithBasePath);
|
||||
const listenerOptions = getListenerOptions(configWithBasePath);
|
||||
server = createServer(serverOptions, listenerOptions);
|
||||
server = createServer(serverOptions);
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { sslSchema, getServerOptions, getListenerOptions } from '@kbn/server-http-tools';
|
||||
import { sslSchema, getServerOptions } from '@kbn/server-http-tools';
|
||||
|
||||
export const hapiStartMock = jest.fn();
|
||||
export const hapiStopMock = jest.fn();
|
||||
|
@ -18,12 +18,10 @@ export const createServerMock = jest.fn().mockImplementation(() => ({
|
|||
route: hapiRouteMock,
|
||||
}));
|
||||
export const getServerOptionsMock = jest.fn().mockImplementation(getServerOptions);
|
||||
export const getListenerOptionsMock = jest.fn().mockImplementation(getListenerOptions);
|
||||
|
||||
jest.doMock('@kbn/server-http-tools', () => ({
|
||||
createServer: createServerMock,
|
||||
getServerOptions: getServerOptionsMock,
|
||||
getListenerOptions: getListenerOptionsMock,
|
||||
sslSchema,
|
||||
SslConfig: jest.fn(),
|
||||
}));
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import {
|
||||
createServerMock,
|
||||
getServerOptionsMock,
|
||||
getListenerOptionsMock,
|
||||
hapiStartMock,
|
||||
hapiStopMock,
|
||||
hapiRouteMock,
|
||||
|
@ -56,9 +55,6 @@ describe('Server', () => {
|
|||
expect(getServerOptionsMock.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({ ...mockConfig })
|
||||
);
|
||||
expect(getListenerOptionsMock.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({ ...mockConfig })
|
||||
);
|
||||
});
|
||||
|
||||
test('starts the Hapi server', async () => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import type { Server as HapiServer, ServerRoute as HapiServerRoute } from '@hapi/hapi';
|
||||
import { createServer, getServerOptions, getListenerOptions } from '@kbn/server-http-tools';
|
||||
import { createServer, getServerOptions } from '@kbn/server-http-tools';
|
||||
import type { IConfigService } from '@kbn/config';
|
||||
import type { Logger, LoggerFactory } from '@kbn/logging';
|
||||
import { ServerConfig } from './server_config';
|
||||
|
@ -40,7 +40,7 @@ export class Server {
|
|||
|
||||
async start(): Promise<ServerStart> {
|
||||
const serverConfig = new ServerConfig(this.config.atPathSync<ServerConfigType>('server'));
|
||||
this.server = createServer(getServerOptions(serverConfig), getListenerOptions(serverConfig));
|
||||
this.server = createServer(getServerOptions(serverConfig));
|
||||
|
||||
await this.server.start();
|
||||
this.log.info(`Server running on ${this.server.info.uri}`);
|
||||
|
|
|
@ -9,8 +9,9 @@
|
|||
export type { IHttpConfig, ISslConfig, ICorsConfig } from './src/types';
|
||||
export { createServer } from './src/create_server';
|
||||
export { defaultValidationErrorHandler } from './src/default_validation_error_handler';
|
||||
export { getListenerOptions } from './src/get_listener_options';
|
||||
export { getServerOptions, getServerTLSOptions } from './src/get_server_options';
|
||||
export { getServerListener } from './src/get_listener';
|
||||
export { getServerOptions } from './src/get_server_options';
|
||||
export { getServerTLSOptions } from './src/get_tls_options';
|
||||
export { getRequestId } from './src/get_request_id';
|
||||
export { setTlsConfig } from './src/set_tls_config';
|
||||
export { sslSchema, SslConfig } from './src/ssl';
|
||||
|
|
|
@ -7,23 +7,7 @@
|
|||
*/
|
||||
|
||||
import { Server, ServerOptions } from '@hapi/hapi';
|
||||
import { ListenerOptions } from './get_listener_options';
|
||||
|
||||
export function createServer(serverOptions: ServerOptions, listenerOptions: ListenerOptions) {
|
||||
const server = new Server(serverOptions);
|
||||
|
||||
server.listener.keepAliveTimeout = listenerOptions.keepaliveTimeout;
|
||||
server.listener.setTimeout(listenerOptions.socketTimeout);
|
||||
server.listener.on('timeout', (socket) => {
|
||||
socket.destroy();
|
||||
});
|
||||
server.listener.on('clientError', (err, socket) => {
|
||||
if (socket.writable) {
|
||||
socket.end(Buffer.from('HTTP/1.1 400 Bad Request\r\n\r\n', 'ascii'));
|
||||
} else {
|
||||
socket.destroy(err);
|
||||
}
|
||||
});
|
||||
|
||||
return server;
|
||||
export function createServer(serverOptions: ServerOptions) {
|
||||
return new Server(serverOptions);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const getServerTLSOptionsMock = jest.fn();
|
||||
|
||||
jest.doMock('./get_tls_options', () => {
|
||||
const actual = jest.requireActual('./get_tls_options');
|
||||
return {
|
||||
...actual,
|
||||
getServerTLSOptions: getServerTLSOptionsMock,
|
||||
};
|
||||
});
|
||||
|
||||
export const createHttpServerMock = jest.fn(() => {
|
||||
return {
|
||||
on: jest.fn(),
|
||||
setTimeout: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('http', () => {
|
||||
const actual = jest.requireActual('http');
|
||||
return {
|
||||
...actual,
|
||||
createServer: createHttpServerMock,
|
||||
};
|
||||
});
|
||||
|
||||
export const createHttpsServerMock = jest.fn(() => {
|
||||
return {
|
||||
on: jest.fn(),
|
||||
setTimeout: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('https', () => {
|
||||
const actual = jest.requireActual('https');
|
||||
return {
|
||||
...actual,
|
||||
createServer: createHttpsServerMock,
|
||||
};
|
||||
});
|
139
packages/kbn-server-http-tools/src/get_listener.test.ts
Normal file
139
packages/kbn-server-http-tools/src/get_listener.test.ts
Normal file
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
getServerTLSOptionsMock,
|
||||
createHttpServerMock,
|
||||
createHttpsServerMock,
|
||||
} from './get_listener.test.mocks';
|
||||
import moment from 'moment';
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
import type { IHttpConfig } from './types';
|
||||
import { getServerListener } from './get_listener';
|
||||
|
||||
const createConfig = (parts: Partial<IHttpConfig>): IHttpConfig => ({
|
||||
host: 'localhost',
|
||||
port: 5601,
|
||||
socketTimeout: 120000,
|
||||
keepaliveTimeout: 120000,
|
||||
payloadTimeout: 20000,
|
||||
shutdownTimeout: moment.duration(30, 'seconds'),
|
||||
maxPayload: ByteSizeValue.parse('1048576b'),
|
||||
...parts,
|
||||
cors: {
|
||||
enabled: false,
|
||||
allowCredentials: false,
|
||||
allowOrigin: ['*'],
|
||||
...parts.cors,
|
||||
},
|
||||
ssl: {
|
||||
enabled: false,
|
||||
...parts.ssl,
|
||||
},
|
||||
restrictInternalApis: false,
|
||||
});
|
||||
|
||||
describe('getServerListener', () => {
|
||||
beforeEach(() => {
|
||||
getServerTLSOptionsMock.mockReset();
|
||||
createHttpServerMock.mockClear();
|
||||
createHttpsServerMock.mockClear();
|
||||
});
|
||||
|
||||
describe('when TLS is enabled', () => {
|
||||
it('calls getServerTLSOptions with the correct parameters', () => {
|
||||
const config = createConfig({ ssl: { enabled: true } });
|
||||
|
||||
getServerListener(config);
|
||||
|
||||
expect(getServerTLSOptionsMock).toHaveBeenCalledTimes(1);
|
||||
expect(getServerTLSOptionsMock).toHaveBeenCalledWith(config.ssl);
|
||||
});
|
||||
|
||||
it('calls https.createServer with the correct parameters', () => {
|
||||
const config = createConfig({ ssl: { enabled: true } });
|
||||
|
||||
getServerTLSOptionsMock.mockReturnValue({ stub: true });
|
||||
|
||||
getServerListener(config);
|
||||
|
||||
expect(createHttpsServerMock).toHaveBeenCalledTimes(1);
|
||||
expect(createHttpsServerMock).toHaveBeenCalledWith({
|
||||
stub: true,
|
||||
keepAliveTimeout: config.keepaliveTimeout,
|
||||
});
|
||||
});
|
||||
|
||||
it('properly configures the listener', () => {
|
||||
const config = createConfig({ ssl: { enabled: true } });
|
||||
const server = getServerListener(config);
|
||||
|
||||
expect(server.setTimeout).toHaveBeenCalledTimes(1);
|
||||
expect(server.setTimeout).toHaveBeenCalledWith(config.socketTimeout);
|
||||
|
||||
expect(server.on).toHaveBeenCalledTimes(2);
|
||||
expect(server.on).toHaveBeenCalledWith('clientError', expect.any(Function));
|
||||
expect(server.on).toHaveBeenCalledWith('timeout', expect.any(Function));
|
||||
});
|
||||
|
||||
it('returns the https server', () => {
|
||||
const config = createConfig({ ssl: { enabled: true } });
|
||||
|
||||
const server = getServerListener(config);
|
||||
|
||||
const expectedServer = createHttpsServerMock.mock.results[0].value;
|
||||
|
||||
expect(server).toBe(expectedServer);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when TLS is disabled', () => {
|
||||
it('does not call getServerTLSOptions', () => {
|
||||
const config = createConfig({ ssl: { enabled: false } });
|
||||
|
||||
getServerListener(config);
|
||||
|
||||
expect(getServerTLSOptionsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls http.createServer with the correct parameters', () => {
|
||||
const config = createConfig({ ssl: { enabled: false } });
|
||||
|
||||
getServerTLSOptionsMock.mockReturnValue({ stub: true });
|
||||
|
||||
getServerListener(config);
|
||||
|
||||
expect(createHttpServerMock).toHaveBeenCalledTimes(1);
|
||||
expect(createHttpServerMock).toHaveBeenCalledWith({
|
||||
keepAliveTimeout: config.keepaliveTimeout,
|
||||
});
|
||||
});
|
||||
|
||||
it('properly configures the listener', () => {
|
||||
const config = createConfig({ ssl: { enabled: false } });
|
||||
const server = getServerListener(config);
|
||||
|
||||
expect(server.setTimeout).toHaveBeenCalledTimes(1);
|
||||
expect(server.setTimeout).toHaveBeenCalledWith(config.socketTimeout);
|
||||
|
||||
expect(server.on).toHaveBeenCalledTimes(2);
|
||||
expect(server.on).toHaveBeenCalledWith('clientError', expect.any(Function));
|
||||
expect(server.on).toHaveBeenCalledWith('timeout', expect.any(Function));
|
||||
});
|
||||
|
||||
it('returns the http server', () => {
|
||||
const config = createConfig({ ssl: { enabled: false } });
|
||||
|
||||
const server = getServerListener(config);
|
||||
|
||||
const expectedServer = createHttpServerMock.mock.results[0].value;
|
||||
|
||||
expect(server).toBe(expectedServer);
|
||||
});
|
||||
});
|
||||
});
|
54
packages/kbn-server-http-tools/src/get_listener.ts
Normal file
54
packages/kbn-server-http-tools/src/get_listener.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import { getServerTLSOptions } from './get_tls_options';
|
||||
import type { IHttpConfig, ServerListener } from './types';
|
||||
|
||||
interface GetServerListenerOptions {
|
||||
configureTLS?: boolean;
|
||||
}
|
||||
|
||||
export function getServerListener(
|
||||
config: IHttpConfig,
|
||||
options: GetServerListenerOptions = {}
|
||||
): ServerListener {
|
||||
return configureHttp1Listener(config, options);
|
||||
}
|
||||
|
||||
const configureHttp1Listener = (
|
||||
config: IHttpConfig,
|
||||
{ configureTLS = true }: GetServerListenerOptions = {}
|
||||
): ServerListener => {
|
||||
const useTLS = configureTLS && config.ssl.enabled;
|
||||
const tlsOptions = useTLS ? getServerTLSOptions(config.ssl) : undefined;
|
||||
|
||||
const listener = useTLS
|
||||
? https.createServer({
|
||||
...tlsOptions,
|
||||
keepAliveTimeout: config.keepaliveTimeout,
|
||||
})
|
||||
: http.createServer({
|
||||
keepAliveTimeout: config.keepaliveTimeout,
|
||||
});
|
||||
|
||||
listener.setTimeout(config.socketTimeout);
|
||||
listener.on('timeout', (socket) => {
|
||||
socket.destroy();
|
||||
});
|
||||
listener.on('clientError', (err, socket) => {
|
||||
if (socket.writable) {
|
||||
socket.end(Buffer.from('HTTP/1.1 400 Bad Request\r\n\r\n', 'ascii'));
|
||||
} else {
|
||||
socket.destroy(err);
|
||||
}
|
||||
});
|
||||
|
||||
return listener;
|
||||
};
|
|
@ -6,16 +6,12 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { IHttpConfig } from './types';
|
||||
export const getServerListenerMock = jest.fn();
|
||||
|
||||
export interface ListenerOptions {
|
||||
keepaliveTimeout: number;
|
||||
socketTimeout: number;
|
||||
}
|
||||
|
||||
export function getListenerOptions(config: IHttpConfig): ListenerOptions {
|
||||
jest.doMock('./get_listener', () => {
|
||||
const actual = jest.requireActual('./get_listener');
|
||||
return {
|
||||
keepaliveTimeout: config.keepaliveTimeout,
|
||||
socketTimeout: config.socketTimeout,
|
||||
...actual,
|
||||
getServerListener: getServerListenerMock,
|
||||
};
|
||||
}
|
||||
});
|
|
@ -6,10 +6,11 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { getServerListenerMock } from './get_server_options.test.mocks';
|
||||
import moment from 'moment';
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
import type { IHttpConfig } from './types';
|
||||
import { getServerOptions } from './get_server_options';
|
||||
import { IHttpConfig } from './types';
|
||||
|
||||
jest.mock('fs', () => {
|
||||
const original = jest.requireActual('fs');
|
||||
|
@ -43,69 +44,42 @@ const createConfig = (parts: Partial<IHttpConfig>): IHttpConfig => ({
|
|||
});
|
||||
|
||||
describe('getServerOptions', () => {
|
||||
beforeEach(() =>
|
||||
jest.requireMock('fs').readFileSync.mockImplementation((path: string) => `content-${path}`)
|
||||
);
|
||||
beforeEach(() => {
|
||||
jest.requireMock('fs').readFileSync.mockImplementation((path: string) => `content-${path}`);
|
||||
getServerListenerMock.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('properly configures TLS with default options', () => {
|
||||
const httpConfig = createConfig({
|
||||
ssl: {
|
||||
enabled: true,
|
||||
key: 'some-key-path',
|
||||
certificate: 'some-certificate-path',
|
||||
},
|
||||
});
|
||||
it('calls `getServerListener` to retrieve the listener that will be provided in the config', () => {
|
||||
const listener = Symbol('listener');
|
||||
getServerListenerMock.mockReturnValue(listener);
|
||||
|
||||
expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"ca": undefined,
|
||||
"cert": "some-certificate-path",
|
||||
"ciphers": undefined,
|
||||
"honorCipherOrder": true,
|
||||
"key": "some-key-path",
|
||||
"passphrase": undefined,
|
||||
"rejectUnauthorized": undefined,
|
||||
"requestCert": undefined,
|
||||
"secureOptions": undefined,
|
||||
}
|
||||
`);
|
||||
const httpConfig = createConfig({});
|
||||
const serverOptions = getServerOptions(httpConfig, { configureTLS: true });
|
||||
|
||||
expect(getServerListenerMock).toHaveBeenCalledTimes(1);
|
||||
expect(getServerListenerMock).toHaveBeenCalledWith(httpConfig, { configureTLS: true });
|
||||
|
||||
expect(serverOptions.listener).toBe(listener);
|
||||
});
|
||||
|
||||
it('properly configures TLS with client authentication', () => {
|
||||
const httpConfig = createConfig({
|
||||
ssl: {
|
||||
enabled: true,
|
||||
key: 'some-key-path',
|
||||
certificate: 'some-certificate-path',
|
||||
certificateAuthorities: ['ca-1', 'ca-2'],
|
||||
cipherSuites: ['suite-a', 'suite-b'],
|
||||
keyPassphrase: 'passPhrase',
|
||||
rejectUnauthorized: true,
|
||||
requestCert: true,
|
||||
getSecureOptions: () => 42,
|
||||
},
|
||||
});
|
||||
|
||||
expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"ca": Array [
|
||||
"ca-1",
|
||||
"ca-2",
|
||||
],
|
||||
"cert": "some-certificate-path",
|
||||
"ciphers": "suite-a:suite-b",
|
||||
"honorCipherOrder": true,
|
||||
"key": "some-key-path",
|
||||
"passphrase": "passPhrase",
|
||||
"rejectUnauthorized": true,
|
||||
"requestCert": true,
|
||||
"secureOptions": 42,
|
||||
}
|
||||
`);
|
||||
it('properly configures the tls option depending on the config and the configureTLS flag', () => {
|
||||
expect(
|
||||
getServerOptions(createConfig({ ssl: { enabled: true } }), { configureTLS: true }).tls
|
||||
).toBe(true);
|
||||
expect(getServerOptions(createConfig({ ssl: { enabled: true } }), {}).tls).toBe(true);
|
||||
expect(
|
||||
getServerOptions(createConfig({ ssl: { enabled: true } }), { configureTLS: false }).tls
|
||||
).toBe(false);
|
||||
expect(
|
||||
getServerOptions(createConfig({ ssl: { enabled: false } }), { configureTLS: true }).tls
|
||||
).toBe(false);
|
||||
expect(
|
||||
getServerOptions(createConfig({ ssl: { enabled: false } }), { configureTLS: false }).tls
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('properly configures CORS when cors enabled', () => {
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { RouteOptionsCors, ServerOptions } from '@hapi/hapi';
|
||||
import { ServerOptions as TLSOptions } from 'https';
|
||||
import type { RouteOptionsCors, ServerOptions } from '@hapi/hapi';
|
||||
import type { IHttpConfig } from './types';
|
||||
import { defaultValidationErrorHandler } from './default_validation_error_handler';
|
||||
import { IHttpConfig, ISslConfig } from './types';
|
||||
import { getServerListener } from './get_listener';
|
||||
|
||||
const corsAllowedHeaders = ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf'];
|
||||
|
||||
|
@ -27,6 +27,10 @@ export function getServerOptions(config: IHttpConfig, { configureTLS = true } =
|
|||
const options: ServerOptions = {
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
// manually configuring the listener
|
||||
listener: getServerListener(config, { configureTLS }),
|
||||
// must set to true when manually passing a TLS listener, false otherwise
|
||||
tls: configureTLS && config.ssl.enabled,
|
||||
routes: {
|
||||
cache: {
|
||||
privacy: 'private',
|
||||
|
@ -51,31 +55,5 @@ export function getServerOptions(config: IHttpConfig, { configureTLS = true } =
|
|||
},
|
||||
};
|
||||
|
||||
if (configureTLS) {
|
||||
options.tls = getServerTLSOptions(config.ssl);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Kibana `SslConfig` into `TLSOptions` that are accepted by the Hapi server,
|
||||
* and by https.Server.setSecureContext()
|
||||
*/
|
||||
export function getServerTLSOptions(ssl: ISslConfig): TLSOptions | undefined {
|
||||
if (!ssl.enabled) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
ca: ssl.certificateAuthorities,
|
||||
cert: ssl.certificate,
|
||||
ciphers: ssl.cipherSuites?.join(':'),
|
||||
// We use the server's cipher order rather than the client's to prevent the BEAST attack.
|
||||
honorCipherOrder: true,
|
||||
key: ssl.key,
|
||||
passphrase: ssl.keyPassphrase,
|
||||
secureOptions: ssl.getSecureOptions ? ssl.getSecureOptions() : undefined,
|
||||
requestCert: ssl.requestCert,
|
||||
rejectUnauthorized: ssl.rejectUnauthorized,
|
||||
};
|
||||
}
|
||||
|
|
110
packages/kbn-server-http-tools/src/get_tls_options.test.ts
Normal file
110
packages/kbn-server-http-tools/src/get_tls_options.test.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
import type { IHttpConfig } from './types';
|
||||
import { getServerTLSOptions } from './get_tls_options';
|
||||
|
||||
jest.mock('fs', () => {
|
||||
const original = jest.requireActual('fs');
|
||||
return {
|
||||
// Hapi Inert patches native methods
|
||||
...original,
|
||||
readFileSync: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const createConfig = (parts: Partial<IHttpConfig>): IHttpConfig => ({
|
||||
host: 'localhost',
|
||||
port: 5601,
|
||||
socketTimeout: 120000,
|
||||
keepaliveTimeout: 120000,
|
||||
payloadTimeout: 20000,
|
||||
shutdownTimeout: moment.duration(30, 'seconds'),
|
||||
maxPayload: ByteSizeValue.parse('1048576b'),
|
||||
...parts,
|
||||
cors: {
|
||||
enabled: false,
|
||||
allowCredentials: false,
|
||||
allowOrigin: ['*'],
|
||||
...parts.cors,
|
||||
},
|
||||
ssl: {
|
||||
enabled: false,
|
||||
...parts.ssl,
|
||||
},
|
||||
restrictInternalApis: false,
|
||||
});
|
||||
|
||||
describe('getServerTLSOptions', () => {
|
||||
beforeEach(() =>
|
||||
jest.requireMock('fs').readFileSync.mockImplementation((path: string) => `content-${path}`)
|
||||
);
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('properly configures TLS with default options', () => {
|
||||
const httpConfig = createConfig({
|
||||
ssl: {
|
||||
enabled: true,
|
||||
key: 'some-key-path',
|
||||
certificate: 'some-certificate-path',
|
||||
},
|
||||
});
|
||||
|
||||
expect(getServerTLSOptions(httpConfig.ssl)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"ca": undefined,
|
||||
"cert": "some-certificate-path",
|
||||
"ciphers": undefined,
|
||||
"honorCipherOrder": true,
|
||||
"key": "some-key-path",
|
||||
"passphrase": undefined,
|
||||
"rejectUnauthorized": undefined,
|
||||
"requestCert": undefined,
|
||||
"secureOptions": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('properly configures TLS with client authentication', () => {
|
||||
const httpConfig = createConfig({
|
||||
ssl: {
|
||||
enabled: true,
|
||||
key: 'some-key-path',
|
||||
certificate: 'some-certificate-path',
|
||||
certificateAuthorities: ['ca-1', 'ca-2'],
|
||||
cipherSuites: ['suite-a', 'suite-b'],
|
||||
keyPassphrase: 'passPhrase',
|
||||
rejectUnauthorized: true,
|
||||
requestCert: true,
|
||||
getSecureOptions: () => 42,
|
||||
},
|
||||
});
|
||||
|
||||
expect(getServerTLSOptions(httpConfig.ssl)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"ca": Array [
|
||||
"ca-1",
|
||||
"ca-2",
|
||||
],
|
||||
"cert": "some-certificate-path",
|
||||
"ciphers": "suite-a:suite-b",
|
||||
"honorCipherOrder": true,
|
||||
"key": "some-key-path",
|
||||
"passphrase": "passPhrase",
|
||||
"rejectUnauthorized": true,
|
||||
"requestCert": true,
|
||||
"secureOptions": 42,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
32
packages/kbn-server-http-tools/src/get_tls_options.ts
Normal file
32
packages/kbn-server-http-tools/src/get_tls_options.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ServerOptions as TLSOptions } from 'https';
|
||||
import { ISslConfig } from './types';
|
||||
|
||||
/**
|
||||
* Converts Kibana `SslConfig` into `TLSOptions` that are accepted by the Hapi server,
|
||||
* and by https.Server.setSecureContext()
|
||||
*/
|
||||
export function getServerTLSOptions(ssl: ISslConfig): TLSOptions | undefined {
|
||||
if (!ssl.enabled) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
ca: ssl.certificateAuthorities,
|
||||
cert: ssl.certificate,
|
||||
ciphers: ssl.cipherSuites?.join(':'),
|
||||
// We use the server's cipher order rather than the client's to prevent the BEAST attack.
|
||||
honorCipherOrder: true,
|
||||
key: ssl.key,
|
||||
passphrase: ssl.keyPassphrase,
|
||||
secureOptions: ssl.getSecureOptions ? ssl.getSecureOptions() : undefined,
|
||||
requestCert: ssl.requestCert,
|
||||
rejectUnauthorized: ssl.rejectUnauthorized,
|
||||
};
|
||||
}
|
|
@ -8,8 +8,8 @@
|
|||
|
||||
export const getServerTLSOptionsMock = jest.fn();
|
||||
|
||||
jest.doMock('./get_server_options', () => {
|
||||
const actual = jest.requireActual('./get_server_options');
|
||||
jest.doMock('./get_tls_options', () => {
|
||||
const actual = jest.requireActual('./get_tls_options');
|
||||
return {
|
||||
...actual,
|
||||
getServerTLSOptions: getServerTLSOptionsMock,
|
||||
|
|
|
@ -7,18 +7,17 @@
|
|||
*/
|
||||
|
||||
import type { Server as HapiServer } from '@hapi/hapi';
|
||||
import type { Server as HttpServer } from 'http';
|
||||
import type { Server as TlsServer } from 'https';
|
||||
import type { ISslConfig } from './types';
|
||||
import { getServerTLSOptions } from './get_server_options';
|
||||
import type { ISslConfig, ServerListener } from './types';
|
||||
import { getServerTLSOptions } from './get_tls_options';
|
||||
|
||||
function isServerTLS(server: HttpServer): server is TlsServer {
|
||||
function isTLSListener(server: ServerListener): server is TlsServer {
|
||||
return 'setSecureContext' in server;
|
||||
}
|
||||
|
||||
export const setTlsConfig = (hapiServer: HapiServer, sslConfig: ISslConfig) => {
|
||||
const server = hapiServer.listener;
|
||||
if (!isServerTLS(server)) {
|
||||
if (!isTLSListener(server)) {
|
||||
throw new Error('tried to set TLS config on a non-TLS http server');
|
||||
}
|
||||
const tlsOptions = getServerTLSOptions(sslConfig);
|
||||
|
|
|
@ -6,9 +6,19 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Server as HttpServer } from 'http';
|
||||
import type { Server as HttpsServer } from 'https';
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
import type { Duration } from 'moment';
|
||||
|
||||
/**
|
||||
* Composite type of all possible kind of Listener types.
|
||||
*
|
||||
* Unfortunately, there's no real common interface between all those concrete classes,
|
||||
* as `net.Server` and `tls.Server` don't list all the APIs we're using (such as event binding)
|
||||
*/
|
||||
export type ServerListener = HttpServer | HttpsServer;
|
||||
|
||||
export interface IHttpConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
|
|
|
@ -8,12 +8,7 @@
|
|||
|
||||
import supertest from 'supertest';
|
||||
import { KBN_CERT_PATH, KBN_KEY_PATH, ES_KEY_PATH, ES_CERT_PATH } from '@kbn/dev-utils';
|
||||
import {
|
||||
createServer,
|
||||
getListenerOptions,
|
||||
getServerOptions,
|
||||
setTlsConfig,
|
||||
} from '@kbn/server-http-tools';
|
||||
import { createServer, getServerOptions, setTlsConfig } from '@kbn/server-http-tools';
|
||||
import {
|
||||
HttpConfig,
|
||||
config as httpConfig,
|
||||
|
@ -47,8 +42,7 @@ describe('setTlsConfig', () => {
|
|||
const firstConfig = new HttpConfig(rawHttpConfig, CSP_CONFIG, EXTERNAL_URL_CONFIG);
|
||||
|
||||
const serverOptions = getServerOptions(firstConfig);
|
||||
const listenerOptions = getListenerOptions(firstConfig);
|
||||
const server = createServer(serverOptions, listenerOptions);
|
||||
const server = createServer(serverOptions);
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue