[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:
Pierre Gayvallet 2024-05-21 17:11:20 +02:00 committed by GitHub
parent 2c9a89e921
commit db316ad475
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 463 additions and 170 deletions

View file

@ -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({

View file

@ -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

View file

@ -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: ... } }).

View file

@ -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',

View file

@ -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(),
}));

View file

@ -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 () => {

View file

@ -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}`);

View file

@ -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';

View file

@ -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);
}

View file

@ -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,
};
});

View 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);
});
});
});

View 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;
};

View file

@ -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,
};
}
});

View file

@ -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', () => {

View file

@ -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,
};
}

View 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,
}
`);
});
});

View 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,
};
}

View file

@ -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,

View file

@ -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);

View file

@ -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;

View file

@ -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',