[Reporting] Internally correct the hostname to "localhost" if "server.host" is "0.0.0.0" (#117022) (#117441)

* [Reporting] Internal correction for server hostname if "server.host" is "0.0.0.0"

* add schema validation

* correction to the failover logic the test is validation

* update per feedback

* more check against invalid hostnames

* remove silly  translations for server logs

* remove unused translations

* improve the tests
This commit is contained in:
Tim Sullivan 2021-11-03 17:14:31 -07:00 committed by GitHub
parent fc96462343
commit d891dfc506
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 123 additions and 73 deletions

View file

@ -5,27 +5,29 @@
* 2.0.
*/
import { CoreSetup, PluginInitializerContext } from 'src/core/server';
import * as Rx from 'rxjs';
import { CoreSetup, HttpServerInfo, PluginInitializerContext } from 'src/core/server';
import { coreMock } from 'src/core/server/mocks';
import { LevelLogger } from '../lib';
import { createMockConfigSchema } from '../test_helpers';
import { LevelLogger } from '../lib/level_logger';
import { createMockConfigSchema, createMockLevelLogger } from '../test_helpers';
import { ReportingConfigType } from './';
import { createConfig$ } from './create_config';
const createMockConfig = (
mockInitContext: PluginInitializerContext<unknown>
): Rx.Observable<ReportingConfigType> => mockInitContext.config.create();
describe('Reporting server createConfig$', () => {
let mockCoreSetup: CoreSetup;
let mockInitContext: PluginInitializerContext;
let mockLogger: LevelLogger;
let mockLogger: jest.Mocked<LevelLogger>;
beforeEach(() => {
mockCoreSetup = coreMock.createSetup();
mockInitContext = coreMock.createPluginInitializerContext(
createMockConfigSchema({ kibanaServer: {} })
);
mockLogger = {
warn: jest.fn(),
debug: jest.fn(),
clone: jest.fn().mockImplementation(() => mockLogger),
} as unknown as LevelLogger;
mockLogger = createMockLevelLogger();
});
afterEach(() => {
@ -37,19 +39,12 @@ describe('Reporting server createConfig$', () => {
...createMockConfigSchema({ kibanaServer: {} }),
encryptionKey: undefined,
});
const mockConfig$: any = mockInitContext.config.create();
const mockConfig$ = createMockConfig(mockInitContext);
const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise();
expect(result.encryptionKey).toMatch(/\S{32,}/); // random 32 characters
expect(result.kibanaServer).toMatchInlineSnapshot(`
Object {
"hostname": "localhost",
"port": 80,
"protocol": "http",
}
`);
expect((mockLogger.warn as any).mock.calls.length).toBe(1);
expect((mockLogger.warn as any).mock.calls[0]).toMatchObject([
expect(mockLogger.warn.mock.calls.length).toBe(1);
expect(mockLogger.warn.mock.calls[0]).toMatchObject([
'Generating a random key for xpack.reporting.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.reporting.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.',
]);
});
@ -60,10 +55,10 @@ describe('Reporting server createConfig$', () => {
encryptionKey: 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii',
})
);
const mockConfig$: any = mockInitContext.config.create();
const mockConfig$ = createMockConfig(mockInitContext);
const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise();
expect(result.encryptionKey).toMatch('iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii');
expect((mockLogger.warn as any).mock.calls.length).toBe(0);
expect(mockLogger.warn.mock.calls.length).toBe(0);
});
it('uses the user-provided encryption key, reporting kibanaServer settings to override server info', async () => {
@ -77,7 +72,7 @@ describe('Reporting server createConfig$', () => {
},
})
);
const mockConfig$: any = mockInitContext.config.create();
const mockConfig$ = createMockConfig(mockInitContext);
const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise();
expect(result).toMatchInlineSnapshot(`
@ -108,7 +103,7 @@ describe('Reporting server createConfig$', () => {
},
}
`);
expect((mockLogger.warn as any).mock.calls.length).toBe(0);
expect(mockLogger.warn.mock.calls.length).toBe(0);
});
it('uses user-provided disableSandbox: false', async () => {
@ -118,11 +113,11 @@ describe('Reporting server createConfig$', () => {
capture: { browser: { chromium: { disableSandbox: false } } },
})
);
const mockConfig$: any = mockInitContext.config.create();
const mockConfig$ = createMockConfig(mockInitContext);
const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise();
expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: false });
expect((mockLogger.warn as any).mock.calls.length).toBe(0);
expect(mockLogger.warn.mock.calls.length).toBe(0);
});
it('uses user-provided disableSandbox: true', async () => {
@ -132,11 +127,11 @@ describe('Reporting server createConfig$', () => {
capture: { browser: { chromium: { disableSandbox: true } } },
})
);
const mockConfig$: any = mockInitContext.config.create();
const mockConfig$ = createMockConfig(mockInitContext);
const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise();
expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: true });
expect((mockLogger.warn as any).mock.calls.length).toBe(0);
expect(mockLogger.warn.mock.calls.length).toBe(0);
});
it('provides a default for disableSandbox', async () => {
@ -145,10 +140,49 @@ describe('Reporting server createConfig$', () => {
encryptionKey: '888888888888888888888888888888888',
})
);
const mockConfig$: any = mockInitContext.config.create();
const mockConfig$ = createMockConfig(mockInitContext);
const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise();
expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: expect.any(Boolean) });
expect((mockLogger.warn as any).mock.calls.length).toBe(0);
expect(mockLogger.warn.mock.calls.length).toBe(0);
});
for (const hostname of [
'0',
'0.0',
'0.0.0',
'0.0.0.0',
'0000:0000:0000:0000:0000:0000:0000:0000',
'::',
]) {
it(`apply failover logic when hostname is given as ${hostname}`, async () => {
mockInitContext = coreMock.createPluginInitializerContext(
createMockConfigSchema({
encryptionKey: 'aaaaaaaaaaaaabbbbbbbbbbbbaaaaaaaaa',
// overwrite settings added by createMockConfigSchema and apply the default settings
// TODO make createMockConfigSchema _not_ default xpack.reporting.kibanaServer.hostname to 'localhost'
kibanaServer: {
hostname: undefined,
port: undefined,
},
})
);
mockCoreSetup.http.getServerInfo = jest.fn().mockImplementation(
(): HttpServerInfo => ({
name: 'cool server',
hostname,
port: 5601,
protocol: 'http',
})
);
const mockConfig$ = createMockConfig(mockInitContext);
const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise();
expect(result.kibanaServer).toMatchObject({
hostname: 'localhost',
port: 5601,
protocol: 'http',
});
});
}
});

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import crypto from 'crypto';
import { upperFirst } from 'lodash';
import ipaddr from 'ipaddr.js';
import { sum, upperFirst } from 'lodash';
import { Observable } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
import { CoreSetup } from 'src/core/server';
@ -33,20 +33,29 @@ export function createConfig$(
let encryptionKey = config.encryptionKey;
if (encryptionKey === undefined) {
logger.warn(
i18n.translate('xpack.reporting.serverConfig.randomEncryptionKey', {
defaultMessage:
'Generating a random key for xpack.reporting.encryptionKey. To prevent sessions from being invalidated on ' +
'restart, please set xpack.reporting.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.',
})
'Generating a random key for xpack.reporting.encryptionKey. To prevent sessions from being invalidated on ' +
'restart, please set xpack.reporting.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
);
encryptionKey = crypto.randomBytes(16).toString('hex');
}
const { kibanaServer: reportingServer } = config;
const serverInfo = core.http.getServerInfo();
// kibanaServer.hostname, default to server.host
const kibanaServerHostname = reportingServer.hostname
// set kibanaServer.hostname, default to server.host, don't allow "0.0.0.0" as it breaks in Windows
let kibanaServerHostname = reportingServer.hostname
? reportingServer.hostname
: serverInfo.hostname;
if (
ipaddr.isValid(kibanaServerHostname) &&
!sum(ipaddr.parse(kibanaServerHostname).toByteArray())
) {
logger.warn(
`Found 'server.host: "0.0.0.0"' in Kibana configuration. Reporting is not able to use this as the Kibana server hostname.` +
` To enable PNG/PDF Reporting to work, 'xpack.reporting.kibanaServer.hostname: localhost' is automatically set in the configuration.` +
` You can prevent this message by adding 'xpack.reporting.kibanaServer.hostname: localhost' in kibana.yml.`
);
kibanaServerHostname = 'localhost';
}
// kibanaServer.port, default to server.port
const kibanaServerPort = reportingServer.port ? reportingServer.port : serverInfo.port;
// kibanaServer.protocol, default to server.protocol
@ -66,36 +75,24 @@ export function createConfig$(
mergeMap(async (config) => {
if (config.capture.browser.chromium.disableSandbox != null) {
// disableSandbox was set by user
return config;
return { ...config };
}
// disableSandbox was not set by user, apply default for OS
const { os, disableSandbox } = await getDefaultChromiumSandboxDisabled();
const osName = [os.os, os.dist, os.release].filter(Boolean).map(upperFirst).join(' ');
logger.debug(
i18n.translate('xpack.reporting.serverConfig.osDetected', {
defaultMessage: `Running on OS: '{osName}'`,
values: { osName },
})
);
logger.debug(`Running on OS: '{osName}'`);
if (disableSandbox === true) {
logger.warn(
i18n.translate('xpack.reporting.serverConfig.autoSet.sandboxDisabled', {
defaultMessage: `Chromium sandbox provides an additional layer of protection, but is not supported for {osName} OS. Automatically setting '{configKey}: true'.`,
values: {
configKey: 'xpack.reporting.capture.browser.chromium.disableSandbox',
osName,
},
})
`Chromium sandbox provides an additional layer of protection, but is not supported for ${osName} OS.` +
` Automatically setting 'xpack.reporting.capture.browser.chromium.disableSandbox: true'.`
);
} else {
logger.info(
i18n.translate('xpack.reporting.serverConfig.autoSet.sandboxEnabled', {
defaultMessage: `Chromium sandbox provides an additional layer of protection, and is supported for {osName} OS. Automatically enabling Chromium sandbox.`,
values: { osName },
})
`Chromium sandbox provides an additional layer of protection, and is supported for ${osName} OS.` +
` Automatically enabling Chromium sandbox.`
);
}

View file

@ -288,11 +288,25 @@ describe('Reporting Config Schema', () => {
`);
});
it(`logs the proper validation messages`, () => {
// kibanaServer
const throwValidationErr = () => ConfigSchema.validate({ kibanaServer: { hostname: '0' } });
expect(throwValidationErr).toThrowError(
`[kibanaServer.hostname]: value must be a valid hostname (see RFC 1123).`
);
});
for (const address of ['0', '0.0', '0.0.0']) {
it(`fails to validate "kibanaServer.hostname" with an invalid hostname: "${address}"`, () => {
expect(() =>
ConfigSchema.validate({
kibanaServer: { hostname: address },
})
).toThrowError(`[kibanaServer.hostname]: value must be a valid hostname (see RFC 1123).`);
});
}
for (const address of ['0.0.0.0', '0000:0000:0000:0000:0000:0000:0000:0000', '::']) {
it(`fails to validate "kibanaServer.hostname" hostname as zero: "${address}"`, () => {
expect(() =>
ConfigSchema.validate({
kibanaServer: { hostname: address },
})
).toThrowError(
`[kibanaServer.hostname]: cannot use '0.0.0.0' as Kibana host name, consider using the default (localhost) instead`
);
});
}
});

View file

@ -6,10 +6,22 @@
*/
import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema';
import ipaddr from 'ipaddr.js';
import { sum } from 'lodash';
import moment from 'moment';
const KibanaServerSchema = schema.object({
hostname: schema.maybe(schema.string({ hostname: true })),
hostname: schema.maybe(
schema.string({
hostname: true,
validate(value) {
if (ipaddr.isValid(value) && !sum(ipaddr.parse(value).toByteArray())) {
// prevent setting a hostname that fails in Chromium on Windows
return `cannot use '0.0.0.0' as Kibana host name, consider using the default (localhost) instead`;
}
},
})
),
port: schema.maybe(schema.number()),
protocol: schema.maybe(
schema.string({

View file

@ -16,7 +16,6 @@ export function createMockLevelLogger() {
const logger = new LevelLogger(loggingSystemMock.create()) as jest.Mocked<LevelLogger>;
logger.clone.mockImplementation(createMockLevelLogger);
// logger.debug.mockImplementation(consoleLogger('debug')); // uncomment this to see debug logs in jest tests
logger.info.mockImplementation(consoleLogger('info'));
logger.warn.mockImplementation(consoleLogger('warn'));
@ -24,5 +23,7 @@ export function createMockLevelLogger() {
logger.error.mockImplementation(consoleLogger('error'));
logger.trace.mockImplementation(consoleLogger('trace'));
logger.clone.mockImplementation(() => logger);
return logger;
}

View file

@ -19243,10 +19243,6 @@
"xpack.reporting.screenCapturePanelContent.canvasLayoutLabel": "全ページレイアウト",
"xpack.reporting.screenCapturePanelContent.optimizeForPrintingHelpText": "複数のページを使用します。ページごとに最大2のビジュアライゼーションが表示されます",
"xpack.reporting.screenCapturePanelContent.optimizeForPrintingLabel": "印刷用に最適化",
"xpack.reporting.serverConfig.autoSet.sandboxDisabled": "Chromiumサンドボックスは保護が強化されていますが、{osName} OSではサポートされていません。自動的に'{configKey}: true'を設定しています。",
"xpack.reporting.serverConfig.autoSet.sandboxEnabled": "Chromiumサンドボックスは保護が強化され、{osName} OSでサポートされています。自動的にChromiumサンドボックスを有効にしています。",
"xpack.reporting.serverConfig.osDetected": "OSは'{osName}'で実行しています",
"xpack.reporting.serverConfig.randomEncryptionKey": "xpack.reporting.encryptionKey のランダムキーを生成しています。再起動時にセッションが無効にならないようにするには、kibana.yml でxpack.reporting.encryptionKey を設定するか、bin/kibana-encryption-keys コマンドを使用してください。",
"xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV レポート",
"xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF レポート",
"xpack.reporting.shareContextMenu.pngReportsButtonLabel": "PNG レポート",

View file

@ -19520,10 +19520,6 @@
"xpack.reporting.screenCapturePanelContent.canvasLayoutLabel": "全页面布局",
"xpack.reporting.screenCapturePanelContent.optimizeForPrintingHelpText": "使用多页,每页最多显示 2 个可视化",
"xpack.reporting.screenCapturePanelContent.optimizeForPrintingLabel": "打印优化",
"xpack.reporting.serverConfig.autoSet.sandboxDisabled": "Chromium 沙盒提供附加保护层,但不受 {osName} OS 支持。自动设置“{configKey}: true”。",
"xpack.reporting.serverConfig.autoSet.sandboxEnabled": "Chromium 沙盒提供附加保护层,受 {osName} OS 支持。自动启用 Chromium 沙盒。",
"xpack.reporting.serverConfig.osDetected": "正在以下 OS 上运行:“{osName}”",
"xpack.reporting.serverConfig.randomEncryptionKey": "为 xpack.reporting.encryptionKey 生成随机密钥。为防止会话在重启时失效,请在 kibana.yml 中设置 xpack.reporting.encryptionKey 或使用 bin/kibana-encryption-keys 命令。",
"xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV 报告",
"xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF 报告",
"xpack.reporting.shareContextMenu.pngReportsButtonLabel": "PNG 报告",