Skip to content

Commit

Permalink
Merge branch 'master' into np-decouple-configSchema
Browse files Browse the repository at this point in the history
  • Loading branch information
elasticmachine authored Dec 2, 2019
2 parents f5af513 + d2846b6 commit 8d02bbe
Show file tree
Hide file tree
Showing 142 changed files with 2,514 additions and 1,226 deletions.
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 @@ -616,6 +617,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;
}

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;
});
} 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

0 comments on commit 8d02bbe

Please sign in to comment.