[proxyConfig] expanded and tested the proxy config

This commit is contained in:
spalger 2016-03-10 18:26:20 -07:00
parent 694c2c00b9
commit f0aaff5817
9 changed files with 515 additions and 60 deletions

View file

@ -42,9 +42,15 @@ module.exports = function (kibana) {
ssl: Joi.object({
verify: Joi.boolean(),
}).default(),
hosts: Joi.array().items(
proxyConfig: Joi.array().items(
Joi.object().keys({
host: Joi.array().items(Joi.string()).single().default(['.*']),
match: Joi.object().keys({
protocol: Joi.string().default('*'),
host: Joi.string().default('*'),
port: Joi.string().default('*'),
path: Joi.string().default('*')
}),
timeout: Joi.number(),
ssl: Joi.object().keys({
verify: Joi.boolean(),
@ -55,7 +61,13 @@ module.exports = function (kibana) {
})
).default([
{
host: '.*',
match: {
protocol: '*',
hostname: '*',
port: '*',
path: '*'
},
timeout: 180000,
ssl: {
verify: true
@ -72,7 +84,7 @@ module.exports = function (kibana) {
throw new Error('sense.ssl.version is no longer supported.');
}
const hostBasedProxyConfig = new HostBasedProxyConfigCollection(options.hosts);
const hostBasedProxyConfig = new HostBasedProxyConfigCollection(options.proxyConfig);
// http://hapijs.com/api/8.8.1#route-configuration
server.route({
@ -119,7 +131,7 @@ module.exports = function (kibana) {
}
},
...hostBasedProxyConfig.forUri(uri)
...hostBasedProxyConfig.configForUri(uri)
})
}
});

View file

@ -21,6 +21,7 @@
"eslint": "^1.7.3",
"eslint-config-airbnb": "^0.1.0",
"eslint-plugin-react": "^3.6.3",
"expect.js": "^0.3.1",
"grunt": "^0.4.5",
"grunt-aws": "^0.6.1",
"grunt-cli": "^0.1.13",
@ -31,12 +32,15 @@
"grunt-replace": "^0.11.0",
"grunt-run": "^0.5.2",
"gruntify-eslint": "^1.2.0",
"jit-grunt": "^0.9.1"
"jit-grunt": "^0.9.1",
"mocha": "^2.4.5",
"sinon": "^1.17.3"
},
"dependencies": {
"boom": "2.8.0",
"joi": "6.6.1",
"lodash": "3.10.1"
"lodash": "3.10.1",
"minimatch": "^3.0.0"
},
"license": "Apache-2.0"
}

View file

@ -0,0 +1,170 @@
/* eslint-env mocha */
import expect from 'expect.js';
import sinon from 'sinon';
import fs from 'fs';
import { Agent as HttpsAgent } from 'https';
import { parse as parseUrl } from 'url';
import { ProxyConfig } from '../proxy_config'
const matchGoogle = {
protocol: 'https',
host: 'google.com',
path: '/search'
}
const parsedGoogle = parseUrl('https://google.com/search');
const parsedLocalEs = parseUrl('https://localhost:5601/search');
describe('ProxyConfig', function () {
beforeEach(function () {
sinon.stub(fs, 'readFileSync', function (path) {
return { path }
});
});
afterEach(function () {
fs.readFileSync.restore();
});
describe('#getForParsedUri', function () {
context('parsed url does not match', function () {
it('returns {}', function () {
const config = new ProxyConfig({
match: matchGoogle,
timeout: 100
});
expect(config.getForParsedUri(parsedLocalEs)).to.eql({});
});
});
context('parsed url does match', function () {
it('assigns timeout value', function () {
const football = {};
const config = new ProxyConfig({
match: matchGoogle,
timeout: football
});
expect(config.getForParsedUri(parsedGoogle).timeout).to.be(football);
});
it('assigns ssl.verify to rejectUnauthorized', function () {
const football = {};
const config = new ProxyConfig({
match: matchGoogle,
ssl: {
verify: football
}
});
expect(config.getForParsedUri(parsedGoogle).rejectUnauthorized).to.be(football);
});
context('uri us http', function () {
context('ca is set', function () {
it('creates but does not output the agent', function () {
const config = new ProxyConfig({
ssl: {
ca: 'path/to/ca'
}
});
expect(config.sslAgent).to.be.an(HttpsAgent);
expect(config.getForParsedUri({ protocol: 'http:' }).agent).to.be(undefined);
});
});
context('cert is set', function () {
it('creates but does not output the agent', function () {
const config = new ProxyConfig({
ssl: {
cert: 'path/to/cert'
}
});
expect(config.sslAgent).to.be.an(HttpsAgent);
expect(config.getForParsedUri({ protocol: 'http:' }).agent).to.be(undefined);
});
});
context('key is set', function () {
it('creates but does not output the agent', function () {
const config = new ProxyConfig({
ssl: {
key: 'path/to/key'
}
});
expect(config.sslAgent).to.be.an(HttpsAgent);
expect(config.getForParsedUri({ protocol: 'http:' }).agent).to.be(undefined);
});
});
context('cert + key are set', function () {
it('creates but does not output the agent', function () {
const config = new ProxyConfig({
ssl: {
cert: 'path/to/cert',
key: 'path/to/key'
}
});
expect(config.sslAgent).to.be.an(HttpsAgent);
expect(config.getForParsedUri({ protocol: 'http:' }).agent).to.be(undefined);
});
});
});
context('uri us https', function () {
context('ca is set', function () {
it('creates and outputs the agent', function () {
const config = new ProxyConfig({
ssl: {
ca: 'path/to/ca'
}
});
expect(config.sslAgent).to.be.an(HttpsAgent);
expect(config.getForParsedUri({ protocol: 'https:' }).agent).to.be(config.sslAgent);
});
});
context('cert is set', function () {
it('creates and outputs the agent', function () {
const config = new ProxyConfig({
ssl: {
cert: 'path/to/cert'
}
});
expect(config.sslAgent).to.be.an(HttpsAgent);
expect(config.getForParsedUri({ protocol: 'https:' }).agent).to.be(config.sslAgent);
});
});
context('key is set', function () {
it('creates and outputs the agent', function () {
const config = new ProxyConfig({
ssl: {
key: 'path/to/key'
}
});
expect(config.sslAgent).to.be.an(HttpsAgent);
expect(config.getForParsedUri({ protocol: 'https:' }).agent).to.be(config.sslAgent);
});
});
context('cert + key are set', function () {
it('creates and outputs the agent', function () {
const config = new ProxyConfig({
ssl: {
cert: 'path/to/cert',
key: 'path/to/key'
}
});
expect(config.sslAgent).to.be.an(HttpsAgent);
expect(config.getForParsedUri({ protocol: 'https:' }).agent).to.be(config.sslAgent);
});
});
});
});
});
});

View file

@ -0,0 +1,156 @@
/* eslint-env mocha */
import expect from 'expect.js';
import sinon from 'sinon';
import fs from 'fs';
import { Agent as HttpsAgent } from 'https';
import { ProxyConfigCollection } from '../proxy_config_collection'
describe('ProxyConfigCollection', function () {
beforeEach(function () {
sinon.stub(fs, 'readFileSync', () => new Buffer(0));
});
afterEach(function () {
fs.readFileSync.restore();
});
const proxyConfigs = [
{
match: {
protocol: 'https',
host: 'localhost',
port: 5601,
path: '/.kibana'
},
timeout: 1,
},
{
match: {
protocol: 'https',
host: 'localhost',
port: 5601
},
timeout: 2,
},
{
match: {
host: 'localhost',
port: 5601
},
timeout: 3,
},
{
match: {
host: 'localhost'
},
timeout: 4,
},
{
match: {},
timeout: 5
}
]
function getTimeout(uri) {
const collection = new ProxyConfigCollection(proxyConfigs);
return collection.configForUri(uri).timeout;
}
context('http://localhost:5601', function () {
it('defaults to the first matching timeout', function () {
expect(getTimeout('http://localhost:5601')).to.be(3)
});
});
context('https://localhost:5601/.kibana', function () {
it('defaults to the first matching timeout', function () {
expect(getTimeout('https://localhost:5601/.kibana')).to.be(1);
});
});
context('http://localhost:5602', function () {
it('defaults to the first matching timeout', function () {
expect(getTimeout('http://localhost:5602')).to.be(4);
});
});
context('https://localhost:5602', function () {
it('defaults to the first matching timeout', function () {
expect(getTimeout('https://localhost:5602')).to.be(4);
});
});
context('http://localhost:5603', function () {
it('defaults to the first matching timeout', function () {
expect(getTimeout('http://localhost:5603')).to.be(4);
});
});
context('https://localhost:5603', function () {
it('defaults to the first matching timeout', function () {
expect(getTimeout('https://localhost:5603')).to.be(4);
});
});
context('https://localhost:5601/index', function () {
it('defaults to the first matching timeout', function () {
expect(getTimeout('https://localhost:5601/index')).to.be(2);
});
});
context('http://localhost:5601/index', function () {
it('defaults to the first matching timeout', function () {
expect(getTimeout('http://localhost:5601/index')).to.be(3);
});
});
context('https://localhost:5601/index/type', function () {
it('defaults to the first matching timeout', function () {
expect(getTimeout('https://localhost:5601/index/type')).to.be(2);
});
});
context('http://notlocalhost', function () {
it('defaults to the first matching timeout', function () {
expect(getTimeout('http://notlocalhost')).to.be(5);
});
});
context('collection with ssl config and root level verify:false', function () {
function makeCollection() {
return new ProxyConfigCollection([
{
match: { host: '*.internal.org' },
ssl: { ca: 'path/to/ca' }
},
{
match: { host: '*' },
ssl: { verify: false }
}
]);
}
it('verifies for config that produces ssl agent', function () {
const conf = makeCollection().configForUri('https://es.internal.org/_search');
expect(conf).to.have.property('rejectUnauthorized', true);
expect(conf.agent).to.be.an(HttpsAgent);
});
it('disabled verification for * config', function () {
const conf = makeCollection().configForUri('https://extenal.org/_search');
expect(conf).to.have.property('rejectUnauthorized', false);
expect(conf.agent).to.be(undefined);
});
});
});

View file

@ -0,0 +1,55 @@
/* eslint-env mocha */
import expect from 'expect.js'
import { WildcardMatcher } from '../wildcard_matcher'
function should(candidate, ...constructorArgs) {
if (!new WildcardMatcher(...constructorArgs).match(candidate)) {
throw new Error(`Expected pattern ${[...constructorArgs]} to match ${candidate}`);
}
}
function shouldNot(candidate, ...constructorArgs) {
if (new WildcardMatcher(...constructorArgs).match(candidate)) {
throw new Error(`Expected pattern ${[...constructorArgs]} to not match ${candidate}`);
}
}
describe('WildcardMatcher', function () {
context('pattern = *', function () {
it('matches http', () => should('http', '*'));
it('matches https', () => should('https', '*'));
it('matches nothing', () => should('', '*'));
it('does not match /', () => shouldNot('/', '*'));
it('matches localhost', () => should('localhost', '*'));
it('matches a path', () => should('/index/type/_search', '*'));
context('defaultValue = /', function () {
it('matches /', () => should('/', '*', '/'));
});
});
context('pattern = http', function () {
it('matches http', () => should('http', 'http'));
it('does not match https', () => shouldNot('https', 'http'));
it('does not match nothing', () => shouldNot('', 'http'));
it('does not match localhost', () => shouldNot('localhost', 'http'));
it('does not match a path', () => shouldNot('/index/type/_search', 'http'));
});
context('pattern = 560{1..9}', function () {
it('does not match http', () => shouldNot('http', '560{1..9}'));
it('does not matches 5600', () => shouldNot('5600', '560{1..9}'));
it('matches 5601', () => should('5601', '560{1..9}'));
it('matches 5602', () => should('5602', '560{1..9}'));
it('matches 5603', () => should('5603', '560{1..9}'));
it('matches 5604', () => should('5604', '560{1..9}'));
it('matches 5605', () => should('5605', '560{1..9}'));
it('matches 5606', () => should('5606', '560{1..9}'));
it('matches 5607', () => should('5607', '560{1..9}'));
it('matches 5608', () => should('5608', '560{1..9}'));
it('matches 5609', () => should('5609', '560{1..9}'));
it('does not matches 5610', () => shouldNot('5610', '560{1..9}'));
});
});

View file

@ -1,53 +0,0 @@
import { defaultsDeep, memoize, values } from 'lodash'
import { Agent as HttpAgent } from 'http'
import { Agent as HttpsAgent } from 'https'
import { readFileSync } from 'lodash'
import { parse as parseUrl } from 'url'
const makeHttpsAgent = memoize(
opts => new HttpsAgent(opts),
opts => JSON.stringify(opts)
)
export class HostBasedProxyConfig {
constructor(config) {
config = Object.assign({}, config);
this.host = config.host;
this.re = new RegExp(this.host);
const ssl = config.ssl || {};
this.verifySsl = ssl.verify;
const sslAgentOpts = {
ca: ssl.ca && readFileSync(ssl.ca),
cert: ssl.cert && readFileSync(ssl.cert),
key: ssl.key && readFileSync(ssl.key),
};
if (values(sslAgentOpts).filter(Boolean).length) {
this.sslAgent = new HttpsAgent(sslAgentOpts);
}
}
getSettings({ protocol, host }) {
if (!this.re.test(`${protocol}//${host}`)) return {};
return {
rejectUnauthorized: this.verifySsl,
agent: protocol === 'https:' ? this.sslAgent : undefined
};
}
}
export class HostBasedProxyConfigCollection {
constructor(hosts = []) {
this.hosts = hosts.map(settings => new HostBasedProxyConfig(settings))
}
forUri(uri) {
const parsedUri = parseUrl(uri);
const settings = this.hosts.map(host => host.getSettings(parsedUri));
return defaultsDeep({}, ...settings);
}
}

70
server/proxy_config.js Normal file
View file

@ -0,0 +1,70 @@
import { memoize, values } from 'lodash'
import { format as formatUrl } from 'url'
import { Agent as HttpsAgent } from 'https'
import { readFileSync } from 'fs'
import { WildcardMatcher } from './wildcard_matcher'
const makeHttpsAgent = memoize(
opts => new HttpsAgent(opts),
opts => JSON.stringify(opts)
)
export class ProxyConfig {
constructor(config) {
config = Object.assign({}, config);
// -----
// read "match" info
// -----
const rawMatches = Object.assign({}, config.match);
this.id = formatUrl({
protocol: rawMatches.protocol,
hostname: rawMatches.host,
port: rawMatches.port,
pathname: rawMatches.path
}) || '*';
this.matchers = {
protocol: new WildcardMatcher(rawMatches.protocol),
host: new WildcardMatcher(rawMatches.host),
port: new WildcardMatcher(rawMatches.port),
path: new WildcardMatcher(rawMatches.path, '/'),
};
// -----
// read config vars
// -----
this.timeout = config.timeout;
this.sslAgent = this._makeSslAgent(config);
}
_makeSslAgent(config) {
const ssl = config.ssl || {};
this.verifySsl = ssl.verify;
const sslAgentOpts = {
ca: ssl.ca && readFileSync(ssl.ca),
cert: ssl.cert && readFileSync(ssl.cert),
key: ssl.key && readFileSync(ssl.key),
};
if (values(sslAgentOpts).filter(Boolean).length) {
return new HttpsAgent(sslAgentOpts);
}
}
getForParsedUri({ protocol, hostname, port, pathname }) {
let match = this.matchers.protocol.match(protocol.slice(0, -1));
match = match && this.matchers.host.match(hostname);
match = match && this.matchers.port.match(port);
match = match && this.matchers.path.match(pathname);
if (!match) return {};
return {
timeout: this.timeout,
rejectUnauthorized: this.sslAgent ? true : this.verifySsl,
agent: protocol === 'https:' ? this.sslAgent : undefined
};
}
}

View file

@ -0,0 +1,17 @@
import { defaultsDeep } from 'lodash'
import { ProxyConfig } from './proxy_config'
import { parse as parseUrl } from 'url'
export class ProxyConfigCollection {
constructor(configs = []) {
this.configs = configs.map(settings => new ProxyConfig(settings))
}
configForUri(uri) {
const parsedUri = parseUrl(uri);
const settings = this.configs.map(config => config.getForParsedUri(parsedUri));
return defaultsDeep({}, ...settings);
}
}

View file

@ -0,0 +1,24 @@
import { Minimatch } from 'minimatch'
export class WildcardMatcher {
constructor(wildcardPattern, emptyVal) {
this.emptyVal = emptyVal;
this.pattern = String(wildcardPattern || '*');
this.matcher = new Minimatch(this.pattern, {
noglobstar: true,
dot: true,
nocase: true,
matchBase: true,
nocomment: true
})
}
match(candidate) {
const empty = !candidate || candidate === this.emptyVal;
if (empty && this.pattern === '*') {
return true;
}
return this.matcher.match(candidate || '')
}
}