From 7f2f3ac35c663b1332594044cc1095bdfb458df4 Mon Sep 17 00:00:00 2001 From: Matt Hauck Date: Thu, 5 Feb 2015 12:17:26 -0800 Subject: [PATCH 1/3] Add support for auto host rewriting and protocol rewriting auto host rewriting allows rewriting to work as expected in most cases without extra cumbersome configuration protocol rewriting allows node-http-proxy to be able to listen over HTTPS and properly reverse-proxy to sites running over HTTP (to avoid doing SSL twice) --- lib/http-proxy.js | 2 + lib/http-proxy/passes/web-outgoing.js | 12 +- ...lib-http-proxy-passes-web-outgoing-test.js | 125 +++++++++++++----- 3 files changed, 106 insertions(+), 33 deletions(-) diff --git a/lib/http-proxy.js b/lib/http-proxy.js index b1ad646f4..78522b63b 100644 --- a/lib/http-proxy.js +++ b/lib/http-proxy.js @@ -42,6 +42,8 @@ module.exports.createProxyServer = * localAddress : * changeOrigin: * hostRewrite: rewrites the location hostname on (301/302/307/308) redirects, Default: null. + * autoRewrite: rewrites the location host/port on (301/302/307/308) redirects based on requested host/port. Default: false. + * protocolRewrite: rewrites the location protocol on (301/302/307/308) redirects to 'http' or 'https'. Default: null. * } * * NOTE: `options.ws` and `options.ssl` are optional. diff --git a/lib/http-proxy/passes/web-outgoing.js b/lib/http-proxy/passes/web-outgoing.js index 67a1a6d98..99f886477 100644 --- a/lib/http-proxy/passes/web-outgoing.js +++ b/lib/http-proxy/passes/web-outgoing.js @@ -47,11 +47,19 @@ var redirectRegex = /^30(1|2|7|8)$/; }, function setRedirectHostRewrite(req, res, proxyRes, options) { - if (options.hostRewrite + if ((options.hostRewrite || options.autoRewrite || options.protocolRewrite) && proxyRes.headers['location'] && redirectRegex.test(proxyRes.statusCode)) { var u = url.parse(proxyRes.headers['location']); - u.host = options.hostRewrite; + if (options.hostRewrite) { + u.host = options.hostRewrite; + } else if (options.autoRewrite) { + u.host = req.headers['host']; + } + if (options.protocolRewrite) { + u.protocol = options.protocolRewrite; + } + proxyRes.headers['location'] = u.format(); } }, diff --git a/test/lib-http-proxy-passes-web-outgoing-test.js b/test/lib-http-proxy-passes-web-outgoing-test.js index ee1077e03..342f024ea 100644 --- a/test/lib-http-proxy-passes-web-outgoing-test.js +++ b/test/lib-http-proxy-passes-web-outgoing-test.js @@ -3,55 +3,118 @@ var httpProxy = require('../lib/http-proxy/passes/web-outgoing'), describe('lib/http-proxy/passes/web-outgoing.js', function () { describe('#setRedirectHostRewrite', function () { - context('rewrites location host to option', function() { - beforeEach(function() { - this.proxyRes = { - statusCode: 301, - headers: { - location: "http://f.com/" - } - }; + beforeEach(function() { + this.req = { + headers: { + host: "x2.com" + } + }; + this.proxyRes = { + statusCode: 301, + headers: { + location: "http://f.com/" + } + }; + }); + context('rewrites location host with hostRewrite', function() { + beforeEach(function() { this.options = { hostRewrite: "x.com" }; }); + [301, 302, 307, 308].forEach(function(code) { + it('on ' + code, function() { + this.proxyRes.statusCode = code; + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); + expect(this.proxyRes.headers.location).to.eql('http://'+this.options.hostRewrite+'/'); + }); + }); - it('on 301', function() { - this.proxyRes.statusCode = 301; - httpProxy.setRedirectHostRewrite({}, {}, this.proxyRes, this.options); - expect(this.proxyRes.headers.location).to.eql('http://'+this.options.hostRewrite+'/'); + it('not on 200', function() { + this.proxyRes.statusCode = 200; + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); + expect(this.proxyRes.headers.location).to.eql('http://f.com/'); }); - it('on 302', function() { - this.proxyRes.statusCode = 302; - httpProxy.setRedirectHostRewrite({}, {}, this.proxyRes, this.options); - expect(this.proxyRes.headers.location).to.eql('http://'+this.options.hostRewrite+'/'); + it('not when hostRewrite is unset', function() { + delete this.options.hostRewrite; + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); + expect(this.proxyRes.headers.location).to.eql('http://f.com/'); }); - it('on 307', function() { - this.proxyRes.statusCode = 307; - httpProxy.setRedirectHostRewrite({}, {}, this.proxyRes, this.options); + it('takes precedence over autoRewrite', function() { + this.options.autoRewrite = true; + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); expect(this.proxyRes.headers.location).to.eql('http://'+this.options.hostRewrite+'/'); }); + }); - it('on 308', function() { - this.proxyRes.statusCode = 308; - httpProxy.setRedirectHostRewrite({}, {}, this.proxyRes, this.options); - expect(this.proxyRes.headers.location).to.eql('http://'+this.options.hostRewrite+'/'); + context('rewrites location host with autoRewrite', function() { + beforeEach(function() { + this.options = { + autoRewrite: true, + }; + }); + [301, 302, 307, 308].forEach(function(code) { + it('on ' + code, function() { + this.proxyRes.statusCode = code; + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); + expect(this.proxyRes.headers.location).to.eql('http://'+this.req.headers.host+'/'); + }); }); it('not on 200', function() { this.proxyRes.statusCode = 200; - httpProxy.setRedirectHostRewrite({}, {}, this.proxyRes, this.options); + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); expect(this.proxyRes.headers.location).to.eql('http://f.com/'); }); - it('not when hostRewrite is unset', function() { - httpProxy.setRedirectHostRewrite({}, {}, this.proxyRes, {}); + it('not when autoRewrite is unset', function() { + delete this.options.autoRewrite; + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); expect(this.proxyRes.headers.location).to.eql('http://f.com/'); }); }); + + context('rewrites location protocol with protocolRewrite', function() { + beforeEach(function() { + this.options = { + protocolRewrite: 'https', + }; + }); + [301, 302, 307, 308].forEach(function(code) { + it('on ' + code, function() { + this.proxyRes.statusCode = code; + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); + expect(this.proxyRes.headers.location).to.eql('https://f.com/'); + }); + }); + + it('not on 200', function() { + this.proxyRes.statusCode = 200; + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); + expect(this.proxyRes.headers.location).to.eql('http://f.com/'); + }); + + it('not when protocolRewrite is unset', function() { + delete this.options.protocolRewrite; + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); + expect(this.proxyRes.headers.location).to.eql('http://f.com/'); + }); + + it('works together with hostRewrite', function() { + this.options.hostRewrite = 'x.com' + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); + expect(this.proxyRes.headers.location).to.eql('https://x.com/'); + }); + + it('works together with autoRewrite', function() { + this.options.autoRewrite = true + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); + expect(this.proxyRes.headers.location).to.eql('https://x2.com/'); + }); + }); }); describe('#setConnection', function () { @@ -64,7 +127,7 @@ describe('lib/http-proxy/passes/web-outgoing.js', function () { } }, {}, proxyRes); - expect(proxyRes.headers.connection).to.eql('close'); + expect(proxyRes.headers.connection).to.eql('close'); }); it('set the right connection with 1.0 - req.connection', function() { @@ -76,7 +139,7 @@ describe('lib/http-proxy/passes/web-outgoing.js', function () { } }, {}, proxyRes); - expect(proxyRes.headers.connection).to.eql('hey'); + expect(proxyRes.headers.connection).to.eql('hey'); }); it('set the right connection - req.connection', function() { @@ -88,7 +151,7 @@ describe('lib/http-proxy/passes/web-outgoing.js', function () { } }, {}, proxyRes); - expect(proxyRes.headers.connection).to.eql('hola'); + expect(proxyRes.headers.connection).to.eql('hola'); }); it('set the right connection - `keep-alive`', function() { @@ -100,7 +163,7 @@ describe('lib/http-proxy/passes/web-outgoing.js', function () { } }, {}, proxyRes); - expect(proxyRes.headers.connection).to.eql('keep-alive'); + expect(proxyRes.headers.connection).to.eql('keep-alive'); }); }); @@ -153,4 +216,4 @@ describe('lib/http-proxy/passes/web-outgoing.js', function () { }); }); - + From 14415a50741d1f258da884686455d87d68eb8121 Mon Sep 17 00:00:00 2001 From: Matt Hauck Date: Mon, 9 Mar 2015 11:49:28 -0700 Subject: [PATCH 2/3] refactor some tests for greater readability --- ...lib-http-proxy-passes-web-outgoing-test.js | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/test/lib-http-proxy-passes-web-outgoing-test.js b/test/lib-http-proxy-passes-web-outgoing-test.js index 342f024ea..7aef725cc 100644 --- a/test/lib-http-proxy-passes-web-outgoing-test.js +++ b/test/lib-http-proxy-passes-web-outgoing-test.js @@ -6,113 +6,110 @@ describe('lib/http-proxy/passes/web-outgoing.js', function () { beforeEach(function() { this.req = { headers: { - host: "x2.com" + host: "ext-auto.com" } }; this.proxyRes = { statusCode: 301, headers: { - location: "http://f.com/" + location: "http://backend.com/" } }; + this.options = { + target: "http://backend.com" + }; }); context('rewrites location host with hostRewrite', function() { beforeEach(function() { - this.options = { - hostRewrite: "x.com" - }; + this.options.hostRewrite = "ext-manual.com"; }); [301, 302, 307, 308].forEach(function(code) { it('on ' + code, function() { this.proxyRes.statusCode = code; httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); - expect(this.proxyRes.headers.location).to.eql('http://'+this.options.hostRewrite+'/'); + expect(this.proxyRes.headers.location).to.eql('http://ext-manual.com/'); }); }); it('not on 200', function() { this.proxyRes.statusCode = 200; httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); - expect(this.proxyRes.headers.location).to.eql('http://f.com/'); + expect(this.proxyRes.headers.location).to.eql('http://backend.com/'); }); it('not when hostRewrite is unset', function() { delete this.options.hostRewrite; httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); - expect(this.proxyRes.headers.location).to.eql('http://f.com/'); + expect(this.proxyRes.headers.location).to.eql('http://backend.com/'); }); it('takes precedence over autoRewrite', function() { this.options.autoRewrite = true; httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); - expect(this.proxyRes.headers.location).to.eql('http://'+this.options.hostRewrite+'/'); + expect(this.proxyRes.headers.location).to.eql('http://ext-manual.com/'); }); }); context('rewrites location host with autoRewrite', function() { beforeEach(function() { - this.options = { - autoRewrite: true, - }; + this.options.autoRewrite = true; }); [301, 302, 307, 308].forEach(function(code) { it('on ' + code, function() { this.proxyRes.statusCode = code; httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); - expect(this.proxyRes.headers.location).to.eql('http://'+this.req.headers.host+'/'); + expect(this.proxyRes.headers.location).to.eql('http://ext-auto.com/'); }); }); it('not on 200', function() { this.proxyRes.statusCode = 200; httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); - expect(this.proxyRes.headers.location).to.eql('http://f.com/'); + expect(this.proxyRes.headers.location).to.eql('http://backend.com/'); }); it('not when autoRewrite is unset', function() { delete this.options.autoRewrite; httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); - expect(this.proxyRes.headers.location).to.eql('http://f.com/'); + expect(this.proxyRes.headers.location).to.eql('http://backend.com/'); }); }); context('rewrites location protocol with protocolRewrite', function() { beforeEach(function() { - this.options = { - protocolRewrite: 'https', - }; + this.options.protocolRewrite = 'https'; }); [301, 302, 307, 308].forEach(function(code) { it('on ' + code, function() { this.proxyRes.statusCode = code; httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); - expect(this.proxyRes.headers.location).to.eql('https://f.com/'); + expect(this.proxyRes.headers.location).to.eql('https://backend.com/'); }); }); it('not on 200', function() { this.proxyRes.statusCode = 200; httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); - expect(this.proxyRes.headers.location).to.eql('http://f.com/'); + expect(this.proxyRes.headers.location).to.eql('http://backend.com/'); }); it('not when protocolRewrite is unset', function() { delete this.options.protocolRewrite; httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); - expect(this.proxyRes.headers.location).to.eql('http://f.com/'); + expect(this.proxyRes.headers.location).to.eql('http://backend.com/'); }); it('works together with hostRewrite', function() { - this.options.hostRewrite = 'x.com' + this.options.hostRewrite = 'ext-manual.com' httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); - expect(this.proxyRes.headers.location).to.eql('https://x.com/'); + expect(this.proxyRes.headers.location).to.eql('https://ext-manual.com/'); }); it('works together with autoRewrite', function() { this.options.autoRewrite = true httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); - expect(this.proxyRes.headers.location).to.eql('https://x2.com/'); + expect(this.proxyRes.headers.location).to.eql('https://ext-auto.com/'); }); }); }); From 26029ba7ac948b5dc0befb2091cc9a5862d0641c Mon Sep 17 00:00:00 2001 From: Matt Hauck Date: Mon, 9 Mar 2015 13:17:52 -0700 Subject: [PATCH 3/3] only rewrite redirect urls when it matches target if functioning as a reverse proxy for host1.foo.com, with a backend target of backend.foo.com:8080, the node proxy should only rewrite the redirect if it is a redirect to somewhere on backend.foo.com:8080 --- lib/http-proxy/passes/web-outgoing.js | 7 +++++ ...lib-http-proxy-passes-web-outgoing-test.js | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/http-proxy/passes/web-outgoing.js b/lib/http-proxy/passes/web-outgoing.js index 99f886477..977f1f747 100644 --- a/lib/http-proxy/passes/web-outgoing.js +++ b/lib/http-proxy/passes/web-outgoing.js @@ -50,7 +50,14 @@ var redirectRegex = /^30(1|2|7|8)$/; if ((options.hostRewrite || options.autoRewrite || options.protocolRewrite) && proxyRes.headers['location'] && redirectRegex.test(proxyRes.statusCode)) { + var target = url.parse(options.target); var u = url.parse(proxyRes.headers['location']); + + // make sure the redirected host matches the target host before rewriting + if (target.host != u.host) { + return; + } + if (options.hostRewrite) { u.host = options.hostRewrite; } else if (options.autoRewrite) { diff --git a/test/lib-http-proxy-passes-web-outgoing-test.js b/test/lib-http-proxy-passes-web-outgoing-test.js index 7aef725cc..5b91c0bb2 100644 --- a/test/lib-http-proxy-passes-web-outgoing-test.js +++ b/test/lib-http-proxy-passes-web-outgoing-test.js @@ -49,6 +49,20 @@ describe('lib/http-proxy/passes/web-outgoing.js', function () { httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); expect(this.proxyRes.headers.location).to.eql('http://ext-manual.com/'); }); + + it('not when the redirected location does not match target host', function() { + this.proxyRes.statusCode = 302; + this.proxyRes.headers.location = "http://some-other/"; + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); + expect(this.proxyRes.headers.location).to.eql('http://some-other/'); + }); + + it('not when the redirected location does not match target port', function() { + this.proxyRes.statusCode = 302; + this.proxyRes.headers.location = "http://backend.com:8080/"; + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); + expect(this.proxyRes.headers.location).to.eql('http://backend.com:8080/'); + }); }); context('rewrites location host with autoRewrite', function() { @@ -74,6 +88,20 @@ describe('lib/http-proxy/passes/web-outgoing.js', function () { httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); expect(this.proxyRes.headers.location).to.eql('http://backend.com/'); }); + + it('not when the redirected location does not match target host', function() { + this.proxyRes.statusCode = 302; + this.proxyRes.headers.location = "http://some-other/"; + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); + expect(this.proxyRes.headers.location).to.eql('http://some-other/'); + }); + + it('not when the redirected location does not match target port', function() { + this.proxyRes.statusCode = 302; + this.proxyRes.headers.location = "http://backend.com:8080/"; + httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); + expect(this.proxyRes.headers.location).to.eql('http://backend.com:8080/'); + }); }); context('rewrites location protocol with protocolRewrite', function() {