diff --git a/lib/gatekeeper/middleware.js b/lib/gatekeeper/middleware.js index c57b2a3d..af9ab14f 100644 --- a/lib/gatekeeper/middleware.js +++ b/lib/gatekeeper/middleware.js @@ -12,3 +12,4 @@ module.exports.rateLimit = require('./middleware/rate_limit'); module.exports.bufferRequest = bufferedRequest.bufferRequest; module.exports.proxyBufferedRequest = bufferedRequest.proxyBufferedRequest; module.exports.rewriteRequest = require('./middleware/rewrite_request'); +module.exports.rewriteResponse = require('./middleware/rewrite_response'); diff --git a/lib/gatekeeper/middleware/rewrite_response.js b/lib/gatekeeper/middleware/rewrite_response.js new file mode 100644 index 00000000..49b3fddf --- /dev/null +++ b/lib/gatekeeper/middleware/rewrite_response.js @@ -0,0 +1,57 @@ +'use strict'; + +var _ = require('lodash'); + +var RewriteResponse = function() { + this.initialize.apply(this, arguments); +}; + +_.extend(RewriteResponse.prototype, { + initialize: function() { + }, + + handleResponse: function(request, response, next) { + // Wait until response.writeHead() is called to modify the response so we + // can ensure that all the headers have been received, but not yet sent. + var origWriteHead = response.writeHead; + response.writeHead = function() { + this.setDefaultHeaders(request, response); + this.setOverrideHeaders(request, response); + + return origWriteHead.apply(response, arguments); + }.bind(this); + + next(); + }, + + setDefaultHeaders: function(request, response) { + var headers = request.apiUmbrellaGatekeeper.settings.default_response_headers; + if(headers) { + for(var i = 0, len = headers.length; i < len; i++) { + var header = headers[i]; + var existingValue = response.getHeader(header.key); + if(!existingValue) { + response.setHeader(header.key, header.value); + } + } + } + }, + + setOverrideHeaders: function(request, response) { + var headers = request.apiUmbrellaGatekeeper.settings.override_response_headers; + if(headers) { + for(var i = 0, len = headers.length; i < len; i++) { + var header = headers[i]; + response.setHeader(header.key, header.value); + } + } + }, +}); + +module.exports = function rewriteResponse() { + var middleware = new RewriteResponse(); + + return function(request, response, next) { + middleware.handleResponse(request, response, next); + }; +}; diff --git a/lib/gatekeeper/worker.js b/lib/gatekeeper/worker.js index e13404e7..498482cf 100644 --- a/lib/gatekeeper/worker.js +++ b/lib/gatekeeper/worker.js @@ -99,6 +99,7 @@ _.extend(Worker.prototype, { middleware.rateLimit(this), middleware.rewriteRequest(), middleware.proxyBufferedRequest(this.server.proxy), + middleware.rewriteResponse(), ]; this.stack = httpProxy.stack(this.middlewares, this.server.proxy); diff --git a/test/server/api_matcher.js b/test/server/api_matcher.js index 2626b8ad..2970b827 100644 --- a/test/server/api_matcher.js +++ b/test/server/api_matcher.js @@ -311,7 +311,6 @@ describe('ApiUmbrellaGatekeper', function() { describe('mismatched case sensitivity', function() { shared.itBehavesLikeGatekeeperBlocked('/info/SPECIFIC/', 404, 'NOT_FOUND'); }); - }); }); diff --git a/test/server/response_rewriting.js b/test/server/response_rewriting.js new file mode 100644 index 00000000..74eded18 --- /dev/null +++ b/test/server/response_rewriting.js @@ -0,0 +1,157 @@ +'use strict'; + +require('../test_helper'); + +var request = require('request'); + +describe('response rewriting', function() { + describe('setting default response headers', function() { + shared.runServer({ + apis: [ + { + frontend_host: 'localhost', + backend_host: 'example.com', + url_matches: [ + { + frontend_prefix: '/', + backend_prefix: '/', + } + ], + settings: { + default_response_headers: [ + { key: 'X-Add1', value: 'test1' }, + { key: 'X-Add2', value: 'test2' }, + { key: 'X-Existing1', value: 'test3' }, + { key: 'X-EXISTING2', value: 'test4' }, + { key: 'x-existing3', value: 'test5' }, + ], + }, + sub_settings: [ + { + http_method: 'any', + regex: '^/headers/sub', + settings: { + default_response_headers: [ + { key: 'X-Add2', value: 'overridden' }, + ], + }, + }, + ], + }, + ], + }); + + describe('default', function() { + it('sets new header values', function(done) { + request.get('http://localhost:9333/headers/?api_key=' + this.apiKey, function(error, response) { + should.not.exist(error); + response.statusCode.should.eql(200); + response.headers['x-add1'].should.eql('test1'); + response.headers['x-add2'].should.eql('test2'); + done(); + }); + }); + + it('leaves existing headers (case insensitive)', function(done) { + request.get('http://localhost:9333/headers/?api_key=' + this.apiKey, function(error, response) { + should.not.exist(error); + response.statusCode.should.eql(200); + response.headers['x-existing1'].should.eql('existing1'); + response.headers['x-existing2'].should.eql('existing2'); + response.headers['x-existing3'].should.eql('existing3'); + done(); + }); + }); + }); + + describe('sub-url match', function() { + it('overrides the default header settings', function(done) { + request.get('http://localhost:9333/headers/sub/?api_key=' + this.apiKey, function(error, response) { + should.not.exist(error); + response.statusCode.should.eql(200); + should.not.exist(response.headers['x-add1']); + response.headers['x-add2'].should.eql('overridden'); + response.headers['x-existing1'].should.eql('existing1'); + response.headers['x-existing2'].should.eql('existing2'); + response.headers['x-existing3'].should.eql('existing3'); + done(); + }); + }); + }); + }); + + describe('setting override response headers', function() { + shared.runServer({ + apis: [ + { + frontend_host: 'localhost', + backend_host: 'example.com', + url_matches: [ + { + frontend_prefix: '/', + backend_prefix: '/', + } + ], + settings: { + override_response_headers: [ + { key: 'X-Add1', value: 'test1' }, + { key: 'X-Add2', value: 'test2' }, + { key: 'X-Existing1', value: 'test3' }, + { key: 'X-EXISTING2', value: 'test4' }, + { key: 'x-existing3', value: 'test5' }, + ], + }, + sub_settings: [ + { + http_method: 'any', + regex: '^/headers/sub', + settings: { + override_response_headers: [ + { key: 'X-Existing3', value: 'overridden' }, + ], + }, + }, + ], + }, + ], + }); + + describe('default', function() { + it('sets new header values', function(done) { + request.get('http://localhost:9333/headers/?api_key=' + this.apiKey, function(error, response) { + should.not.exist(error); + response.statusCode.should.eql(200); + response.headers['x-add1'].should.eql('test1'); + response.headers['x-add2'].should.eql('test2'); + done(); + }); + }); + + it('overrides existing headers (case insensitive)', function(done) { + request.get('http://localhost:9333/headers/?api_key=' + this.apiKey, function(error, response) { + should.not.exist(error); + response.statusCode.should.eql(200); + response.headers['x-existing1'].should.eql('test3'); + response.headers['x-existing2'].should.eql('test4'); + response.headers['x-existing3'].should.eql('test5'); + done(); + }); + }); + }); + + describe('sub-url match', function() { + it('overrides the default header settings', function(done) { + request.get('http://localhost:9333/headers/sub/?api_key=' + this.apiKey, function(error, response) { + should.not.exist(error); + response.statusCode.should.eql(200); + should.not.exist(response.headers['x-add1']); + should.not.exist(response.headers['x-add2']); + response.headers['x-existing1'].should.eql('existing1'); + response.headers['x-existing2'].should.eql('existing2'); + response.headers['x-existing3'].should.eql('overridden'); + done(); + }); + }); + }); + }); +}); diff --git a/test/support/example_backend_app.js b/test/support/example_backend_app.js index 8490a302..9969897d 100644 --- a/test/support/example_backend_app.js +++ b/test/support/example_backend_app.js @@ -82,6 +82,13 @@ app.get('/chunked', function(req, res) { }, 100); }); +app.get('/headers/*', function(req, res) { + res.set('X-Existing1', 'existing1'); + res.set('x-existing2', 'existing2'); + res.set('X-EXISTING3', 'existing3'); + res.send('Hello World'); +}); + app.all('/info/*', function(req, res) { var rawUrl = req.protocol + '://' + req.hostname + req.url; res.json({