Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/setup/settings.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,12 @@ running behind a proxy. Use the `server.rewriteBasePath` setting to tell Kibana
if it should remove the basePath from requests it receives, and to prevent a
deprecation warning at startup. This setting cannot end in a slash (`/`).

[[server-compression]]`server.compression.enabled:`:: *Default: `true`* Set to `false` to disable HTTP compression for all responses.

`server.compression.referrerWhitelist:`:: *Default: none* Specifies an array of trusted hostnames, such as the Kibana host, or a reverse
proxy sitting in front of it. This determines whether HTTP compression may be used for responses, based on the request's `Referer` header.
This setting may not be used when `server.compression.enabled` is set to `false`.

[[server-cors]]`server.cors:`:: *Default: `false`* Set to `true` to enable CORS support. This setting is required to configure `server.cors.origin`.

`server.cors.origin:`:: *Default: none* Specifies origins. "origin" must be an array. To use this setting, you must set `server.cors` to `true`. To accept all origins, use `server.cors.origin: ["*"]`.
Expand Down
43 changes: 38 additions & 5 deletions src/core/server/http/__snapshots__/http_config.test.ts.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ configService.atPath.mockReturnValue(
ssl: {
verificationMode: 'none',
},
compression: { enabled: true },
} as any)
);

Expand Down
56 changes: 49 additions & 7 deletions src/core/server/http/http_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,26 @@ import { config, HttpConfig } from '.';
import { Env } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';

const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost'];
const invalidHostname = 'asdf$%^';

test('has defaults for config', () => {
const httpSchema = config.schema;
const obj = {};
expect(httpSchema.validate(obj)).toMatchSnapshot();
});

test('accepts valid hostnames', () => {
const { host: host1 } = config.schema.validate({ host: 'www.example.com' });
const { host: host2 } = config.schema.validate({ host: '8.8.8.8' });
const { host: host3 } = config.schema.validate({ host: '::1' });
const { host: host4 } = config.schema.validate({ host: 'localhost' });

expect({ host1, host2, host3, host4 }).toMatchSnapshot('valid host names');
for (const val of validHostnames) {
const { host } = config.schema.validate({ host: val });
expect({ host }).toMatchSnapshot();
}
});

test('throws if invalid hostname', () => {
const httpSchema = config.schema;
const obj = {
host: 'asdf$%^',
host: invalidHostname,
};
expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot();
});
Expand Down Expand Up @@ -296,3 +297,44 @@ describe('with TLS', () => {
expect(httpConfig.ssl.rejectUnauthorized).toBe(true);
});
});

describe('with compression', () => {
test('accepts valid referrer whitelist', () => {
const {
compression: { referrerWhitelist },
} = config.schema.validate({
compression: {
referrerWhitelist: validHostnames,
},
});

expect(referrerWhitelist).toMatchSnapshot();
});

test('throws if invalid referrer whitelist', () => {
const httpSchema = config.schema;
const invalidHostnames = {
compression: {
referrerWhitelist: [invalidHostname],
},
};
const emptyArray = {
compression: {
referrerWhitelist: [],
},
};
expect(() => httpSchema.validate(invalidHostnames)).toThrowErrorMatchingSnapshot();
expect(() => httpSchema.validate(emptyArray)).toThrowErrorMatchingSnapshot();
});

test('throws if referrer whitelist is specified and compression is disabled', () => {
const httpSchema = config.schema;
const obj = {
compression: {
enabled: false,
referrerWhitelist: validHostnames,
},
};
expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot();
});
});
16 changes: 16 additions & 0 deletions src/core/server/http/http_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,26 @@ export const config = {
socketTimeout: schema.number({
defaultValue: 120000,
}),
compression: schema.object({
enabled: schema.boolean({ defaultValue: true }),
referrerWhitelist: schema.maybe(
schema.arrayOf(
schema.string({
hostname: true,
}),
{ minSize: 1 }
)
),
}),
},
{
validate: rawConfig => {
if (!rawConfig.basePath && rawConfig.rewriteBasePath) {
return 'cannot use [rewriteBasePath] when [basePath] is not specified';
}
if (!rawConfig.compression.enabled && rawConfig.compression.referrerWhitelist) {
return 'cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false';
}

if (
rawConfig.ssl.enabled &&
Expand Down Expand Up @@ -109,6 +123,7 @@ export class HttpConfig {
public publicDir: string;
public defaultRoute?: string;
public ssl: SslConfig;
public compression: { enabled: boolean; referrerWhitelist?: string[] };

/**
* @internal
Expand All @@ -126,5 +141,6 @@ export class HttpConfig {
this.publicDir = env.staticFilesDir;
this.ssl = new SslConfig(rawConfig.ssl || {});
this.defaultRoute = rawConfig.defaultRoute;
this.compression = rawConfig.compression;
}
}
85 changes: 85 additions & 0 deletions src/core/server/http/http_server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ beforeEach(() => {
maxPayload: new ByteSizeValue(1024),
port: 10002,
ssl: { enabled: false },
compression: { enabled: true },
} as HttpConfig;

configWithSSL = {
Expand Down Expand Up @@ -578,6 +579,90 @@ test('exposes route details of incoming request to a route handler', async () =>
});
});

describe('conditional compression', () => {
async function setupServer(innerConfig: HttpConfig) {
const { registerRouter, server: innerServer } = await server.setup(innerConfig);
const router = new Router('', logger, enhanceWithContext);
// we need the large body here so that compression would normally be used
const largeRequest = {
body: 'hello'.repeat(500),
headers: { 'Content-Type': 'text/html; charset=UTF-8' },
};
router.get({ path: '/', validate: false }, (_context, _req, res) => res.ok(largeRequest));
registerRouter(router);
await server.start();
return innerServer.listener;
}
Comment on lines +582 to +595
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks like an integration test and should be in src/core/server/http/integration_tests instead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call, I'll move it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As discussed offline, looks like these tests / usage of supertest are in the correct place after all =)


test('with `compression.enabled: true`', async () => {
const listener = await setupServer(config);

const response = await supertest(listener)
.get('/')
.set('accept-encoding', 'gzip');

expect(response.header).toHaveProperty('content-encoding', 'gzip');
});

test('with `compression.enabled: false`', async () => {
const listener = await setupServer({
...config,
compression: { enabled: false },
});

const response = await supertest(listener)
.get('/')
.set('accept-encoding', 'gzip');

expect(response.header).not.toHaveProperty('content-encoding');
});

describe('with defined `compression.referrerWhitelist`', () => {
let listener: Server;
beforeEach(async () => {
listener = await setupServer({
...config,
compression: { enabled: true, referrerWhitelist: ['foo'] },
});
});

test('enables compression for no referer', async () => {
const response = await supertest(listener)
.get('/')
.set('accept-encoding', 'gzip');

expect(response.header).toHaveProperty('content-encoding', 'gzip');
});

test('enables compression for whitelisted referer', async () => {
const response = await supertest(listener)
.get('/')
.set('accept-encoding', 'gzip')
.set('referer', 'http://foo:1234');

expect(response.header).toHaveProperty('content-encoding', 'gzip');
});

test('disables compression for non-whitelisted referer', async () => {
const response = await supertest(listener)
.get('/')
.set('accept-encoding', 'gzip')
.set('referer', 'http://bar:1234');

expect(response.header).not.toHaveProperty('content-encoding');
});

test('disables compression for invalid referer', async () => {
const response = await supertest(listener)
.get('/')
.set('accept-encoding', 'gzip')
.set('referer', 'http://asdf$%^');

expect(response.header).not.toHaveProperty('content-encoding');
});
});
});

test('exposes route details of incoming request to a route handler (POST + payload options)', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);

Expand Down
29 changes: 29 additions & 0 deletions src/core/server/http/http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import { Request, Server } from 'hapi';
import url from 'url';

import { Logger, LoggerFactory } from '../logging';
import { HttpConfig } from './http_config';
Expand Down Expand Up @@ -96,6 +97,7 @@ export class HttpServer {

const basePathService = new BasePath(config.basePath);
this.setupBasePathRewrite(config, basePathService);
this.setupConditionalCompression(config);

return {
registerRouter: this.registerRouter.bind(this),
Expand Down Expand Up @@ -187,6 +189,33 @@ export class HttpServer {
});
}

private setupConditionalCompression(config: HttpConfig) {
if (this.server === undefined) {
throw new Error('Server is not created yet');
}

const { enabled, referrerWhitelist: list } = config.compression;
if (!enabled) {
this.log.debug('HTTP compression is disabled');
this.server.ext('onRequest', (request, h) => {
request.info.acceptEncoding = '';
return h.continue;
Comment on lines +200 to +202
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Isn't this quite brutal? I think this header can be used for other things than compression?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not according to MDN and RFC 7231 -- the only valid non-compression-related directive is identity, which is a synonym for "no encoding".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oups, this and accept are not the same header.

});
} else if (list) {
this.log.debug(`HTTP compression is only enabled for any referrer in the following: ${list}`);
this.server.ext('onRequest', (request, h) => {
const { referrer } = request.info;
if (referrer !== '') {
const { hostname } = url.parse(referrer);
if (!hostname || !list.includes(hostname)) {
request.info.acceptEncoding = '';
}
}
return h.continue;
});
}
}

private registerOnPostAuth(fn: OnPostAuthHandler) {
if (this.server === undefined) {
throw new Error('Server is not created yet');
Expand Down
1 change: 1 addition & 0 deletions src/core/server/http/http_tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ describe('timeouts', () => {
host: '127.0.0.1',
maxPayload: new ByteSizeValue(1024),
ssl: {},
compression: { enabled: true },
} as HttpConfig);
registerRouter(router);

Expand Down
1 change: 1 addition & 0 deletions src/core/server/http/integration_tests/lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ configService.atPath.mockReturnValue(
ssl: {
enabled: false,
},
compression: { enabled: true },
} as any)
);

Expand Down
1 change: 1 addition & 0 deletions src/core/server/http/integration_tests/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ configService.atPath.mockReturnValue(
ssl: {
enabled: false,
},
compression: { enabled: true },
} as any)
);

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading