diff --git a/config/kibana.yml b/config/kibana.yml index ce9fe28dae91..7c7378fb5d29 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -18,6 +18,10 @@ # default to `true` starting in Kibana 7.0. #server.rewriteBasePath: false +# Specifies the public URL at which Kibana is available for end users. If +# `server.basePath` is configured this URL should end with the same basePath. +#server.publicBaseUrl: "" + # The maximum payload size in bytes for incoming server requests. #server.maxPayloadBytes: 1048576 diff --git a/docs/development/core/public/kibana-plugin-core-public.ibasepath.md b/docs/development/core/public/kibana-plugin-core-public.ibasepath.md index 7407c8a89da8..3afce9fee2a7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.ibasepath.md +++ b/docs/development/core/public/kibana-plugin-core-public.ibasepath.md @@ -18,6 +18,7 @@ export interface IBasePath | --- | --- | --- | | [get](./kibana-plugin-core-public.ibasepath.get.md) | () => string | Gets the basePath string. | | [prepend](./kibana-plugin-core-public.ibasepath.prepend.md) | (url: string) => string | Prepends path with the basePath. | +| [publicBaseUrl](./kibana-plugin-core-public.ibasepath.publicbaseurl.md) | string | The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the [IBasePath.serverBasePath](./kibana-plugin-core-public.ibasepath.serverbasepath.md). | | [remove](./kibana-plugin-core-public.ibasepath.remove.md) | (url: string) => string | Removes the prepended basePath from the path. | | [serverBasePath](./kibana-plugin-core-public.ibasepath.serverbasepath.md) | string | Returns the server's root basePath as configured, without any namespace prefix.See for getting the basePath value for a specific request | diff --git a/docs/development/core/public/kibana-plugin-core-public.ibasepath.publicbaseurl.md b/docs/development/core/public/kibana-plugin-core-public.ibasepath.publicbaseurl.md new file mode 100644 index 000000000000..f45cc6eba295 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.ibasepath.publicbaseurl.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IBasePath](./kibana-plugin-core-public.ibasepath.md) > [publicBaseUrl](./kibana-plugin-core-public.ibasepath.publicbaseurl.md) + +## IBasePath.publicBaseUrl property + +The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the [IBasePath.serverBasePath](./kibana-plugin-core-public.ibasepath.serverbasepath.md). + +Signature: + +```typescript +readonly publicBaseUrl?: string; +``` + +## Remarks + +Should be used for generating external URL links back to this Kibana instance. + diff --git a/docs/development/core/server/kibana-plugin-core-server.basepath.md b/docs/development/core/server/kibana-plugin-core-server.basepath.md index a5e09e34759a..54ab029d987a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.basepath.md +++ b/docs/development/core/server/kibana-plugin-core-server.basepath.md @@ -22,6 +22,7 @@ The constructor for this class is marked as internal. Third-party code should no | --- | --- | --- | --- | | [get](./kibana-plugin-core-server.basepath.get.md) | | (request: KibanaRequest | LegacyRequest) => string | returns basePath value, specific for an incoming request. | | [prepend](./kibana-plugin-core-server.basepath.prepend.md) | | (path: string) => string | Prepends path with the basePath. | +| [publicBaseUrl](./kibana-plugin-core-server.basepath.publicbaseurl.md) | | string | The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the [BasePath.serverBasePath](./kibana-plugin-core-server.basepath.serverbasepath.md). | | [remove](./kibana-plugin-core-server.basepath.remove.md) | | (path: string) => string | Removes the prepended basePath from the path. | | [serverBasePath](./kibana-plugin-core-server.basepath.serverbasepath.md) | | string | returns the server's basePathSee [BasePath.get](./kibana-plugin-core-server.basepath.get.md) for getting the basePath value for a specific request | | [set](./kibana-plugin-core-server.basepath.set.md) | | (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void | sets basePath value, specific for an incoming request. | diff --git a/docs/development/core/server/kibana-plugin-core-server.basepath.publicbaseurl.md b/docs/development/core/server/kibana-plugin-core-server.basepath.publicbaseurl.md new file mode 100644 index 000000000000..65842333ac24 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.basepath.publicbaseurl.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [BasePath](./kibana-plugin-core-server.basepath.md) > [publicBaseUrl](./kibana-plugin-core-server.basepath.publicbaseurl.md) + +## BasePath.publicBaseUrl property + +The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the [BasePath.serverBasePath](./kibana-plugin-core-server.basepath.serverbasepath.md). + +Signature: + +```typescript +readonly publicBaseUrl?: string; +``` + +## Remarks + +Should be used for generating external URL links back to this Kibana instance. + diff --git a/docs/development/core/server/kibana-plugin-core-server.httpserverinfo.md b/docs/development/core/server/kibana-plugin-core-server.httpserverinfo.md index 3541824a2e81..890bfaa834dc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpserverinfo.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpserverinfo.md @@ -4,6 +4,7 @@ ## HttpServerInfo interface +Information about what hostname, port, and protocol the server process is running on. Note that this may not match the URL that end-users access Kibana at. For the public URL, see [BasePath.publicBaseUrl](./kibana-plugin-core-server.basepath.publicbaseurl.md). Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 1abf95f92263..1a4209ff87c5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -86,7 +86,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpResourcesRenderOptions](./kibana-plugin-core-server.httpresourcesrenderoptions.md) | Allows to configure HTTP response parameters | | [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) | Extended set of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) helpers used to respond with HTML or JS resource. | | [HttpResponseOptions](./kibana-plugin-core-server.httpresponseoptions.md) | HTTP response parameters | -| [HttpServerInfo](./kibana-plugin-core-server.httpserverinfo.md) | | +| [HttpServerInfo](./kibana-plugin-core-server.httpserverinfo.md) | Information about what hostname, port, and protocol the server process is running on. Note that this may not match the URL that end-users access Kibana at. For the public URL, see [BasePath.publicBaseUrl](./kibana-plugin-core-server.basepath.publicbaseurl.md). | | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to hapi server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. | | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | | | [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) | | diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 3786cbc7d83b..ed6bc9b1f55b 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -442,6 +442,11 @@ running behind a proxy. Use the <> (if configured). This setting cannot end in a slash (`/`). + | [[server-compression]] `server.compression.enabled:` | Set to `false` to disable HTTP compression for all responses. *Default: `true`* diff --git a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index 5d8fb1e28beb..6351a227ff90 100644 --- a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -16,6 +16,7 @@ Object { "maxPayload": 1000, "name": "kibana-hostname", "port": 1234, + "publicBaseUrl": "https://myhost.com/abc", "rewriteBasePath": false, "socketTimeout": 2000, "ssl": Object { @@ -47,6 +48,7 @@ Object { "maxPayload": 1000, "name": "kibana-hostname", "port": 1234, + "publicBaseUrl": "http://myhost.com/abc", "rewriteBasePath": false, "socketTimeout": 2000, "ssl": Object { diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts index 036ff5e80b3e..75d1bec48eea 100644 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts +++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts @@ -90,6 +90,7 @@ describe('#get', () => { keepaliveTimeout: 5000, socketTimeout: 2000, port: 1234, + publicBaseUrl: 'https://myhost.com/abc', rewriteBasePath: false, ssl: { enabled: true, keyPassphrase: 'some-phrase', someNewValue: 'new' }, compression: { enabled: true }, @@ -113,6 +114,7 @@ describe('#get', () => { keepaliveTimeout: 5000, socketTimeout: 2000, port: 1234, + publicBaseUrl: 'http://myhost.com/abc', rewriteBasePath: false, ssl: { enabled: false, certificate: 'cert', key: 'key' }, compression: { enabled: true }, diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts index e8fca8735a6d..a13eb4661290 100644 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts +++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts @@ -84,6 +84,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { maxPayload: configValue.maxPayloadBytes, name: configValue.name, port: configValue.port, + publicBaseUrl: configValue.publicBaseUrl, rewriteBasePath: configValue.rewriteBasePath, ssl: configValue.ssl, keepaliveTimeout: configValue.keepaliveTimeout, diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index b01dd205440a..201f2e5f8f14 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -56,6 +56,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "basePath": "/test", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "/test", } @@ -2078,6 +2079,7 @@ exports[`CollapsibleNav renders the default nav 1`] = ` "basePath": "/test", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "/test", } @@ -2313,6 +2315,7 @@ exports[`CollapsibleNav renders the default nav 2`] = ` "basePath": "/test", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "/test", } @@ -2549,6 +2552,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` "basePath": "/test", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "/test", } diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 16f48836cab5..5ce5a5f635d6 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -243,6 +243,7 @@ exports[`Header renders 1`] = ` "basePath": "/test", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "/test", } @@ -5354,6 +5355,7 @@ exports[`Header renders 1`] = ` "basePath": "/test", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "/test", } diff --git a/src/core/public/http/base_path.test.ts b/src/core/public/http/base_path.test.ts index 6468e674d5e7..e749934f06af 100644 --- a/src/core/public/http/base_path.test.ts +++ b/src/core/public/http/base_path.test.ts @@ -98,4 +98,13 @@ describe('BasePath', () => { expect(new BasePath('/foo/bar', '/foo').serverBasePath).toEqual('/foo'); }); }); + + describe('publicBaseUrl', () => { + it('returns value passed into construtor', () => { + expect(new BasePath('/foo/bar', '/foo').publicBaseUrl).toEqual(undefined); + expect(new BasePath('/foo/bar', '/foo', 'http://myhost.com/foo').publicBaseUrl).toEqual( + 'http://myhost.com/foo' + ); + }); + }); }); diff --git a/src/core/public/http/base_path.ts b/src/core/public/http/base_path.ts index 78e9cf75ff80..44666450ee98 100644 --- a/src/core/public/http/base_path.ts +++ b/src/core/public/http/base_path.ts @@ -22,7 +22,8 @@ import { modifyUrl } from '@kbn/std'; export class BasePath { constructor( private readonly basePath: string = '', - public readonly serverBasePath: string = basePath + public readonly serverBasePath: string = basePath, + public readonly publicBaseUrl?: string ) {} public get = () => { diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index 98de1d919c48..2eaaefe28575 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -41,7 +41,8 @@ export class HttpService implements CoreService { const kibanaVersion = injectedMetadata.getKibanaVersion(); const basePath = new BasePath( injectedMetadata.getBasePath(), - injectedMetadata.getServerBasePath() + injectedMetadata.getServerBasePath(), + injectedMetadata.getPublicBaseUrl() ); const fetchService = new Fetch({ basePath, kibanaVersion }); const loadingCount = this.loadingCount.setup({ fatalErrors }); diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index 2361d981b859..7285d1a4288d 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -102,6 +102,15 @@ export interface IBasePath { * See {@link BasePath.get} for getting the basePath value for a specific request */ readonly serverBasePath: string; + + /** + * The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the + * {@link IBasePath.serverBasePath}. + * + * @remarks + * Should be used for generating external URL links back to this Kibana instance. + */ + readonly publicBaseUrl?: string; } /** diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index 33d04eedebb0..96282caa62c0 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -23,6 +23,7 @@ const createSetupContractMock = () => { const setupContract: jest.Mocked = { getBasePath: jest.fn(), getServerBasePath: jest.fn(), + getPublicBaseUrl: jest.fn(), getKibanaVersion: jest.fn(), getKibanaBranch: jest.fn(), getCspConfig: jest.fn(), diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index bd8c9e91f15a..283710980e3c 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -44,6 +44,7 @@ export interface InjectedMetadataParams { branch: string; basePath: string; serverBasePath: string; + publicBaseUrl: string; category?: AppCategory; csp: { warnLegacyBrowsers: boolean; @@ -95,6 +96,10 @@ export class InjectedMetadataService { return this.state.serverBasePath; }, + getPublicBaseUrl: () => { + return this.state.publicBaseUrl; + }, + getAnonymousStatusPage: () => { return this.state.anonymousStatusPage; }, @@ -142,6 +147,7 @@ export class InjectedMetadataService { export interface InjectedMetadataSetup { getBasePath: () => string; getServerBasePath: () => string; + getPublicBaseUrl: () => string; getKibanaBuildNumber: () => number; getKibanaBranch: () => string; getKibanaVersion: () => string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 3c4608773b78..0b1d3f8263a2 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -760,6 +760,7 @@ export interface IAnonymousPaths { export interface IBasePath { get: () => string; prepend: (url: string) => string; + readonly publicBaseUrl?: string; remove: (url: string) => string; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "BasePath" readonly serverBasePath: string; diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index daea60122c3c..7020c5eee650 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -24,6 +24,14 @@ Object { } `; +exports[`basePath throws if appends a slash 1`] = `"[basePath]: must start with a slash, don't end with one"`; + +exports[`basePath throws if is an empty string 1`] = `"[basePath]: must start with a slash, don't end with one"`; + +exports[`basePath throws if missing prepended slash 1`] = `"[basePath]: must start with a slash, don't end with one"`; + +exports[`basePath throws if not specified, but rewriteBasePath is set 1`] = `"cannot use [rewriteBasePath] when [basePath] is not specified"`; + exports[`has defaults for config 1`] = ` Object { "autoListen": true, @@ -89,14 +97,6 @@ Object { } `; -exports[`throws if basepath appends a slash 1`] = `"[basePath]: must start with a slash, don't end with one"`; - -exports[`throws if basepath is an empty string 1`] = `"[basePath]: must start with a slash, don't end with one"`; - -exports[`throws if basepath is missing prepended slash 1`] = `"[basePath]: must start with a slash, don't end with one"`; - -exports[`throws if basepath is not specified, but rewriteBasePath is set 1`] = `"cannot use [rewriteBasePath] when [basePath] is not specified"`; - exports[`throws if invalid hostname 1`] = `"[host]: value must be a valid hostname (see RFC 1123)."`; exports[`with TLS throws if TLS is enabled but \`redirectHttpFromPort\` is equal to \`port\` 1`] = `"Kibana does not accept http traffic to [port] when ssl is enabled (only https is allowed), so [ssl.redirectHttpFromPort] cannot be configured to the same value. Both are [1234]."`; diff --git a/src/core/server/http/base_path_service.test.ts b/src/core/server/http/base_path_service.test.ts index 01790b7c77e0..62d395505866 100644 --- a/src/core/server/http/base_path_service.test.ts +++ b/src/core/server/http/base_path_service.test.ts @@ -34,6 +34,18 @@ describe('BasePath', () => { }); }); + describe('publicBaseUrl', () => { + it('defaults to an undefined', () => { + const basePath = new BasePath(); + expect(basePath.publicBaseUrl).toBe(undefined); + }); + + it('returns the publicBaseUrl', () => { + const basePath = new BasePath('/server', 'http://myhost.com/server'); + expect(basePath.publicBaseUrl).toBe('http://myhost.com/server'); + }); + }); + describe('#get()', () => { it('returns base path associated with an incoming Legacy.Request request', () => { const request = httpServerMock.createRawRequest(); diff --git a/src/core/server/http/base_path_service.ts b/src/core/server/http/base_path_service.ts index 059eb36f42dd..1269546d334c 100644 --- a/src/core/server/http/base_path_service.ts +++ b/src/core/server/http/base_path_service.ts @@ -34,10 +34,19 @@ export class BasePath { * See {@link BasePath.get} for getting the basePath value for a specific request */ public readonly serverBasePath: string; + /** + * The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the + * {@link BasePath.serverBasePath}. + * + * @remarks + * Should be used for generating external URL links back to this Kibana instance. + */ + public readonly publicBaseUrl?: string; /** @internal */ - constructor(serverBasePath: string = '') { + constructor(serverBasePath: string = '', publicBaseUrl?: string) { this.serverBasePath = serverBasePath; + this.publicBaseUrl = publicBaseUrl; } /** diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index c843773da72b..6538c1ae973b 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -119,36 +119,104 @@ test('can specify max payload as string', () => { expect(configValue.maxPayload.getValueInBytes()).toBe(2 * 1024 * 1024); }); -test('throws if basepath is missing prepended slash', () => { - const httpSchema = config.schema; - const obj = { - basePath: 'foo', - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); +describe('basePath', () => { + test('throws if missing prepended slash', () => { + const httpSchema = config.schema; + const obj = { + basePath: 'foo', + }; + expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); + }); + + test('throws if appends a slash', () => { + const httpSchema = config.schema; + const obj = { + basePath: '/foo/', + }; + expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); + }); + + test('throws if is an empty string', () => { + const httpSchema = config.schema; + const obj = { + basePath: '', + }; + expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); + }); + + test('throws if not specified, but rewriteBasePath is set', () => { + const httpSchema = config.schema; + const obj = { + rewriteBasePath: true, + }; + expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); + }); }); -test('throws if basepath appends a slash', () => { - const httpSchema = config.schema; - const obj = { - basePath: '/foo/', - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); -}); +describe('publicBaseUrl', () => { + test('throws if invalid HTTP(S) URL', () => { + const httpSchema = config.schema; + expect(() => + httpSchema.validate({ publicBaseUrl: 'myhost.com' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[publicBaseUrl]: expected URI with scheme [http|https]."` + ); + expect(() => + httpSchema.validate({ publicBaseUrl: '//myhost.com' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[publicBaseUrl]: expected URI with scheme [http|https]."` + ); + expect(() => + httpSchema.validate({ publicBaseUrl: 'ftp://myhost.com' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[publicBaseUrl]: expected URI with scheme [http|https]."` + ); + }); -test('throws if basepath is an empty string', () => { - const httpSchema = config.schema; - const obj = { - basePath: '', - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); -}); + test('throws if includes hash, query, or auth', () => { + const httpSchema = config.schema; + expect(() => + httpSchema.validate({ publicBaseUrl: 'http://myhost.com/?a=b' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[publicBaseUrl] may only contain a protocol, host, port, and pathname"` + ); + expect(() => + httpSchema.validate({ publicBaseUrl: 'http://myhost.com/#a' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[publicBaseUrl] may only contain a protocol, host, port, and pathname"` + ); + expect(() => + httpSchema.validate({ publicBaseUrl: 'http://user:pass@myhost.com' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[publicBaseUrl] may only contain a protocol, host, port, and pathname"` + ); + }); -test('throws if basepath is not specified, but rewriteBasePath is set', () => { - const httpSchema = config.schema; - const obj = { - rewriteBasePath: true, - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); + test('throws if basePath and publicBaseUrl are specified, but do not match', () => { + const httpSchema = config.schema; + expect(() => + httpSchema.validate({ + basePath: '/foo', + publicBaseUrl: 'https://myhost.com/', + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[publicBaseUrl] must contain the [basePath]: / !== /foo"` + ); + }); + + test('does not throw if valid URL and matches basePath', () => { + const httpSchema = config.schema; + expect(() => httpSchema.validate({ publicBaseUrl: 'http://myhost.com' })).not.toThrow(); + expect(() => httpSchema.validate({ publicBaseUrl: 'http://myhost.com/' })).not.toThrow(); + expect(() => httpSchema.validate({ publicBaseUrl: 'https://myhost.com' })).not.toThrow(); + expect(() => + httpSchema.validate({ publicBaseUrl: 'https://myhost.com/foo', basePath: '/foo' }) + ).not.toThrow(); + expect(() => httpSchema.validate({ publicBaseUrl: 'http://myhost.com:8080' })).not.toThrow(); + expect(() => + httpSchema.validate({ publicBaseUrl: 'http://myhost.com:4/foo', basePath: '/foo' }) + ).not.toThrow(); + }); }); test('accepts only valid uuids for server.uuid', () => { diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index be64def29462..9a425fa64550 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -19,6 +19,7 @@ import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema'; import { hostname } from 'os'; +import url from 'url'; import { CspConfigType, CspConfig, ICspConfig } from '../csp'; import { SslConfig, sslSchema } from './ssl_config'; @@ -32,11 +33,12 @@ const match = (regex: RegExp, errorMsg: string) => (str: string) => // before update to make sure it's in sync with validation rules in Legacy // https://github.com/elastic/kibana/blob/master/src/legacy/server/config/schema.js export const config = { - path: 'server', + path: 'server' as const, schema: schema.object( { name: schema.string({ defaultValue: () => hostname() }), autoListen: schema.boolean({ defaultValue: true }), + publicBaseUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), basePath: schema.maybe( schema.string({ validate: match(validBasePathRegex, "must start with a slash, don't end with one"), @@ -106,6 +108,17 @@ export const config = { if (!rawConfig.basePath && rawConfig.rewriteBasePath) { return 'cannot use [rewriteBasePath] when [basePath] is not specified'; } + + if (rawConfig.publicBaseUrl) { + const parsedUrl = url.parse(rawConfig.publicBaseUrl); + if (parsedUrl.query || parsedUrl.hash || parsedUrl.auth) { + return `[publicBaseUrl] may only contain a protocol, host, port, and pathname`; + } + if (parsedUrl.path !== (rawConfig.basePath ?? '/')) { + return `[publicBaseUrl] must contain the [basePath]: ${parsedUrl.path} !== ${rawConfig.basePath}`; + } + } + if (!rawConfig.compression.enabled && rawConfig.compression.referrerWhitelist) { return 'cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false'; } @@ -138,6 +151,7 @@ export class HttpConfig { public customResponseHeaders: Record; public maxPayload: ByteSizeValue; public basePath?: string; + public publicBaseUrl?: string; public rewriteBasePath: boolean; public ssl: SslConfig; public compression: { enabled: boolean; referrerWhitelist?: string[] }; @@ -165,6 +179,7 @@ export class HttpConfig { this.maxPayload = rawHttpConfig.maxPayload; this.name = rawHttpConfig.name; this.basePath = rawHttpConfig.basePath; + this.publicBaseUrl = rawHttpConfig.publicBaseUrl; this.keepaliveTimeout = rawHttpConfig.keepaliveTimeout; this.socketTimeout = rawHttpConfig.socketTimeout; this.rewriteBasePath = rawHttpConfig.rewriteBasePath; diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 892396b8e2ad..43f5264ff22e 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -119,7 +119,7 @@ export class HttpServer { await this.server.register([HapiStaticFiles]); this.config = config; - const basePathService = new BasePath(config.basePath); + const basePathService = new BasePath(config.basePath, config.publicBaseUrl); this.setupBasePathRewrite(config, basePathService); this.setupConditionalCompression(config); this.setupRequestStateAssignment(config); diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 4fc972c9679b..552f41d91241 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -60,8 +60,12 @@ export type InternalHttpServiceStartMock = jest.Mocked basePath: BasePathMocked; }; -const createBasePathMock = (serverBasePath = '/mock-server-basepath'): BasePathMocked => ({ +const createBasePathMock = ( + serverBasePath = '/mock-server-basepath', + publicBaseUrl = 'http://myhost.com/mock-server-basepath' +): BasePathMocked => ({ serverBasePath, + publicBaseUrl, get: jest.fn().mockReturnValue(serverBasePath), set: jest.fn(), prepend: jest.fn(), diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts index 4345783e46e1..afd7b0174d15 100644 --- a/src/core/server/http/types.ts +++ b/src/core/server/http/types.ts @@ -316,7 +316,12 @@ export interface InternalHttpServiceStart extends HttpServiceStart { isListening: () => boolean; } -/** @public */ +/** + * Information about what hostname, port, and protocol the server process is + * running on. Note that this may not match the URL that end-users access + * Kibana at. For the public URL, see {@link BasePath.publicBaseUrl}. + * @public + */ export interface HttpServerInfo { /** The name of the Kibana server */ name: string; diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index 07ca59a48c6b..a3f6b27f135b 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -36,6 +36,7 @@ Object { "user": Object {}, }, }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -79,6 +80,7 @@ Object { "user": Object {}, }, }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -126,6 +128,7 @@ Object { }, }, }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -169,6 +172,7 @@ Object { "user": Object {}, }, }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -212,6 +216,7 @@ Object { "user": Object {}, }, }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index 738787f94090..4bbb2bd4811c 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -52,7 +52,7 @@ export class RenderingService { packageInfo: this.coreContext.env.packageInfo, }; const basePath = http.basePath.get(request); - const serverBasePath = http.basePath.serverBasePath; + const { serverBasePath, publicBaseUrl } = http.basePath; const settings = { defaults: uiSettings.getRegistered(), user: includeUserSettings ? await uiSettings.getUserProvided() : {}, @@ -72,6 +72,7 @@ export class RenderingService { branch: env.packageInfo.branch, basePath, serverBasePath, + publicBaseUrl, env, anonymousStatusPage: status.isStatusPageAnonymous(), i18n: { diff --git a/src/core/server/rendering/types.ts b/src/core/server/rendering/types.ts index 75bbac1a243e..1954fc1c79e5 100644 --- a/src/core/server/rendering/types.ts +++ b/src/core/server/rendering/types.ts @@ -40,6 +40,7 @@ export interface RenderingMetadata { branch: string; basePath: string; serverBasePath: string; + publicBaseUrl?: string; env: { mode: EnvironmentMode; packageInfo: PackageInfo; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 770048d2cff1..b65ba329cec1 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -269,9 +269,10 @@ export interface AuthToolkit { // @public export class BasePath { // @internal - constructor(serverBasePath?: string); + constructor(serverBasePath?: string, publicBaseUrl?: string); get: (request: KibanaRequest | LegacyRequest) => string; prepend: (path: string) => string; + readonly publicBaseUrl?: string; remove: (path: string) => string; readonly serverBasePath: string; set: (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; @@ -862,7 +863,7 @@ export interface HttpResponseOptions { // @public export type HttpResponsePayload = undefined | string | Record | Buffer | Stream; -// @public (undocumented) +// @public export interface HttpServerInfo { hostname: string; name: string; diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index a9b5eec45a75..0e85a46d7d24 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -69,6 +69,7 @@ export default () => customResponseHeaders: HANDLED_IN_NEW_PLATFORM, keepaliveTimeout: HANDLED_IN_NEW_PLATFORM, maxPayloadBytes: HANDLED_IN_NEW_PLATFORM, + publicBaseUrl: HANDLED_IN_NEW_PLATFORM, socketTimeout: HANDLED_IN_NEW_PLATFORM, ssl: HANDLED_IN_NEW_PLATFORM, compression: HANDLED_IN_NEW_PLATFORM, diff --git a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap index 4fb061ec816a..b64b485f6561 100644 --- a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -13,6 +13,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` "basePath": "", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "", }, @@ -376,6 +377,7 @@ exports[`DashboardEmptyScreen renders correctly with visualize paragraph 1`] = ` "basePath": "", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "", }, @@ -747,6 +749,7 @@ exports[`DashboardEmptyScreen renders correctly without visualize paragraph 1`] "basePath": "", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "", }, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index fd90663e4700..2e262ce43731 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -262,6 +262,7 @@ exports[`SavedObjectsTable should render normally 1`] = ` "basePath": "", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "", } diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index 17f15b6aa1c3..d06fd0df98a8 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -175,6 +175,7 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` "basePath": "", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "", }, diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap index b1921452354d..cad06255ffe9 100644 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -316,6 +316,7 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO "basePath": "", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "", },