From e39a9f93d2f9ab6ea769fad5e9dda25d022d8a1a Mon Sep 17 00:00:00 2001 From: indexzero Date: Wed, 9 Mar 2011 02:12:58 -0500 Subject: [PATCH] [api] Further work on refactor for node 0.4.0 --- lib/node-http-proxy.js | 752 ++++++++++++++++++----------------- lib/proxy-table.js | 64 ++- test/forward-proxy-test.js | 60 --- test/helpers.js | 110 +++-- test/node-http-proxy-test.js | 45 ++- test/proxy-table-test.js | 12 +- 6 files changed, 546 insertions(+), 497 deletions(-) delete mode 100644 test/forward-proxy-test.js diff --git a/lib/node-http-proxy.js b/lib/node-http-proxy.js index 7961996..2680250 100644 --- a/lib/node-http-proxy.js +++ b/lib/node-http-proxy.js @@ -25,11 +25,31 @@ */ var util = require('util'), + eyes = require('eyes'), http = require('http'), events = require('events'), + winston = require('winston'), ProxyTable = require('./proxy-table').ProxyTable, maxSockets = 100; + +function _getAgent (host, port) { + // + // TODO (indexzero): Make this configurable for http / https + // + var agent = http.getAgent(host, port); + agent.maxSockets = maxSockets; + return agent; +} + +exports.getMaxSockets = function () { + return maxSockets; +}; + +exports.setMaxSockets = function (value) { + maxSockets = value; +}; + exports.createServer = function () { var args, callback, port, host, forward, silent, proxyTable, options = {}; @@ -41,64 +61,45 @@ exports.createServer = function () { port = args[0]; host = args[1]; options = args[2] || {}; - } else if (args.length === 1) { + } + else if (args.length === 1) { options = args[0] || {}; if (!options.router && !callback) { throw new Error('Cannot create server with no router and no callback'); } } - - router = options.router; - forward = options.forward; - silent = typeof options.silent !== 'undefined' ? options.silent : true; - - if (router) { - proxyTable = new ProxyTable(router, options.silent); - proxyTable.on('updateRoutes', function (routes) { - server.emit('updateRoutes', routes); - }); - } + + var proxy = new HttpProxy(options); var server = http.createServer(function (req, res) { - function log (message) { - if (!silent) { - util.log(message); - } - } - - var proxy = new HttpProxy(req, res); - log('Incoming HTTP request to: ' + req.headers.host + req.url); - - if (forward) { - var forwardProxy = new HttpProxy(req, res); - log('Forwarding HTTP request to: ' + forward.host + ':' + forward.port); - forwardProxy.forwardRequest(forward.port, forward.host); - } + winston.verbose('Incoming HTTP request to: ' + req.headers.host + req.url); // If we were passed a callback to process the request // or response in some way, then call it. if (callback) { callback(req, res, proxy); - } else if (port && host) { - log('Proxying HTTP request to: ' + host + ':' + port); - proxy.proxyRequest(port, host); - } else if (proxyTable) { - proxyTable.proxyRequest(proxy); - } else { + } + else if (port && host) { + winston.verbose('Proxying HTTP request to: ' + host + ':' + port); + proxy.proxyRequest(req, res, port, host); + } + else if (proxy.proxyTable) { + winston.verbose('Proxying request using proxy table'); + proxy.proxyRequest(req, res); + } + else { throw new Error('Cannot proxy without port, host, or router.') } }); server.on('close', function () { - if (proxyTable) proxyTable.close(); + proxy.close(); }); if (!callback) { // WebSocket support: if callback is empty tunnel // websocket request automatically server.on('upgrade', function(req, socket, head) { - var proxy = new HttpProxy(req, socket, head); - // Tunnel websocket requests too proxy.proxyWebSocketRequest(port, host); }); @@ -107,363 +108,402 @@ exports.createServer = function () { return server; }; -exports.setMaxSockets = function (value) { - maxSockets = value; -}; - -exports.ProxyTable = ProxyTable; - -var HttpProxy = exports.HttpProxy = function (req, res, head) { - this.events = {}; - this.req = req; +var HttpProxy = exports.HttpProxy = function (options) { + events.EventEmitter.call(this); + this.options = options; - // If this request is upgrade request - // No response will be passed - if (!req.headers.upgrade) { - this.res = res; - this.watch(req); - } else { - // Second argument will be socket - this.sock = res; - this.head = head; - this.watch(res); + if (options.router) { + var self = this; + this.proxyTable = new ProxyTable(options.router, options.silent || false); + this.proxyTable.on('routes', function (routes) { + self.emit('routes', routes); + }); } }; -HttpProxy.prototype = { - toArray: function (obj) { - var len = obj.length, - arr = new Array(len); - for (var i = 0; i < len; ++i) { - arr[i] = obj[i]; +util.inherits(HttpProxy, events.EventEmitter); + +HttpProxy.prototype.close = function () { + if (this.proxyTable) this.proxyTable.close(); +}; + +/** + * Pause `data` and `end` events on the given `obj`. + * Middleware performing async tasks _should_ utilize + * this utility (or similar), to re-emit data once + * the async operation has completed, otherwise these + * events may be lost. + * + * var pause = utils.pause(req); + * fs.readFile(path, function(){ + * next(); + * pause.resume(); + * }); + * + * @param {Object} obj + * @return {Object} + * @api public + */ +HttpProxy.prototype.pause = function (obj) { + var onData, onEnd, events = []; + + // buffer data + obj.on('data', onData = function (data, encoding) { + events.push(['data', data, encoding]); + }); + + // buffer end + obj.on('end', onEnd = function (data, encoding) { + events.push(['end', data, encoding]); + }); + + return { + end: function () { + obj.removeListener('data', onData); + obj.removeListener('end', onEnd); + }, + resume: function () { + this.end(); + for (var i = 0, len = events.length; i < len; ++i) { + obj.emit.apply(obj, events[i]); + } } - return arr; - }, + }; +}; - watch: function (req) { - this.events = []; - var self = this; - - this.onData = function () { - self.events.push(['data'].concat(self.toArray(arguments))); - }; - this.onEnd = function () { - self.events.push(['end'].concat(self.toArray(arguments))); - }; - - req.addListener('data', this.onData); - req.addListener('end', this.onEnd); - }, - - unwatch: function (req) { - req.removeListener('data', this.onData); - req.removeListener('end', this.onEnd); - - // Rebroadcast any events that have been buffered - for (var i = 0, len = this.events.length; i < len; ++i) { - req.emit.apply(req, this.events[i]); - } - }, - - proxyRequest: function (port, server) { - var self = this, req = this.req, res = this.res, reverseProxy; +HttpProxy.prototype.proxyRequest = function (req, res, port, host, paused) { + var self = this, reverseProxy, location; + + // + // Check the proxy table for this instance to see if we need + // to get the proxy location for the request supplied. We will + // always ignore the proxyTable if an explicit `port` and `host` + // arguments are supplied to `proxyRequest`. + // + if (this.proxyTable && !host) { + location = this.proxyTable.getProxyLocation(req); - // Create an error handler so we can use it temporarily - function error (obj) { - var fn = function (err) { - res.writeHead(500, {'Content-Type': 'text/plain'}); + if (!location) { + res.writeHead(400); + return res.end(); + } + + // + // When using the ProxyTable in conjunction with an HttpProxy instance + // only the following arguments are valid: + // + // * proxy.proxyRequest(req, res, port, host, paused): This will be skipped + // * proxy.proxyRequest(req, res, paused): Paused will get updated appropriately + // * proxy.proxyRequest(req, res): No effect `undefined = undefined` + // + paused = port; + port = location.port; + host = location.host; + } + + if (this.options.forward) { + winston.verbose('Forwarding HTTP request to: ' + this.options.forward.host + ':' + this.options.forward.port); + this._forwardRequest(req); + } + + // Create an error handler so we can use it temporarily + function error (obj) { + var fn = function (err) { + res.writeHead(500, {'Content-Type': 'text/plain'}); - if (req.method !== 'HEAD') { - res.write('An error has occurred: ' + JSON.stringify(err)); - } + if (req.method !== 'HEAD') { + res.write('An error has occurred: ' + JSON.stringify(err)); + } + + // Response end may never come so removeListener here + obj.removeListener('error', fn); + res.end(); + }; + + return fn; + }; + + // Open new HTTP request to internal resource with will act as a reverse proxy pass + reverseProxy = http.request({ + host: host, + port: port, + agent: _getAgent(host, port), + method: req.method, + path: req.url, + headers: req.headers + }, function (response) { + + // Process the reverse_proxy response when it's received. + if (response.headers.connection) { + if (req.headers.connection) response.headers.connection = req.headers.connection; + else response.headers.connection = 'close'; + } + + // Set the response headers of the client response + res.writeHead(response.statusCode, response.headers); + + // Status code = 304 + // No 'data' event and no 'end' + if (response.statusCode === 304) { + return res.end(); + } + + // Add event handler for the proxied response in chunks + response.on('data', function (chunk) { + if (req.method !== 'HEAD') { + res.write(chunk); + } + }); + + // Add event listener for end of proxied response + response.on('end', function () { + reverseProxy.removeListener('error', reverseProxyError); + res.end(); + }); + }); + + // Add a listener for the connection timeout event + var reverseProxyError = error(reverseProxy); + reverseProxy.on('error', reverseProxyError); + + // Chunk the client request body as chunks from the proxied request come in + req.on('data', function (chunk) { + reverseProxy.write(chunk); + }); + + // At the end of the client request, we are going to stop the proxied request + req.on('end', function () { + reverseProxy.end(); + }); + + if (paused) { + paused.resume(); + } +}; + +HttpProxy.prototype._forwardRequest = function (req) { + var self = this, port, host, forwardProxy; + + port = this.options.forward.port; + host = this.options.forward.host; + + // Open new HTTP request to internal resource with will act as a reverse proxy pass + forwardProxy = http.request({ + host: host, + port: port, + agent: _getAgent(host, port), + method: req.method, + path: req.url, + headers: req.headers + }, function (response) { + // + // Ignore the response from the forward proxy since this is a 'fire-and-forget' proxy. + // Remark (indexzero): We will eventually emit a 'forward' event here for performance tuning. + // + }); + + // Add a listener for the connection timeout event + forwardProxy.on('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.on('data', function (chunk) { + forwardProxy.write(chunk); + }) + + // At the end of the client request, we are going to stop the proxied request + req.on('end', function () { + forwardProxy.end(); + }); +}; + +HttpProxy.prototype.proxyWebSocketRequest = function (port, server, host, data) { + var self = this, req = self.req, socket = self.sock, head = self.head, + headers = new _headers(req.headers), CRLF = '\r\n'; + + // Will generate clone of headers + // To not change original + function _headers(headers) { + var h = {}; + for (var i in headers) { + h[i] = headers[i]; + } + return h; + } + + // WebSocket requests has method = GET + if (req.method !== 'GET' || headers.upgrade.toLowerCase() !== 'websocket') { + // This request is not WebSocket request + return; + } + + // Turn of all bufferings + // For server set KeepAlive + // For client set encoding + function _socket(socket, server) { + socket.setTimeout(0); + socket.setNoDelay(true); + if (server) { + socket.setKeepAlive(true, 0); + } + else { + socket.setEncoding('utf8'); + } + } + + // Client socket + _socket(socket); + + // If host is undefined + // Get it from headers + if (!host) { + host = headers.Host; + } + + // Remote host address + var remote_host = server + (port - 80 === 0 ? '' : ':' + port); + + // Change headers + headers.Host = remote_host; + headers.Origin = 'http://' + remote_host; + + // Open request + var p = manager.getPool(port, server); + + p.getClient(function(client) { + // Based on 'pool/main.js' + var request = client.request('GET', req.url, headers); + + var errorListener = function (error) { + client.removeListener('error', errorListener); - // Response end may never come so removeListener here - obj.removeListener('error', fn); - res.end(); - }; - - return fn; - }; - - // Open new HTTP request to internal resource with will act as a reverse proxy pass - reverseProxy = http.request({ - host: server, - port: port, - method: req.method, - path: req.url, - headers: req.headers - }, function (response) { - - // Process the reverse_proxy response when it's received. - if (response.headers.connection) { - if (req.headers.connection) response.headers.connection = req.headers.connection; - else response.headers.connection = 'close'; - } - - // Set the response headers of the client response - res.writeHead(response.statusCode, response.headers); - - // Status code = 304 - // No 'data' event and no 'end' - if (response.statusCode === 304) { - res.end(); - return; - } - - // Add event handler for the proxied response in chunks - response.addListener('data', function (chunk) { - if (req.method !== 'HEAD') { - res.write(chunk, 'binary'); - self.body += chunk; - } - }); - - // Add event listener for end of proxied response - response.addListener('end', function () { - reverseProxy.removeListener('error', reverseProxyError); - res.end(); - }); - }); - - // Add a listener for the connection timeout event - var reverseProxyError = error(reverseProxy); - reverseProxy.addListener('error', reverseProxyError); - - // Chunk the client request body as chunks from the proxied request come in - req.addListener('data', function (chunk) { - reverseProxy.write(chunk, 'binary'); - }) - - // At the end of the client request, we are going to stop the proxied request - req.addListener('end', function () { - reverseProxy.end(); - }); - - self.unwatch(req); - }, - - forwardRequest: function (port, server) { - var self = this, req = this.req, forwardProxy; - - // Open new HTTP request to internal resource with will act as a reverse proxy pass - forwardProxy = http.request({ - host: server, - port: port, - method: req.method, - path: req.url, - headers: req.headers - }, function (response) { - // - // Ignore the response from the forward proxy since this is a 'fire-and-forget' proxy. - // Remark (indexzero): We will eventually emit a 'forward' event here for performance tuning. - // - }); - - // Add a listener for the connection timeout event - forwardProxy.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) { - forwardProxy.write(chunk, 'binary'); - }) - - // At the end of the client request, we are going to stop the proxied request - req.addListener('end', function () { - forwardProxy.end(); - }); - - self.unwatch(req); - }, - - proxyWebSocketRequest: function (port, server, host) { - var self = this, req = self.req, socket = self.sock, head = self.head, - headers = new _headers(req.headers), CRLF = '\r\n'; - - // Will generate clone of headers - // To not change original - function _headers(headers) { - var h = {}; - for (var i in headers) { - h[i] = headers[i]; - } - return h; + // Remove the client from the pool's available clients since it has errored + p.clients.splice(p.clients.indexOf(client), 1); + socket.end(); } - // WebSocket requests has method = GET - if (req.method !== 'GET' || headers.upgrade.toLowerCase() !== 'websocket') { - // This request is not WebSocket request - return; - } + // Not disconnect on update + client.on('upgrade', function(request, remote_socket, head) { + // Prepare socket + _socket(remote_socket, true); - // Turn of all bufferings - // For server set KeepAlive - // For client set encoding - function _socket(socket, server) { - socket.setTimeout(0); - socket.setNoDelay(true); - if (server) { - socket.setKeepAlive(true, 0); - } - else { - socket.setEncoding('utf8'); - } - } + // Emit event + onUpgrade(remote_socket); + }); - // Client socket - _socket(socket); - - // If host is undefined - // Get it from headers - if (!host) { - host = headers.Host; - } - - // Remote host address - var remote_host = server + (port - 80 === 0 ? '' : ':' + port); - - // Change headers - headers.Host = remote_host; - headers.Origin = 'http://' + remote_host; - - // Open request - var p = manager.getPool(port, server); - - p.getClient(function(client) { - // Based on 'pool/main.js' - var request = client.request('GET', req.url, headers); - - var errorListener = function (error) { + client.on('error', errorListener); + request.on('response', function (response) { + response.on('end', function () { client.removeListener('error', errorListener); - - // Remove the client from the pool's available clients since it has errored - p.clients.splice(p.clients.indexOf(client), 1); - socket.end(); - } - - // Not disconnect on update - client.on('upgrade', function(request, remote_socket, head) { - // Prepare socket - _socket(remote_socket, true); - - // Emit event - onUpgrade(remote_socket); - }); - - client.on('error', errorListener); - request.on('response', function (response) { - response.on('end', function () { - client.removeListener('error', errorListener); - client.busy = false; - p.onFree(client); - }) + client.busy = false; + p.onFree(client); }) - client.busy = true; + }) + client.busy = true; - var handshake; - request.socket.on('data', handshake = function(data) { - // Handshaking + var handshake; + request.socket.on('data', handshake = function(data) { + // Handshaking - // Ok, kind of harmfull part of code - // Socket.IO is sending hash at the end of handshake - // If protocol = 76 - // But we need to replace 'host' and 'origin' in response - // So we split data to printable data and to non-printable - // (Non-printable will come after double-CRLF) - var sdata = data.toString(); + // Ok, kind of harmfull part of code + // Socket.IO is sending hash at the end of handshake + // If protocol = 76 + // But we need to replace 'host' and 'origin' in response + // So we split data to printable data and to non-printable + // (Non-printable will come after double-CRLF) + var sdata = data.toString(); - // Get Printable - sdata = sdata.substr(0, sdata.search(CRLF + CRLF)); + // Get Printable + sdata = sdata.substr(0, sdata.search(CRLF + CRLF)); - // Get Non-Printable - data = data.slice(Buffer.byteLength(sdata), data.length); + // Get Non-Printable + data = data.slice(Buffer.byteLength(sdata), data.length); - // Replace host and origin - sdata = sdata.replace(remote_host, host) - .replace(remote_host, host); + // Replace host and origin + sdata = sdata.replace(remote_host, host) + .replace(remote_host, host); - try { - // Write printable - socket.write(sdata); - - // Write non-printable - socket.write(data); - } - catch (e) { - request.end(); - socket.end(); - } - - // Catch socket errors - socket.on('error', function() { - request.end(); - }); - - // Remove data listener now that the 'handshake' is complete - request.socket.removeListener('data', handshake); - }); - - // Write upgrade-head try { - request.write(head); + // Write printable + socket.write(sdata); + + // Write non-printable + socket.write(data); } - catch(e) { + catch (e) { request.end(); socket.end(); } - self.unwatch(socket); + + // Catch socket errors + socket.on('error', function() { + request.end(); + }); + + // Remove data listener now that the 'handshake' is complete + request.socket.removeListener('data', handshake); }); - // Request + // Write upgrade-head + try { + request.write(head); + } + catch(e) { + request.end(); + socket.end(); + } + self.unwatch(socket); + }); - function onUpgrade(reverse_proxy) { - var listeners = {}; - - // We're now connected to the server, so lets change server socket - reverse_proxy.on('data', listeners._r_data = function(data) { - // Pass data to client - if (socket.writable) { - try { - socket.write(data); - } - catch (e) { - socket.end(); - reverse_proxy.end(); - } - } - }); + // Request - socket.on('data', listeners._data = function(data) { - // Pass data from client to server + function onUpgrade(reverse_proxy) { + var listeners = {}; + + // We're now connected to the server, so lets change server socket + reverse_proxy.on('data', listeners._r_data = function(data) { + // Pass data to client + if (socket.writable) { try { - reverse_proxy.write(data); + socket.write(data); } catch (e) { - reverse_proxy.end(); socket.end(); + reverse_proxy.end(); } - }); - - // Detach event listeners from reverse_proxy - function detach() { - reverse_proxy.removeListener('close', listeners._r_close); - reverse_proxy.removeListener('data', listeners._r_data); - socket.removeListener('data', listeners._data); - socket.removeListener('close', listeners._close); } + }); - // Hook disconnections - reverse_proxy.on('end', listeners._r_close = function() { - socket.end(); - detach(); - }); - - socket.on('end', listeners._close = function() { + socket.on('data', listeners._data = function(data) { + // Pass data from client to server + try { + reverse_proxy.write(data); + } + catch (e) { reverse_proxy.end(); - detach(); - }); - }; - } + socket.end(); + } + }); + + // Detach event listeners from reverse_proxy + function detach() { + reverse_proxy.removeListener('close', listeners._r_close); + reverse_proxy.removeListener('data', listeners._r_data); + socket.removeListener('data', listeners._data); + socket.removeListener('close', listeners._close); + } + + // Hook disconnections + reverse_proxy.on('end', listeners._r_close = function() { + socket.end(); + detach(); + }); + + socket.on('end', listeners._close = function() { + reverse_proxy.end(); + detach(); + }); + }; }; \ No newline at end of file diff --git a/lib/proxy-table.js b/lib/proxy-table.js index f089dcd..ce585d2 100644 --- a/lib/proxy-table.js +++ b/lib/proxy-table.js @@ -28,27 +28,39 @@ var util = require('util'), events = require('events'), fs = require('fs'); -var ProxyTable = function (router, silent) { +// +// ### function ProxyTable (router, silent) +// #### @router {Object} Object containing the host based routes +// #### @silent {Boolean} Value indicating whether we should suppress logs +// Constructor function for the ProxyTable responsible for getting +// locations of proxy targets based on ServerRequest headers; specifically +// the HTTP host header. +// +var ProxyTable = exports.ProxyTable = function (router, silent) { events.EventEmitter.call(this); this.silent = typeof silent !== 'undefined' ? silent : true; if (typeof router === 'object') { + // // If we are passed an object literal setup // the routes with RegExps from the router - this.updateRoutes(router); + // + this.setRoutes(router); } else if (typeof router === 'string') { + // // If we are passed a string then assume it is a // file path, parse that file and watch it for changes + // var self = this; this.routeFile = router; - this.updateRoutes(JSON.parse(fs.readFileSync(router)).router); + this.setRoutes(JSON.parse(fs.readFileSync(router)).router); fs.watchFile(this.routeFile, function (c,p) { fs.readFile(self.routeFile, function (err, data) { if (err) throw err; - self.updateRoutes(JSON.parse(data).router); - self.emit('updateRoutes', self.routes); + self.setRoutes(JSON.parse(data).router); + self.emit('routes', self.routes); }); }); } @@ -57,9 +69,15 @@ var ProxyTable = function (router, silent) { } }; +// Inherit from events.EventEmitter util.inherits(ProxyTable, events.EventEmitter); -ProxyTable.prototype.updateRoutes = function (router) { +// +// ### function setRoutes (router) +// #### @router {Object} Object containing the host based routes +// Sets the host-based routes to be used by this instance. +// +ProxyTable.prototype.setRoutes = function (router) { if (!router) throw new Error('Cannot update ProxyTable routes without router.'); var self = this; @@ -76,8 +94,14 @@ ProxyTable.prototype.updateRoutes = function (router) { }); }; -ProxyTable.prototype.proxyRequest = function (proxy) { - var target = proxy.req.headers.host.split(':')[0] + proxy.req.url; +// +// ### function getProxyLocation (req) +// #### @req {ServerRequest} The incoming server request to get proxy information about. +// Returns the proxy location based on the HTTP Headers in the ServerRequest `req` +// available to this instance. +// +ProxyTable.prototype.getProxyLocation = function (req) { + var target = req.headers.host.split(':')[0] + req.url; for (var i in this.routes) { var match, route = this.routes[i]; if (match = target.match(route.route)) { @@ -89,25 +113,23 @@ ProxyTable.prototype.proxyRequest = function (proxy) { util.log('Proxy Table proxying request to: ' + host + ':' + port); } - proxy.proxyRequest(port, host); - return; + return { + port: port, + host: host + }; } } - if (proxy.res) { - proxy.res.writeHead(404, {'Content-Type': 'text/plain'}); - proxy.res.end(); - } - else if (proxy.sock) { - // Remark: How do we perform '404' over a socket? - proxy.sock.destroy(); - } + return null; }; +// +// ### close function () +// Cleans up the event listeneners maintained +// by this instance. +// ProxyTable.prototype.close = function () { if (typeof this.routeFile === 'string') { fs.unwatchFile(this.routeFile); } -}; - -exports.ProxyTable = ProxyTable; \ No newline at end of file +}; \ No newline at end of file diff --git a/test/forward-proxy-test.js b/test/forward-proxy-test.js deleted file mode 100644 index 0d8e8ba..0000000 --- a/test/forward-proxy-test.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * forward-proxy-test.js: Tests for node-http-proxy forwarding functionality. - * - * (C) 2010, Charlie Robbins - * - */ - -var fs = require('fs'), - vows = require('vows'), - util = require('util'), - path = require('path'), - request = require('request'), - assert = require('assert'), - helpers = require('./helpers'); - -var runner = new helpers.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/forward-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 fddf3b4..87a1e95 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -17,7 +17,7 @@ exports.assertProxiedWithTarget = function (runner, host, proxyPort, port, creat var test = { topic: function () { - var options = { + var that = this, options = { method: 'GET', uri: 'http://localhost:' + proxyPort, headers: { @@ -25,13 +25,22 @@ exports.assertProxiedWithTarget = function (runner, host, proxyPort, port, creat } }; - if (createProxy) createProxy(); - if (port) runner.startTargetServer(port, output); - request(options, this.callback); + function startTest () { + if (port) { + return runner.startTargetServer(port, output, function () { + request(options, that.callback); + }); + } + + request(options, this.callback); + } + + return createProxy ? createProxy(startTest) : startTest(); } }; - test[assertion] = function (err, res, body) { + test[assertion] = function (err, res, body) {; + assert.isNull(err); assert.equal(body, output); }; @@ -43,7 +52,7 @@ exports.assertProxiedWithNoTarget = function (runner, proxyPort, statusCode, cre var test = { topic: function () { - var options = { + var that = this, options = { method: 'GET', uri: 'http://localhost:' + proxyPort, headers: { @@ -51,12 +60,18 @@ exports.assertProxiedWithNoTarget = function (runner, proxyPort, statusCode, cre } }; - if (createProxy) createProxy(); + if (createProxy) { + return createProxy(function () { + request(options, that.callback); + }); + } + request(options, this.callback); } }; test[assertion] = function (err, res, body) { + assert.isNull(err); assert.equal(res.statusCode, statusCode); }; @@ -65,89 +80,98 @@ exports.assertProxiedWithNoTarget = function (runner, proxyPort, statusCode, cre var TestRunner = exports.TestRunner = function () { this.testServers = []; -} +}; // // Creates the reverse proxy server // -TestRunner.prototype.startProxyServer = function (port, targetPort, host) { - var proxyServer = httpProxy.createServer(targetPort, host); - proxyServer.listen(port); - this.testServers.push(proxyServer); - return proxyServer; +TestRunner.prototype.startProxyServer = function (port, targetPort, host, callback) { + var that = this, proxyServer = httpProxy.createServer(targetPort, host); + + proxyServer.listen(port, function () { + that.testServers.push(proxyServer); + callback(); + }); }; // // Creates the reverse proxy server with a specified latency // -TestRunner.prototype.startLatentProxyServer = function (port, targetPort, host, latency) { +TestRunner.prototype.startLatentProxyServer = function (port, targetPort, host, latency, callback) { // Initialize the nodeProxy and start proxying the request - var proxyServer = httpProxy.createServer(function (req, res, proxy) { + var that = this, proxyServer = httpProxy.createServer(function (req, res, proxy) { + var data = proxy.pause(req); + setTimeout(function () { - proxy.proxyRequest(targetPort, host); + proxy.proxyRequest(req, res, targetPort, host, data); }, latency); }); - proxyServer.listen(port); - this.testServers.push(proxyServer); - return proxyServer; + proxyServer.listen(port, function () { + that.testServers.push(proxyServer); + callback(); + }); }; // // Creates the reverse proxy server with a ProxyTable // -TestRunner.prototype.startProxyServerWithTable = function (port, options) { - var proxyServer = httpProxy.createServer(options); - proxyServer.listen(port); - this.testServers.push(proxyServer); +TestRunner.prototype.startProxyServerWithTable = function (port, options, callback) { + var that = this, proxyServer = httpProxy.createServer(options); + proxyServer.listen(port, function () { + that.testServers.push(proxyServer); + callback(); + }); + return proxyServer; }; // // Creates a latent reverse proxy server using a ProxyTable // -TestRunner.prototype.startProxyServerWithTableAndLatency = function (port, latency, router) { +TestRunner.prototype.startProxyServerWithTableAndLatency = function (port, latency, options, callback) { // Initialize the nodeProxy and start proxying the request - var proxyTable = new httpProxy.ProxyTable(router); - var proxyServer = http.createServer(function (req, res) { - var proxy = new httpProxy.HttpProxy(req, res); + var proxyServer, that = this, proxy = new httpProxy.HttpProxy(options); + proxyServer = http.createServer(function (req, res) { + var paused = proxy.pause(req); setTimeout(function () { - proxyTable.proxyRequest(proxy); + proxy.proxyRequest(req, res, paused); }, latency); }); - proxyServer.on('close', function () { - proxyTable.close(); + proxyServer.listen(port, function () { + that.testServers.push(proxyServer); + callback(); }); - proxyServer.listen(port); - this.testServers.push(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; +TestRunner.prototype.startProxyServerWithForwarding = function (port, targetPort, host, options, callback) { + var that = this, proxyServer = httpProxy.createServer(targetPort, host, options); + proxyServer.listen(port, function () { + that.testServers.push(proxyServer); + callback(); + }); }; // // Creates the 'hellonode' server // -TestRunner.prototype.startTargetServer = function (port, output) { - var targetServer = http.createServer(function (req, res) { +TestRunner.prototype.startTargetServer = function (port, output, callback) { + var that = this, targetServer = http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); - res.write(output) + res.write(output); res.end(); }); - targetServer.listen(port); - this.testServers.push(targetServer); - return targetServer; + targetServer.listen(port, function () { + that.testServers.push(targetServer); + callback(); + }); }; // diff --git a/test/node-http-proxy-test.js b/test/node-http-proxy-test.js index bbbcdec..b2d7d61 100644 --- a/test/node-http-proxy-test.js +++ b/test/node-http-proxy-test.js @@ -29,7 +29,21 @@ var vows = require('vows'), request = require('request'), assert = require('assert'), helpers = require('./helpers'); - + +var forwardOptions = { + forward: { + port: 8300, + host: 'localhost' + } +}; + +var badForwardOptions = { + forward: { + port: 9000, + host: 'localhost' + } +}; + var runner = new helpers.TestRunner(), assertProxiedWithTarget = helpers.assertProxiedWithTarget, assertProxiedWithNoTarget = helpers.assertProxiedWithNoTarget; @@ -37,20 +51,33 @@ var runner = new helpers.TestRunner(), vows.describe('node-http-proxy').addBatch({ "When using server created by httpProxy.createServer()": { "with no latency" : { - "and a valid target server": assertProxiedWithTarget(runner, 'localhost', 8080, 8081, function () { - runner.startProxyServer(8080, 8081, 'localhost'); + "and a valid target server": assertProxiedWithTarget(runner, 'localhost', 8080, 8081, function (callback) { + runner.startProxyServer(8080, 8081, 'localhost', callback); }), - "and without a valid target server": assertProxiedWithNoTarget(runner, 8082, 500, function () { - runner.startProxyServer(8082, 9000, 'localhost'); + "and without a valid target server": assertProxiedWithNoTarget(runner, 8082, 500, function (callback) { + runner.startProxyServer(8082, 9000, 'localhost', callback); }) }, "with latency": { - "and a valid target server": assertProxiedWithTarget(runner, 'localhost', 8083, 8084, function () { - runner.startLatentProxyServer(8083, 8084, 'localhost', 1000); + "and a valid target server": assertProxiedWithTarget(runner, 'localhost', 8083, 8084, function (callback) { + runner.startLatentProxyServer(8083, 8084, 'localhost', 1000, callback); }), - "and without a valid target server": assertProxiedWithNoTarget(runner, 8085, 500, function () { - runner.startLatentProxyServer(8085, 9000, 'localhost', 1000); + "and without a valid target server": assertProxiedWithNoTarget(runner, 8085, 500, function (callback) { + runner.startLatentProxyServer(8085, 9000, 'localhost', 1000, callback); }) + }, + "with forwarding enabled": { + topic: function () { + runner.startTargetServer(8300, 'forward proxy', this.callback); + }, + "with no latency" : { + "and a valid target server": assertProxiedWithTarget(runner, 'localhost', 8120, 8121, function (callback) { + runner.startProxyServerWithForwarding(8120, 8121, 'localhost', forwardOptions, callback); + }), + "and without a valid forward server": assertProxiedWithTarget(runner, 'localhost', 8122, 8123, function (callback) { + runner.startProxyServerWithForwarding(8122, 8123, 'localhost', badForwardOptions, callback); + }) + } } } }).addBatch({ diff --git a/test/proxy-table-test.js b/test/proxy-table-test.js index a9596ef..ac36f93 100644 --- a/test/proxy-table-test.js +++ b/test/proxy-table-test.js @@ -37,8 +37,7 @@ vows.describe('node-http-proxy/proxy-table').addBatch({ "When using server created by httpProxy.createServer()": { "when passed a routing table": { topic: function () { - this.server = runner.startProxyServerWithTable(8090, defaultOptions); - return null; + this.server = runner.startProxyServerWithTable(8090, defaultOptions, this.callback); }, "an incoming request to foo.com": assertProxiedWithTarget(runner, 'foo.com', 8090, 8091), "an incoming request to bar.com": assertProxiedWithTarget(runner, 'bar.com', 8090, 8092), @@ -49,9 +48,7 @@ vows.describe('node-http-proxy/proxy-table').addBatch({ fs.writeFileSync(routeFile, JSON.stringify(fileOptions)); this.server = runner.startProxyServerWithTable(8100, { router: routeFile - }); - - return null; + }, this.callback); }, "an incoming request to foo.com": assertProxiedWithTarget(runner, 'foo.com', 8100, 8101), "an incoming request to bar.com": assertProxiedWithTarget(runner, 'bar.com', 8100, 8102), @@ -67,7 +64,7 @@ vows.describe('node-http-proxy/proxy-table').addBatch({ config.router['dynamic.com'] = "127.0.0.1:8103" fs.writeFileSync(routeFile, JSON.stringify(config)); - this.server.on('updateRoutes', function () { + this.server.on('routes', function () { var options = { method: 'GET', uri: 'http://localhost:8100', @@ -93,8 +90,7 @@ vows.describe('node-http-proxy/proxy-table').addBatch({ this.server = runner.startProxyServerWithTableAndLatency(8110, 100, { 'foo.com': 'localhost:8111', 'bar.com': 'localhost:8112' - }); - return null; + }, this.callback); }, "an incoming request to foo.com": assertProxiedWithTarget(runner, 'foo.com', 8110, 8111), "an incoming request to bar.com": assertProxiedWithTarget(runner, 'bar.com', 8110, 8112),