Commit c06f4bf7 authored by indexzero's avatar indexzero

[test doc api] Added forward proxy functionality with tests

parent bedc7a3a
...@@ -9,6 +9,8 @@ ...@@ -9,6 +9,8 @@
- reverse-proxies incoming http.Server requests - reverse-proxies incoming http.Server requests
- can be used as a CommonJS module in node.js - can be used as a CommonJS module in node.js
- uses event buffering to support application latency in proxied requests - 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 - minimal request overhead and latency
- fully-tested - fully-tested
- battled-hardened through production usage @ [nodejitsu.com][0] - 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 ...@@ -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 1. Standalone HTTP Proxy server
2. Inside of another HTTP server (like Connect) 2. Inside of another HTTP server (like Connect)
3. From the command-line as a proxy daemon 3. In conjunction with a Proxy Routing Table
4. 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 ### Setup a basic stand-alone proxy server
<pre> <pre>
...@@ -132,8 +135,23 @@ The above route table will take incoming requests to 'foo.com' and forward them ...@@ -132,8 +135,23 @@ The above route table will take incoming requests to 'foo.com' and forward them
<pre> <pre>
var proxyServer = httpProxy.createServer(options); var proxyServer = httpProxy.createServer(options);
proxyServer.listen(80); proxyServer.listen(80);
</pre>
### 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:
<pre>
var proxyServerWithForwarding = httpProxy.createServer(9000, 'localhost', {
forward: {
port: 9000,
host: 'staging.com'
}
});
proxyServerWithForwarding.listen(80);
</pre> </pre>
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'.
<br/>
### Why doesn't node-http-proxy have more advanced features like x, y, or z? ### 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. 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.
......
...@@ -66,6 +66,17 @@ httpProxy.createServer(function (req, res, proxy) { ...@@ -66,6 +66,17 @@ httpProxy.createServer(function (req, res, proxy) {
}).listen(8002); }).listen(8002);
sys.puts('http proxy server '.blue + 'started '.green.bold + 'on port '.blue + '8002 '.yellow + 'with latency'.magenta.underline); 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 // Http Server with proxyRequest Handler and Latency
// //
...@@ -75,8 +86,8 @@ http.createServer(function (req, res) { ...@@ -75,8 +86,8 @@ http.createServer(function (req, res) {
setTimeout(function() { setTimeout(function() {
proxy.proxyRequest(9000, 'localhost'); proxy.proxyRequest(9000, 'localhost');
}, 200); }, 200);
}).listen(8003); }).listen(8004);
sys.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '8003 '.yellow + 'with proxyRequest handler'.cyan.underline + ' and latency'.magenta); sys.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '8004 '.yellow + 'with proxyRequest handler'.cyan.underline + ' and latency'.magenta);
// //
// Target Http Server // Target Http Server
...@@ -87,3 +98,14 @@ http.createServer(function (req, res) { ...@@ -87,3 +98,14 @@ http.createServer(function (req, res) {
res.end(); res.end();
}).listen(9000); }).listen(9000);
sys.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '9000 '.yellow); 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);
...@@ -60,6 +60,11 @@ exports.createServer = function () { ...@@ -60,6 +60,11 @@ exports.createServer = function () {
var server = http.createServer(function (req, res) { var server = http.createServer(function (req, res) {
var proxy = new HttpProxy(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 // If we were passed a callback to process the request
// or response in some way, then call it. // or response in some way, then call it.
if (callback) { if (callback) {
...@@ -154,10 +159,8 @@ HttpProxy.prototype = { ...@@ -154,10 +159,8 @@ HttpProxy.prototype = {
}, },
proxyRequest: function (port, server) { proxyRequest: function (port, server) {
// Remark: nodeProxy.body exists solely for testability
var self = this, req = this.req, res = this.res; 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 // Open new HTTP request to internal resource with will act as a reverse proxy pass
var p = manager.getPool(port, server); var p = manager.getPool(port, server);
...@@ -167,7 +170,6 @@ HttpProxy.prototype = { ...@@ -167,7 +170,6 @@ HttpProxy.prototype = {
// should be emitting this event. // should be emitting this event.
}); });
var client = http.createClient(port, server);
p.request(req.method, req.url, req.headers, function (reverse_proxy) { p.request(req.method, req.url, req.headers, function (reverse_proxy) {
// Create an error handler so we can use it temporarily // Create an error handler so we can use it temporarily
function error (obj) { function error (obj) {
...@@ -187,11 +189,8 @@ HttpProxy.prototype = { ...@@ -187,11 +189,8 @@ HttpProxy.prototype = {
}; };
// Add a listener for the connection timeout event // Add a listener for the connection timeout event
var reverseProxyError = error(reverse_proxy), var reverseProxyError = error(reverse_proxy);
clientError = error(client);
reverse_proxy.addListener('error', reverseProxyError); reverse_proxy.addListener('error', reverseProxyError);
client.addListener('error', clientError);
// Add a listener for the reverse_proxy response event // Add a listener for the reverse_proxy response event
reverse_proxy.addListener('response', function (response) { reverse_proxy.addListener('response', function (response) {
...@@ -235,9 +234,37 @@ HttpProxy.prototype = { ...@@ -235,9 +234,37 @@ HttpProxy.prototype = {
reverse_proxy.end(); reverse_proxy.end();
}); });
// On 'close' event remove 'error' listener self.unwatch(req);
client.addListener('close', function() { });
client.removeListener('error', clientError); },
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); self.unwatch(req);
......
/*
* 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
...@@ -6,8 +6,63 @@ ...@@ -6,8 +6,63 @@
*/ */
var http = require('http'), var http = require('http'),
vows = require('vows'),
assert = require('assert'),
request = require('request'),
httpProxy = require('./../lib/node-http-proxy'); 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 () { var TestRunner = function () {
this.testServers = []; this.testServers = [];
} }
...@@ -70,6 +125,16 @@ TestRunner.prototype.startProxyServerWithTableAndLatency = function (port, laten ...@@ -70,6 +125,16 @@ TestRunner.prototype.startProxyServerWithTableAndLatency = function (port, laten
return proxyServer; 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 // Creates the 'hellonode' server
// //
......
...@@ -28,81 +28,30 @@ var vows = require('vows'), ...@@ -28,81 +28,30 @@ var vows = require('vows'),
sys = require('sys'), sys = require('sys'),
request = require('request'), request = require('request'),
assert = require('assert'), 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({ vows.describe('node-http-proxy').addBatch({
"When using server created by httpProxy.createServer()": { "When using server created by httpProxy.createServer()": {
"an incoming request to the helloNode server": { "with no latency" : {
"with no latency" : { "and a valid target server": assertProxiedWithTarget(runner, 'localhost', 8080, 8081, function () {
"and a valid target server": { runner.startProxyServer(8080, 8081, 'localhost');
topic: function () { }),
this.output = 'hello world'; "and without a valid target server": assertProxiedWithNoTarget(runner, 8082, 500, function () {
var options = { runner.startProxyServer(8082, 9000, 'localhost');
method: 'GET', })
uri: 'http://localhost:8080' },
}; "with latency": {
"and a valid target server": assertProxiedWithTarget(runner, 'localhost', 8083, 8084, function () {
runner.startProxyServer(8080, 8081, 'localhost'), runner.startLatentProxyServer(8083, 8084, 'localhost', 1000);
runner.startTargetServer(8081, this.output); }),
"and without a valid target server": assertProxiedWithNoTarget(runner, 8085, 500, function () {
runner.startLatentProxyServer(8085, 9000, 'localhost', 1000);
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);
}
}
}
} }
} }
}).addBatch({ }).addBatch({
......
...@@ -11,10 +11,13 @@ var fs = require('fs'), ...@@ -11,10 +11,13 @@ var fs = require('fs'),
path = require('path'), path = require('path'),
request = require('request'), request = require('request'),
assert = require('assert'), assert = require('assert'),
TestRunner = require('./helpers').TestRunner; helpers = require('./helpers'),
TestRunner = helpers.TestRunner;
var runner = new 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 = { var fileOptions = {
router: { router: {
...@@ -30,51 +33,6 @@ var defaultOptions = { ...@@ -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({ vows.describe('proxy-table').addBatch({
"When using server created by httpProxy.createServer()": { "When using server created by httpProxy.createServer()": {
"when passed a routing table": { "when passed a routing table": {
...@@ -82,9 +40,9 @@ vows.describe('proxy-table').addBatch({ ...@@ -82,9 +40,9 @@ vows.describe('proxy-table').addBatch({
this.server = runner.startProxyServerWithTable(8090, defaultOptions); this.server = runner.startProxyServerWithTable(8090, defaultOptions);
return null; return null;
}, },
"an incoming request to foo.com": createTargetTest('foo.com', 8090, 8091), "an incoming request to foo.com": assertProxiedWithTarget(runner, 'foo.com', 8090, 8091),
"an incoming request to bar.com": createTargetTest('bar.com', 8090, 8092), "an incoming request to bar.com": assertProxiedWithTarget(runner, 'bar.com', 8090, 8092),
"an incoming request to unknown.com": createNoTargetTest(8090) "an incoming request to unknown.com": assertProxiedWithNoTarget(runner, 8090, 404)
}, },
"when passed a routing file": { "when passed a routing file": {
topic: function () { topic: function () {
...@@ -95,9 +53,9 @@ vows.describe('proxy-table').addBatch({ ...@@ -95,9 +53,9 @@ vows.describe('proxy-table').addBatch({
return null; return null;
}, },
"an incoming request to foo.com": createTargetTest('foo.com', 8100, 8101), "an incoming request to foo.com": assertProxiedWithTarget(runner, 'foo.com', 8100, 8101),
"an incoming request to bar.com": createTargetTest('bar.com', 8100, 8102), "an incoming request to bar.com": assertProxiedWithTarget(runner, 'bar.com', 8100, 8102),
"an incoming request to unknown.com": createNoTargetTest(8100), "an incoming request to unknown.com": assertProxiedWithNoTarget(runner, 8100, 404),
"an incoming request to dynamic.com": { "an incoming request to dynamic.com": {
"after the file has been modified": { "after the file has been modified": {
topic: function () { topic: function () {
...@@ -138,9 +96,9 @@ vows.describe('proxy-table').addBatch({ ...@@ -138,9 +96,9 @@ vows.describe('proxy-table').addBatch({
}); });
return null; return null;
}, },
"an incoming request to foo.com": createTargetTest('foo.com', 8110, 8111), "an incoming request to foo.com": assertProxiedWithTarget(runner, 'foo.com', 8110, 8111),
"an incoming request to bar.com": createTargetTest('bar.com', 8110, 8112), "an incoming request to bar.com": assertProxiedWithTarget(runner, 'bar.com', 8110, 8112),
"an incoming request to unknown.com": createNoTargetTest(8110) "an incoming request to unknown.com": assertProxiedWithNoTarget(runner, 8110, 404)
} }
}).addBatch({ }).addBatch({
"When the tests are over": { "When the tests are over": {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment