Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[spacetime] Http/2 support #123748

Closed
wants to merge 13 commits into from
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,7 @@
"form-data": "^4.0.0",
"geckodriver": "^3.0.1",
"glob-watcher": "5.0.3",
"got": "11.8.3",
"gulp": "4.0.2",
"gulp-babel": "^8.0.0",
"gulp-brotli": "^3.0.0",
Expand Down
20 changes: 20 additions & 0 deletions packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import Url from 'url';
import { Agent as HttpsAgent, ServerOptions as TlsOptions } from 'https';
// import Http2 from 'http2';
// import Wreck from '@hapi/wreck';
import apm from 'elastic-apm-node';
import { Server, Request } from '@hapi/hapi';
import HapiProxy from '@hapi/h2o2';
Expand Down Expand Up @@ -82,6 +84,14 @@ export class BasePathProxyServer {
});
}

// if (this.httpConfig.protocol === 'http2') {
// // @ts-expect-error
// serverOptions.listener = serverOptions.tls
// ? Http2.createSecureServer(serverOptions.tls as TlsOptions)
// : // http2 can be used between Kibana and a reverse proxy, setting TLS up is not required
// Http2.createServer();
// }

this.setupRoutes(options);

await this.server.start();
Expand Down Expand Up @@ -125,12 +135,22 @@ export class BasePathProxyServer {
path: '/',
});

// const httpClient = {
// request(method, uri, options, callback) {
// maxSockets = options.agent.maxSockets;

// return { statusCode: 200 };
// },
// };
// TODO try to use https://github.com/szmarczak/http2-wrapper to proxy http2 requests
this.server.route({
handler: {
proxy: {
agent: this.httpsAgent,
passThrough: true,
xforward: true,
// // @ts-expect-error not defined
// httpClient,
mapUri: async (request: Request) => {
return {
// Passing in this header to merge it is a workaround until this is fixed:
Expand Down
5 changes: 5 additions & 0 deletions packages/kbn-cli-dev-mode/src/config/http_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -45,6 +48,7 @@ export const httpConfigSchema = schema.object(
export type HttpConfigType = TypeOf<typeof httpConfigSchema>;

export class HttpConfig implements IHttpConfig {
protocol: 'http1' | 'http2';
basePath?: string;
host: string;
port: number;
Expand All @@ -57,6 +61,7 @@ export class HttpConfig implements IHttpConfig {

constructor(rawConfig: HttpConfigType) {
this.basePath = rawConfig.basePath;
this.protocol = rawConfig.protocol;
this.host = rawConfig.host;
this.port = rawConfig.port;
this.maxPayload = rawConfig.maxPayload;
Expand Down
3 changes: 2 additions & 1 deletion packages/kbn-server-http-tools/src/create_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { ListenerOptions } from './get_listener_options';
export function createServer(serverOptions: ServerOptions, listenerOptions: ListenerOptions) {
const server = new Server(serverOptions);

server.listener.keepAliveTimeout = listenerOptions.keepaliveTimeout;
// TODO add a condition based on protocol version
// server.listener.keepAliveTimeout = listenerOptions.keepaliveTimeout;
server.listener.setTimeout(listenerOptions.socketTimeout);
server.listener.on('timeout', (socket) => {
socket.destroy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ export interface ListenerOptions {
}

export function getListenerOptions(config: IHttpConfig): ListenerOptions {
// @ts-expect-error TODO add a condition based on protocol version
return {
keepaliveTimeout: config.keepaliveTimeout,
// keepaliveTimeout: config.keepaliveTimeout,
socketTimeout: config.socketTimeout,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jest.mock('fs', () => {
});

const createConfig = (parts: Partial<IHttpConfig>): IHttpConfig => ({
protocol: 'http1',
host: 'localhost',
port: 5601,
socketTimeout: 120000,
Expand Down
10 changes: 9 additions & 1 deletion packages/kbn-server-http-tools/src/get_server_options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import Http2 from 'http2';
import { RouteOptionsCors, ServerOptions } from '@hapi/hapi';
import { ServerOptions as TLSOptions } from 'https';
import { defaultValidationErrorHandler } from './default_validation_error_handler';
Expand Down Expand Up @@ -71,5 +71,13 @@ export function getServerOptions(config: IHttpConfig, { configureTLS = true } =
options.tls = tlsOptions;
}

if (config.protocol === 'http2') {
// @ts-expect-error
options.listener = options.tls
? Http2.createSecureServer(options.tls as TLSOptions)
: // http2 can be used between Kibana and a reverse proxy, setting TLS up is not required
Http2.createServer();
}

return options;
}
1 change: 1 addition & 0 deletions packages/kbn-server-http-tools/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 6 additions & 5 deletions packages/kbn-std/src/pick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
*/

export function pick<T extends object, K extends keyof T>(obj: T, keys: readonly K[]): Pick<T, K> {
return keys.reduce((acc, key) => {
if (obj.hasOwnProperty(key)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the case of http/2, headers object is created via Object.create(null) so it hasn't got hasOwnProperty method

const acc = Object.create(null) as Pick<T, K>;
for (const key of keys) {
// @ts-expect-error node type declaration doesn't know about the method yet
if (Object.hasOwn(obj, key)) {
acc[key] = obj[key];
}

return acc;
}, {} as Pick<T, K>);
}
return acc;
}
2 changes: 2 additions & 0 deletions packages/kbn-test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ RUNTIME_DEPS = [
"@npm//form-data",
"@npm//getopts",
"@npm//globby",
"@npm//got",
"@npm//he",
"@npm//history",
"@npm//jest",
Expand Down Expand Up @@ -87,6 +88,7 @@ TYPES_DEPS = [
"@npm//exit-hook",
"@npm//form-data",
"@npm//getopts",
"@npm//got",
"@npm//jest",
"@npm//jest-cli",
"@npm//jest-snapshot",
Expand Down
43 changes: 25 additions & 18 deletions packages/kbn-test/src/kbn_client/kbn_client_requester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@

import Url from 'url';
import Https from 'https';
import Qs from 'querystring';

import Axios, { AxiosResponse, ResponseType } from 'axios';
import got, { Agents } from 'got';
import { ToolingLog, isAxiosRequestError, isAxiosResponseError } from '@kbn/dev-utils';

const isConcliftOnGetError = (error: any) => {
Expand Down Expand Up @@ -67,11 +66,11 @@ export interface ReqOptions {
path: string;
query?: Record<string, any>;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: any;
body?: Record<string, any> | string;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: check whether we use string anywhere

retries?: number;
headers?: Record<string, string>;
ignoreErrors?: number[];
responseType?: ResponseType;
responseType?: 'text' | 'json' | 'buffer';
}

const delay = (ms: number) =>
Expand All @@ -86,16 +85,18 @@ interface Options {

export class KbnClientRequester {
private readonly url: string;
private readonly httpsAgent: Https.Agent | null;
private readonly httpsAgent: Https.Agent | undefined;
private ca?: Buffer[];

constructor(private readonly log: ToolingLog, options: Options) {
this.url = options.url;
this.ca = options.certificateAuthorities;
this.httpsAgent =
Url.parse(options.url).protocol === 'https:'
? new Https.Agent({
ca: options.certificateAuthorities,
})
: null;
: undefined;
}

private pickUrl() {
Expand All @@ -111,35 +112,41 @@ export class KbnClientRequester {
return Url.resolve(baseUrl, relative);
}

async request<T>(options: ReqOptions): Promise<AxiosResponse<T>> {
async request<T>(options: ReqOptions): Promise<{ data: T; status: number; statusText?: string }> {
const url = this.resolveUrl(options.path);
const description = options.description || `${options.method} ${url}`;
let attempt = 0;
const maxAttempts = options.retries ?? DEFAULT_MAX_ATTEMPTS;

const agents: Agents = {
https: this.httpsAgent,
};
while (true) {
attempt += 1;

try {
const response = await Axios.request({
const response = await got({
// TODO make configurable
http2: true,
https: {
certificateAuthority: this.ca,
},
method: options.method,
url,
data: options.body,
params: options.query,
searchParams: options.query,
headers: {
...options.headers,
'kbn-xsrf': 'kbn-client',
},
httpsAgent: this.httpsAgent,
body: typeof options.body === 'object' ? JSON.stringify(options.body) : options.body,
agent: agents,
responseType: options.responseType,
// work around https://github.com/axios/axios/issues/2791
transformResponse: options.responseType === 'text' ? [(x) => x] : undefined,
maxContentLength: 30000000,
maxBodyLength: 30000000,
paramsSerializer: (params) => Qs.stringify(params),
});

return response;
return {
data: response.body as T,
status: response.statusCode,
statusText: response.statusMessage,
};
} catch (error) {
const conflictOnGet = isConcliftOnGetError(error);
const requestedRetries = options.retries !== undefined;
Expand Down
5 changes: 5 additions & 0 deletions src/core/server/http/http_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ const configSchema = schema.object(
}
},
}),
protocol: schema.oneOf([schema.literal('http1'), schema.literal('http2')], {
defaultValue: 'http1',
}),
host: schema.string({
defaultValue: 'localhost',
hostname: true,
Expand Down Expand Up @@ -183,6 +186,7 @@ export const config: ServiceConfigDescriptor<HttpConfigType> = {
};

export class HttpConfig implements IHttpConfig {
public protocol: 'http1' | 'http2';
public name: string;
public autoListen: boolean;
public host: string;
Expand Down Expand Up @@ -217,6 +221,7 @@ export class HttpConfig implements IHttpConfig {
rawExternalUrlConfig: ExternalUrlConfig
) {
this.autoListen = rawHttpConfig.autoListen;
this.protocol = rawHttpConfig.protocol;
this.host = rawHttpConfig.host;
this.port = rawHttpConfig.port;
this.cors = rawHttpConfig.cors;
Expand Down
5 changes: 3 additions & 2 deletions src/plugins/bfetch/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ export interface BfetchServerStart {}

const streamingHeaders = {
'Content-Type': 'application/x-ndjson',
Connection: 'keep-alive',
'Transfer-Encoding': 'chunked',
// TODO both shouldn't be used when http2 is enabled
// Connection: 'keep-alive',
// 'Transfer-Encoding': 'chunked',
};

export class BfetchServerPlugin
Expand Down
7 changes: 7 additions & 0 deletions src/setup_node_env/exit_on_warning.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ var IGNORE_WARNINGS = [
// We need to discard that warning
name: 'ProductNotSupportedSecurityError',
},
{
// emitted whenever a header not supported by http2 is set.
// it's not actionale for the end user.
name: 'UnsupportedWarning',
// message:
// 'header is not valid, the value will be dropped from the header and will never be in use.',
},
];

if (process.noProcessWarnings !== true) {
Expand Down
20 changes: 18 additions & 2 deletions test/functional/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

// import Fs 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';

Expand All @@ -30,7 +31,14 @@ export default async function ({ readConfigFile }) {
pageObjects,
services,

servers: commonConfig.get('servers'),
servers: {
...commonConfig.get('servers'),
kibana: {
...commonConfig.get('servers.kibana'),
// protocol: 'https',
// certificateAuthorities: [Fs.readFileSync(CA_CERT_PATH)],
},
},

esTestCluster: {
...commonConfig.get('esTestCluster'),
Expand All @@ -46,6 +54,13 @@ export default async function ({ readConfigFile }) {

// to be re-enabled once kibana/issues/102552 is completed
'--xpack.reporting.enabled=false',

// '--server.ssl.enabled=true',
// `--server.ssl.key=${KBN_KEY_PATH}`,
// `--server.ssl.certificate=${KBN_CERT_PATH}`,
// `--server.ssl.certificateAuthorities=${CA_CERT_PATH}`,

// '--server.protocol=http2',
],
},

Expand Down Expand Up @@ -111,6 +126,7 @@ export default async function ({ readConfigFile }) {
},
browser: {
type: 'chrome',
acceptInsecureCerts: true,
},

security: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import { ToolingLog } from '@kbn/dev-utils';
import { KbnClient } from '@kbn/test';
import { AxiosResponse } from 'axios';
import { indexFleetEndpointPolicy } from '../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import {
PACKAGE_POLICY_API_ROUTES,
Expand All @@ -16,9 +15,7 @@ import {
import { setupFleetForEndpoint } from '../../../common/endpoint/data_loaders/setup_fleet_for_endpoint';
import { GetPolicyListResponse } from '../../../public/management/pages/policy/types';

const fetchEndpointPolicies = (
kbnClient: KbnClient
): Promise<AxiosResponse<GetPolicyListResponse>> => {
const fetchEndpointPolicies = (kbnClient: KbnClient): Promise<{ data: GetPolicyListResponse }> => {
return kbnClient.request<GetPolicyListResponse>({
method: 'GET',
path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN,
Expand Down
Loading