Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Fix for issue #318 Reverse Proxy port rewriting for HTTP3xx Location headers #376

Closed
wants to merge 2 commits into from

8 participants

@jayv

This is a fix with tests that should solve issue #318 Reverse Proxy port rewriting for HTTP3xx Location headers.

@sequoiar

in case redirect to the different host instead of origin target host, the proxy target entry need to overwrite as well.

@jayv

@sequoiar

I've spent a couple hours writing this patch with full test coverage, it's been used by quite a few people and it should merge in cleanly. Yet still no word (good or bad) from nodejitsu in almost 6 months, if you want this fix I guess you're on your own.

@vojtajina

@indexzero @dscape Any updates on this ?

@stickel

:+1: @indexzero @nodejitsu Any word on getting this merged in?

@antoine-richard

Would be very useful if merged.
Does anybody have some update on this?

@indexzero
Owner

See my comments on #519. This can be accomplished without a bunch of new code.

@indexzero indexzero closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Nov 5, 2012
  1. @jayv
Commits on Feb 24, 2013
  1. @jayv
This page is out of date. Refresh to see the latest.
View
60 lib/node-http-proxy/http-proxy.js
@@ -1,33 +1,34 @@
/*
- node-http-proxy.js: http proxy for node.js
+ node-http-proxy.js: http proxy for node.js
- Copyright (c) 2010 Charlie Robbins, Mikeal Rogers, Marak Squires, Fedor Indutny
+ Copyright (c) 2010 Charlie Robbins, Mikeal Rogers, Marak Squires, Fedor Indutny
- Permission is hereby granted, free of charge, to any person obtaining
- a copy of this software and associated documentation files (the
- "Software"), to deal in the Software without restriction, including
- without limitation the rights to use, copy, modify, merge, publish,
- distribute, sublicense, and/or sell copies of the Software, and to
- permit persons to whom the Software is furnished to do so, subject to
- the following conditions:
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
- The above copyright notice and this permission notice shall be
- included in all copies or substantial portions of the Software.
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-*/
+ */
var events = require('events'),
- http = require('http'),
- util = require('util'),
- httpProxy = require('../node-http-proxy');
+ http = require('http'),
+ util = require('util'),
+ httpProxy = require('../node-http-proxy'),
+ ReverseProxyHelper = require('./reverse-proxy-helper.js').ReverseProxyHelper;
//
// ### function HttpProxy (options)
@@ -66,6 +67,8 @@ var HttpProxy = exports.HttpProxy = function (options) {
this.forward = options.forward;
this.target = options.target;
+ this.reverseProxyHelper = new ReverseProxyHelper(self.target);
+
//
// Setup the necessary instances instance variables for
// the `target` and `forward` `host:port` combinations
@@ -104,6 +107,7 @@ var HttpProxy = exports.HttpProxy = function (options) {
this.source = options.source || { host: 'localhost', port: 8000 };
this.source.https = this.source.https || options.https;
this.changeOrigin = options.changeOrigin || false;
+
};
// Inherit from events.EventEmitter
@@ -225,10 +229,11 @@ HttpProxy.prototype.proxyRequest = function (req, res, buffer) {
// origin of the host header to the target URL! Please
// don't revert this without documenting it!
//
+ var originalHost = req.headers.host;
if (this.changeOrigin) {
outgoing.headers.host = this.target.host + ':' + this.target.port;
}
-
+
//
// Open new HTTP request to internal resource with will act
// as a reverse proxy pass
@@ -247,14 +252,7 @@ HttpProxy.prototype.proxyRequest = function (req, res, buffer) {
delete response.headers['transfer-encoding'];
}
- if ((response.statusCode === 301) || (response.statusCode === 302)) {
- if (self.source.https && !self.target.https) {
- response.headers.location = response.headers.location.replace(/^http\:/, 'https:');
- }
- if (self.target.https && !self.source.https) {
- response.headers.location = response.headers.location.replace(/^https\:/, 'http:');
- }
- }
+ self.reverseProxyHelper.rewriteLocationHeader(req, response, originalHost);
// Set the headers of the client response
res.writeHead(response.statusCode, response.headers);
View
122 lib/node-http-proxy/reverse-proxy-helper.js
@@ -0,0 +1,122 @@
+/*
+ reverse-proxy-helper.js: http reverse proxy helper methods.
+
+ Copyright (c) 2012 Jo Voordeckers - @jovoordeckers - jo.voordeckers@gmail.com
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ */
+
+//
+// #### function ReverseProxyHelper(target)
+// #### @target {Object} Proxy options, target (proxied) server
+// Helper functions for reverse proxy mode.
+var ReverseProxyHelper = exports.ReverseProxyHelper = function(target) {
+ var self = this;
+ self.target = target;
+}
+
+//
+// #### function httpRedirect(repsonse)
+// #### @response {ServerResponse} Response from the target server
+// Check if the response is a server-side HTTP 30x Redirect.
+ReverseProxyHelper.prototype.isHttpRedirect = function(response) {
+ return (response.statusCode === 301 || response.statusCode === 302) && !!response.headers && !!response.headers.location;
+}
+
+//
+// #### function decomposeUrl(url)
+// #### @url {String} absolute URL String
+// Return an absolute URL in decomposed form { proto, host, port, path }.
+ReverseProxyHelper.prototype.decomposeUrl = function (url) {
+
+ if (url) {
+
+ var urlMatch = url.match(/(https?)\:\/\/([a-zA-Z0-9\-\.]+)(?:\:(\d{1,5}))?((?:\/|\?).+)?/);
+
+ if (urlMatch) {
+
+ var decomp = {
+ proto: urlMatch[1],
+ host: urlMatch[2],
+ port: urlMatch[3] ? Number(urlMatch[3]) : (urlMatch[1] === "http" ? 80 : 443),
+ path: urlMatch[4]
+ };
+
+ return decomp;
+
+ }
+ }
+
+ return null;
+}
+
+//
+// #### function rewriteLocationHeader(request, response, originalHost)
+// #### @request {ServerRequest} Incoming HTTP Request intercepted by the proxy
+// #### @response {ServerResponse} Outgoing HTTP Request to write proxied data to
+// #### @originalHost {String} Original Host header of the incoming request, before manipulation
+// If needed rewrite the Location header to be consistent with the source and target configuration.
+// This will only rewrite if a Host header is present in the original request and
+// the X-Forwarded-Proto in the proxy request header.
+ReverseProxyHelper.prototype.rewriteLocationHeader = function (request, response, originalHost) {
+
+ var self = this,
+ decompConn;
+
+
+ function isRedirectToTarget(decomp) { // Check if the redirect URL assumes a redirect to the target server
+
+ var sourceProto = request.headers["x-forwarded-proto"];
+
+ decompConn = self.decomposeUrl(sourceProto+"://"+originalHost);
+
+ if (!decompConn) return false;
+
+ var targetProto = (self.target.https ? "https" : "http")
+ var sameProto = targetProto == decomp.proto;
+ var samePort = Number(self.target.port) === decomp.port;
+ var isLocalHost = "127.0.0.1" === decomp.host || "localhost" === decomp.host;
+
+ return sameProto && samePort && ( decompConn.host === decomp.host || self.target.host == decomp.host || isLocalHost);
+ }
+
+ if (self.isHttpRedirect(response)) {
+
+ var decomp = this.decomposeUrl(response.headers.location);
+
+ if (decomp && isRedirectToTarget(decomp)) {
+
+ var defaultPort = (decompConn.port === 80 || decompConn.port === 443);
+
+ var proto = decompConn.proto + "://",
+ host = decompConn.host,
+ port = defaultPort ? "" : ":" + decompConn.port,
+ path = decomp.path;
+
+ response.headers['x-reverse-proxy-location-rewritten-from'] = response.headers.location;
+
+ response.headers.location = proto + host + port + path;
+
+ response.headers['x-reverse-proxy-location-rewritten-to'] = response.headers.location;
+
+ }
+ }
+}
View
339 test/http/reverse-proxy-helper-test.js
@@ -0,0 +1,339 @@
+/*
+ reverse-proxy-helper-test.js: test for http reverse proxy helper methods.
+
+ Copyright (c) 2012 Jo Voordeckers - @jovoordeckers - jo.voordeckers@gmail.com
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ */
+
+var assert = require('assert'),
+ vows = require('vows'),
+ ReverseProxyHelper = require('../../lib/node-http-proxy/reverse-proxy-helper').ReverseProxyHelper,
+ req = { headers: {} },
+ reqHttps = { headers: {} };
+
+req.headers["x-forwarded-proto"] = "http";
+reqHttps.headers["x-forwarded-proto"] = "https";
+
+vows.describe("Reverse Proxy Helper").addBatch({
+
+ "when decomposing URL ":{
+
+ "http://server-host.com/myRequest":{
+ topic:function () {
+ return new ReverseProxyHelper().decomposeUrl("http://server-host.com/myRequest");
+ },
+ "the proto is http":function (deco) {
+ assert.equal(deco.proto, "http");
+ },
+ "the port is 80":function (topic) {
+ assert.equal(topic.port, 80);
+ },
+ "the host is server-host.com":function (topic) {
+ assert.equal(topic.host, "server-host.com");
+ },
+ "the path is /myRequest":function (topic) {
+ assert.equal(topic.path, "/myRequest");
+ }
+ },
+
+ "https://server-host.com/myRequest":{
+ topic:function () {
+ return new ReverseProxyHelper().decomposeUrl("https://server-host.com/myRequest");
+ },
+ "the proto is https":function (deco) {
+ assert.equal(deco.proto, "https");
+ },
+ "the port is 443":function (topic) {
+ assert.equal(topic.port, 443);
+ },
+ "the host is server-host.com":function (topic) {
+ assert.equal(topic.host, "server-host.com");
+ },
+ "the path is /myRequest":function (topic) {
+ assert.equal(topic.path, "/myRequest");
+ }
+ },
+
+ "http://server-host.com:8181/myRequest":{
+
+ topic:function () {
+ return new ReverseProxyHelper().decomposeUrl("http://server-host.com:8181/myRequest");
+ },
+ "the proto is http":function (deco) {
+ assert.equal(deco.proto, "http");
+ },
+ "the port is 8181":function (topic) {
+ assert.equal(topic.port, 8181);
+ },
+ "the host is server-host.com":function (topic) {
+ assert.equal(topic.host, "server-host.com");
+ },
+ "the path is /myRequest":function (topic) {
+ assert.equal(topic.path, "/myRequest");
+ }
+ },
+
+ "https://server-host.com:8181/myRequest":{
+
+ topic:function () {
+ return new ReverseProxyHelper().decomposeUrl("https://server-host.com:8181/myRequest");
+ },
+ "the proto is https":function (deco) {
+ assert.equal(deco.proto, "https");
+ },
+ "the port is 8181":function (topic) {
+ assert.equal(topic.port, 8181);
+ },
+ "the host is server-host.com":function (topic) {
+ assert.equal(topic.host, "server-host.com");
+ },
+ "the path is /myRequest":function (topic) {
+ assert.equal(topic.path, "/myRequest");
+ }
+ },
+
+ "http://server-host.com:8080":{
+
+ topic:function () {
+ return new ReverseProxyHelper().decomposeUrl("http://server-host.com:8080");
+ },
+ "the proto is http":function (deco) {
+ assert.equal(deco.proto, "http");
+ },
+ "the port is 8080":function (topic) {
+ assert.equal(topic.port, 8080);
+ },
+ "the host is server-host.com":function (topic) {
+ assert.equal(topic.host, "server-host.com");
+ },
+ "the path is undefined":function (topic) {
+ assert.equal(topic.path, undefined);
+ }
+ },
+
+ "http://server-host.com:8080/?user=foo":{
+
+ topic:function () {
+ return new ReverseProxyHelper().decomposeUrl("http://server-host.com:8080?user=foo");
+ },
+ "the proto is http":function (deco) {
+ assert.equal(deco.proto, "http");
+ },
+ "the port is 8080":function (topic) {
+ assert.equal(topic.port, 8080);
+ },
+ "the host is server-host.com":function (topic) {
+ assert.equal(topic.host, "server-host.com");
+ },
+ "the path is ?user=foo":function (topic) {
+ assert.equal(topic.path, "?user=foo");
+ }
+ },
+
+ "http://server-host.com:8080/myRequest?user=foo":{
+
+ topic:function () {
+ return new ReverseProxyHelper().decomposeUrl("http://server-host.com:8080/myRequest?user=foo");
+ },
+ "the proto is http":function (deco) {
+ assert.equal(deco.proto, "http");
+ },
+ "the port is 8080":function (topic) {
+ assert.equal(topic.port, 8080);
+ },
+ "the host is server-host.com":function (topic) {
+ assert.equal(topic.host, "server-host.com");
+ },
+ "the path is /myRequest?user=foo":function (topic) {
+ assert.equal(topic.path, "/myRequest?user=foo");
+ }
+ },
+
+ "http://127.0.0.1:8080/myRequest?user=foo":{
+
+ topic:function () {
+ return new ReverseProxyHelper().decomposeUrl("http://127.0.0.1:8080/myRequest?user=foo");
+ },
+ "the proto is http":function (deco) {
+ assert.equal(deco.proto, "http");
+ },
+ "the port is 8080":function (topic) {
+ assert.equal(topic.port, 8080);
+ },
+ "the host is 127.0.0.1":function (topic) {
+ assert.equal(topic.host, "127.0.0.1");
+ },
+ "the path is /myRequest?user=foo":function (topic) {
+ assert.equal(topic.path, "/myRequest?user=foo");
+ }
+ },
+
+ "https://127.0.0.1:8080/myRequest?user=foo":{
+
+ topic:function () {
+ return new ReverseProxyHelper().decomposeUrl("https://127.0.0.1:8080/myRequest?user=foo");
+ },
+ "the proto is http":function (deco) {
+ assert.equal(deco.proto, "https");
+ },
+ "the port is 8080":function (topic) {
+ assert.equal(topic.port, 8080);
+ },
+ "the host is 127.0.0.1":function (topic) {
+ assert.equal(topic.host, "127.0.0.1");
+ },
+ "the path is /myRequest?user=foo":function (topic) {
+ assert.equal(topic.path, "/myRequest?user=foo");
+ }
+ },
+
+ "/someApp/foo?bar=baz":{
+
+ topic:function () {
+ return new ReverseProxyHelper().decomposeUrl("/someApp/foo?bar=baz");
+ },
+ "decomposes to undefined, not an absolute URL":function (deco) {
+ assert.equal(deco, undefined);
+ }
+ }
+
+ },
+
+ "when response with statusCode":{
+ "200":{
+ topic:function () {
+ return new ReverseProxyHelper().isHttpRedirect({ statusCode:200 });
+ },
+ "no redirect":function (topic) {
+ assert.equal(topic, false);
+ }
+ },
+ "301 and no headers":{
+ topic:function () {
+ return new ReverseProxyHelper().isHttpRedirect({ statusCode:301 });
+ },
+ "no redirect - headers missing":function (topic) {
+ assert.equal(topic, false);
+ }
+ },
+ "302 and no headers":{
+ topic:function () {
+ return new ReverseProxyHelper().isHttpRedirect({ statusCode:302 });
+ },
+ "no redirect - headers missing":function (topic) {
+ assert.equal(topic, false);
+ }
+ },
+ "301 and location headers":{
+ topic:function () {
+ return new ReverseProxyHelper().isHttpRedirect({ statusCode:301, headers:{ location:'http://some/url' }});
+ },
+ "redirect - with location header":function (topic) {
+ assert.equal(topic, true);
+ }
+ },
+ "302 and location headers":{
+ topic:function () {
+ return new ReverseProxyHelper().isHttpRedirect({ statusCode:302, headers:{ location:'http://some/url' }});
+ },
+ "redirect - with location header":function (topic) {
+ assert.equal(topic, true);
+ }
+ }
+ },
+
+ "when location header":{
+ "/someApp/foo?bar=baz":{
+ topic:function () {
+ var origHost = "source.com";
+ var target = { host:"target.com", port:8080 };
+ var resp = { headers : { location: "/someApp/foo?bar=baz" }};
+ new ReverseProxyHelper(target).rewriteLocationHeader(req, resp, origHost)
+ return resp;
+ },
+ "don't rewrite (relative URL)":function (topic) {
+ assert.equal(topic.headers.location, "/someApp/foo?bar=baz");
+ }
+ },
+ "http://target.com:8080/someApp/foo?bar=baz":{
+ topic:function () {
+ var origHost = "source.com";
+ var target = { host:"target.com", port:8080 };
+ var resp = { statusCode: 301, headers : { location: "http://target.com:8080/someApp/foo?bar=baz" }};
+ new ReverseProxyHelper(target).rewriteLocationHeader(req, resp, origHost)
+ return resp;
+ },
+ "rewrite to http://source.com/someApp/foo?bar=baz":function (topic) {
+ assert.equal(topic.headers.location, "http://source.com/someApp/foo?bar=baz");
+ }
+ },
+ "http://source.com:8080/someApp/foo?bar=baz and https source":{
+ topic:function () {
+ var origHost = "source.com";
+ var target = { host:"target.com", port:8080, https: false };
+ var resp = { statusCode: 301, headers : { location: "http://source.com:8080/someApp/foo?bar=baz" }};
+ new ReverseProxyHelper(target).rewriteLocationHeader(reqHttps, resp, origHost)
+ return resp;
+ },
+ "rewrite to https://source.com/someApp/foo?bar=baz":function (topic) {
+ assert.equal(topic.headers.location, "https://source.com/someApp/foo?bar=baz");
+ }
+ },
+ "http://source.com/someApp/foo?bar=baz and https source same port source and target":{
+ topic:function () {
+ var origHost = "source.com:80";
+ var target = { host:"target.com", port:80, https: false };
+ var resp = { statusCode: 301, headers : { location: "http://source.com/someApp/foo?bar=baz" }};
+ new ReverseProxyHelper(target).rewriteLocationHeader(reqHttps, resp, origHost)
+ return resp;
+ },
+ "rewrite to https://source.com/someApp/foo?bar=baz":function (topic) {
+ assert.equal(topic.headers.location, "https://source.com/someApp/foo?bar=baz");
+ }
+ },
+ "http://localhost:8080/someApp/foo?bar=baz":{
+ topic:function () {
+ var origHost = "localhost:8181";
+ var target = { host:"localhost", port:8080, https: false };
+ var resp = { statusCode: 301, headers : { location: "http://localhost:8080/someApp/foo?bar=baz" }};
+ new ReverseProxyHelper(target).rewriteLocationHeader(req, resp, origHost);
+ return resp;
+ },
+ "rewrite to http://localhost:8181/someApp/foo?bar=baz":function (topic) {
+ assert.equal(topic.headers.location, "http://localhost:8181/someApp/foo?bar=baz");
+ }
+ },
+ "https://localhost:8080/someApp/foo?bar=baz and https target server":{
+ topic:function () {
+ var origHost = "localhost:8181";
+ var target = { host:"localhost", port:8080, https: true };
+ var resp = { statusCode: 301, headers : { location: "https://localhost:8080/someApp/foo?bar=baz" }};
+ new ReverseProxyHelper(target).rewriteLocationHeader(req, resp, origHost);
+ return resp;
+ },
+ "rewrite to http://localhost:8181/someApp/foo?bar=baz":function (topic) {
+ assert.equal(topic.headers.location, "http://localhost:8181/someApp/foo?bar=baz");
+ }
+ },
+ }
+
+}).export(module);
Something went wrong with that request. Please try again.