Skip to content

Commit

Permalink
refactor listener creation logic + add http2 listener support
Browse files Browse the repository at this point in the history
  • Loading branch information
pgayvallet committed May 17, 2024
1 parent 2205584 commit d893a06
Show file tree
Hide file tree
Showing 15 changed files with 139 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import url from 'url';
import { v4 as uuidv4 } from 'uuid';
import {
createServer,
getListenerOptions,
getServerListener,
getServerOptions,
setTlsConfig,
getRequestId,
Expand Down Expand Up @@ -235,9 +235,9 @@ export class HttpServer {
this.config = config;

const serverOptions = getServerOptions(config);
const listenerOptions = getListenerOptions(config);
const serverListener = getServerListener(config);

this.server = createServer(serverOptions, listenerOptions);
this.server = createServer(serverOptions, serverListener);
await this.server.register([HapiStaticFiles]);
if (config.compression.brotli.enabled) {
await this.server.register({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, getServerListener, getServerOptions } from '@kbn/server-http-tools';
import type { Logger } from '@kbn/logging';

import { HttpConfig } from './http_config';
Expand Down Expand Up @@ -36,7 +36,7 @@ export class HttpsRedirectServer {
...getServerOptions(config, { configureTLS: false }),
port: config.ssl.redirectHttpFromPort,
},
getListenerOptions(config)
getServerListener(config, { configureTLS: false })
);

this.server.ext('onRequest', (request: Request, responseToolkit: ResponseToolkit) => {
Expand Down
6 changes: 3 additions & 3 deletions packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, getServerListener, getServerOptions } from '@kbn/server-http-tools';

import { DevConfig, HttpConfig } from './config';
import { Log } from './log';
Expand Down Expand Up @@ -67,8 +67,8 @@ export class BasePathProxyServer {

public async start(options: BasePathProxyServerOptions) {
const serverOptions = getServerOptions(this.httpConfig);
const listenerOptions = getListenerOptions(this.httpConfig);
this.server = createServer(serverOptions, listenerOptions);
const serverListener = getServerListener(this.httpConfig);
this.server = createServer(serverOptions, serverListener);

// Register hapi plugin that adds proxying functionality. It can be configured
// through the route configuration object (see { handler: { proxy: ... } }).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import moment from 'moment';
import supertest from 'supertest';
import {
getServerOptions,
getListenerOptions,
getServerListener,
createServer,
IHttpConfig,
} from '@kbn/server-http-tools';
Expand All @@ -34,6 +34,7 @@ describe('BasePathProxyServer', () => {
logger = new TestLog();

config = {
protocol: 'http1',
host: '127.0.0.1',
port: 10012,
shutdownTimeout: moment.duration(30, 'seconds'),
Expand All @@ -51,8 +52,8 @@ describe('BasePathProxyServer', () => {
};

const serverOptions = getServerOptions(config);
const listenerOptions = getListenerOptions(config);
server = createServer(serverOptions, listenerOptions);
const serverListener = getServerListener(config);
server = createServer(serverOptions, serverListener);

// setup and start the proxy server
const proxyConfig: IHttpConfig = { ...config, port: 10013 };
Expand Down Expand Up @@ -276,8 +277,8 @@ describe('BasePathProxyServer', () => {
} as IHttpConfig;

const serverOptions = getServerOptions(configWithBasePath);
const listenerOptions = getListenerOptions(configWithBasePath);
server = createServer(serverOptions, listenerOptions);
const serverListener = getServerListener(configWithBasePath);
server = createServer(serverOptions, serverListener);

server.route({
method: 'GET',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Side Public License, v 1.
*/

import { sslSchema, getServerOptions, getListenerOptions } from '@kbn/server-http-tools';
import { sslSchema, getServerOptions, getServerListener } from '@kbn/server-http-tools';

export const hapiStartMock = jest.fn();
export const hapiStopMock = jest.fn();
Expand All @@ -18,12 +18,12 @@ export const createServerMock = jest.fn().mockImplementation(() => ({
route: hapiRouteMock,
}));
export const getServerOptionsMock = jest.fn().mockImplementation(getServerOptions);
export const getListenerOptionsMock = jest.fn().mockImplementation(getListenerOptions);
export const getServerListenerMock = jest.fn().mockImplementation(getServerListener);

jest.doMock('@kbn/server-http-tools', () => ({
createServer: createServerMock,
getServerOptions: getServerOptionsMock,
getListenerOptions: getListenerOptionsMock,
getServerListener: getServerListenerMock,
sslSchema,
SslConfig: jest.fn(),
}));
4 changes: 2 additions & 2 deletions packages/kbn-health-gateway-server/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, getServerListener } from '@kbn/server-http-tools';
import type { IConfigService } from '@kbn/config';
import type { Logger, LoggerFactory } from '@kbn/logging';
import { ServerConfig } from './server_config';
Expand Down Expand Up @@ -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), getServerListener(serverConfig));

await this.server.start();
this.log.info(`Server running on ${this.server.info.uri}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export const config: ServiceConfigDescriptor<ServerConfigType> = {
};

export class ServerConfig implements IHttpConfig {
readonly protocol = 'http1';
host: string;
port: number;
maxPayload: ByteSizeValue;
Expand Down
13 changes: 10 additions & 3 deletions packages/kbn-server-http-tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@
* Side Public License, v 1.
*/

export type { IHttpConfig, ISslConfig, ICorsConfig } from './src/types';
export type {
IHttpConfig,
ISslConfig,
ICorsConfig,
ServerProtocol,
ServerListener,
} 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';
23 changes: 7 additions & 16 deletions packages/kbn-server-http-tools/src/create_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,14 @@
* Side Public License, v 1.
*/

import { Server, ServerOptions } from '@hapi/hapi';
import { ListenerOptions } from './get_listener_options';
import { Server, type ServerOptions } from '@hapi/hapi';
import type { ServerListener } from './types';

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);
}
export function createServer(serverOptions: ServerOptions, listener: ServerListener): Server {
const server = new Server({
...serverOptions,
// HAPI type signatures are outdated and only define http1 listener
listener: listener as ServerOptions['listener'],
});

return server;
Expand Down
73 changes: 73 additions & 0 deletions packages/kbn-server-http-tools/src/get_listener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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 http2 from 'http2';
import { getServerTLSOptions } from './get_tls_options';
import type { IHttpConfig, ServerListener } from './types';

interface GetServerListenerOptions {
configureTLS?: boolean;
}

export function getServerListener(
config: IHttpConfig,
options: GetServerListenerOptions = {}
): ServerListener {
const useHTTP2 = config.protocol === 'http2';
return useHTTP2
? configureHttp2Listener(config, options)
: configureHttp1Listener(config, options);
}

export 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('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;
};

export const configureHttp2Listener = (
config: IHttpConfig,
{ configureTLS = true }: GetServerListenerOptions = {}
): ServerListener => {
const useTLS = configureTLS && config.ssl.enabled;
const tlsOptions = useTLS ? getServerTLSOptions(config.ssl) : undefined;

const listener = useTLS
? http2.createSecureServer({
...tlsOptions,
})
: http2.createServer({});

listener.setTimeout(config.socketTimeout);

return listener;
};
21 changes: 0 additions & 21 deletions packages/kbn-server-http-tools/src/get_listener_options.ts

This file was deleted.

13 changes: 7 additions & 6 deletions packages/kbn-server-http-tools/src/get_server_options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,24 @@
import { RouteOptionsCors, ServerOptions } from '@hapi/hapi';
import { defaultValidationErrorHandler } from './default_validation_error_handler';
import { IHttpConfig } from './types';
import { getServerTLSOptions } from './get_tls_options';

const corsAllowedHeaders = ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf'];

/**
* Converts Kibana `HttpConfig` into `ServerOptions` that are accepted by the Hapi server.
*/
export function getServerOptions(config: IHttpConfig, { configureTLS = true } = {}) {
export function getServerOptions(
config: IHttpConfig,
{ configureTLS = true }: { configureTLS?: boolean } = {}
) {
const cors: RouteOptionsCors | false = config.cors.enabled
? {
credentials: config.cors.allowCredentials,
origin: config.cors.allowOrigin,
headers: corsAllowedHeaders,
}
: false;

const options: ServerOptions = {
host: config.host,
port: config.port,
Expand All @@ -49,11 +52,9 @@ export function getServerOptions(config: IHttpConfig, { configureTLS = true } =
isHttpOnly: true,
isSameSite: false, // necessary to allow using Kibana inside an iframe
},
// must set to true when manually passing a listener
tls: configureTLS && config.ssl.enabled,
};

if (configureTLS) {
options.tls = getServerTLSOptions(config.ssl);
}

return options;
}
9 changes: 4 additions & 5 deletions packages/kbn-server-http-tools/src/set_tls_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
17 changes: 15 additions & 2 deletions packages/kbn-server-http-tools/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,24 @@
* Side Public License, v 1.
*/

import { ByteSizeValue } from '@kbn/config-schema';
import type { Server as HttpServer } from 'http';
import type { Server as HttpsServer } from 'https';
import type { Http2SecureServer, Http2Server } from 'http2';
import type { Duration } from 'moment';
import { ByteSizeValue } from '@kbn/config-schema';

/**
* 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 = Http2Server | Http2SecureServer | HttpServer | HttpsServer;

export type ServerProtocol = 'http1' | 'http2';

export interface IHttpConfig {
protocol: 'http1' | 'http2';
protocol: ServerProtocol;
host: string;
port: number;
maxPayload: ByteSizeValue;
Expand Down
Loading

0 comments on commit d893a06

Please sign in to comment.