diff --git a/README.md b/README.md index 89c7de257..0077c1529 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# node-http-proxy - v0.3.1 +# node-http-proxy - v0.4.0 @@ -6,16 +6,15 @@ ### Features -- 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] -- written entirely in javascript -- easy to use api +* Reverse proxies incoming http.ServerRequest streams +* Can be used as a CommonJS module in node.js +* Uses event buffering to support application latency in proxied requests +* Reverse or Forward Proxy based on simple JSON-based configuration +* Minimal request overhead and latency +* Full suite of functional tests +* Battled-hardened through __production usage__ @ [nodejitsu.com][0] +* Written entirely in Javascript +* Easy to use API ### When to use node-http-proxy @@ -147,18 +146,34 @@ A Proxy Table is a simple lookup table that maps incoming requests to proxy targ
   var options = {
     router: {
-      'foo.com': '127.0.0.1:8001',
-      'bar.com': '127.0.0.1:8002'
+      'foo.com/baz': '127.0.0.1:8001',
+      'foo.com/buz': '127.0.0.1:8002',
+      'bar.com/buz': '127.0.0.1:8003'
     }
   };
 
-The above route table will take incoming requests to 'foo.com' and forward them to '127.0.0.1:8001'. Likewise it will take incoming requests to 'bar.com' and forward them to '127.0.0.1:8002'. The routes themselves are later converted to regular expressions to enable more complex matching functionality. We can create a proxy server with these options by using the following code: +The above route table will take incoming requests to 'foo.com/baz' and forward them to '127.0.0.1:8001'. Likewise it will take incoming requests to 'foo.com/buz' and forward them to '127.0.0.1:8002'. The routes themselves are later converted to regular expressions to enable more complex matching functionality. We can create a proxy server with these options by using the following code:
   var proxyServer = httpProxy.createServer(options);
   proxyServer.listen(80);
 
+### Proxy requests using a 'Hostname Only' ProxyTable +As mentioned in the previous section, all routes passes to the ProxyTable are by default converted to regular expressions that are evaluated at proxy-time. This is good for complex URL rewriting of proxy requests, but less efficient when one simply wants to do pure hostname routing based on the HTTP 'Host' header. If you are only concerned with hostname routing, you change the lookup used by the internal ProxyTable: + +
+  var options = {
+    hostnameOnly, true,
+    router: {
+      'foo.com': '127.0.0.1:8001',
+      'bar.com': '127.0.0.1:8002'
+    }
+  }
+
+ +Notice here that I have not included paths on the individual domains because this is not possible when using only the HTTP 'Host' header. Care to learn more? See [RFC2616: HTTP/1.1, Section 14.23, "Host"][1]. + ### 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:
@@ -219,4 +234,5 @@ 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.
 
-[0]: http://nodejitsu.com
\ No newline at end of file
+[0]: http://nodejitsu.com
+[1]: http://www.ietf.org/rfc/rfc2616.txt
\ No newline at end of file
diff --git a/docs/node-http-proxy.html b/docs/node-http-proxy.html
index 31f1af13d..bd2fb82ed 100644
--- a/docs/node-http-proxy.html
+++ b/docs/node-http-proxy.html
@@ -148,7 +148,7 @@ 

@options {Object} Options for this instance.

if (options.router) { var self = this; - this.proxyTable = new ProxyTable(options.router, options.silent || false); + this.proxyTable = new ProxyTable(options.router, options.silent, options.hostnameOnly); this.proxyTable.on('routes', function (routes) { self.emit('routes', routes); }); @@ -157,7 +157,7 @@

@options {Object} Options for this instance.

@obj {Object} Object to pause events from

-

Pause data and end events on the given obj. +

Buffer data and end events from the given obj. Consumers of HttpProxy performing async tasks must utilize this utility, to re-emit data once the async operation has completed, otherwise these @@ -239,8 +239,12 @@

@buffer {Object} Optional Result from httpProxy.buffe winston.verbose('Forwarding HTTP request to: ' + this.options.forward.host + ':' + this.options.forward.port); this._forwardRequest(req); } - - function proxyError(err) { +

function proxyError (err)

+ +

@err {Error} Error contacting the proxy target

+ +

Short-circuits res in the event of any error when +contacting the proxy target at host / port.

  function proxyError(err) {
     errState = true;
     res.writeHead(500, { 'Content-Type': 'text/plain' });
 
@@ -250,7 +254,7 @@ 

@buffer {Object} Optional Result from httpProxy.buffe res.end(); } -

Open new HTTP request to internal resource with will act as a reverse proxy pass

  reverseProxy = http.request({
+  

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),
@@ -258,37 +262,50 @@ 

@buffer {Object} Optional Result from httpProxy.buffe path: req.url, headers: req.headers }, function (response) { -

Process the reverseProxy response when it's received.

    if (response.headers.connection) {
+    

Process the reverseProxy 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) {
+    }

Set the headers of the client response

    res.writeHead(response.statusCode, response.headers);

response.statusCode === 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) {
+    }

For each data chunk received from the reverseProxy +response write it to the outgoing res.

    response.on('data', function (chunk) {
       if (req.method !== 'HEAD') {
         res.write(chunk);
       }
-    });

Add event listener for end of proxied response

    response.on('end', function () {
+    });

When the reverseProxy response ends, end the +corresponding outgoing res unless we have entered +an error state. In which case, assume res.end() has +already been called and the 'error' event listener +removed.

    response.on('end', function () {
       if (!errState) {
         reverseProxy.removeListener('error', proxyError);
         res.end();
       }
     });
   });
-  

Add a listener for the connection timeout event

  reverseProxy.once('error', proxyError);
-  

Chunk the client request body as chunks from the proxied request come in

  req.on('data', function (chunk) {
+  

Handle 'error' events from the reverseProxy.

  reverseProxy.once('error', proxyError);
+  

For each data chunk received from the incoming +req write it to the reverseProxy request.

  req.on('data', function (chunk) {
     if (!errState) {
       reverseProxy.write(chunk);
     }
-  });

At the end of the client request, we are going to stop the proxied request

  req.on('end', function () {
-    if (!errState) {
+  });

When the incoming req ends, end the corresponding reverseProxy +request unless we have entered an error state.

  req.on('end', function () {

Remark (indexzero | 3/10/2011): This is a short-term workaround for a suspect error from net.js when +http.ClientRequest.end() is called in reproducable, but uninvestigated scenarios

+ +
net.js:313
+  throw new Error('Socket.end() called already; cannot write.');
+       ^
+Error: Socket.end() called already; cannot write.
+  at Socket.write (net.js:313:13)
+
    if (!errState /*&& (!reverseProxy.socket || reverseProxy.socket._writeQueueLast() !== 42)*/) {
       reverseProxy.end();
     }
-  });

If we have been passed buffered data, resume it.

  if (buffer && !errState) {
+  });

If we have been passed buffered data, resume it.

  if (buffer && !errState) {
     buffer.resume();
   }
 };
-  

@private function _forwardRequest (req)

+

@private function _forwardRequest (req)

@req {ServerRequest} Incoming HTTP Request to proxy.

@@ -298,36 +315,36 @@

@req {ServerRequest} Incoming HTTP Request to proxy.

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({
+  

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. + }, 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.

+

Add a listener for the connection timeout event.

Remark: Ignoring this error in the event - forward target doesn't exist.

  forwardProxy.on('error', function (err) { });

Chunk the client request body as chunks from the proxied request come in

  req.on('data', function (chunk) {
+        forward target doesn't exist.

  forwardProxy.on('error', function (err) { });

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 () {
+  })

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 + 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 + }

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);
@@ -338,20 +355,20 @@ 

@req {ServerRequest} Incoming HTTP Request to proxy.

else { socket.setEncoding('utf8'); } - }

Client socket

  _socket(socket);

If host is undefined + }

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);
+  

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);
+  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);
-      

Remove the client from the pool's available clients since it has errored

      p.clients.splice(p.clients.indexOf(client), 1);
+      

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);
+    }

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);
@@ -365,23 +382,23 @@ 

@req {ServerRequest} Incoming HTTP Request to proxy.

client.busy = true; var handshake; - request.socket.on('data', handshake = function(data) {

Handshaking

Ok, kind of harmfull part of code + 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();

Get Printable

      sdata = sdata.substr(0, sdata.search(CRLF + CRLF));

Get Non-Printable

      data = data.slice(Buffer.byteLength(sdata), data.length);

Replace host and origin

      sdata = sdata.replace(remote_host, host)
+(Non-printable will come after double-CRLF)

      var sdata = data.toString();

Get Printable

      sdata = sdata.substr(0, sdata.search(CRLF + CRLF));

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);
 
-      try {

Write printable

        socket.write(sdata);

Write non-printable

        socket.write(data);
+      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() {
+      }

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 {
+      });

Remove data listener now that the 'handshake' is complete

      request.socket.removeListener('data', handshake);
+    });

Write upgrade-head

    try {
       request.write(head);
     } 
     catch(e) {
@@ -389,9 +406,9 @@ 

@req {ServerRequest} Incoming HTTP Request to proxy.

socket.end(); } self.unwatch(socket); - });

Request

  function onUpgrade(reverse_proxy) {
+  });

Request

  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) {
+    

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);
         } 
@@ -402,19 +419,19 @@ 

@req {ServerRequest} Incoming HTTP Request to proxy.

} }); - socket.on('data', listeners._data = function(data) {

Pass data from client to server

      try {
+    socket.on('data', listeners._data = function(data) {

Pass data from client to server

      try {
         reverse_proxy.write(data);
       } 
       catch (e) {
         reverse_proxy.end();
         socket.end();
       }
-    });

Detach event listeners from reverse_proxy

    function detach() {
+    });

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() {
+    }

Hook disconnections

    reverse_proxy.on('end', listeners._r_close = function() {
       socket.end();
       detach();
     });
diff --git a/docs/proxy-table.html b/docs/proxy-table.html
index b81ce7274..d8462fb03 100644
--- a/docs/proxy-table.html
+++ b/docs/proxy-table.html
@@ -33,11 +33,15 @@ 

@router {Object} Object containing the host based routes

@silent {Boolean} Value indicating whether we should suppress logs

+

@hostnameOnly {Boolean} Value indicating if we should route based on hostname string only

+

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) {
+the HTTP host header.

var ProxyTable = exports.ProxyTable = function (router, silent, hostnameOnly) {
   events.EventEmitter.call(this);
+  
   this.silent = typeof silent !== 'undefined' ? silent : true;
+  this.hostnameOnly = typeof hostnameOnly !== 'undefined' ? hostnameOnly : false;
   
   if (typeof router === 'object') {

If we are passed an object literal setup the routes with RegExps from the router

    this.setRoutes(router);
@@ -47,11 +51,11 @@ 

@silent {Boolean} Value indicating whether we should suppress logs

this.routeFile = router; this.setRoutes(JSON.parse(fs.readFileSync(router)).router); - fs.watchFile(this.routeFile, function (c,p) { + fs.watchFile(this.routeFile, function () { fs.readFile(self.routeFile, function (err, data) { if (err) throw err; self.setRoutes(JSON.parse(data).router); - self.emit('routes', self.routes); + self.emit('routes', self.hostnameOnly === false ? self.routes : self.router); }); }); } @@ -65,29 +69,35 @@

@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;
   this.router = router;
-  this.routes = [];
   
-  Object.keys(router).forEach(function (path) {
-    var route = new RegExp(path, 'i');
+  if (this.hostnameOnly === false) {
+    var self = this;
+    this.routes = [];
+
+    Object.keys(router).forEach(function (path) {
+      var route = new RegExp(path, 'i');
 
-    self.routes.push({
-      route: route,
-      target: router[path]
+      self.routes.push({
+        route: route,
+        target: router[path]
+      });
     });
-  });
+  }
 };

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)) {
-      var location = route.target.split(':'),
+  if (!req || !req.headers || !req.headers.host) {
+    return null;
+  }
+  
+  var target = req.headers.host.split(':')[0];
+  if (this.hostnameOnly == true) {
+    if (this.router.hasOwnProperty(target)) {
+      var location = this.router[target].split(':'),
           host = location[0],
           port = location.length === 1 ? 80 : location[1];
       
@@ -99,6 +109,24 @@ 

@req {ServerRequest} The incoming server request to get proxy information ab }; } } + else { + target += req.url; + for (var i in this.routes) { + var match, route = this.routes[i]; + if (match = target.match(route.route)) { + var location = route.target.split(':'), + host = location[0], + port = location.length === 1 ? 80 : location[1]; + + winston.verbose('Proxy Table proxying request to: ' + host + ':' + port); + + return { + port: port, + host: host + }; + } + } + } return null; };

close function ()

diff --git a/lib/node-http-proxy.js b/lib/node-http-proxy.js index 1594d48f4..86ed7937b 100644 --- a/lib/node-http-proxy.js +++ b/lib/node-http-proxy.js @@ -169,7 +169,7 @@ var HttpProxy = exports.HttpProxy = function (options) { if (options.router) { var self = this; - this.proxyTable = new ProxyTable(options.router, options.silent || false); + this.proxyTable = new ProxyTable(options.router, options.silent, options.hostnameOnly); this.proxyTable.on('routes', function (routes) { self.emit('routes', routes); }); @@ -182,7 +182,7 @@ util.inherits(HttpProxy, events.EventEmitter); // // ### function buffer (obj) // #### @obj {Object} Object to pause events from -// Pause `data` and `end` events on the given `obj`. +// Buffer `data` and `end` events from the given `obj`. // Consumers of HttpProxy performing async tasks // __must__ utilize this utility, to re-emit data once // the async operation has completed, otherwise these @@ -284,6 +284,12 @@ HttpProxy.prototype.proxyRequest = function (req, res, port, host, buffer) { this._forwardRequest(req); } + // + // #### function proxyError (err) + // #### @err {Error} Error contacting the proxy target + // Short-circuits `res` in the event of any error when + // contacting the proxy target at `host` / `port`. + // function proxyError(err) { errState = true; res.writeHead(500, { 'Content-Type': 'text/plain' }); @@ -305,29 +311,33 @@ HttpProxy.prototype.proxyRequest = function (req, res, port, host, buffer) { headers: req.headers }, function (response) { - // Process the reverseProxy response when it's received. + // Process the `reverseProxy` `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 + // Set the headers of the client response res.writeHead(response.statusCode, response.headers); - // Status code = 304 - // No 'data' event and no 'end' + // `response.statusCode === 304`: No 'data' event and no 'end' if (response.statusCode === 304) { return res.end(); } - // Add event handler for the proxied response in chunks + // For each data `chunk` received from the `reverseProxy` + // `response` write it to the outgoing `res`. response.on('data', function (chunk) { if (req.method !== 'HEAD') { res.write(chunk); } }); - // Add event listener for end of proxied response + // When the `reverseProxy` `response` ends, end the + // corresponding outgoing `res` unless we have entered + // an error state. In which case, assume `res.end()` has + // already been called and the 'error' event listener + // removed. response.on('end', function () { if (!errState) { reverseProxy.removeListener('error', proxyError); @@ -336,19 +346,33 @@ HttpProxy.prototype.proxyRequest = function (req, res, port, host, buffer) { }); }); - // Add a listener for the connection timeout event + // Handle 'error' events from the `reverseProxy`. reverseProxy.once('error', proxyError); - // Chunk the client request body as chunks from the proxied request come in + // For each data `chunk` received from the incoming + // `req` write it to the `reverseProxy` request. req.on('data', function (chunk) { if (!errState) { reverseProxy.write(chunk); } }); - // At the end of the client request, we are going to stop the proxied request + // + // When the incoming `req` ends, end the corresponding `reverseProxy` + // request unless we have entered an error state. + // req.on('end', function () { - if (!errState) { + // + // __Remark__ *(indexzero | 3/10/2011)*: This is a short-term workaround for a suspect error from net.js when + // `http.ClientRequest.end()` is called in reproducable, but uninvestigated scenarios + // + // net.js:313 + // throw new Error('Socket.end() called already; cannot write.'); + // ^ + // Error: Socket.end() called already; cannot write. + // at Socket.write (net.js:313:13) + // + if (!errState && (!reverseProxy.socket || reverseProxy.socket._writeQueueLast() !== 42)) { reverseProxy.end(); } }); diff --git a/lib/proxy-table.js b/lib/proxy-table.js index 012320f73..9236c62a0 100644 --- a/lib/proxy-table.js +++ b/lib/proxy-table.js @@ -33,13 +33,16 @@ var util = require('util'), // ### function ProxyTable (router, silent) // #### @router {Object} Object containing the host based routes // #### @silent {Boolean} Value indicating whether we should suppress logs +// #### @hostnameOnly {Boolean} Value indicating if we should route based on __hostname string only__ // 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) { +var ProxyTable = exports.ProxyTable = function (router, silent, hostnameOnly) { events.EventEmitter.call(this); + this.silent = typeof silent !== 'undefined' ? silent : true; + this.hostnameOnly = typeof hostnameOnly !== 'undefined' ? hostnameOnly : false; if (typeof router === 'object') { // @@ -57,11 +60,11 @@ var ProxyTable = exports.ProxyTable = function (router, silent) { this.routeFile = router; this.setRoutes(JSON.parse(fs.readFileSync(router)).router); - fs.watchFile(this.routeFile, function (c,p) { + fs.watchFile(this.routeFile, function () { fs.readFile(self.routeFile, function (err, data) { if (err) throw err; self.setRoutes(JSON.parse(data).router); - self.emit('routes', self.routes); + self.emit('routes', self.hostnameOnly === false ? self.routes : self.router); }); }); } @@ -81,18 +84,21 @@ util.inherits(ProxyTable, events.EventEmitter); ProxyTable.prototype.setRoutes = function (router) { if (!router) throw new Error('Cannot update ProxyTable routes without router.'); - var self = this; this.router = router; - this.routes = []; - Object.keys(router).forEach(function (path) { - var route = new RegExp(path, 'i'); + if (this.hostnameOnly === false) { + var self = this; + this.routes = []; + + Object.keys(router).forEach(function (path) { + var route = new RegExp(path, 'i'); - self.routes.push({ - route: route, - target: router[path] + self.routes.push({ + route: route, + target: router[path] + }); }); - }); + } }; // @@ -102,11 +108,14 @@ ProxyTable.prototype.setRoutes = function (router) { // 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)) { - var location = route.target.split(':'), + if (!req || !req.headers || !req.headers.host) { + return null; + } + + var target = req.headers.host.split(':')[0]; + if (this.hostnameOnly == true) { + if (this.router.hasOwnProperty(target)) { + var location = this.router[target].split(':'), host = location[0], port = location.length === 1 ? 80 : location[1]; @@ -118,6 +127,24 @@ ProxyTable.prototype.getProxyLocation = function (req) { }; } } + else { + target += req.url; + for (var i in this.routes) { + var match, route = this.routes[i]; + if (match = target.match(route.route)) { + var location = route.target.split(':'), + host = location[0], + port = location.length === 1 ? 80 : location[1]; + + winston.verbose('Proxy Table proxying request to: ' + host + ':' + port); + + return { + port: port, + host: host + }; + } + } + } return null; }; diff --git a/test/proxy-table-test.js b/test/proxy-table-test.js index b8f7de49c..df3179aa7 100644 --- a/test/proxy-table-test.js +++ b/test/proxy-table-test.js @@ -33,15 +33,33 @@ var defaultOptions = { } }; +var hostnameOptions = { + hostnameOnly: true, + router: { + "foo.com": "127.0.0.1:8091", + "bar.com": "127.0.0.1:8092" + }, +} + 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, this.callback); + "and routing by RegExp": { + topic: function () { + 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), + "an incoming request to unknown.com": assertProxiedWithNoTarget(runner, 8090, 404) }, - "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) + "and routing by Hostname": { + topic: function () { + this.server = runner.startProxyServerWithTable(8093, hostnameOptions, this.callback); + }, + "an incoming request to foo.com": assertProxiedWithTarget(runner, 'foo.com', 8093, 8094), + "an incoming request to bar.com": assertProxiedWithTarget(runner, 'bar.com', 8093, 8095), + "an incoming request to unknown.com": assertProxiedWithNoTarget(runner, 8093, 404) + } }, "when passed a routing file": { topic: function () {