[elasticsearch/proxy] strip _ query string param

With elasticsearch starting to validate query string parameters, we need to stop sending the extra "_" query string paramters (used to cache-bust in browsers). The changes are larger than "necessary" because the url parsing and formatting logic was updated to use the bonafide `url.parse` and `url.format` methods, rather than a series of specialized string mutations. The tests for this code were also expanded as a part of this effort.
This commit is contained in:
spalger 2016-10-04 13:20:35 -07:00
parent e78c829574
commit d7bcab9f96
2 changed files with 109 additions and 32 deletions

View file

@ -1,5 +1,6 @@
import expect from 'expect.js';
import mapUri from '../map_uri';
import { get, defaults } from 'lodash';
import sinon from 'sinon';
describe('plugins/elasticsearch', function () {
@ -7,6 +8,16 @@ describe('plugins/elasticsearch', function () {
let request;
function stubServer(settings) {
const values = defaults(settings || {}, {
'elasticsearch.url': 'http://localhost:9200',
'elasticsearch.requestHeadersWhitelist': ['authorization'],
'elasticsearch.customHeaders': {}
});
const config = { get: (key, def) => get(values, key, def) };
return { config: () => config };
}
beforeEach(function () {
request = {
path: '/elasticsearch/some/path',
@ -23,10 +34,9 @@ describe('plugins/elasticsearch', function () {
});
it('sends custom headers if set', function () {
const get = sinon.stub();
get.withArgs('elasticsearch.requestHeadersWhitelist').returns([]);
get.withArgs('elasticsearch.customHeaders').returns({ foo: 'bar' });
const server = { config: () => ({ get }) };
const server = stubServer({
'elasticsearch.customHeaders': { foo: 'bar' }
});
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
@ -35,10 +45,10 @@ describe('plugins/elasticsearch', function () {
});
it('sends configured custom headers even if the same named header exists in request', function () {
const get = sinon.stub();
get.withArgs('elasticsearch.requestHeadersWhitelist').returns(['x-my-custom-header']);
get.withArgs('elasticsearch.customHeaders').returns({'x-my-custom-header': 'asconfigured'});
const server = { config: () => ({ get }) };
const server = stubServer({
'elasticsearch.requestHeadersWhitelist': ['x-my-custom-header'],
'elasticsearch.customHeaders': {'x-my-custom-header': 'asconfigured'}
});
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
@ -47,10 +57,9 @@ describe('plugins/elasticsearch', function () {
});
it('only proxies the whitelisted request headers', function () {
const get = sinon.stub();
get.withArgs('elasticsearch.requestHeadersWhitelist').returns(['x-my-custom-HEADER', 'Authorization']);
get.withArgs('elasticsearch.customHeaders').returns({});
const server = { config: () => ({ get }) };
const server = stubServer({
'elasticsearch.requestHeadersWhitelist': ['x-my-custom-HEADER', 'Authorization'],
});
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
@ -61,10 +70,9 @@ describe('plugins/elasticsearch', function () {
});
it('proxies no headers if whitelist is set to []', function () {
const get = sinon.stub();
get.withArgs('elasticsearch.requestHeadersWhitelist').returns([]);
get.withArgs('elasticsearch.customHeaders').returns({});
const server = { config: () => ({ get }) };
const server = stubServer({
'elasticsearch.requestHeadersWhitelist': [],
});
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
@ -73,10 +81,11 @@ describe('plugins/elasticsearch', function () {
});
it('proxies no headers if whitelist is set to no value', function () {
const get = sinon.stub();
get.withArgs('elasticsearch.requestHeadersWhitelist').returns([ null ]); // This is how Joi returns it
get.withArgs('elasticsearch.customHeaders').returns({});
const server = { config: () => ({ get }) };
const server = stubServer({
// joi converts `elasticsearch.requestHeadersWhitelist: null` into
// an array with a null inside because of the `array().single()` rule.
'elasticsearch.requestHeadersWhitelist': [ null ],
});
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
@ -84,5 +93,47 @@ describe('plugins/elasticsearch', function () {
});
});
it('strips the /elasticsearch prefix from the path', () => {
request.path = '/elasticsearch/es/path';
const server = stubServer();
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
expect(upstreamUri).to.be('http://localhost:9200/es/path');
});
});
it('extends the es.url path', function () {
request.path = '/elasticsearch/index/type';
const server = stubServer({ 'elasticsearch.url': 'https://localhost:9200/base-path' });
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
expect(upstreamUri).to.be('https://localhost:9200/base-path/index/type');
});
});
it('extends the es.url query string', function () {
request.path = '/elasticsearch/*';
request.query = { foo: 'bar' };
const server = stubServer({ 'elasticsearch.url': 'https://localhost:9200/?base=query' });
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
expect(upstreamUri).to.be('https://localhost:9200/*?foo=bar&base=query');
});
});
it('filters the _ querystring param', function () {
request.path = '/elasticsearch/*';
request.query = { _: Date.now() };
const server = stubServer();
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
expect(upstreamUri).to.be('http://localhost:9200/*');
});
});
});
});

View file

@ -1,22 +1,48 @@
import querystring from 'querystring';
import { resolve } from 'url';
import { defaults, omit, trimLeft, trimRight } from 'lodash';
import { parse as parseUrl, format as formatUrl, resolve } from 'url';
import filterHeaders from './filter_headers';
import setHeaders from './set_headers';
export default function mapUri(server, prefix) {
const config = server.config();
function joinPaths(pathA, pathB) {
return trimRight(pathA, '/') + '/' + trimLeft(pathB, '/');
}
return function (request, done) {
const path = request.path.replace('/elasticsearch', '');
let url = config.get('elasticsearch.url');
if (path) {
if (/\/$/.test(url)) url = url.substring(0, url.length - 1);
url += path;
const {
protocol: esUrlProtocol,
slashes: esUrlHasSlashes,
auth: esUrlAuth,
hostname: esUrlHostname,
port: esUrlPort,
pathname: esUrlBasePath,
query: esUrlQuery
} = parseUrl(config.get('elasticsearch.url'), true);
// copy most url components directly from the elasticsearch.url
const mappedUrlComponents = {
protocol: esUrlProtocol,
slashes: esUrlHasSlashes,
auth: esUrlAuth,
hostname: esUrlHostname,
port: esUrlPort
};
// pathname
const reqSubPath = request.path.replace('/elasticsearch', '');
mappedUrlComponents.pathname = joinPaths(esUrlBasePath, reqSubPath);
// querystring
const mappedQuery = defaults(omit(request.query, '_'), esUrlQuery || {});
if (Object.keys(mappedQuery).length) {
mappedUrlComponents.query = mappedQuery;
}
const query = querystring.stringify(request.query);
if (query) url += '?' + query;
const filteredHeaders = filterHeaders(request.headers, config.get('elasticsearch.requestHeadersWhitelist'));
const customHeaders = setHeaders(filteredHeaders, config.get('elasticsearch.customHeaders'));
done(null, url, customHeaders);
const mappedHeaders = setHeaders(filteredHeaders, config.get('elasticsearch.customHeaders'));
const mappedUrl = formatUrl(mappedUrlComponents);
done(null, mappedUrl, mappedHeaders);
};
};