diff --git a/README.md b/README.md index 0aacee39d..ff912ce86 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ - reverse-proxies incoming http.Server requests - can be used as a CommonJS module in node.js - uses event buffering to support application latency in proxied requests +- can proxy based on simple JSON-based configuration +- forward proxying based on simple JSON-based configuration - minimal request overhead and latency - fully-tested - battled-hardened through production usage @ [nodejitsu.com][0] @@ -35,8 +37,9 @@ There are several ways to use node-http-proxy; the library is designed to be fle 1. Standalone HTTP Proxy server 2. Inside of another HTTP server (like Connect) -3. From the command-line as a proxy daemon -4. In conjunction with a Proxy Routing Table +3. In conjunction with a Proxy Routing Table +4. As a forward-proxy with a reverse proxy +5. From the command-line as a proxy daemon ### Setup a basic stand-alone proxy server
@@ -132,8 +135,23 @@ The above route table will take incoming requests to 'foo.com' and forward them
 
   var proxyServer = httpProxy.createServer(options);
   proxyServer.listen(80);
+
+ +### Proxy requests with an additional forward proxy +Sometimes in addition to a reverse proxy, you may want your front-facing server to forward traffic to another location. For example, if you wanted to load test your staging environment. This is possible when using node-http-proxy using similar JSON-based configuration to a proxy table: +
+  var proxyServerWithForwarding = httpProxy.createServer(9000, 'localhost', {
+    forward: {
+      port: 9000,
+      host: 'staging.com'
+    }
+  });
+  proxyServerWithForwarding.listen(80);
 
+The forwarding option can be used in conjunction with the proxy table options by simply including both the 'forward' and 'router' properties in the options passed to 'createServer'. + +
### Why doesn't node-http-proxy have more advanced features like x, y, or z? If you have a suggestion for a feature currently not supported, feel free to open a [support issue](http://github.com/nodejitsu/node-http-proxy/issues). node-http-proxy is designed to just proxy http requests from one server to another, but we will be soon releasing many other complimentary projects that can be used in conjunction with node-http-proxy. diff --git a/demo.js b/demo.js index 9cebe667d..d04d024e3 100644 --- a/demo.js +++ b/demo.js @@ -66,6 +66,17 @@ httpProxy.createServer(function (req, res, proxy) { }).listen(8002); sys.puts('http proxy server '.blue + 'started '.green.bold + 'on port '.blue + '8002 '.yellow + 'with latency'.magenta.underline); +// +// +// +httpProxy.createServer(9000, 'localhost', { + forward: { + port: 9001, + host: 'localhost' + } +}).listen(8003); +sys.puts('http proxy server '.blue + 'started '.green.bold + 'on port '.blue + '8003 '.yellow + 'with forward proxy'.magenta.underline) + // // Http Server with proxyRequest Handler and Latency // @@ -75,8 +86,8 @@ http.createServer(function (req, res) { setTimeout(function() { proxy.proxyRequest(9000, 'localhost'); }, 200); -}).listen(8003); -sys.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '8003 '.yellow + 'with proxyRequest handler'.cyan.underline + ' and latency'.magenta); +}).listen(8004); +sys.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '8004 '.yellow + 'with proxyRequest handler'.cyan.underline + ' and latency'.magenta); // // Target Http Server @@ -87,3 +98,14 @@ http.createServer(function (req, res) { res.end(); }).listen(9000); sys.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '9000 '.yellow); + +// +// Target Http Forwarding Server +// +http.createServer(function (req, res) { + sys.puts('Receiving forward for: ' + req.url) + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.write('request successfully forwarded to: ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)); + res.end(); +}).listen(9001); +sys.puts('http forward server '.blue + 'started '.green.bold + 'on port '.blue + '9001 '.yellow); diff --git a/lib/node-http-proxy.js b/lib/node-http-proxy.js index 41f840f9b..7f4575199 100644 --- a/lib/node-http-proxy.js +++ b/lib/node-http-proxy.js @@ -60,6 +60,11 @@ exports.createServer = function () { var server = http.createServer(function (req, res) { var proxy = new HttpProxy(req, res); + if (options && options.forward) { + var forward = new HttpProxy(req, res); + forward.forwardRequest(options.forward.port, options.forward.host); + } + // If we were passed a callback to process the request // or response in some way, then call it. if (callback) { @@ -154,10 +159,8 @@ HttpProxy.prototype = { }, proxyRequest: function (port, server) { - // Remark: nodeProxy.body exists solely for testability var self = this, req = this.req, res = this.res; - self.body = ''; - + // Open new HTTP request to internal resource with will act as a reverse proxy pass var p = manager.getPool(port, server); @@ -167,7 +170,6 @@ HttpProxy.prototype = { // should be emitting this event. }); - var client = http.createClient(port, server); p.request(req.method, req.url, req.headers, function (reverse_proxy) { // Create an error handler so we can use it temporarily function error (obj) { @@ -187,11 +189,8 @@ HttpProxy.prototype = { }; // Add a listener for the connection timeout event - var reverseProxyError = error(reverse_proxy), - clientError = error(client); - + var reverseProxyError = error(reverse_proxy); reverse_proxy.addListener('error', reverseProxyError); - client.addListener('error', clientError); // Add a listener for the reverse_proxy response event reverse_proxy.addListener('response', function (response) { @@ -235,9 +234,37 @@ HttpProxy.prototype = { reverse_proxy.end(); }); - // On 'close' event remove 'error' listener - client.addListener('close', function() { - client.removeListener('error', clientError); + self.unwatch(req); + }); + }, + + forwardRequest: function (port, server) { + var self = this, req = this.req; + + // Open new HTTP request to internal resource with will act as a reverse proxy pass + var p = manager.getPool(port, server); + + p.on('error', function (err) { + // Remark: We should probably do something here + // but this is a hot-fix because I don't think 'pool' + // should be emitting this event. + }); + + p.request(req.method, req.url, req.headers, function (forward_proxy) { + // Add a listener for the connection timeout event + forward_proxy.addListener('error', function (err) { + // Remark: Ignoring this error in the event + // forward target doesn't exist. + }); + + // Chunk the client request body as chunks from the proxied request come in + req.addListener('data', function (chunk) { + forward_proxy.write(chunk, 'binary'); + }) + + // At the end of the client request, we are going to stop the proxied request + req.addListener('end', function () { + forward_proxy.end(); }); self.unwatch(req); diff --git a/test/forward-proxy-test.js b/test/forward-proxy-test.js new file mode 100644 index 000000000..08db99bdf --- /dev/null +++ b/test/forward-proxy-test.js @@ -0,0 +1,61 @@ +/* + * forward-proxy-test.js: Tests for node-http-proxy forwarding functionality. + * + * (C) 2010, Charlie Robbins + * + */ + +var fs = require('fs'), + vows = require('vows'), + sys = require('sys'), + path = require('path'), + request = require('request'), + assert = require('assert'), + helpers = require('./helpers'), + TestRunner = helpers.TestRunner; + +var runner = new TestRunner(), + assertProxiedWithTarget = helpers.assertProxiedWithTarget, + assertProxiedWithNoTarget = helpers.assertProxiedWithNoTarget; + +var forwardOptions = { + forward: { + port: 8300, + host: 'localhost' + } +}; + +var badForwardOptions = { + forward: { + port: 9000, + host: 'localhost' + } +}; + +vows.describe('node-http-proxy').addBatch({ + "When using server created by httpProxy.createServer()": { + "with forwarding enabled": { + topic: function () { + runner.startTargetServer(8300, 'forward proxy'); + return null; + }, + "with no latency" : { + "and a valid target server": assertProxiedWithTarget(runner, 'localhost', 8120, 8121, function () { + runner.startProxyServerWithForwarding(8120, 8121, 'localhost', forwardOptions); + }), + "and without a valid forward server": assertProxiedWithTarget(runner, 'localhost', 8122, 8123, function () { + runner.startProxyServerWithForwarding(8122, 8123, 'localhost', badForwardOptions); + }) + } + } + } +}).addBatch({ + "When the tests are over": { + topic: function () { + return runner.closeServers(); + }, + "the servers should clean up": function () { + assert.isTrue(true); + } + } +}).export(module); \ No newline at end of file diff --git a/test/helpers.js b/test/helpers.js index 5d33cd0dc..6bca02c87 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -6,8 +6,63 @@ */ var http = require('http'), + vows = require('vows'), + assert = require('assert'), + request = require('request'), httpProxy = require('./../lib/node-http-proxy'); +exports.assertProxiedWithTarget = function (runner, host, proxyPort, port, createProxy) { + var assertion = "should receive 'hello " + host + "'", + output = 'hello ' + host; + + var test = { + topic: function () { + var options = { + method: 'GET', + uri: 'http://localhost:' + proxyPort, + headers: { + host: host + } + }; + + if (createProxy) createProxy(); + if (port) runner.startTargetServer(port, output); + request(options, this.callback); + } + }; + + test[assertion] = function (err, res, body) { + assert.equal(body, output); + }; + + return test; +}; + +exports.assertProxiedWithNoTarget = function (runner, proxyPort, statusCode, createProxy) { + var assertion = "should receive " + statusCode + " responseCode"; + + var test = { + topic: function () { + var options = { + method: 'GET', + uri: 'http://localhost:' + proxyPort, + headers: { + host: 'unknown.com' + } + }; + + if (createProxy) createProxy(); + request(options, this.callback); + } + }; + + test[assertion] = function (err, res, body) { + assert.equal(res.statusCode, statusCode); + }; + + return test; +} + var TestRunner = function () { this.testServers = []; } @@ -70,6 +125,16 @@ TestRunner.prototype.startProxyServerWithTableAndLatency = function (port, laten return proxyServer; }; +// +// Creates proxy server forwarding to the specified options +// +TestRunner.prototype.startProxyServerWithForwarding = function (port, targetPort, host, options) { + var proxyServer = httpProxy.createServer(targetPort, host, options); + proxyServer.listen(port); + this.testServers.push(proxyServer); + return proxyServer; +}; + // // Creates the 'hellonode' server // diff --git a/test/node-http-proxy-test.js b/test/node-http-proxy-test.js index 0d0c20e90..54878ba8b 100644 --- a/test/node-http-proxy-test.js +++ b/test/node-http-proxy-test.js @@ -28,81 +28,30 @@ var vows = require('vows'), sys = require('sys'), request = require('request'), assert = require('assert'), - TestRunner = require('./helpers').TestRunner; + helpers = require('./helpers'), + TestRunner = helpers.TestRunner; -var runner = new TestRunner(); +var runner = new TestRunner(), + assertProxiedWithTarget = helpers.assertProxiedWithTarget, + assertProxiedWithNoTarget = helpers.assertProxiedWithNoTarget; vows.describe('node-http-proxy').addBatch({ "When using server created by httpProxy.createServer()": { - "an incoming request to the helloNode server": { - "with no latency" : { - "and a valid target server": { - topic: function () { - this.output = 'hello world'; - var options = { - method: 'GET', - uri: 'http://localhost:8080' - }; - - runner.startProxyServer(8080, 8081, 'localhost'), - runner.startTargetServer(8081, this.output); - - - request(options, this.callback); - }, - "should received 'hello world'": function (err, res, body) { - assert.equal(body, this.output); - } - }, - "and without a valid target server": { - topic: function () { - runner.startProxyServer(8082, 9000, 'localhost'); - var options = { - method: 'GET', - uri: 'http://localhost:8082' - }; - - request(options, this.callback); - }, - "should receive 500 response code": function (err, res, body) { - assert.equal(res.statusCode, 500); - } - } - }, - "with latency": { - "and a valid target server": { - topic: function () { - this.output = 'hello world'; - var options = { - method: 'GET', - uri: 'http://localhost:8083' - }; - - runner.startLatentProxyServer(8083, 8084, 'localhost', 1000), - runner.startTargetServer(8084, this.output); - - - request(options, this.callback); - }, - "should receive 'hello world'": function (err, res, body) { - assert.equal(body, this.output); - } - }, - "and without a valid target server": { - topic: function () { - runner.startLatentProxyServer(8085, 9000, 'localhost', 1000); - var options = { - method: 'GET', - uri: 'http://localhost:8085' - }; - - request(options, this.callback); - }, - "should receive 500 response code": function (err, res, body) { - assert.equal(res.statusCode, 500); - } - } - } + "with no latency" : { + "and a valid target server": assertProxiedWithTarget(runner, 'localhost', 8080, 8081, function () { + runner.startProxyServer(8080, 8081, 'localhost'); + }), + "and without a valid target server": assertProxiedWithNoTarget(runner, 8082, 500, function () { + runner.startProxyServer(8082, 9000, 'localhost'); + }) + }, + "with latency": { + "and a valid target server": assertProxiedWithTarget(runner, 'localhost', 8083, 8084, function () { + runner.startLatentProxyServer(8083, 8084, 'localhost', 1000); + }), + "and without a valid target server": assertProxiedWithNoTarget(runner, 8085, 500, function () { + runner.startLatentProxyServer(8085, 9000, 'localhost', 1000); + }) } } }).addBatch({ diff --git a/test/proxy-table-test.js b/test/proxy-table-test.js index c8acce488..4f95c2051 100644 --- a/test/proxy-table-test.js +++ b/test/proxy-table-test.js @@ -11,10 +11,13 @@ var fs = require('fs'), path = require('path'), request = require('request'), assert = require('assert'), - TestRunner = require('./helpers').TestRunner; + helpers = require('./helpers'), + TestRunner = helpers.TestRunner; var runner = new TestRunner(), - routeFile = path.join(__dirname, 'config.json'); + routeFile = path.join(__dirname, 'config.json'), + assertProxiedWithTarget = helpers.assertProxiedWithTarget, + assertProxiedWithNoTarget = helpers.assertProxiedWithNoTarget; var fileOptions = { router: { @@ -30,51 +33,6 @@ var defaultOptions = { } }; -function createTargetTest (host, proxyPort, port) { - var assertion = "should receive 'hello " + host + "'", - output = 'hello ' + host; - - var test = { - topic: function () { - var options = { - method: 'GET', - uri: 'http://localhost:' + proxyPort, - headers: { - host: host - } - }; - - if (port) runner.startTargetServer(port, output); - request(options, this.callback); - } - }; - - test[assertion] = function (err, res, body) { - assert.equal(body, output); - }; - - return test; -}; - -function createNoTargetTest (proxyPort) { - return { - topic: function () { - var options = { - method: 'GET', - uri: 'http://localhost:' + proxyPort, - headers: { - host: 'unknown.com' - } - }; - - request(options, this.callback); - }, - "should receive 404 response code": function (err, res, body) { - assert.equal(res.statusCode, 404); - } - }; -} - vows.describe('proxy-table').addBatch({ "When using server created by httpProxy.createServer()": { "when passed a routing table": { @@ -82,9 +40,9 @@ vows.describe('proxy-table').addBatch({ this.server = runner.startProxyServerWithTable(8090, defaultOptions); return null; }, - "an incoming request to foo.com": createTargetTest('foo.com', 8090, 8091), - "an incoming request to bar.com": createTargetTest('bar.com', 8090, 8092), - "an incoming request to unknown.com": createNoTargetTest(8090) + "an incoming request to foo.com": assertProxiedWithTarget(runner, 'foo.com', 8090, 8091), + "an incoming request to bar.com": assertProxiedWithTarget(runner, 'bar.com', 8090, 8092), + "an incoming request to unknown.com": assertProxiedWithNoTarget(runner, 8090, 404) }, "when passed a routing file": { topic: function () { @@ -95,9 +53,9 @@ vows.describe('proxy-table').addBatch({ return null; }, - "an incoming request to foo.com": createTargetTest('foo.com', 8100, 8101), - "an incoming request to bar.com": createTargetTest('bar.com', 8100, 8102), - "an incoming request to unknown.com": createNoTargetTest(8100), + "an incoming request to foo.com": assertProxiedWithTarget(runner, 'foo.com', 8100, 8101), + "an incoming request to bar.com": assertProxiedWithTarget(runner, 'bar.com', 8100, 8102), + "an incoming request to unknown.com": assertProxiedWithNoTarget(runner, 8100, 404), "an incoming request to dynamic.com": { "after the file has been modified": { topic: function () { @@ -138,9 +96,9 @@ vows.describe('proxy-table').addBatch({ }); return null; }, - "an incoming request to foo.com": createTargetTest('foo.com', 8110, 8111), - "an incoming request to bar.com": createTargetTest('bar.com', 8110, 8112), - "an incoming request to unknown.com": createNoTargetTest(8110) + "an incoming request to foo.com": assertProxiedWithTarget(runner, 'foo.com', 8110, 8111), + "an incoming request to bar.com": assertProxiedWithTarget(runner, 'bar.com', 8110, 8112), + "an incoming request to unknown.com": assertProxiedWithNoTarget(runner, 8110, 404) } }).addBatch({ "When the tests are over": {