From 364ee522c0cb0082990212e591380731d05e50ac Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 15 May 2024 08:38:57 +0200 Subject: [PATCH 01/36] add `protocol` to the http config --- .../core-http-server-internal/src/http_config.ts | 15 +++++++++------ packages/core/http/core-http-server/index.ts | 1 + .../http/core-http-server/src/http_contract.ts | 5 +++++ .../kbn-cli-dev-mode/src/config/http_config.ts | 5 +++++ packages/kbn-server-http-tools/src/types.ts | 1 + 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/core/http/core-http-server-internal/src/http_config.ts b/packages/core/http/core-http-server-internal/src/http_config.ts index ac5072958a512..c4a286329085b 100644 --- a/packages/core/http/core-http-server-internal/src/http_config.ts +++ b/packages/core/http/core-http-server-internal/src/http_config.ts @@ -6,16 +6,14 @@ * Side Public License, v 1. */ +import { hostname, EOL } from 'node:os'; +import url, { URL } from 'node:url'; +import type { Duration } from 'moment'; import { ByteSizeValue, offeringBasedSchema, schema, TypeOf } from '@kbn/config-schema'; import { IHttpConfig, SslConfig, sslSchema } from '@kbn/server-http-tools'; import type { ServiceConfigDescriptor } from '@kbn/core-base-server-internal'; import { uuidRegexp } from '@kbn/core-base-server-internal'; -import type { ICspConfig, IExternalUrlConfig } from '@kbn/core-http-server'; - -import { hostname, EOL } from 'node:os'; -import url, { URL } from 'node:url'; - -import type { Duration } from 'moment'; +import type { ICspConfig, IExternalUrlConfig, HttpProtocol } from '@kbn/core-http-server'; import type { IHttpEluMonitorConfig } from '@kbn/core-http-server/src/elu_monitor'; import type { HandlerResolutionStrategy } from '@kbn/core-http-router-server-internal'; import { CspConfigType, CspConfig } from './csp'; @@ -121,6 +119,9 @@ const configSchema = schema.object( } }, }), + protocol: schema.oneOf([schema.literal('http1'), schema.literal('http2')], { + defaultValue: 'http1', + }), host: schema.string({ defaultValue: 'localhost', hostname: true, @@ -283,6 +284,7 @@ export const config: ServiceConfigDescriptor = { export class HttpConfig implements IHttpConfig { public name: string; public autoListen: boolean; + public protocol: HttpProtocol; public host: string; public keepaliveTimeout: number; public socketTimeout: number; @@ -350,6 +352,7 @@ export class HttpConfig implements IHttpConfig { ); this.maxPayload = rawHttpConfig.maxPayload; this.name = rawHttpConfig.name; + this.protocol = rawHttpConfig.protocol; this.basePath = rawHttpConfig.basePath; this.publicBaseUrl = rawHttpConfig.publicBaseUrl; this.keepaliveTimeout = rawHttpConfig.keepaliveTimeout; diff --git a/packages/core/http/core-http-server/index.ts b/packages/core/http/core-http-server/index.ts index 859e6ff8efbd5..2b288be521d3a 100644 --- a/packages/core/http/core-http-server/index.ts +++ b/packages/core/http/core-http-server/index.ts @@ -141,6 +141,7 @@ export type { HttpServicePreboot, HttpServiceSetup, HttpServiceStart, + HttpProtocol, } from './src/http_contract'; export type { diff --git a/packages/core/http/core-http-server/src/http_contract.ts b/packages/core/http/core-http-server/src/http_contract.ts index 09250abf8adae..4b09527017e5e 100644 --- a/packages/core/http/core-http-server/src/http_contract.ts +++ b/packages/core/http/core-http-server/src/http_contract.ts @@ -402,3 +402,8 @@ export interface HttpServerInfo { /** The protocol used by the server */ protocol: 'http' | 'https' | 'socket'; } + +/** + * @public + */ +export type HttpProtocol = 'http1' | 'http2'; diff --git a/packages/kbn-cli-dev-mode/src/config/http_config.ts b/packages/kbn-cli-dev-mode/src/config/http_config.ts index 0c2f86a89b105..3f2ac9542a328 100644 --- a/packages/kbn-cli-dev-mode/src/config/http_config.ts +++ b/packages/kbn-cli-dev-mode/src/config/http_config.ts @@ -12,6 +12,9 @@ import { Duration } from 'moment'; export const httpConfigSchema = schema.object( { + protocol: schema.oneOf([schema.literal('http1'), schema.literal('http2')], { + defaultValue: 'http1', + }), host: schema.string({ defaultValue: 'localhost', hostname: true, @@ -49,6 +52,7 @@ export const httpConfigSchema = schema.object( export type HttpConfigType = TypeOf; export class HttpConfig implements IHttpConfig { + protocol: 'http1' | 'http2'; basePath?: string; host: string; port: number; @@ -62,6 +66,7 @@ export class HttpConfig implements IHttpConfig { restrictInternalApis: boolean; constructor(rawConfig: HttpConfigType) { + this.protocol = rawConfig.protocol; this.basePath = rawConfig.basePath; this.host = rawConfig.host; this.port = rawConfig.port; diff --git a/packages/kbn-server-http-tools/src/types.ts b/packages/kbn-server-http-tools/src/types.ts index 693cb6feb46fe..8bb0fecf71f9d 100644 --- a/packages/kbn-server-http-tools/src/types.ts +++ b/packages/kbn-server-http-tools/src/types.ts @@ -10,6 +10,7 @@ import { ByteSizeValue } from '@kbn/config-schema'; import type { Duration } from 'moment'; export interface IHttpConfig { + protocol: 'http1' | 'http2'; host: string; port: number; maxPayload: ByteSizeValue; From 220558493f17ee8f6236b59d2d94363a55fd1d1f Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 15 May 2024 08:52:20 +0200 Subject: [PATCH 02/36] extract getServerTLSOptions --- .../src/get_server_options.ts | 26 ++------------- .../src/get_tls_options.ts | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+), 24 deletions(-) create mode 100644 packages/kbn-server-http-tools/src/get_tls_options.ts diff --git a/packages/kbn-server-http-tools/src/get_server_options.ts b/packages/kbn-server-http-tools/src/get_server_options.ts index 37a8f5f69cc2b..a338a60aac46a 100644 --- a/packages/kbn-server-http-tools/src/get_server_options.ts +++ b/packages/kbn-server-http-tools/src/get_server_options.ts @@ -7,9 +7,9 @@ */ import { RouteOptionsCors, ServerOptions } from '@hapi/hapi'; -import { ServerOptions as TLSOptions } from 'https'; import { defaultValidationErrorHandler } from './default_validation_error_handler'; -import { IHttpConfig, ISslConfig } from './types'; +import { IHttpConfig } from './types'; +import { getServerTLSOptions } from './get_tls_options'; const corsAllowedHeaders = ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf']; @@ -57,25 +57,3 @@ export function getServerOptions(config: IHttpConfig, { configureTLS = true } = 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, - }; -} diff --git a/packages/kbn-server-http-tools/src/get_tls_options.ts b/packages/kbn-server-http-tools/src/get_tls_options.ts new file mode 100644 index 0000000000000..eb55327cef326 --- /dev/null +++ b/packages/kbn-server-http-tools/src/get_tls_options.ts @@ -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, + }; +} From d893a068d871d5aff2cc7d95f9e025d8a56e8699 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 15 May 2024 13:11:57 +0200 Subject: [PATCH 03/36] refactor listener creation logic + add http2 listener support --- .../src/http_server.ts | 6 +- .../src/https_redirect_server.ts | 4 +- .../src/base_path_proxy_server.ts | 6 +- .../base_path_proxy_server.test.ts | 11 +-- .../src/server/server.test.mocks.ts | 6 +- .../src/server/server.ts | 4 +- .../src/server/server_config.ts | 1 + packages/kbn-server-http-tools/index.ts | 13 +++- .../src/create_server.ts | 23 ++---- .../kbn-server-http-tools/src/get_listener.ts | 73 +++++++++++++++++++ .../src/get_listener_options.ts | 21 ------ .../src/get_server_options.ts | 13 ++-- .../src/set_tls_config.ts | 9 +-- packages/kbn-server-http-tools/src/types.ts | 17 ++++- .../http/set_tls_config.test.ts | 6 +- 15 files changed, 139 insertions(+), 74 deletions(-) create mode 100644 packages/kbn-server-http-tools/src/get_listener.ts delete mode 100644 packages/kbn-server-http-tools/src/get_listener_options.ts diff --git a/packages/core/http/core-http-server-internal/src/http_server.ts b/packages/core/http/core-http-server-internal/src/http_server.ts index 236b9567ddcb7..ce5fd57ad3fa4 100644 --- a/packages/core/http/core-http-server-internal/src/http_server.ts +++ b/packages/core/http/core-http-server-internal/src/http_server.ts @@ -12,7 +12,7 @@ import url from 'url'; import { v4 as uuidv4 } from 'uuid'; import { createServer, - getListenerOptions, + getServerListener, getServerOptions, setTlsConfig, getRequestId, @@ -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({ diff --git a/packages/core/http/core-http-server-internal/src/https_redirect_server.ts b/packages/core/http/core-http-server-internal/src/https_redirect_server.ts index 501c83377fe0a..b9ce55273961c 100644 --- a/packages/core/http/core-http-server-internal/src/https_redirect_server.ts +++ b/packages/core/http/core-http-server-internal/src/https_redirect_server.ts @@ -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'; @@ -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) => { diff --git a/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts b/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts index 46cd67e1e0642..fe894675ed8b1 100644 --- a/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts +++ b/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts @@ -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'; @@ -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: ... } }). diff --git a/packages/kbn-cli-dev-mode/src/integration_tests/base_path_proxy_server.test.ts b/packages/kbn-cli-dev-mode/src/integration_tests/base_path_proxy_server.test.ts index 5e8a7a50a7b32..3158332e79f31 100644 --- a/packages/kbn-cli-dev-mode/src/integration_tests/base_path_proxy_server.test.ts +++ b/packages/kbn-cli-dev-mode/src/integration_tests/base_path_proxy_server.test.ts @@ -12,7 +12,7 @@ import moment from 'moment'; import supertest from 'supertest'; import { getServerOptions, - getListenerOptions, + getServerListener, createServer, IHttpConfig, } from '@kbn/server-http-tools'; @@ -34,6 +34,7 @@ describe('BasePathProxyServer', () => { logger = new TestLog(); config = { + protocol: 'http1', host: '127.0.0.1', port: 10012, shutdownTimeout: moment.duration(30, 'seconds'), @@ -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 }; @@ -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', diff --git a/packages/kbn-health-gateway-server/src/server/server.test.mocks.ts b/packages/kbn-health-gateway-server/src/server/server.test.mocks.ts index 543fe9b29e9cc..70053d1c2e329 100644 --- a/packages/kbn-health-gateway-server/src/server/server.test.mocks.ts +++ b/packages/kbn-health-gateway-server/src/server/server.test.mocks.ts @@ -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(); @@ -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(), })); diff --git a/packages/kbn-health-gateway-server/src/server/server.ts b/packages/kbn-health-gateway-server/src/server/server.ts index e75df33859981..c8ddb8884f397 100644 --- a/packages/kbn-health-gateway-server/src/server/server.ts +++ b/packages/kbn-health-gateway-server/src/server/server.ts @@ -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'; @@ -40,7 +40,7 @@ export class Server { async start(): Promise { const serverConfig = new ServerConfig(this.config.atPathSync('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}`); diff --git a/packages/kbn-health-gateway-server/src/server/server_config.ts b/packages/kbn-health-gateway-server/src/server/server_config.ts index d887d5421d6a9..a4f996866df9a 100644 --- a/packages/kbn-health-gateway-server/src/server/server_config.ts +++ b/packages/kbn-health-gateway-server/src/server/server_config.ts @@ -70,6 +70,7 @@ export const config: ServiceConfigDescriptor = { }; export class ServerConfig implements IHttpConfig { + readonly protocol = 'http1'; host: string; port: number; maxPayload: ByteSizeValue; diff --git a/packages/kbn-server-http-tools/index.ts b/packages/kbn-server-http-tools/index.ts index a572cc6ab0832..0554e0b04b19a 100644 --- a/packages/kbn-server-http-tools/index.ts +++ b/packages/kbn-server-http-tools/index.ts @@ -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'; diff --git a/packages/kbn-server-http-tools/src/create_server.ts b/packages/kbn-server-http-tools/src/create_server.ts index 4752e342d5d3e..2b45caeaf9fb7 100644 --- a/packages/kbn-server-http-tools/src/create_server.ts +++ b/packages/kbn-server-http-tools/src/create_server.ts @@ -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; diff --git a/packages/kbn-server-http-tools/src/get_listener.ts b/packages/kbn-server-http-tools/src/get_listener.ts new file mode 100644 index 0000000000000..78f8ab737629f --- /dev/null +++ b/packages/kbn-server-http-tools/src/get_listener.ts @@ -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; +}; diff --git a/packages/kbn-server-http-tools/src/get_listener_options.ts b/packages/kbn-server-http-tools/src/get_listener_options.ts deleted file mode 100644 index 00884312b599f..0000000000000 --- a/packages/kbn-server-http-tools/src/get_listener_options.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 { IHttpConfig } from './types'; - -export interface ListenerOptions { - keepaliveTimeout: number; - socketTimeout: number; -} - -export function getListenerOptions(config: IHttpConfig): ListenerOptions { - return { - keepaliveTimeout: config.keepaliveTimeout, - socketTimeout: config.socketTimeout, - }; -} diff --git a/packages/kbn-server-http-tools/src/get_server_options.ts b/packages/kbn-server-http-tools/src/get_server_options.ts index a338a60aac46a..c9d1072edce8f 100644 --- a/packages/kbn-server-http-tools/src/get_server_options.ts +++ b/packages/kbn-server-http-tools/src/get_server_options.ts @@ -9,14 +9,16 @@ 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, @@ -24,6 +26,7 @@ export function getServerOptions(config: IHttpConfig, { configureTLS = true } = headers: corsAllowedHeaders, } : false; + const options: ServerOptions = { host: config.host, port: config.port, @@ -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; } diff --git a/packages/kbn-server-http-tools/src/set_tls_config.ts b/packages/kbn-server-http-tools/src/set_tls_config.ts index 1f2e1d70fa126..6b0cd35f067ea 100644 --- a/packages/kbn-server-http-tools/src/set_tls_config.ts +++ b/packages/kbn-server-http-tools/src/set_tls_config.ts @@ -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); diff --git a/packages/kbn-server-http-tools/src/types.ts b/packages/kbn-server-http-tools/src/types.ts index 8bb0fecf71f9d..11284455819c7 100644 --- a/packages/kbn-server-http-tools/src/types.ts +++ b/packages/kbn-server-http-tools/src/types.ts @@ -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; diff --git a/src/core/server/integration_tests/http/set_tls_config.test.ts b/src/core/server/integration_tests/http/set_tls_config.test.ts index 6c198d820670f..396214132cdf1 100644 --- a/src/core/server/integration_tests/http/set_tls_config.test.ts +++ b/src/core/server/integration_tests/http/set_tls_config.test.ts @@ -10,7 +10,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, + getServerListener, getServerOptions, setTlsConfig, } from '@kbn/server-http-tools'; @@ -47,8 +47,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, getServerListener(firstConfig)); server.route({ method: 'GET', @@ -80,6 +79,7 @@ describe('setTlsConfig', () => { name: 'kibana', host: '127.0.0.1', port: 10002, + protocol: 'http1', ssl: { enabled: true, certificate: ES_CERT_PATH, From a9f73a1cec3ab828622c74b50eff5dd3ca4c982e Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 15 May 2024 14:23:48 +0200 Subject: [PATCH 04/36] fix types --- packages/kbn-server-http-tools/src/get_server_options.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kbn-server-http-tools/src/get_server_options.test.ts b/packages/kbn-server-http-tools/src/get_server_options.test.ts index 2d8f78a1405ac..1bbf3e123a1ea 100644 --- a/packages/kbn-server-http-tools/src/get_server_options.test.ts +++ b/packages/kbn-server-http-tools/src/get_server_options.test.ts @@ -22,6 +22,7 @@ jest.mock('fs', () => { const createConfig = (parts: Partial): IHttpConfig => ({ host: 'localhost', + protocol: 'http1', port: 5601, socketTimeout: 120000, keepaliveTimeout: 120000, From 7ba90c9697ff516aa63607dbc9e5bc042aa60334 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 15 May 2024 15:31:38 +0200 Subject: [PATCH 05/36] add http2 related warnings to the list of ignored ones --- src/setup_node_env/exit_on_warning.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/setup_node_env/exit_on_warning.js b/src/setup_node_env/exit_on_warning.js index dc6e321074224..c49ba629fee45 100644 --- a/src/setup_node_env/exit_on_warning.js +++ b/src/setup_node_env/exit_on_warning.js @@ -53,6 +53,20 @@ var IGNORE_WARNINGS = [ message: 'The URL https://github.com:crypto-browserify/browserify-rsa.git is invalid. Future versions of Node.js will throw an error.', }, + // supertest in HTTP2 mode uses 0.0.0.0 as the server's name + { + name: 'DeprecationWarning', + code: 'DEP0123', + message: + 'Setting the TLS ServerName to an IP address is not permitted by RFC 6066. This will be ignored in a future version.', + }, + { + // emitted whenever a header not supported by http2 is set. it's not actionable for the end user. + // HAPI sets a connection: close header - see https://github.com/hapijs/hapi/issues/3830 + name: 'UnsupportedWarning', + messageContains: + 'header is not valid, the value will be dropped from the header and will never be in use.', + }, ]; if (process.noProcessWarnings !== true) { @@ -87,10 +101,11 @@ if (process.noProcessWarnings !== true) { function shouldIgnore(warn) { warn = parseWarn(warn); - return IGNORE_WARNINGS.some(function ({ name, code, message, file, line, col }) { + return IGNORE_WARNINGS.some(function ({ name, code, message, messageContains, file, line, col }) { if (name && name !== warn.name) return false; if (code && code !== warn.code) return false; if (message && message !== warn.message) return false; + if (messageContains && !warn.message.includes(messageContains)) return false; if (file && !warn.frames[0].file.endsWith(file)) return false; if (line && line !== warn.frames[0].line) return false; if (col && col !== warn.frames[0].col) return false; From 8b7f775974f49cce4fc0a29bfb654a1acabf2d15 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 16 May 2024 07:59:49 +0200 Subject: [PATCH 06/36] add smoke tests to check supertest behavior --- .../http/http2_protocol.test.ts | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/core/server/integration_tests/http/http2_protocol.test.ts diff --git a/src/core/server/integration_tests/http/http2_protocol.test.ts b/src/core/server/integration_tests/http/http2_protocol.test.ts new file mode 100644 index 0000000000000..0a2b26104b891 --- /dev/null +++ b/src/core/server/integration_tests/http/http2_protocol.test.ts @@ -0,0 +1,100 @@ +/* + * 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 { Server } from 'http'; +import supertest from 'supertest'; +import { of } from 'rxjs'; +import { KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; +import { Router } from '@kbn/core-http-router-server-internal'; +import { + HttpServer, + HttpConfig, + config as httpConfig, + cspConfig, + externalUrlConfig, +} from '@kbn/core-http-server-internal'; +import { mockCoreContext } from '@kbn/core-base-server-mocks'; +import type { Logger } from '@kbn/logging'; + +const CSP_CONFIG = cspConfig.schema.validate({}); +const EXTERNAL_URL_CONFIG = externalUrlConfig.schema.validate({}); + +describe('Http2 - Smoke tests', () => { + let server: HttpServer; + let config: HttpConfig; + let logger: Logger; + let coreContext: ReturnType; + const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {}); + + beforeAll(() => { + // required for self-signed certificates + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + }); + + beforeEach(() => { + coreContext = mockCoreContext.create(); + logger = coreContext.logger.get(); + + const rawConfig = httpConfig.schema.validate({ + name: 'kibana', + protocol: 'http2', + host: '127.0.0.1', + port: 10002, + ssl: { + enabled: true, + certificate: KBN_CERT_PATH, + key: KBN_KEY_PATH, + cipherSuites: ['TLS_AES_256_GCM_SHA384'], + redirectHttpFromPort: 10003, + }, + shutdownTimeout: '5s', + }); + config = new HttpConfig(rawConfig, CSP_CONFIG, EXTERNAL_URL_CONFIG); + + server = new HttpServer(coreContext, 'tests', of(config.shutdownTimeout)); + }); + + describe('When HTTP2 is enabled', () => { + let innerServerListener: Server; + + beforeEach(async () => { + const { registerRouter, server: innerServer } = await server.setup({ config$: of(config) }); + innerServerListener = innerServer.listener; + + const router = new Router('', logger, enhanceWithContext, { + isDev: false, + versionedRouterOptions: { + defaultHandlerResolutionStrategy: 'oldest', + }, + }); + router.post( + { + path: '/', + validate: false, + }, + async (context, req, res) => { + return res.ok({ body: { ok: true } }); + } + ); + registerRouter(router); + + await server.start(); + }); + + afterEach(async () => { + await server.stop(); + }); + + test('Should respond to POST endpoint', async () => { + const response = await supertest(innerServerListener).post('/').http2(); + + expect(response.status).toBe(200); + expect(response.body).toStrictEqual({ ok: true }); + }); + }); +}); From c250d41fe6c5c97a3578c0d11f7c8c249a63dec5 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Fri, 17 May 2024 09:58:57 +0200 Subject: [PATCH 07/36] Enabled ALPN negotiation on the http2 TLS listener --- packages/kbn-server-http-tools/src/get_listener.ts | 2 ++ .../server/integration_tests/http/http2_protocol.test.ts | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/packages/kbn-server-http-tools/src/get_listener.ts b/packages/kbn-server-http-tools/src/get_listener.ts index 78f8ab737629f..64fd260dea7b4 100644 --- a/packages/kbn-server-http-tools/src/get_listener.ts +++ b/packages/kbn-server-http-tools/src/get_listener.ts @@ -64,6 +64,8 @@ export const configureHttp2Listener = ( const listener = useTLS ? http2.createSecureServer({ ...tlsOptions, + // allow ALPN negotiation to HTTP1 + allowHTTP1: true, }) : http2.createServer({}); diff --git a/src/core/server/integration_tests/http/http2_protocol.test.ts b/src/core/server/integration_tests/http/http2_protocol.test.ts index 0a2b26104b891..a095ddf0e6e73 100644 --- a/src/core/server/integration_tests/http/http2_protocol.test.ts +++ b/src/core/server/integration_tests/http/http2_protocol.test.ts @@ -96,5 +96,12 @@ describe('Http2 - Smoke tests', () => { expect(response.status).toBe(200); expect(response.body).toStrictEqual({ ok: true }); }); + + test('Should respond to POST endpoint on HTTP/1.1', async () => { + const response = await supertest(innerServerListener).post('/'); + + expect(response.status).toBe(200); + expect(response.body).toStrictEqual({ ok: true }); + }); }); }); From 9c2f9aa58fab619903dfad4082be447e21cfa7ba Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Fri, 17 May 2024 11:41:50 +0200 Subject: [PATCH 08/36] update test --- .../http/http2_protocol.test.ts | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/core/server/integration_tests/http/http2_protocol.test.ts b/src/core/server/integration_tests/http/http2_protocol.test.ts index a095ddf0e6e73..6df10f55a9449 100644 --- a/src/core/server/integration_tests/http/http2_protocol.test.ts +++ b/src/core/server/integration_tests/http/http2_protocol.test.ts @@ -39,29 +39,31 @@ describe('Http2 - Smoke tests', () => { beforeEach(() => { coreContext = mockCoreContext.create(); logger = coreContext.logger.get(); - - const rawConfig = httpConfig.schema.validate({ - name: 'kibana', - protocol: 'http2', - host: '127.0.0.1', - port: 10002, - ssl: { - enabled: true, - certificate: KBN_CERT_PATH, - key: KBN_KEY_PATH, - cipherSuites: ['TLS_AES_256_GCM_SHA384'], - redirectHttpFromPort: 10003, - }, - shutdownTimeout: '5s', - }); - config = new HttpConfig(rawConfig, CSP_CONFIG, EXTERNAL_URL_CONFIG); - - server = new HttpServer(coreContext, 'tests', of(config.shutdownTimeout)); }); describe('When HTTP2 is enabled', () => { let innerServerListener: Server; + beforeEach(() => { + const rawConfig = httpConfig.schema.validate({ + name: 'kibana', + protocol: 'http2', + host: '127.0.0.1', + port: 10002, + ssl: { + enabled: true, + certificate: KBN_CERT_PATH, + key: KBN_KEY_PATH, + cipherSuites: ['TLS_AES_256_GCM_SHA384'], + redirectHttpFromPort: 10003, + }, + shutdownTimeout: '5s', + }); + config = new HttpConfig(rawConfig, CSP_CONFIG, EXTERNAL_URL_CONFIG); + + server = new HttpServer(coreContext, 'tests', of(config.shutdownTimeout)); + }); + beforeEach(async () => { const { registerRouter, server: innerServer } = await server.setup({ config$: of(config) }); innerServerListener = innerServer.listener; @@ -90,14 +92,14 @@ describe('Http2 - Smoke tests', () => { await server.stop(); }); - test('Should respond to POST endpoint', async () => { + test('Should respond to POST endpoint for an HTTP/2 request', async () => { const response = await supertest(innerServerListener).post('/').http2(); expect(response.status).toBe(200); expect(response.body).toStrictEqual({ ok: true }); }); - test('Should respond to POST endpoint on HTTP/1.1', async () => { + test('Should respond to POST endpoint for an HTTP/1.x request', async () => { const response = await supertest(innerServerListener).post('/'); expect(response.status).toBe(200); From 1eb5d7f03b408e2f06874bf8acdc1c5cd23d6fb4 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 23 May 2024 08:45:42 +0200 Subject: [PATCH 09/36] post-merge fixes --- .../kbn-server-http-tools/src/get_listener.ts | 18 +++++++++--------- .../src/get_server_options.ts | 1 + 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/kbn-server-http-tools/src/get_listener.ts b/packages/kbn-server-http-tools/src/get_listener.ts index 8b6a2de16bdd2..113e9539523fa 100644 --- a/packages/kbn-server-http-tools/src/get_listener.ts +++ b/packages/kbn-server-http-tools/src/get_listener.ts @@ -35,12 +35,12 @@ export const configureHttp1Listener = ( const listener = useTLS ? https.createServer({ - ...tlsOptions, - keepAliveTimeout: config.keepaliveTimeout, - }) + ...tlsOptions, + keepAliveTimeout: config.keepaliveTimeout, + }) : http.createServer({ - keepAliveTimeout: config.keepaliveTimeout, - }); + keepAliveTimeout: config.keepaliveTimeout, + }); listener.setTimeout(config.socketTimeout); listener.on('timeout', (socket) => { @@ -66,10 +66,10 @@ export const configureHttp2Listener = ( const listener = useTLS ? http2.createSecureServer({ - ...tlsOptions, - // allow ALPN negotiation to HTTP1 - allowHTTP1: true, - }) + ...tlsOptions, + // allow ALPN negotiation to HTTP1 + allowHTTP1: true, + }) : http2.createServer({}); listener.setTimeout(config.socketTimeout); diff --git a/packages/kbn-server-http-tools/src/get_server_options.ts b/packages/kbn-server-http-tools/src/get_server_options.ts index d7687a74d5700..b9c7a6e7add63 100644 --- a/packages/kbn-server-http-tools/src/get_server_options.ts +++ b/packages/kbn-server-http-tools/src/get_server_options.ts @@ -29,6 +29,7 @@ export function getServerOptions(config: IHttpConfig, { configureTLS = true } = host: config.host, port: config.port, // manually configuring the listener + // @ts-expect-error HAPI types only define http1/tls listener, not http2 listener: getServerListener(config, { configureTLS }), // must set to true when manually passing a TLS listener, false otherwise tls: configureTLS && config.ssl.enabled, From 0a06093da695b1faef0e0ae7e1e47908d9dbff8c Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 23 May 2024 09:03:30 +0200 Subject: [PATCH 10/36] set right value for protocol on KibanaRequest --- .../src/request.test.ts | 28 +++- .../src/request.ts | 7 +- .../http/http2_protocol.test.ts | 144 +++++++++++++----- 3 files changed, 133 insertions(+), 46 deletions(-) diff --git a/packages/core/http/core-http-router-server-internal/src/request.test.ts b/packages/core/http/core-http-router-server-internal/src/request.test.ts index f895cb0e2fde7..921509adfe589 100644 --- a/packages/core/http/core-http-router-server-internal/src/request.test.ts +++ b/packages/core/http/core-http-router-server-internal/src/request.test.ts @@ -217,11 +217,23 @@ describe('CoreKibanaRequest', () => { }); describe('route.protocol property', () => { - it('return a static value for now as only http1 is supported', () => { + it('return the correct value for http/1.0 requests', () => { const request = hapiMocks.createRequest({ raw: { req: { - httpVersion: '2.0', + httpVersion: '1.0', + }, + }, + }); + const kibanaRequest = CoreKibanaRequest.from(request); + + expect(kibanaRequest.protocol).toEqual('http1'); + }); + it('return the correct value for http/1.1 requests', () => { + const request = hapiMocks.createRequest({ + raw: { + req: { + httpVersion: '1.1', }, }, }); @@ -229,6 +241,18 @@ describe('CoreKibanaRequest', () => { expect(kibanaRequest.protocol).toEqual('http1'); }); + it('return the correct value for http/2 requests', () => { + const request = hapiMocks.createRequest({ + raw: { + req: { + httpVersion: '2.0', + }, + }, + }); + const kibanaRequest = CoreKibanaRequest.from(request); + + expect(kibanaRequest.protocol).toEqual('http2'); + }); }); describe('route.options.authRequired property', () => { diff --git a/packages/core/http/core-http-router-server-internal/src/request.ts b/packages/core/http/core-http-router-server-internal/src/request.ts index d3274b0a2a1fe..76994322352cc 100644 --- a/packages/core/http/core-http-router-server-internal/src/request.ts +++ b/packages/core/http/core-http-router-server-internal/src/request.ts @@ -173,8 +173,7 @@ export class CoreKibanaRequest< }); this.httpVersion = isRealReq ? request.raw.req.httpVersion : '1.0'; - // hardcoded for now as only supporting http1 - this.protocol = 'http1'; + this.protocol = getProtocolFromHttpVersion(this.httpVersion); this.route = deepFreeze(this.getRouteInfo(request)); this.socket = isRealReq @@ -374,3 +373,7 @@ function sanitizeRequest(req: Request): { query: unknown; params: unknown; body: body: req.payload, }; } + +function getProtocolFromHttpVersion(httpVersion: string): HttpProtocol { + return httpVersion.split('.')[0] === '2' ? 'http2' : 'http1'; +} diff --git a/src/core/server/integration_tests/http/http2_protocol.test.ts b/src/core/server/integration_tests/http/http2_protocol.test.ts index 6df10f55a9449..c1d234f00358e 100644 --- a/src/core/server/integration_tests/http/http2_protocol.test.ts +++ b/src/core/server/integration_tests/http/http2_protocol.test.ts @@ -29,41 +29,42 @@ describe('Http2 - Smoke tests', () => { let config: HttpConfig; let logger: Logger; let coreContext: ReturnType; + let innerServerListener: Server; + const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {}); beforeAll(() => { - // required for self-signed certificates + // required for the self-signed certificates used in testing process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; }); beforeEach(() => { coreContext = mockCoreContext.create(); logger = coreContext.logger.get(); - }); - - describe('When HTTP2 is enabled', () => { - let innerServerListener: Server; - - beforeEach(() => { - const rawConfig = httpConfig.schema.validate({ - name: 'kibana', - protocol: 'http2', - host: '127.0.0.1', - port: 10002, - ssl: { - enabled: true, - certificate: KBN_CERT_PATH, - key: KBN_KEY_PATH, - cipherSuites: ['TLS_AES_256_GCM_SHA384'], - redirectHttpFromPort: 10003, - }, - shutdownTimeout: '5s', - }); - config = new HttpConfig(rawConfig, CSP_CONFIG, EXTERNAL_URL_CONFIG); - server = new HttpServer(coreContext, 'tests', of(config.shutdownTimeout)); + const rawConfig = httpConfig.schema.validate({ + name: 'kibana', + protocol: 'http2', + host: '127.0.0.1', + port: 10002, + ssl: { + enabled: true, + certificate: KBN_CERT_PATH, + key: KBN_KEY_PATH, + cipherSuites: ['TLS_AES_256_GCM_SHA384'], + redirectHttpFromPort: 10003, + }, + shutdownTimeout: '5s', }); + config = new HttpConfig(rawConfig, CSP_CONFIG, EXTERNAL_URL_CONFIG); + server = new HttpServer(coreContext, 'tests', of(config.shutdownTimeout)); + }); + + afterEach(async () => { + await server?.stop(); + }); + describe('Basic test against all supported methods', () => { beforeEach(async () => { const { registerRouter, server: innerServer } = await server.setup({ config$: of(config) }); innerServerListener = innerServer.listener; @@ -74,36 +75,95 @@ describe('Http2 - Smoke tests', () => { defaultHandlerResolutionStrategy: 'oldest', }, }); - router.post( - { - path: '/', - validate: false, - }, - async (context, req, res) => { - return res.ok({ body: { ok: true } }); - } - ); + + router.post({ path: '/', validate: false }, async (context, req, res) => { + return res.ok({ + body: { protocol: req.protocol, httpVersion: req.httpVersion }, + }); + }); + router.get({ path: '/', validate: false }, async (context, req, res) => { + return res.ok({ + body: { protocol: req.protocol, httpVersion: req.httpVersion }, + }); + }); + router.put({ path: '/', validate: false }, async (context, req, res) => { + return res.ok({ + body: { protocol: req.protocol, httpVersion: req.httpVersion }, + }); + }); + router.delete({ path: '/', validate: false }, async (context, req, res) => { + return res.ok({ + body: { protocol: req.protocol, httpVersion: req.httpVersion }, + }); + }); + registerRouter(router); await server.start(); }); - afterEach(async () => { - await server.stop(); + describe('POST', () => { + it('should respond to POST endpoint for an HTTP/2 request', async () => { + const response = await supertest(innerServerListener).post('/').http2(); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ protocol: 'http2', httpVersion: '2.0' }); + }); + + it('should respond to POST endpoint for an HTTP/1.x request', async () => { + const response = await supertest(innerServerListener).post('/'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ protocol: 'http1', httpVersion: '1.1' }); + }); }); - test('Should respond to POST endpoint for an HTTP/2 request', async () => { - const response = await supertest(innerServerListener).post('/').http2(); + describe('GET', () => { + it('should respond to GET endpoint for an HTTP/2 request', async () => { + const response = await supertest(innerServerListener).get('/').http2(); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ protocol: 'http2', httpVersion: '2.0' }); + }); + + it('should respond to GET endpoint for an HTTP/1.x request', async () => { + const response = await supertest(innerServerListener).get('/'); - expect(response.status).toBe(200); - expect(response.body).toStrictEqual({ ok: true }); + expect(response.status).toBe(200); + expect(response.body).toEqual({ protocol: 'http1', httpVersion: '1.1' }); + }); }); - test('Should respond to POST endpoint for an HTTP/1.x request', async () => { - const response = await supertest(innerServerListener).post('/'); + describe('DELETE', () => { + it('should respond to DELETE endpoint for an HTTP/2 request', async () => { + const response = await supertest(innerServerListener).delete('/').http2(); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ protocol: 'http2', httpVersion: '2.0' }); + }); + + it('should respond to DELETE endpoint for an HTTP/1.x request', async () => { + const response = await supertest(innerServerListener).delete('/'); - expect(response.status).toBe(200); - expect(response.body).toStrictEqual({ ok: true }); + expect(response.status).toBe(200); + expect(response.body).toEqual({ protocol: 'http1', httpVersion: '1.1' }); + }); + }); + + describe('PUT', () => { + it('should respond to PUT endpoint for an HTTP/2 request', async () => { + const response = await supertest(innerServerListener).put('/').http2(); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ protocol: 'http2', httpVersion: '2.0' }); + }); + + it('should respond to PUT endpoint for an HTTP/1.x request', async () => { + const response = await supertest(innerServerListener).put('/'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ protocol: 'http1', httpVersion: '1.1' }); + }); }); }); }); From 447548d75fae8b113e96862648880794c1737ec7 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 23 May 2024 09:17:19 +0200 Subject: [PATCH 11/36] add get_listener tests for http2 listeners --- .../src/get_listener.test.mocks.ts | 23 ++ .../src/get_listener.test.ts | 211 +++++++++++++----- 2 files changed, 178 insertions(+), 56 deletions(-) diff --git a/packages/kbn-server-http-tools/src/get_listener.test.mocks.ts b/packages/kbn-server-http-tools/src/get_listener.test.mocks.ts index 1fab2d9191367..93266695a18f8 100644 --- a/packages/kbn-server-http-tools/src/get_listener.test.mocks.ts +++ b/packages/kbn-server-http-tools/src/get_listener.test.mocks.ts @@ -45,3 +45,26 @@ jest.doMock('https', () => { createServer: createHttpsServerMock, }; }); + +export const createHttp2SecureServerMock = jest.fn(() => { + return { + on: jest.fn(), + setTimeout: jest.fn(), + }; +}); + +export const createHttp2UnsecureServerMock = jest.fn(() => { + return { + on: jest.fn(), + setTimeout: jest.fn(), + }; +}); + +jest.doMock('http2', () => { + const actual = jest.requireActual('https'); + return { + ...actual, + createServer: createHttp2UnsecureServerMock, + createSecureServer: createHttp2SecureServerMock, + }; +}); diff --git a/packages/kbn-server-http-tools/src/get_listener.test.ts b/packages/kbn-server-http-tools/src/get_listener.test.ts index 21e0a93763490..dd64c2dba82fc 100644 --- a/packages/kbn-server-http-tools/src/get_listener.test.ts +++ b/packages/kbn-server-http-tools/src/get_listener.test.ts @@ -10,6 +10,8 @@ import { getServerTLSOptionsMock, createHttpServerMock, createHttpsServerMock, + createHttp2UnsecureServerMock, + createHttp2SecureServerMock, } from './get_listener.test.mocks'; import moment from 'moment'; import { ByteSizeValue } from '@kbn/config-schema'; @@ -18,6 +20,7 @@ import { getServerListener } from './get_listener'; const createConfig = (parts: Partial): IHttpConfig => ({ host: 'localhost', + protocol: 'http1', port: 5601, socketTimeout: 120000, keepaliveTimeout: 120000, @@ -41,99 +44,195 @@ const createConfig = (parts: Partial): IHttpConfig => ({ describe('getServerListener', () => { beforeEach(() => { getServerTLSOptionsMock.mockReset(); + createHttpServerMock.mockClear(); createHttpsServerMock.mockClear(); + createHttp2UnsecureServerMock.mockClear(); + createHttp2SecureServerMock.mockClear(); }); - describe('when TLS is enabled', () => { - it('calls getServerTLSOptions with the correct parameters', () => { - const config = createConfig({ ssl: { enabled: true } }); + describe('When protocol is `http1`', () => { + describe('when TLS is enabled', () => { + it('calls getServerTLSOptions with the correct parameters', () => { + const config = createConfig({ ssl: { enabled: true } }); - getServerListener(config); + getServerListener(config); - expect(getServerTLSOptionsMock).toHaveBeenCalledTimes(1); - expect(getServerTLSOptionsMock).toHaveBeenCalledWith(config.ssl); - }); + expect(getServerTLSOptionsMock).toHaveBeenCalledTimes(1); + expect(getServerTLSOptionsMock).toHaveBeenCalledWith(config.ssl); + }); - it('calls https.createServer with the correct parameters', () => { - const config = createConfig({ ssl: { enabled: true } }); + it('calls https.createServer with the correct parameters', () => { + const config = createConfig({ ssl: { enabled: true } }); - getServerTLSOptionsMock.mockReturnValue({ stub: true }); + getServerTLSOptionsMock.mockReturnValue({ stub: true }); - getServerListener(config); + getServerListener(config); - expect(createHttpsServerMock).toHaveBeenCalledTimes(1); - expect(createHttpsServerMock).toHaveBeenCalledWith({ - stub: true, - keepAliveTimeout: config.keepaliveTimeout, + 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); + 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); - expect(server.setTimeout).toHaveBeenCalledTimes(1); - expect(server.setTimeout).toHaveBeenCalledWith(config.socketTimeout); + const expectedServer = createHttpsServerMock.mock.results[0].value; - expect(server.on).toHaveBeenCalledTimes(2); - expect(server.on).toHaveBeenCalledWith('clientError', expect.any(Function)); - expect(server.on).toHaveBeenCalledWith('timeout', expect.any(Function)); + expect(server).toBe(expectedServer); + }); }); - it('returns the https server', () => { - const config = createConfig({ ssl: { enabled: true } }); + 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, + }); + }); - const server = getServerListener(config); + it('properly configures the listener', () => { + const config = createConfig({ ssl: { enabled: false } }); + const server = getServerListener(config); - const expectedServer = createHttpsServerMock.mock.results[0].value; + expect(server.setTimeout).toHaveBeenCalledTimes(1); + expect(server.setTimeout).toHaveBeenCalledWith(config.socketTimeout); - expect(server).toBe(expectedServer); + 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); + }); }); }); - describe('when TLS is disabled', () => { - it('does not call getServerTLSOptions', () => { - const config = createConfig({ ssl: { enabled: false } }); + describe('When protocol is `http2`', () => { + const createHttp2Config = (parts: Partial) => + createConfig({ ...parts, protocol: 'http2' }); - getServerListener(config); + describe('when TLS is enabled', () => { + it('calls getServerTLSOptions with the correct parameters', () => { + const config = createHttp2Config({ ssl: { enabled: true } }); - expect(getServerTLSOptionsMock).not.toHaveBeenCalled(); - }); + getServerListener(config); + + expect(getServerTLSOptionsMock).toHaveBeenCalledTimes(1); + expect(getServerTLSOptionsMock).toHaveBeenCalledWith(config.ssl); + }); - it('calls http.createServer with the correct parameters', () => { - const config = createConfig({ ssl: { enabled: false } }); + it('calls http2.createSecureServer with the correct parameters', () => { + const config = createHttp2Config({ ssl: { enabled: true } }); - getServerTLSOptionsMock.mockReturnValue({ stub: true }); + getServerTLSOptionsMock.mockReturnValue({ stub: true }); - getServerListener(config); + getServerListener(config); - expect(createHttpServerMock).toHaveBeenCalledTimes(1); - expect(createHttpServerMock).toHaveBeenCalledWith({ - keepAliveTimeout: config.keepaliveTimeout, + expect(createHttp2SecureServerMock).toHaveBeenCalledTimes(1); + expect(createHttp2SecureServerMock).toHaveBeenCalledWith({ + stub: true, + allowHTTP1: true, + }); }); - }); - it('properly configures the listener', () => { - const config = createConfig({ ssl: { enabled: false } }); - const server = getServerListener(config); + it('properly configures the listener', () => { + const config = createHttp2Config({ ssl: { enabled: true } }); + const server = getServerListener(config); + + expect(server.setTimeout).toHaveBeenCalledTimes(1); + expect(server.setTimeout).toHaveBeenCalledWith(config.socketTimeout); + + expect(server.on).not.toHaveBeenCalled(); + }); + + it('returns the http2 secure server', () => { + const config = createHttp2Config({ ssl: { enabled: true } }); + + const server = getServerListener(config); - expect(server.setTimeout).toHaveBeenCalledTimes(1); - expect(server.setTimeout).toHaveBeenCalledWith(config.socketTimeout); + const expectedServer = createHttp2SecureServerMock.mock.results[0].value; - expect(server.on).toHaveBeenCalledTimes(2); - expect(server.on).toHaveBeenCalledWith('clientError', expect.any(Function)); - expect(server.on).toHaveBeenCalledWith('timeout', expect.any(Function)); + expect(server).toBe(expectedServer); + }); }); - it('returns the http server', () => { - const config = createConfig({ ssl: { enabled: false } }); + describe('when TLS is disabled', () => { + it('does not call getServerTLSOptions', () => { + const config = createHttp2Config({ ssl: { enabled: false } }); + + getServerListener(config); + + expect(getServerTLSOptionsMock).not.toHaveBeenCalled(); + }); + + it('calls http2.createServer with the correct parameters', () => { + const config = createHttp2Config({ ssl: { enabled: false } }); + + getServerTLSOptionsMock.mockReturnValue({ stub: true }); + + getServerListener(config); + + expect(createHttp2UnsecureServerMock).toHaveBeenCalledTimes(1); + expect(createHttp2UnsecureServerMock).toHaveBeenCalledWith({}); + }); - const server = getServerListener(config); + it('properly configures the listener', () => { + const config = createHttp2Config({ ssl: { enabled: false } }); + const server = getServerListener(config); - const expectedServer = createHttpServerMock.mock.results[0].value; + expect(server.setTimeout).toHaveBeenCalledTimes(1); + expect(server.setTimeout).toHaveBeenCalledWith(config.socketTimeout); - expect(server).toBe(expectedServer); + expect(server.on).not.toHaveBeenCalled(); + }); + + it('returns the http2 unsecure server', () => { + const config = createHttp2Config({ ssl: { enabled: false } }); + + const server = getServerListener(config); + + const expectedServer = createHttp2UnsecureServerMock.mock.results[0].value; + + expect(server).toBe(expectedServer); + }); }); }); }); From f43ee8f45e1ca39ddb4d0b8f4f1464e75fead816 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 23 May 2024 09:44:31 +0200 Subject: [PATCH 12/36] adapt bfetch to not use banned headers --- src/core/server/index.ts | 1 + src/plugins/bfetch/server/plugin.ts | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/core/server/index.ts b/src/core/server/index.ts index c77f7311100c8..901e0e3153651 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -232,6 +232,7 @@ export type { HttpServiceStart, RawRequest, FakeRawRequest, + HttpProtocol, } from '@kbn/core-http-server'; export type { IExternalUrlPolicy } from '@kbn/core-http-common'; diff --git a/src/plugins/bfetch/server/plugin.ts b/src/plugins/bfetch/server/plugin.ts index 70f5ff36e6e4a..87e788628d61d 100644 --- a/src/plugins/bfetch/server/plugin.ts +++ b/src/plugins/bfetch/server/plugin.ts @@ -18,6 +18,7 @@ import { RequestHandler, KibanaResponseFactory, AnalyticsServiceStart, + HttpProtocol, } from '@kbn/core/server'; import { map$ } from '@kbn/std'; @@ -65,11 +66,23 @@ export interface BfetchServerSetup { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface BfetchServerStart {} -const streamingHeaders = { - 'Content-Type': 'application/x-ndjson', - Connection: 'keep-alive', - 'Transfer-Encoding': 'chunked', - 'X-Accel-Buffering': 'no', +// NOTE: we will likely want to fully disable bfetch when a client supports/use http2, +// or maybe even fully disable bfetch overall now that we have http2 support +// but this should be done in a later stage by the appropriate team +// so the PR does the easiest fix to just remove http2 banned headers +const getStreamingHeaders = (protocol: HttpProtocol): Record => { + if (protocol === 'http2') { + return { + 'Content-Type': 'application/x-ndjson', + 'X-Accel-Buffering': 'no', + }; + } + return { + 'Content-Type': 'application/x-ndjson', + Connection: 'keep-alive', + 'Transfer-Encoding': 'chunked', + 'X-Accel-Buffering': 'no', + }; }; interface Query { @@ -144,7 +157,7 @@ export class BfetchServerPlugin const data = request.body; const compress = request.query.compress; return response.ok({ - headers: streamingHeaders, + headers: getStreamingHeaders(request.protocol), body: createStream( handlerInstance.getResponseStream(data), logger, From ffbab2608c51968ff89e279df9e426217e6c3712 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 23 May 2024 09:57:47 +0200 Subject: [PATCH 13/36] fix test types --- packages/kbn-server-http-tools/src/get_tls_options.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kbn-server-http-tools/src/get_tls_options.test.ts b/packages/kbn-server-http-tools/src/get_tls_options.test.ts index 0a50209db50c9..e99a885b9c51e 100644 --- a/packages/kbn-server-http-tools/src/get_tls_options.test.ts +++ b/packages/kbn-server-http-tools/src/get_tls_options.test.ts @@ -22,6 +22,7 @@ jest.mock('fs', () => { const createConfig = (parts: Partial): IHttpConfig => ({ host: 'localhost', + protocol: 'http1', port: 5601, socketTimeout: 120000, keepaliveTimeout: 120000, From 537a365293a4c8816f9a4d09ad59198e0411ab3a Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 23 May 2024 12:51:04 +0200 Subject: [PATCH 14/36] update snapshot --- .../src/__snapshots__/http_config.test.ts.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/http/core-http-server-internal/src/__snapshots__/http_config.test.ts.snap b/packages/core/http/core-http-server-internal/src/__snapshots__/http_config.test.ts.snap index b6566b66bd00d..4faa70f664cd7 100644 --- a/packages/core/http/core-http-server-internal/src/__snapshots__/http_config.test.ts.snap +++ b/packages/core/http/core-http-server-internal/src/__snapshots__/http_config.test.ts.snap @@ -78,6 +78,7 @@ Object { }, "payloadTimeout": 20000, "port": 5601, + "protocol": "http1", "requestId": Object { "allowFromAnyIp": false, "ipAllowlist": Array [], From e7819fb71e08de8df5ec76c39e1acf71ca62f419 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 23 May 2024 14:37:35 +0200 Subject: [PATCH 15/36] add http.protocol config validation --- .../__snapshots__/http_config.test.ts.snap | 3 + .../src/http_config.test.ts | 67 +++++++++++++++++++ .../src/http_config.ts | 33 +++++++-- packages/kbn-server-http-tools/index.ts | 2 +- .../src/ssl/constants.ts | 12 ++++ .../kbn-server-http-tools/src/ssl/index.ts | 1 + .../src/ssl/ssl_config.ts | 19 +++--- 7 files changed, 122 insertions(+), 15 deletions(-) create mode 100644 packages/kbn-server-http-tools/src/ssl/constants.ts diff --git a/packages/core/http/core-http-server-internal/src/__snapshots__/http_config.test.ts.snap b/packages/core/http/core-http-server-internal/src/__snapshots__/http_config.test.ts.snap index 4faa70f664cd7..8f50bf45b9580 100644 --- a/packages/core/http/core-http-server-internal/src/__snapshots__/http_config.test.ts.snap +++ b/packages/core/http/core-http-server-internal/src/__snapshots__/http_config.test.ts.snap @@ -68,6 +68,9 @@ Object { }, }, "host": "localhost", + "http2": Object { + "allowUnsecure": false, + }, "keepaliveTimeout": 120000, "maxPayload": ByteSizeValue { "valueInBytes": 1048576, diff --git a/packages/core/http/core-http-server-internal/src/http_config.test.ts b/packages/core/http/core-http-server-internal/src/http_config.test.ts index 6cc9042b14b9c..db69552d46775 100644 --- a/packages/core/http/core-http-server-internal/src/http_config.test.ts +++ b/packages/core/http/core-http-server-internal/src/http_config.test.ts @@ -566,6 +566,73 @@ describe('cdn', () => { }); }); +describe('http2 protocol', () => { + it('throws if http2 is enabled but TLS is not', () => { + expect(() => + config.schema.validate({ + protocol: 'http2', + ssl: { + enabled: false, + }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"http2 requires TLS to be enabled. Use 'http2.allowUnsecure: true' to allow running http2 without a valid h2c setup"` + ); + }); + it('throws if http2 is enabled but TLS has no suitable versions', () => { + expect(() => + config.schema.validate({ + protocol: 'http2', + ssl: { + enabled: true, + supportedProtocols: ['TLSv1.1'], + certificate: '/path/to/certificate', + key: '/path/to/key', + }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"http2 requires 'ssl.supportedProtocols' to include TLSv1.2 or TLSv1.3. Use 'http2.allowUnsecure: true' to allow running http2 without a valid h2c setup"` + ); + }); + it('does not throws if http2 is enabled and TLS is not if http2.allowUnsecure is true', () => { + expect( + config.schema.validate({ + protocol: 'http2', + http2: { + allowUnsecure: true, + }, + ssl: { + enabled: false, + }, + }) + ).toEqual( + expect.objectContaining({ + protocol: 'http2', + }) + ); + }); + it('does not throws if supportedProtocols are not valid for h2c if http2.allowUnsecure is true', () => { + expect( + config.schema.validate({ + protocol: 'http2', + http2: { + allowUnsecure: true, + }, + ssl: { + enabled: true, + supportedProtocols: ['TLSv1.1'], + certificate: '/path/to/certificate', + key: '/path/to/key', + }, + }) + ).toEqual( + expect.objectContaining({ + protocol: 'http2', + }) + ); + }); +}); + describe('HttpConfig', () => { it('converts customResponseHeaders to strings or arrays of strings', () => { const httpSchema = config.schema; diff --git a/packages/core/http/core-http-server-internal/src/http_config.ts b/packages/core/http/core-http-server-internal/src/http_config.ts index c4a286329085b..30edccea51f5c 100644 --- a/packages/core/http/core-http-server-internal/src/http_config.ts +++ b/packages/core/http/core-http-server-internal/src/http_config.ts @@ -6,21 +6,21 @@ * Side Public License, v 1. */ -import { hostname, EOL } from 'node:os'; +import { EOL, hostname } from 'node:os'; import url, { URL } from 'node:url'; import type { Duration } from 'moment'; import { ByteSizeValue, offeringBasedSchema, schema, TypeOf } from '@kbn/config-schema'; -import { IHttpConfig, SslConfig, sslSchema } from '@kbn/server-http-tools'; +import { IHttpConfig, SslConfig, sslSchema, TLS_V1_2, TLS_V1_3 } from '@kbn/server-http-tools'; import type { ServiceConfigDescriptor } from '@kbn/core-base-server-internal'; import { uuidRegexp } from '@kbn/core-base-server-internal'; -import type { ICspConfig, IExternalUrlConfig, HttpProtocol } from '@kbn/core-http-server'; +import type { HttpProtocol, ICspConfig, IExternalUrlConfig } from '@kbn/core-http-server'; import type { IHttpEluMonitorConfig } from '@kbn/core-http-server/src/elu_monitor'; import type { HandlerResolutionStrategy } from '@kbn/core-http-router-server-internal'; -import { CspConfigType, CspConfig } from './csp'; +import { CspConfig, CspConfigType } from './csp'; import { ExternalUrlConfig } from './external_url'; import { - securityResponseHeadersSchema, parseRawSecurityResponseHeadersConfig, + securityResponseHeadersSchema, } from './security_response_headers_config'; import { CdnConfig } from './cdn_config'; @@ -143,6 +143,9 @@ const configSchema = schema.object( payloadTimeout: schema.number({ defaultValue: 20 * SECOND, }), + http2: schema.object({ + allowUnsecure: schema.boolean({ defaultValue: false }), + }), compression: schema.object({ enabled: schema.boolean({ defaultValue: true }), brotli: schema.object({ @@ -258,6 +261,13 @@ const configSchema = schema.object( return 'cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false'; } + if (rawConfig.protocol === 'http2' && !rawConfig.http2.allowUnsecure) { + const err = ensureValidTLSConfigForH2C(rawConfig.ssl); + if (err) { + return err; + } + } + if ( rawConfig.ssl.enabled && rawConfig.ssl.redirectHttpFromPort !== undefined && @@ -379,3 +389,16 @@ export class HttpConfig implements IHttpConfig { const convertHeader = (entry: any): string => { return typeof entry === 'object' ? JSON.stringify(entry) : String(entry); }; + +const ensureValidTLSConfigForH2C = (tlsConfig: TypeOf): string | undefined => { + if (!tlsConfig.enabled) { + return `http2 requires TLS to be enabled. Use 'http2.allowUnsecure: true' to allow running http2 without a valid h2c setup`; + } + if ( + !tlsConfig.supportedProtocols.includes(TLS_V1_2) && + !tlsConfig.supportedProtocols.includes(TLS_V1_3) + ) { + return `http2 requires 'ssl.supportedProtocols' to include ${TLS_V1_2} or ${TLS_V1_3}. Use 'http2.allowUnsecure: true' to allow running http2 without a valid h2c setup`; + } + return undefined; +}; diff --git a/packages/kbn-server-http-tools/index.ts b/packages/kbn-server-http-tools/index.ts index 0554e0b04b19a..e471f115215ce 100644 --- a/packages/kbn-server-http-tools/index.ts +++ b/packages/kbn-server-http-tools/index.ts @@ -20,4 +20,4 @@ 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'; +export { sslSchema, SslConfig, TLS_V1, TLS_V1_1, TLS_V1_2, TLS_V1_3 } from './src/ssl'; diff --git a/packages/kbn-server-http-tools/src/ssl/constants.ts b/packages/kbn-server-http-tools/src/ssl/constants.ts new file mode 100644 index 0000000000000..71632d52fe8e5 --- /dev/null +++ b/packages/kbn-server-http-tools/src/ssl/constants.ts @@ -0,0 +1,12 @@ +/* + * 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 TLS_V1 = 'TLSv1'; +export const TLS_V1_1 = 'TLSv1.1'; +export const TLS_V1_2 = 'TLSv1.2'; +export const TLS_V1_3 = 'TLSv1.3'; diff --git a/packages/kbn-server-http-tools/src/ssl/index.ts b/packages/kbn-server-http-tools/src/ssl/index.ts index cbc3f17f915ef..2144b003a4830 100644 --- a/packages/kbn-server-http-tools/src/ssl/index.ts +++ b/packages/kbn-server-http-tools/src/ssl/index.ts @@ -7,3 +7,4 @@ */ export { SslConfig, sslSchema } from './ssl_config'; +export { TLS_V1, TLS_V1_1, TLS_V1_2, TLS_V1_3 } from './constants'; diff --git a/packages/kbn-server-http-tools/src/ssl/ssl_config.ts b/packages/kbn-server-http-tools/src/ssl/ssl_config.ts index a5d43064baa6b..feb5ff36b820e 100644 --- a/packages/kbn-server-http-tools/src/ssl/ssl_config.ts +++ b/packages/kbn-server-http-tools/src/ssl/ssl_config.ts @@ -11,13 +11,14 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { readPkcs12Keystore, readPkcs12Truststore } from '@kbn/crypto'; import { constants as cryptoConstants } from 'crypto'; import { readFileSync } from 'fs'; +import { TLS_V1, TLS_V1_1, TLS_V1_2, TLS_V1_3 } from './constants'; const protocolMap = new Map([ - ['TLSv1', cryptoConstants.SSL_OP_NO_TLSv1], - ['TLSv1.1', cryptoConstants.SSL_OP_NO_TLSv1_1], - ['TLSv1.2', cryptoConstants.SSL_OP_NO_TLSv1_2], + [TLS_V1, cryptoConstants.SSL_OP_NO_TLSv1], + [TLS_V1_1, cryptoConstants.SSL_OP_NO_TLSv1_1], + [TLS_V1_2, cryptoConstants.SSL_OP_NO_TLSv1_2], // @ts-expect-error According to the docs SSL_OP_NO_TLSv1_3 should exist (https://nodejs.org/docs/latest-v12.x/api/crypto.html) - ['TLSv1.3', cryptoConstants.SSL_OP_NO_TLSv1_3], + [TLS_V1_3, cryptoConstants.SSL_OP_NO_TLSv1_3], ]); export const sslSchema = schema.object( @@ -45,12 +46,12 @@ export const sslSchema = schema.object( redirectHttpFromPort: schema.maybe(schema.number()), supportedProtocols: schema.arrayOf( schema.oneOf([ - schema.literal('TLSv1'), - schema.literal('TLSv1.1'), - schema.literal('TLSv1.2'), - schema.literal('TLSv1.3'), + schema.literal(TLS_V1), + schema.literal(TLS_V1_1), + schema.literal(TLS_V1_2), + schema.literal(TLS_V1_3), ]), - { defaultValue: ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], minSize: 1 } + { defaultValue: [TLS_V1_1, TLS_V1_2, TLS_V1_3], minSize: 1 } ), clientAuthentication: schema.oneOf( [schema.literal('none'), schema.literal('optional'), schema.literal('required')], From 12b4a1cbce53824ac7716110fdb3468584608124 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 23 May 2024 16:49:08 +0200 Subject: [PATCH 16/36] strip http2 illegal headers when serving http2 requests --- .../src/router.ts | 9 ++ .../src/strip_illegal_http2_headers.test.ts | 114 ++++++++++++++++++ .../src/strip_illegal_http2_headers.ts | 49 ++++++++ .../http/http2_protocol.test.ts | 45 +++++++ 4 files changed, 217 insertions(+) create mode 100644 packages/core/http/core-http-router-server-internal/src/strip_illegal_http2_headers.test.ts create mode 100644 packages/core/http/core-http-router-server-internal/src/strip_illegal_http2_headers.ts diff --git a/packages/core/http/core-http-router-server-internal/src/router.ts b/packages/core/http/core-http-router-server-internal/src/router.ts index 9db0829355587..cd8fa84f662e3 100644 --- a/packages/core/http/core-http-router-server-internal/src/router.ts +++ b/packages/core/http/core-http-router-server-internal/src/router.ts @@ -35,6 +35,7 @@ import { HapiResponseAdapter } from './response_adapter'; import { wrapErrors } from './error_wrapper'; import { Method } from './versioned_router/types'; import { prepareRouteConfigValidation } from './util'; +import { stripIllegalHttp2Headers } from './strip_illegal_http2_headers'; export type ContextEnhancer< P, @@ -265,6 +266,14 @@ export class Router { + let logger: MockedLogger; + + beforeEach(() => { + logger = loggerMock.create(); + }); + + it('removes illegal http2 headers', () => { + const headers = { + 'x-foo': 'bar', + 'x-hello': 'dolly', + connection: 'keep-alive', + 'proxy-connection': 'keep-alive', + 'keep-alive': 'true', + upgrade: 'probably', + 'transfer-encoding': 'chunked', + 'http2-settings': 'yeah', + }; + const output = stripIllegalHttp2Headers({ + headers, + isDev: false, + logger, + requestContext: 'requestContext', + }); + + expect(output).toEqual({ + 'x-foo': 'bar', + 'x-hello': 'dolly', + }); + }); + + it('ignores case when detecting headers', () => { + const headers = { + 'x-foo': 'bar', + 'x-hello': 'dolly', + Connection: 'keep-alive', + 'Proxy-Connection': 'keep-alive', + 'kEeP-AlIvE': 'true', + }; + const output = stripIllegalHttp2Headers({ + headers, + isDev: false, + logger, + requestContext: 'requestContext', + }); + + expect(output).toEqual({ + 'x-foo': 'bar', + 'x-hello': 'dolly', + }); + }); + + it('logs a warning about the illegal header when in dev mode', () => { + const headers = { + 'x-foo': 'bar', + Connection: 'keep-alive', + }; + stripIllegalHttp2Headers({ + headers, + isDev: true, + logger, + requestContext: 'requestContext', + }); + + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(logger.warn).toHaveBeenCalledWith( + `Handler for "requestContext" returned an illegal http2 header: Connection. Please check "request.protocol" in handlers before assigning connection headers` + ); + }); + + it('does not log a warning about the illegal header when not in dev mode', () => { + const headers = { + 'x-foo': 'bar', + Connection: 'keep-alive', + }; + stripIllegalHttp2Headers({ + headers, + isDev: false, + logger, + requestContext: 'requestContext', + }); + + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it('does not mutate the original headers', () => { + const headers = { + 'x-foo': 'bar', + Connection: 'keep-alive', + }; + stripIllegalHttp2Headers({ + headers, + isDev: true, + logger, + requestContext: 'requestContext', + }); + + expect(headers).toEqual({ + 'x-foo': 'bar', + Connection: 'keep-alive', + }); + }); +}); diff --git a/packages/core/http/core-http-router-server-internal/src/strip_illegal_http2_headers.ts b/packages/core/http/core-http-router-server-internal/src/strip_illegal_http2_headers.ts new file mode 100644 index 0000000000000..75517fc498254 --- /dev/null +++ b/packages/core/http/core-http-router-server-internal/src/strip_illegal_http2_headers.ts @@ -0,0 +1,49 @@ +/* + * 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 type { Logger } from '@kbn/logging'; +import type { ResponseHeaders } from '@kbn/core-http-server'; + +// from https://github.com/nodejs/node/blob/v22.2.0/lib/internal/http2/util.js#L557 +const ILLEGAL_HTTP2_CONNECTION_HEADERS = new Set([ + 'connection', + 'proxy-connection', + 'keep-alive', + 'upgrade', + 'transfer-encoding', + 'http2-settings', +]); + +/** + * Return a new version of the provided headers, with all illegal http2 headers removed. + * If `isDev` is `true`, will also log a warning if such header is encountered. + */ +export const stripIllegalHttp2Headers = ({ + headers, + isDev, + logger, + requestContext, +}: { + headers: ResponseHeaders; + isDev: boolean; + logger: Logger; + requestContext: string; +}): ResponseHeaders => { + return Object.entries(headers).reduce((output, [headerName, headerValue]) => { + if (ILLEGAL_HTTP2_CONNECTION_HEADERS.has(headerName.toLowerCase())) { + if (isDev) { + logger.warn( + `Handler for "${requestContext}" returned an illegal http2 header: ${headerName}. Please check "request.protocol" in handlers before assigning connection headers` + ); + } + } else { + output[headerName as keyof ResponseHeaders] = headerValue; + } + return output; + }, {} as ResponseHeaders); +}; diff --git a/src/core/server/integration_tests/http/http2_protocol.test.ts b/src/core/server/integration_tests/http/http2_protocol.test.ts index c1d234f00358e..71f40263335d7 100644 --- a/src/core/server/integration_tests/http/http2_protocol.test.ts +++ b/src/core/server/integration_tests/http/http2_protocol.test.ts @@ -166,4 +166,49 @@ describe('Http2 - Smoke tests', () => { }); }); }); + + describe('HTTP2-specific behavior', () => { + beforeEach(async () => { + const { registerRouter, server: innerServer } = await server.setup({ config$: of(config) }); + innerServerListener = innerServer.listener; + + const router = new Router('', logger, enhanceWithContext, { + isDev: false, + versionedRouterOptions: { + defaultHandlerResolutionStrategy: 'oldest', + }, + }); + + router.get({ path: '/illegal_headers', validate: false }, async (context, req, res) => { + return res.ok({ + headers: { + connection: 'close', + }, + body: { protocol: req.protocol }, + }); + }); + + registerRouter(router); + + await server.start(); + }); + + describe('illegal http2 headers', () => { + it('should strip illegal http2 headers without causing errors when serving HTTP/2', async () => { + const response = await supertest(innerServerListener).get('/illegal_headers').http2(); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ protocol: 'http2' }); + expect(response.header).toEqual(expect.not.objectContaining({ connection: 'close' })); + }); + + it('should keep illegal http2 headers when serving HTTP/1.x', async () => { + const response = await supertest(innerServerListener).get('/illegal_headers'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ protocol: 'http1' }); + expect(response.header).toEqual(expect.objectContaining({ connection: 'close' })); + }); + }); + }); }); From 55c0e10278dc9d3cb3128e6e41f7ab0b80cdfe5d Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 23 May 2024 15:04:03 +0000 Subject: [PATCH 17/36] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- .../core/http/core-http-router-server-internal/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/http/core-http-router-server-internal/tsconfig.json b/packages/core/http/core-http-router-server-internal/tsconfig.json index e4a70cbcddeec..f14271e7bb53a 100644 --- a/packages/core/http/core-http-router-server-internal/tsconfig.json +++ b/packages/core/http/core-http-router-server-internal/tsconfig.json @@ -17,7 +17,8 @@ "@kbn/hapi-mocks", "@kbn/core-logging-server-mocks", "@kbn/logging", - "@kbn/core-http-common" + "@kbn/core-http-common", + "@kbn/logging-mocks" ], "exclude": [ "target/**/*", From ec6e95cf6414868cdcf2f138d7687937a481db90 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 23 May 2024 17:11:57 +0200 Subject: [PATCH 18/36] Add documentation for `server.protocol` --- docs/setup/settings.asciidoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index a1f0a4ebed8a4..4405b746ca34f 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -456,6 +456,14 @@ identifies this {kib} instance. *Default: `"your-hostname"`* {kib} is served by a back end server. This setting specifies the port to use. *Default: `5601`* +`server.protocol`:: +experimental[] The http protocol to use, either `http1` or `http2`. Set to `http2` to enable `HTTP/2` support for the {kib} server. +*Default: `http1`* ++ +NOTE: By default, enabling `http2` requires a valid `h2c` configuration, meaning that TLS must be enabled via <> +and <>, if specified, must contain at least `TLSv1.2` or `TLSv1.3`. Strict validation of +the `h2c` setup can be disabled by adding `server.http2.allowUnsecure: true` to the configuration. + [[server-requestId-allowFromAnyIp]] `server.requestId.allowFromAnyIp`:: Sets whether or not the `X-Opaque-Id` header should be trusted from any IP address for identifying requests in logs and forwarded to Elasticsearch. From 84009a536e2e87cd9bdce10e1b0713aafb526786 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Fri, 24 May 2024 16:11:43 +0200 Subject: [PATCH 19/36] add `--http2` dev cli flag --- src/cli/serve/serve.js | 69 ++++++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index d431f6620d7f1..48b94a4fa8c61 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -101,6 +101,12 @@ export function applyConfigOverrides(rawConfig, opts, extraCliOptions, keystoreC const get = _.partial(_.get, rawConfig); const has = _.partial(_.has, rawConfig); + function ensureNotDefined(path, command = '--ssl') { + if (has(path)) { + throw new Error(`Can't use ${command} when "${path}" configuration is already defined.`); + } + } + if (opts.oss) { delete rawConfig.xpack; } @@ -152,49 +158,59 @@ export function applyConfigOverrides(rawConfig, opts, extraCliOptions, keystoreC } } - if (opts.ssl) { + if (opts.http2) { + set('server.protocol', 'http2'); + } + + // HTTP TLS configuration + if (opts.ssl || opts.http2) { // @kbn/dev-utils is part of devDependencies // eslint-disable-next-line import/no-extraneous-dependencies const { CA_CERT_PATH, KBN_KEY_PATH, KBN_CERT_PATH } = require('@kbn/dev-utils'); - const customElasticsearchHosts = opts.elasticsearch - ? opts.elasticsearch.split(',') - : [].concat(get('elasticsearch.hosts') || []); - - function ensureNotDefined(path) { - if (has(path)) { - throw new Error(`Can't use --ssl when "${path}" configuration is already defined.`); - } - } ensureNotDefined('server.ssl.certificate'); ensureNotDefined('server.ssl.key'); ensureNotDefined('server.ssl.keystore.path'); ensureNotDefined('server.ssl.truststore.path'); ensureNotDefined('server.ssl.certificateAuthorities'); - ensureNotDefined('elasticsearch.ssl.certificateAuthorities'); - const elasticsearchHosts = ( - (customElasticsearchHosts.length > 0 && customElasticsearchHosts) || [ - 'https://localhost:9200', - ] - ).map((hostUrl) => { - const parsedUrl = url.parse(hostUrl); - if (parsedUrl.hostname !== 'localhost') { - throw new Error( - `Hostname "${parsedUrl.hostname}" can't be used with --ssl. Must be "localhost" to work with certificates.` - ); - } - return `https://localhost:${parsedUrl.port}`; - }); set('server.ssl.enabled', true); set('server.ssl.certificate', KBN_CERT_PATH); set('server.ssl.key', KBN_KEY_PATH); set('server.ssl.certificateAuthorities', CA_CERT_PATH); - set('elasticsearch.hosts', elasticsearchHosts); - set('elasticsearch.ssl.certificateAuthorities', CA_CERT_PATH); } } + // Kib/ES encryption + if (opts.ssl) { + // @kbn/dev-utils is part of devDependencies + // eslint-disable-next-line import/no-extraneous-dependencies + const { CA_CERT_PATH } = require('@kbn/dev-utils'); + + const customElasticsearchHosts = opts.elasticsearch + ? opts.elasticsearch.split(',') + : [].concat(get('elasticsearch.hosts') || []); + + ensureNotDefined('elasticsearch.ssl.certificateAuthorities'); + + const elasticsearchHosts = ( + (customElasticsearchHosts.length > 0 && customElasticsearchHosts) || [ + 'https://localhost:9200', + ] + ).map((hostUrl) => { + const parsedUrl = url.parse(hostUrl); + if (parsedUrl.hostname !== 'localhost') { + throw new Error( + `Hostname "${parsedUrl.hostname}" can't be used with --ssl. Must be "localhost" to work with certificates.` + ); + } + return `https://localhost:${parsedUrl.port}`; + }); + + set('elasticsearch.hosts', elasticsearchHosts); + set('elasticsearch.ssl.certificateAuthorities', CA_CERT_PATH); + } + if (opts.elasticsearch) set('elasticsearch.hosts', opts.elasticsearch.split(',')); if (opts.port) set('server.port', opts.port); if (opts.host) set('server.host', opts.host); @@ -262,6 +278,7 @@ export default function (program) { command .option('--dev', 'Run the server with development mode defaults') .option('--ssl', 'Run the dev server using HTTPS') + .option('--http2', 'Run the dev server using HTTP2 with TLS') .option('--dist', 'Use production assets from kbn/optimizer') .option( '--no-base-path', From 41b7951ac691e6782c2569abe473a06750b4ca01 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 27 May 2024 14:25:22 +0200 Subject: [PATCH 20/36] add http2 implementation for basepath proxy server --- package.json | 4 +- .../http1.ts} | 20 +- .../src/base_path_proxy/http2.ts | 179 ++++++++++++++++++ .../src/base_path_proxy/index.ts | 33 ++++ .../src/base_path_proxy/types.ts | 24 +++ .../src/base_path_proxy/utils.ts | 15 ++ .../kbn-cli-dev-mode/src/cli_dev_mode.test.ts | 20 +- packages/kbn-cli-dev-mode/src/cli_dev_mode.ts | 8 +- .../base_path_proxy_server.test.ts | 20 +- yarn.lock | 38 +++- 10 files changed, 322 insertions(+), 39 deletions(-) rename packages/kbn-cli-dev-mode/src/{base_path_proxy_server.ts => base_path_proxy/http1.ts} (92%) create mode 100644 packages/kbn-cli-dev-mode/src/base_path_proxy/http2.ts create mode 100644 packages/kbn-cli-dev-mode/src/base_path_proxy/index.ts create mode 100644 packages/kbn-cli-dev-mode/src/base_path_proxy/types.ts create mode 100644 packages/kbn-cli-dev-mode/src/base_path_proxy/utils.ts diff --git a/package.json b/package.json index f4f03cb30722e..9df1ff7985d97 100644 --- a/package.json +++ b/package.json @@ -1623,6 +1623,8 @@ "html": "1.0.0", "html-loader": "^1.3.2", "http-proxy": "^1.18.1", + "http2-proxy": "^5.0.53", + "http2-wrapper": "^2.2.1", "ignore": "^5.3.0", "jest": "^29.6.1", "jest-canvas-mock": "^2.5.2", @@ -1729,4 +1731,4 @@ "zod-to-json-schema": "^3.22.3" }, "packageManager": "yarn@1.22.21" -} \ No newline at end of file +} diff --git a/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts b/packages/kbn-cli-dev-mode/src/base_path_proxy/http1.ts similarity index 92% rename from packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts rename to packages/kbn-cli-dev-mode/src/base_path_proxy/http1.ts index f9aaad7923152..ffbf451384cbf 100644 --- a/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts +++ b/packages/kbn-cli-dev-mode/src/base_path_proxy/http1.ts @@ -11,28 +11,18 @@ import { Agent as HttpsAgent, ServerOptions as TlsOptions } from 'https'; import apm from 'elastic-apm-node'; import { Server, Request } from '@hapi/hapi'; import HapiProxy from '@hapi/h2o2'; -import { sampleSize } from 'lodash'; -import * as Rx from 'rxjs'; import { take } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; import { createServer, getServerOptions } from '@kbn/server-http-tools'; -import { DevConfig, HttpConfig } from './config'; -import { Log } from './log'; +import { DevConfig, HttpConfig } from '../config'; +import { Log } from '../log'; +import { getRandomBasePath } from './utils'; +import type { BasePathProxyServer, BasePathProxyServerOptions } from './types'; const ONE_GIGABYTE = 1024 * 1024 * 1024; -const alphabet = 'abcdefghijklmnopqrztuvwxyz'.split(''); -// Thank you, Spencer! :elasticheart: -const getRandomBasePath = () => - Math.random() * 100 < 1 ? 'spalger' : sampleSize(alphabet, 3).join(''); - -export interface BasePathProxyServerOptions { - shouldRedirectFromOldBasePath: (path: string) => boolean; - delayUntil: () => Rx.Observable; -} - -export class BasePathProxyServer { +export class Http1BasePathProxyServer implements BasePathProxyServer { private readonly httpConfig: HttpConfig; private server?: Server; private httpsAgent?: HttpsAgent; diff --git a/packages/kbn-cli-dev-mode/src/base_path_proxy/http2.ts b/packages/kbn-cli-dev-mode/src/base_path_proxy/http2.ts new file mode 100644 index 0000000000000..6032e6b3054b2 --- /dev/null +++ b/packages/kbn-cli-dev-mode/src/base_path_proxy/http2.ts @@ -0,0 +1,179 @@ +/* + * 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 Url from 'url'; +import { Agent as HttpsAgent, ServerOptions as TlsOptions } from 'https'; +import http2, { Agent as Http2Agent, AutoRequestOptions } from 'http2-wrapper'; +import http2Proxy from 'http2-proxy'; +import { take } from 'rxjs'; +import { getServerOptions, getServerTLSOptions } from '@kbn/server-http-tools'; + +import { DevConfig, HttpConfig } from '../config'; +import { Log } from '../log'; +import type { BasePathProxyServer, BasePathProxyServerOptions } from './types'; +import { getRandomBasePath } from './utils'; + +export class Http2BasePathProxyServer implements BasePathProxyServer { + private readonly httpConfig: HttpConfig; + private server?: http2.Http2SecureServer; + private httpsAgent?: HttpsAgent; + + constructor( + private readonly log: Log, + httpConfig: HttpConfig, + private readonly devConfig: DevConfig + ) { + this.httpConfig = { + ...httpConfig, + basePath: httpConfig.basePath ?? `/${getRandomBasePath()}`, + }; + } + + public get basePath() { + return this.httpConfig.basePath; + } + + public get targetPort() { + return this.devConfig.basePathProxyTargetPort; + } + + public get host() { + return this.httpConfig.host; + } + + public get port() { + return this.httpConfig.port; + } + + public async start(options: BasePathProxyServerOptions) { + const serverOptions = getServerOptions(this.httpConfig); + + if (this.httpConfig.ssl.enabled) { + const tlsOptions = serverOptions.tls as TlsOptions; + this.httpsAgent = new HttpsAgent({ + ca: tlsOptions.ca, + cert: tlsOptions.cert, + key: tlsOptions.key, + passphrase: tlsOptions.passphrase, + rejectUnauthorized: false, + }); + } + + await this.setupServer(options); + + this.log.write( + `basepath proxy server running at ${Url.format({ + host: this.httpConfig.host, + pathname: this.httpConfig.basePath, + })}` + ); + } + + public async stop() { + if (this.server !== undefined) { + await new Promise((resolve, reject) => { + this.server!.close((err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + this.server = undefined; + } + + if (this.httpsAgent !== undefined) { + this.httpsAgent.destroy(); + this.httpsAgent = undefined; + } + } + + private async setupServer({ delayUntil }: Readonly) { + const tlsOptions = getServerTLSOptions(this.httpConfig.ssl); + const server = http2.createSecureServer({ + ...tlsOptions, + rejectUnauthorized: false, + }); + + const http2Agent = new Http2Agent(); + + server.on('error', (e) => { + this.log.bad('error', `error initializing the base path server: ${e.message}`); + throw e; + }); + + server.listen(this.httpConfig.port, this.httpConfig.host, () => { + server.on('request', (inboundRequest, inboundResponse) => { + const requestPath = Url.parse(inboundRequest.url).path ?? '/'; + + if (requestPath === '/') { + // Always redirect from root URL to the URL with basepath. + inboundResponse.writeHead(302, { + location: this.httpConfig.basePath, + }); + inboundResponse.end(); + } else if (requestPath.startsWith(this.httpConfig.basePath!)) { + // Perform proxy request if requested path is within base path + http2Proxy.web( + inboundRequest, + inboundResponse, + { + protocol: 'https', + hostname: this.httpConfig.host, + port: this.devConfig.basePathProxyTargetPort, + onReq: async (request, options) => { + // 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). + await delayUntil().pipe(take(1)).toPromise(); + + const proxyOptions = { + ...options, + ...tlsOptions, + rejectUnauthorized: false, + path: options.path, + agent: { + https: this.httpsAgent ?? false, + http2: http2Agent, + }, + } as AutoRequestOptions; + + const proxyReq = await http2.auto(proxyOptions, (proxyRes) => { + // `http2-proxy` doesn't automatically remove pseudo-headers + for (const name in proxyRes.headers) { + if (name.startsWith(':')) { + delete proxyRes.headers[name]; + } + } + }); + + // `http2-proxy` waits for the `socket` event before calling `h2request.end()` + proxyReq.flushHeaders(); + return proxyReq; + }, + onRes: async (request, response, _proxyRes) => { + // wrong type - proxyRes is declared as Http.ServerResponse but is Http.IncomingMessage + const proxyRes = _proxyRes as unknown as http2.IncomingMessage; + response.setHeader('x-powered-by', 'kibana-base-path-server'); + response.writeHead(proxyRes.statusCode!, proxyRes.headers); + proxyRes.pipe(response); + }, + }, + (err, req, res) => { + if (err) { + this.log.bad('warning', 'base path proxy: error forwarding request', err); + res.statusCode = (err as any).statusCode || 500; + res.end((err as any).stack ?? err.message); + } + } + ); + } + }); + }); + } +} diff --git a/packages/kbn-cli-dev-mode/src/base_path_proxy/index.ts b/packages/kbn-cli-dev-mode/src/base_path_proxy/index.ts new file mode 100644 index 0000000000000..fe0c36b3579d7 --- /dev/null +++ b/packages/kbn-cli-dev-mode/src/base_path_proxy/index.ts @@ -0,0 +1,33 @@ +/* + * 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 type { Log } from '../log'; +import type { CliDevConfig } from '../config'; +import type { BasePathProxyServer } from './types'; +import { Http1BasePathProxyServer } from './http1'; +import { Http2BasePathProxyServer } from './http2'; + +export type { BasePathProxyServer, BasePathProxyServerOptions } from './types'; +export { Http1BasePathProxyServer } from './http1'; +export { Http2BasePathProxyServer } from './http2'; + +export const getBasePathProxyServer = ({ + log, + httpConfig, + devConfig, +}: { + log: Log; + httpConfig: CliDevConfig['http']; + devConfig: CliDevConfig['dev']; +}): BasePathProxyServer => { + if (httpConfig.protocol === 'http2') { + return new Http2BasePathProxyServer(log, httpConfig, devConfig); + } else { + return new Http1BasePathProxyServer(log, httpConfig, devConfig); + } +}; diff --git a/packages/kbn-cli-dev-mode/src/base_path_proxy/types.ts b/packages/kbn-cli-dev-mode/src/base_path_proxy/types.ts new file mode 100644 index 0000000000000..ca343e2744fb0 --- /dev/null +++ b/packages/kbn-cli-dev-mode/src/base_path_proxy/types.ts @@ -0,0 +1,24 @@ +/* + * 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 type { Observable } from 'rxjs'; + +export interface BasePathProxyServer { + readonly basePath: string | undefined; + readonly targetPort: number; + readonly host: string; + readonly port: number; + + start(options: BasePathProxyServerOptions): Promise; + stop(): Promise; +} + +export interface BasePathProxyServerOptions { + shouldRedirectFromOldBasePath: (path: string) => boolean; + delayUntil: () => Observable; +} diff --git a/packages/kbn-cli-dev-mode/src/base_path_proxy/utils.ts b/packages/kbn-cli-dev-mode/src/base_path_proxy/utils.ts new file mode 100644 index 0000000000000..afd05bb90b4ae --- /dev/null +++ b/packages/kbn-cli-dev-mode/src/base_path_proxy/utils.ts @@ -0,0 +1,15 @@ +/* + * 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 { sampleSize } from 'lodash'; + +const alphabet = 'abcdefghijklmnopqrztuvwxyz'.split(''); + +// Thank you, Spencer! :elasticheart: +export const getRandomBasePath = () => + Math.random() * 100 < 1 ? 'spalger' : sampleSize(alphabet, 3).join(''); diff --git a/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts b/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts index 769242436a270..a6b73571f05ca 100644 --- a/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts +++ b/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts @@ -29,8 +29,8 @@ const { Optimizer } = jest.requireMock('./optimizer'); jest.mock('./dev_server'); const { DevServer } = jest.requireMock('./dev_server'); -jest.mock('./base_path_proxy_server'); -const { BasePathProxyServer } = jest.requireMock('./base_path_proxy_server'); +jest.mock('./base_path_proxy'); +const { getBasePathProxyServer } = jest.requireMock('./base_path_proxy'); jest.mock('@kbn/ci-stats-reporter'); const { CiStatsReporter } = jest.requireMock('@kbn/ci-stats-reporter'); @@ -47,7 +47,7 @@ let log: TestLog; beforeEach(() => { process.argv = ['node', './script', 'foo', 'bar', 'baz']; log = new TestLog(); - BasePathProxyServer.mockImplementation(() => mockBasePathProxy); + getBasePathProxyServer.mockImplementation(() => mockBasePathProxy); }); afterEach(() => { @@ -142,7 +142,7 @@ it('passes correct args to sub-classes', () => { ] `); - expect(BasePathProxyServer).not.toHaveBeenCalled(); + expect(getBasePathProxyServer).not.toHaveBeenCalled(); expect(log.messages).toMatchInlineSnapshot(`Array []`); }); @@ -163,13 +163,15 @@ it('disables the watcher', () => { it('enables the basePath proxy', () => { new CliDevMode(createOptions({ cliArgs: { basePath: true } })); - expect(BasePathProxyServer).toHaveBeenCalledTimes(1); - expect(BasePathProxyServer.mock.calls[0]).toMatchInlineSnapshot(` + expect(getBasePathProxyServer).toHaveBeenCalledTimes(1); + expect(getBasePathProxyServer.mock.calls[0]).toMatchInlineSnapshot(` Array [ - , - Object {}, Object { - "basePathProxyTargetPort": 9000, + "devConfig": Object { + "basePathProxyTargetPort": 9000, + }, + "httpConfig": Object {}, + "log": , }, ] `); diff --git a/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts b/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts index 05791256f1ff1..f5e731f6a55af 100644 --- a/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts +++ b/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts @@ -29,7 +29,7 @@ import { Log, CliLog } from './log'; import { Optimizer } from './optimizer'; import { DevServer } from './dev_server'; import { Watcher } from './watcher'; -import { BasePathProxyServer } from './base_path_proxy_server'; +import { getBasePathProxyServer, type BasePathProxyServer } from './base_path_proxy'; import { shouldRedirectFromOldBasePath } from './should_redirect_from_old_base_path'; import { CliDevConfig } from './config'; @@ -110,7 +110,11 @@ export class CliDevMode { this.log = log || new CliLog(!!cliArgs.silent); if (cliArgs.basePath) { - this.basePathProxy = new BasePathProxyServer(this.log, config.http, config.dev); + this.basePathProxy = getBasePathProxyServer({ + log: this.log, + devConfig: config.dev, + httpConfig: config.http, + }); } this.watcher = new Watcher({ diff --git a/packages/kbn-cli-dev-mode/src/integration_tests/base_path_proxy_server.test.ts b/packages/kbn-cli-dev-mode/src/integration_tests/base_path_proxy_server.test.ts index a8d88df7702ec..a074395d86554 100644 --- a/packages/kbn-cli-dev-mode/src/integration_tests/base_path_proxy_server.test.ts +++ b/packages/kbn-cli-dev-mode/src/integration_tests/base_path_proxy_server.test.ts @@ -13,13 +13,13 @@ import supertest from 'supertest'; import { getServerOptions, createServer, type IHttpConfig } from '@kbn/server-http-tools'; import { ByteSizeValue } from '@kbn/config-schema'; -import { BasePathProxyServer, BasePathProxyServerOptions } from '../base_path_proxy_server'; +import { Http1BasePathProxyServer, BasePathProxyServerOptions } from '../base_path_proxy'; import { DevConfig } from '../config/dev_config'; import { TestLog } from '../log'; -describe('BasePathProxyServer', () => { +describe('Http1BasePathProxyServer', () => { let server: Server; - let proxyServer: BasePathProxyServer; + let proxyServer: Http1BasePathProxyServer; let logger: TestLog; let config: IHttpConfig; let basePath: string; @@ -52,7 +52,7 @@ describe('BasePathProxyServer', () => { // setup and start the proxy server const proxyConfig: IHttpConfig = { ...config, port: 10013 }; const devConfig = new DevConfig({ basePathProxyTarget: config.port }); - proxyServer = new BasePathProxyServer(logger, proxyConfig, devConfig); + proxyServer = new Http1BasePathProxyServer(logger, proxyConfig, devConfig); const options: BasePathProxyServerOptions = { shouldRedirectFromOldBasePath: () => true, delayUntil: () => EMPTY, @@ -323,14 +323,18 @@ describe('BasePathProxyServer', () => { }); describe('shouldRedirect', () => { - let proxyServerWithoutShouldRedirect: BasePathProxyServer; + let proxyServerWithoutShouldRedirect: Http1BasePathProxyServer; let proxyWithoutShouldRedirectSupertest: supertest.Agent; beforeEach(async () => { // setup and start a proxy server which does not use "shouldRedirectFromOldBasePath" const proxyConfig: IHttpConfig = { ...config, port: 10004 }; const devConfig = new DevConfig({ basePathProxyTarget: config.port }); - proxyServerWithoutShouldRedirect = new BasePathProxyServer(logger, proxyConfig, devConfig); + proxyServerWithoutShouldRedirect = new Http1BasePathProxyServer( + logger, + proxyConfig, + devConfig + ); const options: Readonly = { shouldRedirectFromOldBasePath: () => false, // Return false to not redirect delayUntil: () => EMPTY, @@ -366,14 +370,14 @@ describe('BasePathProxyServer', () => { }); describe('constructor option for sending in a custom basePath', () => { - let proxyServerWithFooBasePath: BasePathProxyServer; + let proxyServerWithFooBasePath: Http1BasePathProxyServer; let proxyWithFooBasePath: supertest.Agent; beforeEach(async () => { // setup and start a proxy server which uses a basePath of "foo" const proxyConfig = { ...config, port: 10004, basePath: '/foo' }; // <-- "foo" here in basePath const devConfig = new DevConfig({ basePathProxyTarget: config.port }); - proxyServerWithFooBasePath = new BasePathProxyServer(logger, proxyConfig, devConfig); + proxyServerWithFooBasePath = new Http1BasePathProxyServer(logger, proxyConfig, devConfig); const options: Readonly = { shouldRedirectFromOldBasePath: () => true, delayUntil: () => EMPTY, diff --git a/yarn.lock b/yarn.lock index 5f1865d8d5f8a..645b431bbe0ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19129,6 +19129,11 @@ http2-client@^1.2.5: resolved "https://registry.yarnpkg.com/http2-client/-/http2-client-1.3.5.tgz#20c9dc909e3cc98284dd20af2432c524086df181" integrity sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA== +http2-proxy@^5.0.53: + version "5.0.53" + resolved "https://registry.yarnpkg.com/http2-proxy/-/http2-proxy-5.0.53.tgz#fc6cb07d2bb977a388ebeec4449557f2011e5a1f" + integrity sha512-k9OUKrPWau/YeViJGv5peEFgSGPE2n8CDyk/G3f+JfaaJzbFMPAK5PJTd99QYSUvgUwVBGNbZJCY/BEb+kUZNQ== + http2-wrapper@^1.0.0-beta.5.2: version "1.0.0-beta.5.2" resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz#8b923deb90144aea65cf834b016a340fc98556f3" @@ -19137,7 +19142,7 @@ http2-wrapper@^1.0.0-beta.5.2: quick-lru "^5.1.1" resolve-alpn "^1.0.0" -http2-wrapper@^2.1.10: +http2-wrapper@^2.1.10, http2-wrapper@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-2.2.1.tgz#310968153dcdedb160d8b72114363ef5fce1f64a" integrity sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ== @@ -28759,7 +28764,7 @@ string-replace-loader@^2.2.0: loader-utils "^1.2.3" schema-utils "^1.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -28777,6 +28782,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -28886,7 +28900,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -28900,6 +28914,13 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -31772,7 +31793,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -31798,6 +31819,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 0799cba329402ba6011fb01dafc391b794d3b05f Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 27 May 2024 15:09:33 +0200 Subject: [PATCH 21/36] right. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9df1ff7985d97..6bf5679515a89 100644 --- a/package.json +++ b/package.json @@ -1731,4 +1731,4 @@ "zod-to-json-schema": "^3.22.3" }, "packageManager": "yarn@1.22.21" -} +} \ No newline at end of file From fd5b5a8b5c24af48d365d7e2757d83c13e005773 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 27 May 2024 15:15:00 +0200 Subject: [PATCH 22/36] add test for http2 proxy --- .../src/base_path_proxy/http2.ts | 5 +- ...s => http1_base_path_proxy_server.test.ts} | 0 .../http2_base_path_proxy_server.test.ts | 254 ++++++++++++++++++ 3 files changed, 258 insertions(+), 1 deletion(-) rename packages/kbn-cli-dev-mode/src/integration_tests/{base_path_proxy_server.test.ts => http1_base_path_proxy_server.test.ts} (100%) create mode 100644 packages/kbn-cli-dev-mode/src/integration_tests/http2_base_path_proxy_server.test.ts diff --git a/packages/kbn-cli-dev-mode/src/base_path_proxy/http2.ts b/packages/kbn-cli-dev-mode/src/base_path_proxy/http2.ts index 6032e6b3054b2..4777533519d9c 100644 --- a/packages/kbn-cli-dev-mode/src/base_path_proxy/http2.ts +++ b/packages/kbn-cli-dev-mode/src/base_path_proxy/http2.ts @@ -96,11 +96,14 @@ export class Http2BasePathProxyServer implements BasePathProxyServer { private async setupServer({ delayUntil }: Readonly) { const tlsOptions = getServerTLSOptions(this.httpConfig.ssl); - const server = http2.createSecureServer({ + this.server = http2.createSecureServer({ ...tlsOptions, rejectUnauthorized: false, + allowHTTP1: true, }); + const server = this.server; + const http2Agent = new Http2Agent(); server.on('error', (e) => { diff --git a/packages/kbn-cli-dev-mode/src/integration_tests/base_path_proxy_server.test.ts b/packages/kbn-cli-dev-mode/src/integration_tests/http1_base_path_proxy_server.test.ts similarity index 100% rename from packages/kbn-cli-dev-mode/src/integration_tests/base_path_proxy_server.test.ts rename to packages/kbn-cli-dev-mode/src/integration_tests/http1_base_path_proxy_server.test.ts diff --git a/packages/kbn-cli-dev-mode/src/integration_tests/http2_base_path_proxy_server.test.ts b/packages/kbn-cli-dev-mode/src/integration_tests/http2_base_path_proxy_server.test.ts new file mode 100644 index 0000000000000..a12bae003baa4 --- /dev/null +++ b/packages/kbn-cli-dev-mode/src/integration_tests/http2_base_path_proxy_server.test.ts @@ -0,0 +1,254 @@ +/* + * 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 { readFileSync } from 'fs'; +import { Server } from '@hapi/hapi'; +import { EMPTY } from 'rxjs'; +import moment from 'moment'; +import supertest from 'supertest'; +import { KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; +import { getServerOptions, createServer, type IHttpConfig } from '@kbn/server-http-tools'; +import { ByteSizeValue } from '@kbn/config-schema'; + +import { Http2BasePathProxyServer, BasePathProxyServerOptions } from '../base_path_proxy'; +import { DevConfig } from '../config/dev_config'; +import { TestLog } from '../log'; + +describe('Http2BasePathProxyServer', () => { + let server: Server; + let proxyServer: Http2BasePathProxyServer; + let logger: TestLog; + let config: IHttpConfig; + let basePath: string; + let proxySupertest: supertest.Agent; + + beforeAll(() => { + // required for the self-signed certificates used in testing + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + }); + + beforeEach(async () => { + logger = new TestLog(); + + config = { + protocol: 'http2', + host: '127.0.0.1', + port: 10012, + shutdownTimeout: moment.duration(30, 'seconds'), + keepaliveTimeout: 1000, + socketTimeout: 1000, + payloadTimeout: 1000, + cors: { + enabled: false, + allowCredentials: false, + allowOrigin: [], + }, + ssl: { + enabled: true, + certificate: await readFileSync(KBN_CERT_PATH, 'utf-8'), + key: await readFileSync(KBN_KEY_PATH, 'utf-8'), + cipherSuites: ['TLS_AES_256_GCM_SHA384'], + }, + maxPayload: new ByteSizeValue(1024), + restrictInternalApis: false, + }; + + const serverOptions = getServerOptions(config); + server = createServer(serverOptions); + + // setup and start the proxy server + const proxyConfig: IHttpConfig = { ...config, port: 10013 }; + const devConfig = new DevConfig({ basePathProxyTarget: config.port }); + proxyServer = new Http2BasePathProxyServer(logger, proxyConfig, devConfig); + const options: BasePathProxyServerOptions = { + shouldRedirectFromOldBasePath: () => true, + delayUntil: () => EMPTY, + }; + await proxyServer.start(options); + + // set the base path or throw if for some unknown reason it is not setup + if (proxyServer.basePath == null) { + throw new Error('Invalid null base path, all tests will fail'); + } else { + basePath = proxyServer.basePath; + } + proxySupertest = supertest(`https://127.0.0.1:${proxyConfig.port}`, { http2: true }); + }); + + afterEach(async () => { + await server.stop(); + await proxyServer.stop(); + jest.clearAllMocks(); + }); + + test('root URL will return a 302 redirect', async () => { + await proxySupertest.get('/').expect(302); + }); + + test('root URL will return a redirect location with exactly 3 characters that are a-z (or spalger)', async () => { + const res = await proxySupertest.get('/'); + const location = res.header.location; + expect(location).toMatch(/^\/(spalger|[a-z]{3})$/); + }); + + test('forwards request with the correct path', async () => { + server.route({ + method: 'GET', + path: `${basePath}/foo/{test}`, + handler: (request, h) => { + return h.response(request.params.test); + }, + }); + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/some-string`) + .expect(200) + .then((res) => { + expect(res.text).toBe('some-string'); + }); + }); + + test('can serve http/1.x requests', async () => { + server.route({ + method: 'GET', + path: `${basePath}/foo/{test}`, + handler: (request, h) => { + return h.response(request.params.test); + }, + }); + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/some-string`) + .http2(false) + .expect(200) + .then((res) => { + expect(res.text).toBe('some-string'); + }); + }); + + test('forwards request with the correct query params', async () => { + server.route({ + method: 'GET', + path: `${basePath}/foo/`, + handler: (request, h) => { + return h.response(request.query); + }, + }); + + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/?bar=test&quux=123`) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'test', quux: '123' }); + }); + }); + + test('forwards the request body', async () => { + server.route({ + method: 'POST', + path: `${basePath}/foo/`, + handler: (request, h) => { + return h.response(request.payload); + }, + }); + + await server.start(); + + await proxySupertest + .post(`${basePath}/foo/`) + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); + }); + + test('returns the correct status code', async () => { + server.route({ + method: 'GET', + path: `${basePath}/foo/`, + handler: (request, h) => { + return h.response({ foo: 'bar' }).code(417); + }, + }); + + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/`) + .expect(417) + .then((res) => { + expect(res.body).toEqual({ foo: 'bar' }); + }); + }); + + test('returns the response headers', async () => { + server.route({ + method: 'GET', + path: `${basePath}/foo/`, + handler: (request, h) => { + return h.response({ foo: 'bar' }).header('foo', 'bar'); + }, + }); + + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/`) + .expect(200) + .then((res) => { + expect(res.get('foo')).toEqual('bar'); + }); + }); + + test('forwards request cancellation', async () => { + let propagated = false; + + let notifyRequestReceived: () => void; + const requestReceived = new Promise((resolve) => { + notifyRequestReceived = resolve; + }); + + let notifyRequestAborted: () => void; + const requestAborted = new Promise((resolve) => { + notifyRequestAborted = resolve; + }); + + server.route({ + method: 'GET', + path: `${basePath}/foo/{test}`, + handler: async (request, h) => { + notifyRequestReceived(); + + request.raw.req.once('aborted', () => { + notifyRequestAborted(); + propagated = true; + }); + return await new Promise((resolve) => undefined); + }, + }); + await server.start(); + + const request = proxySupertest.get(`${basePath}/foo/some-string`).end(); + + await requestReceived; + + request.abort(); + + await requestAborted; + + expect(propagated).toEqual(true); + }); +}); From 943acfaadb3e9aa9cd86fabf098a9b70fc174b9a Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 27 May 2024 13:29:28 +0000 Subject: [PATCH 23/36] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- packages/kbn-cli-dev-mode/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kbn-cli-dev-mode/tsconfig.json b/packages/kbn-cli-dev-mode/tsconfig.json index 86ab4cf2592c4..ac2d5ef7b7892 100644 --- a/packages/kbn-cli-dev-mode/tsconfig.json +++ b/packages/kbn-cli-dev-mode/tsconfig.json @@ -25,6 +25,7 @@ "@kbn/import-resolver", "@kbn/picomatcher", "@kbn/repo-packages", + "@kbn/dev-utils", ], "exclude": [ "target/**/*", From 8bd0bc71df8920e54d2f5bd75b3bf86ce088a09d Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 27 May 2024 15:23:09 +0200 Subject: [PATCH 24/36] ignore cert errors for playwright --- packages/kbn-journeys/journey/journey_ftr_harness.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-journeys/journey/journey_ftr_harness.ts b/packages/kbn-journeys/journey/journey_ftr_harness.ts index 7ef05c29e9c8e..7339b7ffbca01 100644 --- a/packages/kbn-journeys/journey/journey_ftr_harness.ts +++ b/packages/kbn-journeys/journey/journey_ftr_harness.ts @@ -151,7 +151,7 @@ export class JourneyFtrHarness { private async setupBrowserAndPage() { const browser = await this.getBrowserInstance(); const browserContextArgs = this.auth.isCloud() ? {} : { bypassCSP: true }; - this.context = await browser.newContext(browserContextArgs); + this.context = await browser.newContext({ ...browserContextArgs, ignoreHTTPSErrors: true }); if (this.journeyConfig.shouldAutoLogin()) { const cookie = await this.auth.login(); From 1d579cd0dfe8cafb9b732a7399001791dfc2cd2a Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 27 May 2024 16:22:39 +0200 Subject: [PATCH 25/36] enable HTTP2 for functional oss FTR suites --- test/common/services/deployment.ts | 11 +++++++++++ test/functional/config.base.js | 20 +++++++++++++++++++- test/functional/services/supertest.ts | 20 +++++++++++++++++--- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/test/common/services/deployment.ts b/test/common/services/deployment.ts index b250d39ce65d6..20a8f1de26879 100644 --- a/test/common/services/deployment.ts +++ b/test/common/services/deployment.ts @@ -7,6 +7,7 @@ */ import { get } from 'lodash'; +import { Agent } from 'https'; import fetch from 'node-fetch'; import { getUrl } from '@kbn/test'; @@ -33,12 +34,22 @@ export class DeploymentService extends FtrService { const baseUrl = this.getHostPort(); const username = this.config.get('servers.kibana.username'); const password = this.config.get('servers.kibana.password'); + const protocol = this.config.get('servers.kibana.protocol'); + + let agent: Agent | undefined; + if (protocol === 'https') { + agent = new Agent({ + rejectUnauthorized: false, + }); + } + const response = await fetch(baseUrl + '/api/stats?extended', { method: 'get', headers: { 'Content-Type': 'application/json', Authorization: 'Basic ' + Buffer.from(username + ':' + password).toString('base64'), }, + agent, }); const data = await response.json(); return get(data, 'usage.cloud.is_cloud_enabled', false); diff --git a/test/functional/config.base.js b/test/functional/config.base.js index 2f0ec71ffbdaa..e68a82c9f3a2e 100644 --- a/test/functional/config.base.js +++ b/test/functional/config.base.js @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { readFileSync } from 'fs'; +import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import { pageObjects } from './page_objects'; import { services } from './services'; @@ -16,7 +18,14 @@ export default async function ({ readConfigFile }) { pageObjects, services, - servers: commonConfig.get('servers'), + servers: { + ...commonConfig.get('servers'), + kibana: { + ...commonConfig.get('servers.kibana'), + protocol: 'https', + certificateAuthorities: [readFileSync(CA_CERT_PATH, 'utf-8')], + }, + }, esTestCluster: { ...commonConfig.get('esTestCluster'), @@ -29,6 +38,14 @@ export default async function ({ readConfigFile }) { ...commonConfig.get('kbnTestServer.serverArgs'), '--telemetry.optIn=false', '--savedObjects.maxImportPayloadBytes=10485760', + + // Enable HTTP2 and TLS + '--server.protocol=http2', + '--server.ssl.enabled=true', + `--server.ssl.key=${KBN_KEY_PATH}`, + `--server.ssl.certificate=${KBN_CERT_PATH}`, + `--server.ssl.certificateAuthorities=${CA_CERT_PATH}`, + // override default to not allow hiddenFromHttpApis saved object types access to the HTTP Apis. see https://github.com/elastic/dev/issues/2200 '--savedObjects.allowHttpApiAccess=false', @@ -107,6 +124,7 @@ export default async function ({ readConfigFile }) { }, browser: { type: 'chrome', + acceptInsecureCerts: true, }, security: { diff --git a/test/functional/services/supertest.ts b/test/functional/services/supertest.ts index 32ecc3f51759d..a684230f315ea 100644 --- a/test/functional/services/supertest.ts +++ b/test/functional/services/supertest.ts @@ -8,11 +8,25 @@ import { format as formatUrl } from 'url'; -import supertest from 'supertest'; +import supertest, { AgentOptions } from 'supertest'; import { FtrProviderContext } from '../ftr_provider_context'; export function KibanaSupertestProvider({ getService }: FtrProviderContext) { const config = getService('config'); - const kibanaServerUrl = formatUrl(config.get('servers.kibana')); - return supertest(kibanaServerUrl); + const kibanaServerConfig = config.get('servers.kibana'); + const kibanaServerUrl = formatUrl(kibanaServerConfig); + + const options: AgentOptions = {}; + if (kibanaServerConfig.certificateAuthorities) { + options.ca = kibanaServerConfig.certificateAuthorities; + } + + // or add to server url in packages/kbn-test/src/functional_test_runner/lib/config/schema.ts maybe + const serverArgs = config.get('kbnTestServer.serverArgs', []) as string[]; + const http2Enabled = serverArgs.includes('--server.protocol=http2'); + if (http2Enabled) { + options.http2 = true; + } + + return supertest(kibanaServerUrl, options); } From 2dcdc59f6544dc5b2ff9044231eb0a3503fa708d Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 27 May 2024 16:32:24 +0200 Subject: [PATCH 26/36] fix newsfeed config def --- test/functional/config.base.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/functional/config.base.js b/test/functional/config.base.js index e68a82c9f3a2e..57d62772032f3 100644 --- a/test/functional/config.base.js +++ b/test/functional/config.base.js @@ -13,15 +13,16 @@ import { services } from './services'; export default async function ({ readConfigFile }) { const commonConfig = await readConfigFile(require.resolve('../common/config')); + const servers = commonConfig.get('servers'); return { pageObjects, services, servers: { - ...commonConfig.get('servers'), + ...servers, kibana: { - ...commonConfig.get('servers.kibana'), + ...servers.kibana, protocol: 'https', certificateAuthorities: [readFileSync(CA_CERT_PATH, 'utf-8')], }, @@ -46,6 +47,9 @@ export default async function ({ readConfigFile }) { `--server.ssl.certificate=${KBN_CERT_PATH}`, `--server.ssl.certificateAuthorities=${CA_CERT_PATH}`, + // required because the base config reference its own server definition... + `--newsfeed.service.urlRoot=https://${servers.kibana.hostname}:${servers.kibana.port}`, + // override default to not allow hiddenFromHttpApis saved object types access to the HTTP Apis. see https://github.com/elastic/dev/issues/2200 '--savedObjects.allowHttpApiAccess=false', From ca2fb9728f1edd2d45117aa64e04f21a56f978d1 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 27 May 2024 20:31:35 +0200 Subject: [PATCH 27/36] fix xpack func config --- .../src/kbn_client/kbn_client_requester.ts | 1 + test/api_integration/services/supertest.ts | 20 ++++++++++++++++--- x-pack/test/common/services/spaces.ts | 10 ++++++++++ x-pack/test/functional/config.base.js | 2 +- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/kbn-test/src/kbn_client/kbn_client_requester.ts b/packages/kbn-test/src/kbn_client/kbn_client_requester.ts index 9481e6481b936..1f9718e06794c 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_requester.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_requester.ts @@ -96,6 +96,7 @@ export class KbnClientRequester { Url.parse(options.url).protocol === 'https:' ? new Https.Agent({ ca: options.certificateAuthorities, + rejectUnauthorized: false, }) : null; } diff --git a/test/api_integration/services/supertest.ts b/test/api_integration/services/supertest.ts index d8ce0d918c45a..e369fe3cab7c3 100644 --- a/test/api_integration/services/supertest.ts +++ b/test/api_integration/services/supertest.ts @@ -10,13 +10,27 @@ import { systemIndicesSuperuser } from '@kbn/test'; import { format as formatUrl } from 'url'; -import supertest from 'supertest'; +import supertest, { AgentOptions } from 'supertest'; import { FtrProviderContext } from '../../functional/ftr_provider_context'; export function KibanaSupertestProvider({ getService }: FtrProviderContext): supertest.Agent { const config = getService('config'); - const kibanaServerUrl = formatUrl(config.get('servers.kibana')); - return supertest(kibanaServerUrl); + const kibanaServerConfig = config.get('servers.kibana'); + const kibanaServerUrl = formatUrl(kibanaServerConfig); + + const options: AgentOptions = {}; + if (kibanaServerConfig.certificateAuthorities) { + options.ca = kibanaServerConfig.certificateAuthorities; + } + + // or add to server url in packages/kbn-test/src/functional_test_runner/lib/config/schema.ts maybe + const serverArgs = config.get('kbnTestServer.serverArgs', []) as string[]; + const http2Enabled = serverArgs.includes('--server.protocol=http2'); + if (http2Enabled) { + options.http2 = true; + } + + return supertest(kibanaServerUrl, options); } export function ElasticsearchSupertestProvider({ diff --git a/x-pack/test/common/services/spaces.ts b/x-pack/test/common/services/spaces.ts index ad829e45fccec..44b0fbccbdf8b 100644 --- a/x-pack/test/common/services/spaces.ts +++ b/x-pack/test/common/services/spaces.ts @@ -7,6 +7,7 @@ import type { Space } from '@kbn/spaces-plugin/common'; import Axios from 'axios'; +import Https from 'https'; import { format as formatUrl } from 'url'; import util from 'util'; import { FtrProviderContext } from '../ftr_provider_context'; @@ -16,11 +17,20 @@ export function SpacesServiceProvider({ getService }: FtrProviderContext) { const config = getService('config'); const url = formatUrl(config.get('servers.kibana')); + const certificateAuthorities = config.get('servers.kibana.certificateAuthorities'); + const httpsAgent: Https.Agent | undefined = certificateAuthorities + ? new Https.Agent({ + ca: certificateAuthorities, + rejectUnauthorized: false, + }) + : undefined; + const axios = Axios.create({ headers: { 'kbn-xsrf': 'x-pack/ftr/services/spaces/space' }, baseURL: url, maxRedirects: 0, validateStatus: () => true, // we do our own validation below and throw better error messages + httpsAgent, }); return new (class SpacesService { diff --git a/x-pack/test/functional/config.base.js b/x-pack/test/functional/config.base.js index 1bf939c537b7c..50277ce341b8b 100644 --- a/x-pack/test/functional/config.base.js +++ b/x-pack/test/functional/config.base.js @@ -30,7 +30,7 @@ export default async function ({ readConfigFile }) { services, pageObjects, - servers: kibanaFunctionalConfig.get('servers'), + servers: kibanaCommonConfig.get('servers'), esTestCluster: { license: 'trial', From da6d7d95c9d4a8417d2b772abee3bb9263beee95 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 28 May 2024 09:18:24 +0200 Subject: [PATCH 28/36] making progress with FTR tests --- scripts/functional_test_runner.js | 1 + scripts/functional_tests.js | 1 + src/setup_node_env/exit_on_warning.js | 22 +++++- test/api_integration/services/supertest.ts | 1 + test/common/configure_http2.ts | 80 ++++++++++++++++++++++ test/common/services/deployment.ts | 1 + test/functional/config.base.js | 28 ++------ test/functional/services/supertest.ts | 1 + x-pack/test/common/services/spaces.ts | 1 + 9 files changed, 110 insertions(+), 26 deletions(-) create mode 100644 test/common/configure_http2.ts diff --git a/scripts/functional_test_runner.js b/scripts/functional_test_runner.js index 811e2083c7eeb..3829a26d18935 100644 --- a/scripts/functional_test_runner.js +++ b/scripts/functional_test_runner.js @@ -6,5 +6,6 @@ * Side Public License, v 1. */ +process.isFtrRunner = true; require('../src/setup_node_env'); require('@kbn/test').runFtrCli(); diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index eb1dea2dcab36..75f27e547ad9b 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -6,5 +6,6 @@ * Side Public License, v 1. */ +process.isFtrRunner = true; require('../src/setup_node_env'); require('@kbn/test').runTestsCli(); diff --git a/src/setup_node_env/exit_on_warning.js b/src/setup_node_env/exit_on_warning.js index c49ba629fee45..dc538d08dde49 100644 --- a/src/setup_node_env/exit_on_warning.js +++ b/src/setup_node_env/exit_on_warning.js @@ -67,6 +67,14 @@ var IGNORE_WARNINGS = [ messageContains: 'header is not valid, the value will be dropped from the header and will never be in use.', }, + // We have to enabled NODE_TLS_REJECT_UNAUTHORIZED for FTR testing + // when http2 is enabled to accept dev self-signed certificates + { + ftrOnly: true, + name: 'Warning', + message: + "Setting the NODE_TLS_REJECT_UNAUTHORIZED environment variable to '0' makes TLS connections and HTTPS requests insecure by disabling certificate verification.", + }, ]; if (process.noProcessWarnings !== true) { @@ -82,7 +90,6 @@ if (process.noProcessWarnings !== true) { console.error(); console.error('Terminating process...'); } - process.exit(1); }); @@ -101,7 +108,18 @@ if (process.noProcessWarnings !== true) { function shouldIgnore(warn) { warn = parseWarn(warn); - return IGNORE_WARNINGS.some(function ({ name, code, message, messageContains, file, line, col }) { + + return IGNORE_WARNINGS.some(function ({ + name, + code, + message, + messageContains, + file, + line, + col, + ftrOnly, + }) { + if (ftrOnly && !process.isFtrRunner) return false; if (name && name !== warn.name) return false; if (code && code !== warn.code) return false; if (message && message !== warn.message) return false; diff --git a/test/api_integration/services/supertest.ts b/test/api_integration/services/supertest.ts index e369fe3cab7c3..1b8c446d37397 100644 --- a/test/api_integration/services/supertest.ts +++ b/test/api_integration/services/supertest.ts @@ -21,6 +21,7 @@ export function KibanaSupertestProvider({ getService }: FtrProviderContext): sup const options: AgentOptions = {}; if (kibanaServerConfig.certificateAuthorities) { options.ca = kibanaServerConfig.certificateAuthorities; + options.rejectUnauthorized = false; } // or add to server url in packages/kbn-test/src/functional_test_runner/lib/config/schema.ts maybe diff --git a/test/common/configure_http2.ts b/test/common/configure_http2.ts new file mode 100644 index 0000000000000..2237a67897657 --- /dev/null +++ b/test/common/configure_http2.ts @@ -0,0 +1,80 @@ +/* + * 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 { readFileSync } from 'fs'; +import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; + +type ConfigType = Record; + +/** + * Enables HTTP2 by adding/changing the appropriate config settings + * + * note: it DOES mutate the provided config. + */ +export const configureHTTP2 = (config: ConfigType): ConfigType => { + // tell native node agents to trust unsafe certificates + // this is ugly, but unfortunately required, as some libraries (such as supertest) + // have no real alternatives to accept self-signed certs + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + + // tell webdriver browser to accept self-signed certificates + config.browser.acceptInsecureCerts = true; + + // change the configured kibana server to run on https with the dev CA + config.servers.kibana = { + ...config.servers.kibana, + protocol: 'https', + certificateAuthorities: [readFileSync(CA_CERT_PATH, 'utf-8')], + }; + + const serverArgs = config.kbnTestServer.serverArgs; + + // enable http2 on the kibana server + addOrReplaceKbnServerArg(serverArgs, 'server.protocol', () => 'http2'); + // enable and configure TLS on the kibana server + addOrReplaceKbnServerArg(serverArgs, 'server.ssl.enabled', () => 'true'); + addOrReplaceKbnServerArg(serverArgs, 'server.ssl.key', () => KBN_KEY_PATH); + addOrReplaceKbnServerArg(serverArgs, 'server.ssl.certificate', () => KBN_CERT_PATH); + addOrReplaceKbnServerArg(serverArgs, 'server.ssl.certificateAuthorities', () => CA_CERT_PATH); + // replace the newsfeed test plugin url to use https + addOrReplaceKbnServerArg(serverArgs, 'newsfeed.service.urlRoot', (oldValue) => { + if (!oldValue || !oldValue.includes(config.servers.kibana.hostname)) { + return undefined; + } + return oldValue.replaceAll('http', 'https'); + }); + + return config; +}; + +/** + * Set or replace given `arg` in the provided serverArgs list, using the provided replacer function + */ +const addOrReplaceKbnServerArg = ( + serverArgs: string[], + argName: string, + replacer: (value: string | undefined) => string | undefined +) => { + const argPrefix = `--${argName}=`; + const argIndex = serverArgs.findIndex((value) => value.startsWith(argPrefix)); + + if (argIndex === -1) { + const newArgValue = replacer(undefined); + if (newArgValue !== undefined) { + serverArgs.push(`${argPrefix}${newArgValue}`); + } + } else { + const currentArgValue = serverArgs[argIndex].substring(argPrefix.length); + const newArgValue = replacer(currentArgValue); + if (newArgValue !== undefined) { + serverArgs[argIndex] = `${argPrefix}${newArgValue}`; + } else { + serverArgs.splice(argIndex, 1); + } + } +}; diff --git a/test/common/services/deployment.ts b/test/common/services/deployment.ts index 20a8f1de26879..e61d6b360da19 100644 --- a/test/common/services/deployment.ts +++ b/test/common/services/deployment.ts @@ -39,6 +39,7 @@ export class DeploymentService extends FtrService { let agent: Agent | undefined; if (protocol === 'https') { agent = new Agent({ + // required for self-signed certificates used for HTTPS FTR testing rejectUnauthorized: false, }); } diff --git a/test/functional/config.base.js b/test/functional/config.base.js index 57d62772032f3..779690b3f96e6 100644 --- a/test/functional/config.base.js +++ b/test/functional/config.base.js @@ -6,27 +6,18 @@ * Side Public License, v 1. */ -import { readFileSync } from 'fs'; -import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import { pageObjects } from './page_objects'; import { services } from './services'; +import { configureHTTP2 } from '../common/configure_http2'; export default async function ({ readConfigFile }) { const commonConfig = await readConfigFile(require.resolve('../common/config')); - const servers = commonConfig.get('servers'); - return { + return configureHTTP2({ pageObjects, services, - servers: { - ...servers, - kibana: { - ...servers.kibana, - protocol: 'https', - certificateAuthorities: [readFileSync(CA_CERT_PATH, 'utf-8')], - }, - }, + servers: commonConfig.get('servers'), esTestCluster: { ...commonConfig.get('esTestCluster'), @@ -40,16 +31,6 @@ export default async function ({ readConfigFile }) { '--telemetry.optIn=false', '--savedObjects.maxImportPayloadBytes=10485760', - // Enable HTTP2 and TLS - '--server.protocol=http2', - '--server.ssl.enabled=true', - `--server.ssl.key=${KBN_KEY_PATH}`, - `--server.ssl.certificate=${KBN_CERT_PATH}`, - `--server.ssl.certificateAuthorities=${CA_CERT_PATH}`, - - // required because the base config reference its own server definition... - `--newsfeed.service.urlRoot=https://${servers.kibana.hostname}:${servers.kibana.port}`, - // override default to not allow hiddenFromHttpApis saved object types access to the HTTP Apis. see https://github.com/elastic/dev/issues/2200 '--savedObjects.allowHttpApiAccess=false', @@ -128,7 +109,6 @@ export default async function ({ readConfigFile }) { }, browser: { type: 'chrome', - acceptInsecureCerts: true, }, security: { @@ -502,5 +482,5 @@ export default async function ({ readConfigFile }) { }, defaultRoles: ['test_logstash_reader', 'kibana_admin'], }, - }; + }); } diff --git a/test/functional/services/supertest.ts b/test/functional/services/supertest.ts index a684230f315ea..c6c155585b672 100644 --- a/test/functional/services/supertest.ts +++ b/test/functional/services/supertest.ts @@ -19,6 +19,7 @@ export function KibanaSupertestProvider({ getService }: FtrProviderContext) { const options: AgentOptions = {}; if (kibanaServerConfig.certificateAuthorities) { options.ca = kibanaServerConfig.certificateAuthorities; + options.rejectUnauthorized = false; } // or add to server url in packages/kbn-test/src/functional_test_runner/lib/config/schema.ts maybe diff --git a/x-pack/test/common/services/spaces.ts b/x-pack/test/common/services/spaces.ts index 44b0fbccbdf8b..b4e99cacee571 100644 --- a/x-pack/test/common/services/spaces.ts +++ b/x-pack/test/common/services/spaces.ts @@ -21,6 +21,7 @@ export function SpacesServiceProvider({ getService }: FtrProviderContext) { const httpsAgent: Https.Agent | undefined = certificateAuthorities ? new Https.Agent({ ca: certificateAuthorities, + // required for self-signed certificates used for HTTPS FTR testing rejectUnauthorized: false, }) : undefined; From 69f81f24cc5351389c9c01835e2b5e096ff62ece Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 28 May 2024 14:39:19 +0200 Subject: [PATCH 29/36] enable http2 for the home function test suite --- scripts/functional_test_runner.js | 1 - scripts/functional_tests.js | 1 - src/setup_node_env/exit_on_warning.js | 2 +- test/common/configure_http2.ts | 7 ++++++- test/functional/apps/home/config.ts | 5 +++-- test/functional/config.base.js | 5 ++--- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/scripts/functional_test_runner.js b/scripts/functional_test_runner.js index 3829a26d18935..811e2083c7eeb 100644 --- a/scripts/functional_test_runner.js +++ b/scripts/functional_test_runner.js @@ -6,6 +6,5 @@ * Side Public License, v 1. */ -process.isFtrRunner = true; require('../src/setup_node_env'); require('@kbn/test').runFtrCli(); diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 75f27e547ad9b..eb1dea2dcab36 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -6,6 +6,5 @@ * Side Public License, v 1. */ -process.isFtrRunner = true; require('../src/setup_node_env'); require('@kbn/test').runTestsCli(); diff --git a/src/setup_node_env/exit_on_warning.js b/src/setup_node_env/exit_on_warning.js index dc538d08dde49..82d173cc5e233 100644 --- a/src/setup_node_env/exit_on_warning.js +++ b/src/setup_node_env/exit_on_warning.js @@ -119,7 +119,7 @@ function shouldIgnore(warn) { col, ftrOnly, }) { - if (ftrOnly && !process.isFtrRunner) return false; + if (ftrOnly && !process.env.IS_FTR_RUNNER) return false; if (name && name !== warn.name) return false; if (code && code !== warn.code) return false; if (message && message !== warn.message) return false; diff --git a/test/common/configure_http2.ts b/test/common/configure_http2.ts index 2237a67897657..6a8fc28a6c048 100644 --- a/test/common/configure_http2.ts +++ b/test/common/configure_http2.ts @@ -14,9 +14,14 @@ type ConfigType = Record; /** * Enables HTTP2 by adding/changing the appropriate config settings * - * note: it DOES mutate the provided config. + * Important: this must be used on "final" (non-reused) configs, otherwise + * the override from the children configs could remote the overrides + * done in that helper. */ export const configureHTTP2 = (config: ConfigType): ConfigType => { + // Add env flag to avoid terminating on NODE_TLS_REJECT_UNAUTHORIZED warning + process.env.IS_FTR_RUNNER = 'true'; + // tell native node agents to trust unsafe certificates // this is ugly, but unfortunately required, as some libraries (such as supertest) // have no real alternatives to accept self-signed certs diff --git a/test/functional/apps/home/config.ts b/test/functional/apps/home/config.ts index e487d31dcb657..f295f1b826492 100644 --- a/test/functional/apps/home/config.ts +++ b/test/functional/apps/home/config.ts @@ -7,12 +7,13 @@ */ import { FtrConfigProviderContext } from '@kbn/test'; +import { configureHTTP2 } from '../../../common/configure_http2'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); - return { + return configureHTTP2({ ...functionalConfig.getAll(), testFiles: [require.resolve('.')], - }; + }); } diff --git a/test/functional/config.base.js b/test/functional/config.base.js index 779690b3f96e6..d9f0d63184105 100644 --- a/test/functional/config.base.js +++ b/test/functional/config.base.js @@ -8,12 +8,11 @@ import { pageObjects } from './page_objects'; import { services } from './services'; -import { configureHTTP2 } from '../common/configure_http2'; export default async function ({ readConfigFile }) { const commonConfig = await readConfigFile(require.resolve('../common/config')); - return configureHTTP2({ + return { pageObjects, services, @@ -482,5 +481,5 @@ export default async function ({ readConfigFile }) { }, defaultRoles: ['test_logstash_reader', 'kibana_admin'], }, - }); + }; } From 9fa7428b21c7fefb5889f1110ef3b4916763a160 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 28 May 2024 14:49:59 +0200 Subject: [PATCH 30/36] enable http2 for the console function test suite --- test/functional/apps/console/config.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/functional/apps/console/config.ts b/test/functional/apps/console/config.ts index e487d31dcb657..f295f1b826492 100644 --- a/test/functional/apps/console/config.ts +++ b/test/functional/apps/console/config.ts @@ -7,12 +7,13 @@ */ import { FtrConfigProviderContext } from '@kbn/test'; +import { configureHTTP2 } from '../../../common/configure_http2'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); - return { + return configureHTTP2({ ...functionalConfig.getAll(), testFiles: [require.resolve('.')], - }; + }); } From e61e45f1d5cc4e6cba32e535e02c18b47cf11b3a Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 28 May 2024 16:12:00 +0200 Subject: [PATCH 31/36] self-review --- packages/core/http/core-http-server/src/http_contract.ts | 2 +- packages/kbn-server-http-tools/src/get_listener.ts | 6 +++--- packages/kbn-server-http-tools/src/get_server_options.ts | 2 +- .../server/integration_tests/http/http2_protocol.test.ts | 4 ++-- src/plugins/bfetch/server/plugin.ts | 4 ---- x-pack/test/functional/config.base.js | 2 +- 6 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/core/http/core-http-server/src/http_contract.ts b/packages/core/http/core-http-server/src/http_contract.ts index 62acec659a00d..09be2d4c2933a 100644 --- a/packages/core/http/core-http-server/src/http_contract.ts +++ b/packages/core/http/core-http-server/src/http_contract.ts @@ -408,6 +408,6 @@ export interface HttpServerInfo { * (Only supporting http1 for now) * * - http1: regroups all http/1.x protocols - * - http2: h2/h2c + * - http2: h2 */ export type HttpProtocol = 'http1' | 'http2'; diff --git a/packages/kbn-server-http-tools/src/get_listener.ts b/packages/kbn-server-http-tools/src/get_listener.ts index 113e9539523fa..16e39bc003b00 100644 --- a/packages/kbn-server-http-tools/src/get_listener.ts +++ b/packages/kbn-server-http-tools/src/get_listener.ts @@ -26,7 +26,7 @@ export function getServerListener( : configureHttp1Listener(config, options); } -export const configureHttp1Listener = ( +const configureHttp1Listener = ( config: IHttpConfig, { configureTLS = true }: GetServerListenerOptions = {} ): ServerListener => { @@ -57,7 +57,7 @@ export const configureHttp1Listener = ( return listener; }; -export const configureHttp2Listener = ( +const configureHttp2Listener = ( config: IHttpConfig, { configureTLS = true }: GetServerListenerOptions = {} ): ServerListener => { @@ -67,7 +67,7 @@ export const configureHttp2Listener = ( const listener = useTLS ? http2.createSecureServer({ ...tlsOptions, - // allow ALPN negotiation to HTTP1 + // allow ALPN negotiation fallback to HTTP/1.x allowHTTP1: true, }) : http2.createServer({}); diff --git a/packages/kbn-server-http-tools/src/get_server_options.ts b/packages/kbn-server-http-tools/src/get_server_options.ts index b9c7a6e7add63..bc3033afea373 100644 --- a/packages/kbn-server-http-tools/src/get_server_options.ts +++ b/packages/kbn-server-http-tools/src/get_server_options.ts @@ -29,7 +29,7 @@ export function getServerOptions(config: IHttpConfig, { configureTLS = true } = host: config.host, port: config.port, // manually configuring the listener - // @ts-expect-error HAPI types only define http1/tls listener, not http2 + // @ts-expect-error HAPI types only define http1/https listener, not http2 listener: getServerListener(config, { configureTLS }), // must set to true when manually passing a TLS listener, false otherwise tls: configureTLS && config.ssl.enabled, diff --git a/src/core/server/integration_tests/http/http2_protocol.test.ts b/src/core/server/integration_tests/http/http2_protocol.test.ts index 71f40263335d7..f76076de81d43 100644 --- a/src/core/server/integration_tests/http/http2_protocol.test.ts +++ b/src/core/server/integration_tests/http/http2_protocol.test.ts @@ -64,7 +64,7 @@ describe('Http2 - Smoke tests', () => { await server?.stop(); }); - describe('Basic test against all supported methods', () => { + describe('Basic tests against all supported methods', () => { beforeEach(async () => { const { registerRouter, server: innerServer } = await server.setup({ config$: of(config) }); innerServerListener = innerServer.listener; @@ -167,7 +167,7 @@ describe('Http2 - Smoke tests', () => { }); }); - describe('HTTP2-specific behavior', () => { + describe('HTTP2-specific behaviors', () => { beforeEach(async () => { const { registerRouter, server: innerServer } = await server.setup({ config$: of(config) }); innerServerListener = innerServer.listener; diff --git a/src/plugins/bfetch/server/plugin.ts b/src/plugins/bfetch/server/plugin.ts index 87e788628d61d..12b99c855cae8 100644 --- a/src/plugins/bfetch/server/plugin.ts +++ b/src/plugins/bfetch/server/plugin.ts @@ -66,10 +66,6 @@ export interface BfetchServerSetup { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface BfetchServerStart {} -// NOTE: we will likely want to fully disable bfetch when a client supports/use http2, -// or maybe even fully disable bfetch overall now that we have http2 support -// but this should be done in a later stage by the appropriate team -// so the PR does the easiest fix to just remove http2 banned headers const getStreamingHeaders = (protocol: HttpProtocol): Record => { if (protocol === 'http2') { return { diff --git a/x-pack/test/functional/config.base.js b/x-pack/test/functional/config.base.js index 50277ce341b8b..1bf939c537b7c 100644 --- a/x-pack/test/functional/config.base.js +++ b/x-pack/test/functional/config.base.js @@ -30,7 +30,7 @@ export default async function ({ readConfigFile }) { services, pageObjects, - servers: kibanaCommonConfig.get('servers'), + servers: kibanaFunctionalConfig.get('servers'), esTestCluster: { license: 'trial', From 279b49ca0c6ee6e3e18221e93edbb40ed945b2ee Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 28 May 2024 16:31:18 +0200 Subject: [PATCH 32/36] remove some comments --- test/api_integration/services/supertest.ts | 1 - test/functional/services/supertest.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/test/api_integration/services/supertest.ts b/test/api_integration/services/supertest.ts index 1b8c446d37397..bba3c5d731ca6 100644 --- a/test/api_integration/services/supertest.ts +++ b/test/api_integration/services/supertest.ts @@ -24,7 +24,6 @@ export function KibanaSupertestProvider({ getService }: FtrProviderContext): sup options.rejectUnauthorized = false; } - // or add to server url in packages/kbn-test/src/functional_test_runner/lib/config/schema.ts maybe const serverArgs = config.get('kbnTestServer.serverArgs', []) as string[]; const http2Enabled = serverArgs.includes('--server.protocol=http2'); if (http2Enabled) { diff --git a/test/functional/services/supertest.ts b/test/functional/services/supertest.ts index c6c155585b672..10a9803df263e 100644 --- a/test/functional/services/supertest.ts +++ b/test/functional/services/supertest.ts @@ -22,7 +22,6 @@ export function KibanaSupertestProvider({ getService }: FtrProviderContext) { options.rejectUnauthorized = false; } - // or add to server url in packages/kbn-test/src/functional_test_runner/lib/config/schema.ts maybe const serverArgs = config.get('kbnTestServer.serverArgs', []) as string[]; const http2Enabled = serverArgs.includes('--server.protocol=http2'); if (http2Enabled) { From c25787045dd12c0a9a4ba6158acd3b9ff0de9d1d Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 28 May 2024 16:33:27 +0200 Subject: [PATCH 33/36] remove added whiteline --- test/functional/config.base.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/functional/config.base.js b/test/functional/config.base.js index d9f0d63184105..2f0ec71ffbdaa 100644 --- a/test/functional/config.base.js +++ b/test/functional/config.base.js @@ -29,7 +29,6 @@ export default async function ({ readConfigFile }) { ...commonConfig.get('kbnTestServer.serverArgs'), '--telemetry.optIn=false', '--savedObjects.maxImportPayloadBytes=10485760', - // override default to not allow hiddenFromHttpApis saved object types access to the HTTP Apis. see https://github.com/elastic/dev/issues/2200 '--savedObjects.allowHttpApiAccess=false', From ecebcbd82a1128aeea9cf30d956d07decc71457f Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 30 May 2024 08:48:51 +0200 Subject: [PATCH 34/36] Add new settings to kibana-docker --- .../docker_generator/resources/base/bin/kibana-docker | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 64f94aed60cf6..0ca827fcbcd8e 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -155,11 +155,13 @@ kibana_vars=( server.customResponseHeaders server.defaultRoute server.host + server.http2.allowUnsecure server.keepAliveTimeout server.maxPayload server.maxPayloadBytes server.name server.port + server.protocol server.publicBaseUrl server.requestId.allowFromAnyIp server.requestId.ipAllowlist From d374a6392bd1e4f7479e80eee6c24026b195652a Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 30 May 2024 09:03:16 +0200 Subject: [PATCH 35/36] include protocol in BPP ready message --- packages/kbn-cli-dev-mode/src/base_path_proxy/http2.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kbn-cli-dev-mode/src/base_path_proxy/http2.ts b/packages/kbn-cli-dev-mode/src/base_path_proxy/http2.ts index 4777533519d9c..77119df9c22f9 100644 --- a/packages/kbn-cli-dev-mode/src/base_path_proxy/http2.ts +++ b/packages/kbn-cli-dev-mode/src/base_path_proxy/http2.ts @@ -68,6 +68,7 @@ export class Http2BasePathProxyServer implements BasePathProxyServer { this.log.write( `basepath proxy server running at ${Url.format({ + protocol: this.httpConfig.ssl.enabled ? 'https' : 'http', host: this.httpConfig.host, pathname: this.httpConfig.basePath, })}` From 37e54cf909e08c8133b04b28c22c9f67bf341a0e Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 30 May 2024 09:07:34 +0200 Subject: [PATCH 36/36] review NIT --- test/common/configure_http2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/common/configure_http2.ts b/test/common/configure_http2.ts index 6a8fc28a6c048..7b43650e9b023 100644 --- a/test/common/configure_http2.ts +++ b/test/common/configure_http2.ts @@ -15,7 +15,7 @@ type ConfigType = Record; * Enables HTTP2 by adding/changing the appropriate config settings * * Important: this must be used on "final" (non-reused) configs, otherwise - * the override from the children configs could remote the overrides + * the overrides from the children configs could remove the overrides * done in that helper. */ export const configureHTTP2 = (config: ConfigType): ConfigType => {