diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts index 4ce422e1f65c4..0ca87eae6e235 100644 --- a/src/core/server/http/cookie_session_storage.test.ts +++ b/src/core/server/http/cookie_session_storage.test.ts @@ -62,6 +62,7 @@ configService.atPath.mockReturnValue( disableProtection: true, whitelist: [], }, + customResponseHeaders: {}, } as any) ); diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index 7ac707b0f3d83..0976bfd56682e 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -18,7 +18,8 @@ */ import uuid from 'uuid'; -import { config } from '.'; +import { config, HttpConfig } from './http_config'; +import { CspConfig } from '../csp'; const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost']; const invalidHostname = 'asdf$%^'; @@ -107,6 +108,23 @@ test('throws if xsrf.whitelist element does not start with a slash', () => { ); }); +test('accepts any type of objects for custom headers', () => { + const httpSchema = config.schema; + const obj = { + customResponseHeaders: { + string: 'string', + bool: true, + number: 12, + array: [1, 2, 3], + nested: { + foo: 1, + bar: 'dolly', + }, + }, + }; + expect(() => httpSchema.validate(obj)).not.toThrow(); +}); + describe('with TLS', () => { test('throws if TLS is enabled but `redirectHttpFromPort` is equal to `port`', () => { const httpSchema = config.schema; @@ -173,3 +191,30 @@ describe('with compression', () => { expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); }); }); + +describe('HttpConfig', () => { + it('converts customResponseHeaders to strings or arrays of strings', () => { + const httpSchema = config.schema; + const rawConfig = httpSchema.validate({ + customResponseHeaders: { + string: 'string', + bool: true, + number: 12, + array: [1, 2, 3], + nested: { + foo: 1, + bar: 'dolly', + }, + }, + }); + const httpConfig = new HttpConfig(rawConfig, CspConfig.DEFAULT); + + expect(httpConfig.customResponseHeaders).toEqual({ + string: 'string', + bool: 'true', + number: '12', + array: ['1', '2', '3'], + nested: '{"foo":1,"bar":"dolly"}', + }); + }); +}); diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 73f44f3c5ab5c..7c72e3270743e 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -57,7 +57,7 @@ export const config = { ), schema.boolean({ defaultValue: false }) ), - customResponseHeaders: schema.recordOf(schema.string(), schema.string(), { + customResponseHeaders: schema.recordOf(schema.string(), schema.any(), { defaultValue: {}, }), host: schema.string({ @@ -136,7 +136,7 @@ export class HttpConfig { public socketTimeout: number; public port: number; public cors: boolean | { origin: string[] }; - public customResponseHeaders: Record; + public customResponseHeaders: Record; public maxPayload: ByteSizeValue; public basePath?: string; public rewriteBasePath: boolean; @@ -153,7 +153,15 @@ export class HttpConfig { this.host = rawHttpConfig.host; this.port = rawHttpConfig.port; this.cors = rawHttpConfig.cors; - this.customResponseHeaders = rawHttpConfig.customResponseHeaders; + this.customResponseHeaders = Object.entries(rawHttpConfig.customResponseHeaders ?? {}).reduce( + (headers, [key, value]) => { + return { + ...headers, + [key]: Array.isArray(value) ? value.map(e => convertHeader(e)) : convertHeader(value), + }; + }, + {} + ); this.maxPayload = rawHttpConfig.maxPayload; this.name = rawHttpConfig.name; this.basePath = rawHttpConfig.basePath; @@ -166,3 +174,7 @@ export class HttpConfig { this.xsrf = rawHttpConfig.xsrf; } } + +const convertHeader = (entry: any): string => { + return typeof entry === 'object' ? JSON.stringify(entry) : String(entry); +}; diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts index 49c4d690c6876..0e639aa72a825 100644 --- a/src/core/server/http/test_utils.ts +++ b/src/core/server/http/test_utils.ts @@ -45,6 +45,7 @@ configService.atPath.mockReturnValue( disableProtection: true, whitelist: [], }, + customResponseHeaders: {}, } as any) );