diff --git a/.gitignore b/.gitignore index 234f13a..aa98793 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -test/config.json \ No newline at end of file +test/config.json +node_modules/ +npm-debug.log diff --git a/README.md b/README.md index 3f41263..df43bd4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# node-http-proxy - v0.5.0 +# node-http-proxy - v0.5.3 @@ -23,14 +23,16 @@ Let's suppose you were running multiple http application servers, but you only wanted to expose one machine to the internet. You could setup node-http-proxy on that one machine and then reverse-proxy the incoming http requests to locally running services which were not exposed to the outside network. ### Installing npm (node package manager) -
-  curl http://npmjs.org/install.sh | sh
-
+ +``` +curl http://npmjs.org/install.sh | sh +``` ### Installing node-http-proxy -
-  npm install http-proxy
-
+ +``` +npm install http-proxy +``` ## Using node-http-proxy @@ -47,251 +49,262 @@ In each of these scenarios node-http-proxy can handle any of these types of requ 1. HTTP Requests (http://) 2. HTTPS Requests (https://) 3. WebSocket Requests (ws://) +4. Secure WebSocket Requests (wss://) See the [examples][3] for more working sample code. ### Setup a basic stand-alone proxy server -
-  var http = require('http'),
-      httpProxy = require('http-proxy');
-  //
-  // Create your proxy server
-  //
-  httpProxy.createServer(9000, 'localhost').listen(8000);
 
-  //
-  // Create your target server
-  //
-  http.createServer(function (req, res) {
-    res.writeHead(200, { 'Content-Type': 'text/plain' });
-    res.write('request successfully proxied!' + '\n' + JSON.stringify(req.headers, true, 2));
-    res.end();
-  }).listen(9000);
-
+``` js +var http = require('http'), + httpProxy = require('http-proxy'); +// +// Create your proxy server +// +httpProxy.createServer(9000, 'localhost').listen(8000); + +// +// Create your target server +// +http.createServer(function (req, res) { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write('request successfully proxied!' + '\n' + JSON.stringify(req.headers, true, 2)); + res.end(); +}).listen(9000); +``` ### Setup a stand-alone proxy server with custom server logic -
-  var http = require('http'),
-      httpProxy = require('http-proxy');
-      
-  //
-  // Create a proxy server with custom application logic
-  //
-  httpProxy.createServer(function (req, res, proxy) {
-    //
-    // Put your custom server logic here
-    //
-    proxy.proxyRequest(req, res, {
-      host: 'localhost',
-      port: 9000
-    });
-  }).listen(8000);
 
-  http.createServer(function (req, res) {
-    res.writeHead(200, { 'Content-Type': 'text/plain' });
-    res.write('request successfully proxied: ' + req.url +'\n' + JSON.stringify(req.headers, true, 2));
-    res.end();
-  }).listen(9000);
-
+``` js +var http = require('http'), + httpProxy = require('http-proxy'); + +// +// Create a proxy server with custom application logic +// +httpProxy.createServer(function (req, res, proxy) { + // + // Put your custom server logic here + // + proxy.proxyRequest(req, res, { + host: 'localhost', + port: 9000 + }); +}).listen(8000); + +http.createServer(function (req, res) { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write('request successfully proxied: ' + req.url +'\n' + JSON.stringify(req.headers, true, 2)); + res.end(); +}).listen(9000); +``` ### Setup a stand-alone proxy server with latency (e.g. IO, etc) -
-  var http = require('http'),
-      httpProxy = require('http-proxy');
+
+``` js
+var http = require('http'),
+    httpProxy = require('http-proxy');
+
+//
+// Create a proxy server with custom application logic
+//
+httpProxy.createServer(function (req, res, proxy) {
+  //
+  // Buffer the request so that `data` and `end` events
+  // are not lost during async operation(s).
+  //
+  var buffer = proxy.buffer(req);
   
   //
-  // Create a proxy server with custom application logic
+  // Wait for two seconds then respond: this simulates
+  // performing async actions before proxying a request
   //
-  httpProxy.createServer(function (req, res, proxy) {
-    //
-    // Buffer the request so that `data` and `end` events
-    // are not lost during async operation(s).
-    //
-    var buffer = proxy.buffer(req);
-    
-    //
-    // Wait for two seconds then respond: this simulates
-    // performing async actions before proxying a request
-    //
-    setTimeout(function () {
-      proxy.proxyRequest(req, res, {
-        host: 'localhost',
-        port: 9000, 
-        buffer: buffer
-      });      
-    }, 2000);
-  }).listen(8000);
-
-  http.createServer(function (req, res) {
-    res.writeHead(200, { 'Content-Type': 'text/plain' });
-    res.write('request successfully proxied: ' + req.url +'\n' + JSON.stringify(req.headers, true, 2));
-    res.end();
-  }).listen(9000);
-
- -### Proxy requests within another http server -
-  var http = require('http'),
-      httpProxy = require('http-proxy');
-      
-  //
-  // Create a new instance of HttProxy to use in your server
-  //
-  var proxy = new httpProxy.HttpProxy();
-
-  //
-  // Create a regular http server and proxy its handler
-  //
-  http.createServer(function (req, res) {
-    //
-    // Put your custom server logic here, then proxy
-    //
+  setTimeout(function () {
     proxy.proxyRequest(req, res, {
       host: 'localhost',
-      port: 9000
-    });
-  }).listen(8001);
+      port: 9000, 
+      buffer: buffer
+    });      
+  }, 2000);
+}).listen(8000);
 
-  http.createServer(function (req, res) {
-    res.writeHead(200, { 'Content-Type': 'text/plain' });
-    res.write('request successfully proxied: ' + req.url +'\n' + JSON.stringify(req.headers, true, 2));
-    res.end();
-  }).listen(9000); 
-
+http.createServer(function (req, res) { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write('request successfully proxied: ' + req.url +'\n' + JSON.stringify(req.headers, true, 2)); + res.end(); +}).listen(9000); +``` + +### Proxy requests within another http server + +``` js +var http = require('http'), + httpProxy = require('http-proxy'); + +// +// Create a new instance of HttProxy to use in your server +// +var proxy = new httpProxy.HttpProxy(); + +// +// Create a regular http server and proxy its handler +// +http.createServer(function (req, res) { + // + // Put your custom server logic here, then proxy + // + proxy.proxyRequest(req, res, { + host: 'localhost', + port: 9000 + }); +}).listen(8001); + +http.createServer(function (req, res) { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write('request successfully proxied: ' + req.url +'\n' + JSON.stringify(req.headers, true, 2)); + res.end(); +}).listen(9000); +``` ### Proxy requests using a ProxyTable A Proxy Table is a simple lookup table that maps incoming requests to proxy target locations. Take a look at an example of the options you need to pass to httpProxy.createServer: -
-  var options = {
-    router: {
-      'foo.com/baz': '127.0.0.1:8001',
-      'foo.com/buz': '127.0.0.1:8002',
-      'bar.com/buz': '127.0.0.1:8003'
-    }
-  };
-
+ +``` js +var options = { + router: { + '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/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);
-
+ +``` js +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'
-    }
+``` js
+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"][4]. ### 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);
-
+ +``` js +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'. ### Using node-http-proxy from the command line When you install this package with npm, a node-http-proxy binary will become available to you. Using this binary is easy with some simple options: -
-  usage: node-http-proxy [options] 
 
-  All options should be set with the syntax --option=value
+``` js
+usage: node-http-proxy [options] 
 
-  options:
-    --port   PORT       Port that the proxy server should run on
-    --target HOST:PORT  Location of the server the proxy will target
-    --config OUTFILE    Location of the configuration file for the proxy server
-    --silent            Silence the log output from the proxy server
-    -h, --help          You're staring at it
-
+All options should be set with the syntax --option=value + +options: + --port PORT Port that the proxy server should run on + --target HOST:PORT Location of the server the proxy will target + --config OUTFILE Location of the configuration file for the proxy server + --silent Silence the log output from the proxy server + -h, --help You're staring at it +``` ### Proxying over HTTPS You have all the full flexibility of node-http-proxy offers in HTTPS as well as HTTP. The two basic scenarios are: with a stand-alone proxy server or in conjunction with another HTTPS server. -
-  var fs = require('fs'),
-      https = require('https'),
-      httpProxy = require('http-proxy');
-      
-  var options = {
-    https: {
-      key: fs.readFileSync('path/to/your/key.pem', 'utf8'),
-      cert: fs.readFileSync('path/to/your/cert.pem', 'utf8')
-    }
-  };
-  
-  //
-  // Create a standalone HTTPS proxy server
-  //
-  httpProxy.createServer(8000, 'localhost', options).listen(8001);
-  
-  //
-  // Create an instance of HttpProxy to use with another HTTPS server
-  //
-  var proxy = new httpProxy.HttpProxy({ https: true });
-  https.createServer(options.https, function (req, res) {
-    proxy.proxyRequest(req, res, {
-      host: 'localhost', 
-      port: 8000
-    })
-  }).listen(8002);
-  
-  //
-  // Create the target HTTPS server for both cases
-  //
-  https.createServer(options.https, function (req, res) {
-    res.writeHead(200, { 'Content-Type': 'text/plain' });
-    res.write('hello https\n');
-  	res.end();
-  }).listen(8000);
-
+ +``` js +var fs = require('fs'), + https = require('https'), + httpProxy = require('http-proxy'); + +var options = { + https: { + key: fs.readFileSync('path/to/your/key.pem', 'utf8'), + cert: fs.readFileSync('path/to/your/cert.pem', 'utf8') + } +}; + +// +// Create a standalone HTTPS proxy server +// +httpProxy.createServer(8000, 'localhost', options).listen(8001); + +// +// Create an instance of HttpProxy to use with another HTTPS server +// +var proxy = new httpProxy.HttpProxy({ https: true }); +https.createServer(options.https, function (req, res) { + proxy.proxyRequest(req, res, { + host: 'localhost', + port: 8000 + }) +}).listen(8002); + +// +// Create the target HTTPS server for both cases +// +https.createServer(options.https, function (req, res) { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write('hello https\n'); + res.end(); +}).listen(8000); +``` ### Proxying WebSockets Websockets are handled automatically when using the `httpProxy.createServer()`, but if you want to use it in conjunction with a stand-alone HTTP + WebSocket (such as [socket.io][5]) server here's how: -
-  var http = require('http'),
-      httpProxy = require('http-proxy');
-      
+
+``` js
+var http = require('http'),
+    httpProxy = require('http-proxy');
+    
+//
+// Create an instance of node-http-proxy
+//
+var proxy = new httpProxy.HttpProxy();
+
+var server = http.createServer(function (req, res) {
   //
-  // Create an instance of node-http-proxy
+  // Proxy normal HTTP requests
   //
-  var proxy = new httpProxy.HttpProxy();
-  
-  var server = http.createServer(function (req, res) {
-    //
-    // Proxy normal HTTP requests
-    //
-    proxy.proxyRequest(req, res, {
-      host: 'localhost',
-      port: 8000
-    })
+  proxy.proxyRequest(req, res, {
+    host: 'localhost',
+    port: 8000
+  })
+});
+
+server.on('upgrade', function(req, socket, head) {
+  //
+  // Proxy websocket requests too
+  //
+  proxy.proxyWebSocketRequest(req, socket, head, {
+    host: 'localhost',
+    port: 8000
   });
-  
-  server.on('upgrade', function(req, socket, head) {
-    //
-    // Proxy websocket requests too
-    //
-    proxy.proxyWebSocketRequest(req, socket, head, {
-      host: 'localhost',
-      port: 8000
-    });
-  });
-
+}); +```
### Why doesn't node-http-proxy have more advanced features like x, y, or z? @@ -299,10 +312,11 @@ Websockets are handled automatically when using the `httpProxy.createServer()`, If you have a suggestion for a feature currently not supported, feel free to open a [support issue][6]. 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. ## Run Tests -
-  vows test/*-test.js --spec
-  vows test/*-test.js --spec --https
-
+ +``` +vows test/*-test.js --spec +vows test/*-test.js --spec --https +```
### License @@ -336,4 +350,4 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. [3]: https://github.com/nodejitsu/node-http-proxy/tree/v0.5.0/examples [4]: http://www.ietf.org/rfc/rfc2616.txt [5]: http://socket.io -[6]: http://github.com/nodejitsu/node-http-proxy/issues \ No newline at end of file +[6]: http://github.com/nodejitsu/node-http-proxy/issues diff --git a/docs/node-http-proxy.html b/docs/node-http-proxy.html index 7ef31c9..9e5a61f 100644 --- a/docs/node-http-proxy.html +++ b/docs/node-http-proxy.html @@ -26,18 +26,48 @@ var util = require('util'), http = require('http'), + https = require('https'), events = require('events'), ProxyTable = require('./proxy-table').ProxyTable, - maxSockets = 100;

Version 0.4.2

exports.version = [0, 4, 2];

function _getAgent (host, port)

+ maxSockets = 100;

Version 0.5.3 // 5/17/2011

exports.version = [0, 5, 3];

function _getAgent (host, port, secure)

@host {string} Host of the agent to get

@port {number} Port of the agent to get

-

Retreives an agent from the http module -and sets the maxSockets property appropriately.

function _getAgent (host, port) {

TODO (indexzero): Make this configurable for http / https

  var agent = http.getAgent(host, port);
+

@secure {boolean} Value indicating whether or not to use HTTPS

+ +

Retreives an agent from the http or https module +and sets the maxSockets property appropriately.

function _getAgent (host, port, secure) {
+  var agent = !secure ? http.getAgent(host, port) : https.getAgent({ 
+    host: host, 
+    port: port 
+  });
+      
   agent.maxSockets = maxSockets;
   return agent;
+}

function _getProtocol (secure, outgoing)

+ +

@secure {Object|boolean} Settings for https

+ +

@outgoing {Object} Outgoing request options

+ +

Returns the appropriate protocol based on the settings in +secure. If the protocol is https this function will update +the options in outgoing as appropriate by adding ca, key, +and cert if they exist in secure.

function _getProtocol (secure, outgoing) {
+  var protocol = secure ? https : http;
+  
+  if (typeof secure === 'object') {
+    outgoing = outgoing || {};
+    ['ca', 'cert', 'key'].forEach(function (prop) {
+      if (secure[prop]) {
+        outgoing[prop] = secure[prop];
+      }
+    })
+  }
+  
+  return protocol;
 }

function getMaxSockets ()

Returns the maximum number of sockets @@ -67,11 +97,10 @@ made by all instances of HttpProxy

  • `httpProxy.createServer(9000, 'localhost', options)
  • httpPRoxy.createServer(function (req, res, proxy) { ... })
  • exports.createServer = function () {
    -  var args, callback, port, host, forward, 
    -      silent, options, proxy, server;
    -  
    -  args = Array.prototype.slice.call(arguments);
    -  callback = typeof args[args.length - 1] === 'function' && args.pop();
    +  var args = Array.prototype.slice.call(arguments), 
    +      callback = typeof args[0] === 'function' && args.shift(),
    +      options = {},
    +      port, host, forward, silent, proxy, server;
       
       if (args.length >= 2) {
         port = args[0];
    @@ -86,21 +115,27 @@ made by all instances of HttpProxy

    } proxy = new HttpProxy(options); - server = http.createServer(function (req, res) { - proxy.emit('request', req, request.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);
    +  
    +  handler = function (req, res) {
    +    if (callback) {

    If we were passed a callback to process the request +or response in some way, then call it.

          callback(req, res, proxy);
         } 
    -    else if (port && host) {
    -      proxy.proxyRequest(req, res, port, host);
    +    else if (port && host) {

    If we have a target host and port for the request +then proxy to the specified location.

          proxy.proxyRequest(req, res, {
    +        port: port, 
    +        host: host
    +      });
         }
    -    else if (proxy.proxyTable) {
    -      proxy.proxyRequest(req, res);
    +    else if (proxy.proxyTable) {

    If the proxy is configured with a ProxyTable +instance then use that before failing.

          proxy.proxyRequest(req, res);
         }
    -    else {
    -      throw new Error('Cannot proxy without port, host, or router.')
    +    else {

    Otherwise this server is improperly configured.

          throw new Error('Cannot proxy without port, host, or router.')
         }
    -  });
    +  };
    +  
    +  server = options.https 
    +    ? https.createServer(options.https, handler)
    +    : http.createServer(handler);
       
       server.on('close', function () {
         proxy.close();
    @@ -110,14 +145,19 @@ or response in some way, then call it.

    server.emit('routes', routes); }); - if (!callback) {

    WebSocket support: if callback is empty tunnel -websocket request automatically

        server.on('upgrade', function(req, socket, head) {

    Tunnel websocket requests too

          
    -      proxy.proxyWebSocketRequest(port, host);
    +  if (!callback) {

    WebSocket support: if callback is empty tunnel +websocket request automatically

        server.on('upgrade', function(req, socket, head) {

    Tunnel websocket requests too

          
    +      proxy.proxyWebSocketRequest(req, socket, head, {
    +        port: port,
    +        host: host
    +      });
         });
       }
    -
    +  

    Set the proxy on the server so it is available +to the consumer of the server

      server.proxy = proxy;
    +  
       return server;
    -};

    function HttpProxy (options)

    +};

    function HttpProxy (options)

    @options {Object} Options for this instance.

    @@ -139,17 +179,19 @@ for managing the life-cycle of streaming reverse proxyied HTTP requests.

    var HttpProxy = exports.HttpProxy = function (options) {
       events.EventEmitter.call(this);
       
    -  options = options || {};
    -  this.options = options;
    +  var self          = this;
    +  options           = options || {};
    +  this.forward      = options.forward;
    +  this.https        = options.https;
    +  this.changeOrigin = options.changeOrigin || false;
       
       if (options.router) {
    -    var self = this;
         this.proxyTable = new ProxyTable(options.router, options.silent, options.hostnameOnly);
         this.proxyTable.on('routes', function (routes) {
           self.emit('routes', routes);
         });
       }
    -};

    Inherit from events.EventEmitter

    util.inherits(HttpProxy, events.EventEmitter);

    function buffer (obj)

    +};

    Inherit from events.EventEmitter

    util.inherits(HttpProxy, events.EventEmitter);

    function buffer (obj)

    @obj {Object} Object to pause events from

    @@ -167,7 +209,7 @@ the async operation has completed, otherwise these

    Attribution: This approach is based heavily on Connect. -However, this is not a big leap from the implementation in node-http-proxy < 0.4.0. +However, this is not a big leap from the implementation in node-http-proxy < 0.4.0. This simply chooses to manage the scope of the events on a new Object literal as opposed to on the HttpProxy instance.

    HttpProxy.prototype.buffer = function (obj) {
       var onData, onEnd, events = [];
    @@ -192,51 +234,54 @@ This simply chooses to manage the scope of  the events on a new Object literal a
           }
         }
       };
    -};

    function close ()

    +};

    function close ()

    Frees the resources associated with this instance, if they exist.

    HttpProxy.prototype.close = function () {
       if (this.proxyTable) this.proxyTable.close();
    -};

    function proxyRequest (req, res, [port, host, paused])

    +};

    function proxyRequest (req, res, [port, host, paused])

    @req {ServerRequest} Incoming HTTP Request to proxy.

    @res {ServerResponse} Outgoing HTTP Request to write proxied data to.

    -

    @port {number} Optional Port to use on the proxy target host.

    +

    @options {Object} Options for the outgoing proxy request.

    -

    @host {string} Optional Host of the proxy target.

    - -

    @buffer {Object} Optional Result from httpProxy.buffer(req)

    HttpProxy.prototype.proxyRequest = function (req, res, port, host, buffer) {
    -  var self = this, reverseProxy, location, errState = false, opts;
    -  

    Check the proxy table for this instance to see if we need +

    options.port {number} Port to use on the proxy target host.
    +options.host {string} Host of the proxy target.
    +options.buffer {Object} Result from `httpProxy.buffer(req)`
    +options.https {Object|boolean} Settings for https.
    +
    HttpProxy.prototype.proxyRequest = function (req, res, options) {
    +  var self = this, errState = false, location, outgoing, protocol, reverseProxy;
    +  

    Create an empty options hash if none is passed.

      options = options || {};
    +  

    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) {
    +arguments are supplied to proxyRequest.

      if (this.proxyTable && !options.host) {
         location = this.proxyTable.getProxyLocation(req);
    -    

    If no location is returned from the ProxyTable instance +

    If no location is returned from the ProxyTable instance then respond with 404 since we do not have a valid proxy target.

        if (!location) {
           res.writeHead(404);
           return res.end();
         }
    -    

    When using the ProxyTable in conjunction with an HttpProxy instance +

    When using the ProxyTable in conjunction with an HttpProxy instance only the following arguments are valid:

        buffer = port;
    -    port = location.port;
    -    host = location.host;
    +
  • proxy.proxyRequest(req, res, { host: 'localhost' }): This will be skipped
  • +
  • proxy.proxyRequest(req, res, { buffer: buffer }): Buffer will get updated appropriately
  • +
  • proxy.proxyRequest(req, res): Options will be assigned appropriately.
  • +
        options.port = location.port;
    +    options.host = location.host;
       }
    -  

    Emit the start event indicating that we have begun the proxy operation.

      this.emit('start', req, res, host, port);
    -  

    If forwarding is enabled for this instance, foward proxy the -specified request to the address provided in this.options.forward

      if (this.options.forward) {
    -    this.emit('forward', req, res, this.options.forward.host, this.options.forward.port);
    +  

    Add x-forwarded-for header to availible client IP to apps behind proxy

      req.headers['x-forwarded-for'] = req.connection.remoteAddress;
    +  

    Emit the start event indicating that we have begun the proxy operation.

      this.emit('start', req, res, options);
    +  

    If forwarding is enabled for this instance, foward proxy the +specified request to the address provided in this.forward

      if (this.forward) {
    +    this.emit('forward', req, res, this.forward);
         this._forwardRequest(req);
       }
    -  

    function proxyError (err)

    +

    function proxyError (err)

    @err {Error} Error contacting the proxy target

    @@ -252,28 +297,30 @@ contacting the proxy target at host / port.

    res.end(); } - var opts = { - host: host, - port: port, - agent: _getAgent(host, port), + outgoing = { + host: options.host, + port: options.port, + agent: _getAgent(options.host, options.port, options.https || this.https), method: req.method, path: req.url, headers: req.headers }; -

    Force the connection header to be 'close' until -node.js core re-implements 'keep-alive'.

      opts.headers['connection'] = 'close';
    -  

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

      reverseProxy = http.request(opts, function (response) {
    -    

    Process the reverseProxy response when it's received.

        if (response.headers.connection) {
    +    

    Force the connection header to be 'close' until +node.js core re-implements 'keep-alive'.

      outgoing.headers['connection'] = 'close';
    +  
    +  protocol = _getProtocol(options.https || this.https, outgoing);
    +  

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

      reverseProxy = protocol.request(outgoing, function (response) {
    +    

    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 headers of the client response

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

    response.statusCode === 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();
    -    }

    For each data chunk received from the reverseProxy + }

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

    When the reverseProxy response ends, end the + });

    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 @@ -281,164 +328,182 @@ removed.

    if (!errState) { reverseProxy.removeListener('error', proxyError); res.end(); +

    Emit the end event now that we have completed proxying

            self.emit('end', req, res);
           }
         });
       });
    -  

    Handle 'error' events from the reverseProxy.

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

    For each data chunk received from the incoming +

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

    When the incoming req ends, end the corresponding reverseProxy + });

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

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

    If we have been passed buffered data, resume it.

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

    If we have been passed buffered data, resume it.

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

    @private function _forwardRequest (req)

    +

    @private function _forwardRequest (req)

    @req {ServerRequest} Incoming HTTP Request to proxy.

    Forwards the specified req to the location specified -by this.options.forward ignoring errors and the subsequent response.

    HttpProxy.prototype._forwardRequest = function (req) {
    -  var self = this, port, host, forwardProxy, opts;
    +by this.forward ignoring errors and the subsequent response.

    HttpProxy.prototype._forwardRequest = function (req) {
    +  var self = this, port, host, outgoing, protocol, forwardProxy;
     
    -  port = this.options.forward.port;
    -  host = this.options.forward.host;
    +  port = this.forward.port;
    +  host = this.forward.host;
       
    -  opts = {
    +  outgoing = {
         host: host,
         port: port,
    -    agent: _getAgent(host, port),
    +    agent: _getAgent(host, port, this.forward.https),
         method: req.method,
         path: req.url,
         headers: req.headers
       };
    -  

    Force the connection header to be 'close' until -node.js core re-implements 'keep-alive'.

      opts.headers['connection'] = 'close';
    -  

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

      forwardProxy = http.request(opts, function (response) {

    Ignore the response from the forward proxy since this is a 'fire-and-forget' proxy. +

    Force the connection header to be 'close' until +node.js core re-implements 'keep-alive'.

      outgoing.headers['connection'] = 'close';
    +  
    +  protocol = _getProtocol(this.forward.https, outgoing);
    +  

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

      forwardProxy = protocol.request(outgoing, 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.once('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.once('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 -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 +HttpProxy.prototype.proxyWebSocketRequest = function (req, socket, head, options) { + var self = this, outgoing, errState = false, CRLF = '\r\n';

    WebSocket requests has method = GET

      if (req.method !== 'GET' || req.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) {
    +For client set encoding

      function _socket(socket, keepAlive) {
         socket.setTimeout(0);
         socket.setNoDelay(true);
    -    if (server) {
    -      socket.setKeepAlive(true, 0);
    +    if (keepAlive) {
    +      if (socket.setKeepAlive) {
    +        socket.setKeepAlive(true, 0);
    +      }
    +      else if (socket.pair.cleartext.socket.setKeepAlive) {
    +        socket.pair.cleartext.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);
    -      

    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 = true;
    -
    -    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();

    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);
    -      } 
    -      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);
    -    } 
    -    catch(e) {
    -      request.end();
    +  
    +  function onUpgrade(out, reverseProxy) {
    +    if (!out) {
    +      reverseProxy.end();
           socket.end();
    +      return;
         }
    -    self.unwatch(socket);
    -  });

    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

        reverseProxy.on('data', listeners._r_data = function(data) {

    Pass data to client

          if (out.incoming.socket.writable) {
             try {
    -          socket.write(data);
    +          out.incoming.socket.write(data);
             } 
             catch (e) {
    -          socket.end();
    -          reverse_proxy.end();
    +          out.incoming.socket.end();
    +          reverseProxy.end();
             }
           }
         });
     
    -    socket.on('data', listeners._data = function(data) {

    Pass data from client to server

          try {
    -        reverse_proxy.write(data);
    +    out.incoming.socket.on('data', listeners._data = function(data) {

    Pass data from client to server

          try {
    +        reverseProxy.write(data);
           } 
           catch (e) {
    -        reverse_proxy.end();
    +        reverseProxy.end();
             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 event listeners from reverseProxy

        function detach() {
    +      reverseProxy.removeListener('close', listeners._r_close);
    +      reverseProxy.removeListener('data', listeners._r_data);
    +      out.incoming.socket.removeListener('data', listeners._data);
    +      out.incoming.socket.removeListener('close', listeners._close);
    +    }

    Hook disconnections

        reverseProxy.on('end', listeners._r_close = function() {
    +      out.incoming.socket.end();
           detach();
         });
     
         socket.on('end', listeners._close = function() {
    -      reverse_proxy.end();
    +      reverseProxy.end();
           detach();
         });
    +  };

    Client socket

      _socket(socket);
    +  

    Remote host address

      var protocolName = options.https || this.https ? 'https' : 'http',
    +      agent        = _getAgent(options.host, options.port, options.https || this.https),
    +      remoteHost   = options.host + (options.port - 80 === 0 ? '' : ':' + options.port);

    Change headers

      if (this.changeOrigin) {
    +    req.headers.host   = remoteHost;
    +    req.headers.origin = protocolName + '://' + remoteHost;
    +  }
    +  
    +  outgoing = {
    +    host: options.host,
    +    port: options.port,
    +    method: 'GET',
    +    path: req.url,
    +    headers: req.headers,
    +  };

    Make the outgoing WebSocket request

      var request = agent.appendMessage(outgoing);

    Here we set the incoming req, socket and head data to the outgoing +request so that we can reuse this data later on in the closure scope +available to the upgrade event. This bookkeeping is not tracked anywhere +in nodejs core and is very specific to proxying WebSockets.

      request.agent = agent;
    +  request.incoming = {
    +    request: req,
    +    socket: socket,
    +    head: head
       };
    +  

    If the agent for this particular host and port combination +is not already listening for the upgrade event, then do so once. +This will force us not to disconnect.

    + +

    In addition, it's important to note the closure scope here. Since +there is no mapping of the

      if (!agent._events || agent._events['upgrade'].length === 0) {
    +    agent.on('upgrade', function (out, remoteSocket, head) {

    Prepare socket

          _socket(remoteSocket, true);
    +      

    Emit event

          onUpgrade(remoteSocket._httpMessage, remoteSocket);
    +    });
    +  }
    +  
    +  if (typeof request.socket !== 'undefined') {
    +    request.socket.on('data', function handshake (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(remoteHost, options.host)
    +                   .replace(remoteHost, options.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);
    +  } 
    +  catch (ex) {
    +    request.end();
    +    socket.end();
    +  }
    +  

    If we have been passed buffered data, resume it.

      if (options.buffer && !errState) {
    +    options.buffer.resume();
    +  }
     };
     
     
    \ No newline at end of file diff --git a/examples/basic-proxy-https.js b/examples/basic-proxy-https.js index 9d1a4a2..01f9716 100644 --- a/examples/basic-proxy-https.js +++ b/examples/basic-proxy-https.js @@ -46,8 +46,8 @@ https.createServer(opts, function (req, res) { // Create the proxy server listening on port 443. // httpProxy.createServer(443, 'localhost', { - https: opts, + https: opts }).listen(8080); util.puts('https proxy server'.blue + ' started '.green.bold + 'on port '.blue + '8000'.yellow); -util.puts('https server '.blue + 'started '.green.bold + 'on port '.blue + '8080 '.yellow); \ No newline at end of file +util.puts('https server '.blue + 'started '.green.bold + 'on port '.blue + '8080 '.yellow); diff --git a/examples/basic-proxy.js b/examples/basic-proxy.js index 1c1000e..5827625 100644 --- a/examples/basic-proxy.js +++ b/examples/basic-proxy.js @@ -25,7 +25,7 @@ */ var util = require('util'), - colors = require('colors') + colors = require('colors'), http = require('http'), httpProxy = require('./../lib/node-http-proxy'); diff --git a/examples/forward-proxy.js b/examples/forward-proxy.js index 8a43dad..667d672 100644 --- a/examples/forward-proxy.js +++ b/examples/forward-proxy.js @@ -25,10 +25,10 @@ */ var util = require('util'), - colors = require('colors') + colors = require('colors'), http = require('http'), httpProxy = require('./../lib/node-http-proxy'); - + // // Setup proxy server with forwarding // @@ -52,12 +52,12 @@ http.createServer(function (req, res) { // Target Http Forwarding Server // http.createServer(function (req, res) { - util.puts('Receiving forward for: ' + req.url) + util.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); -util.puts('http proxy server '.blue + 'started '.green.bold + 'on port '.blue + '8003 '.yellow + 'with forward proxy'.magenta.underline) +util.puts('http proxy server '.blue + 'started '.green.bold + 'on port '.blue + '8003 '.yellow + 'with forward proxy'.magenta.underline); util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '9000 '.yellow); util.puts('http forward server '.blue + 'started '.green.bold + 'on port '.blue + '9001 '.yellow); diff --git a/examples/latent-proxy.js b/examples/latent-proxy.js index aea2392..f2e51f8 100644 --- a/examples/latent-proxy.js +++ b/examples/latent-proxy.js @@ -25,10 +25,10 @@ */ var util = require('util'), - colors = require('colors') + colors = require('colors'), http = require('http'), httpProxy = require('./../lib/node-http-proxy'); - + // // Http Proxy Server with Latency // @@ -36,11 +36,11 @@ httpProxy.createServer(function (req, res, proxy) { var buffer = proxy.buffer(req); setTimeout(function() { proxy.proxyRequest(req, res, { - port: 9000, - host: 'localhost', + port: 9000, + host: 'localhost', buffer: buffer }); - }, 200) + }, 200); }).listen(8002); // @@ -53,4 +53,4 @@ http.createServer(function (req, res) { }).listen(9000); util.puts('http proxy server '.blue + 'started '.green.bold + 'on port '.blue + '8002 '.yellow + 'with latency'.magenta.underline); -util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '9000 '.yellow); \ No newline at end of file +util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '9000 '.yellow); diff --git a/examples/proxy-table.js b/examples/proxy-table.js index a3817a2..5036dcd 100644 --- a/examples/proxy-table.js +++ b/examples/proxy-table.js @@ -25,10 +25,10 @@ */ var util = require('util'), - colors = require('colors') + colors = require('colors'), http = require('http'), httpProxy = require('./../lib/node-http-proxy'); - + // // Http Proxy Server with Proxy Table // @@ -47,5 +47,5 @@ http.createServer(function (req, res) { res.end(); }).listen(9000); -util.puts('http proxy server '.blue + 'started '.green.bold + 'on port '.blue + '8001 '.yellow + 'with proxy table'.magenta.underline) +util.puts('http proxy server '.blue + 'started '.green.bold + 'on port '.blue + '8001 '.yellow + 'with proxy table'.magenta.underline); util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '9000 '.yellow); diff --git a/examples/standalone-proxy.js b/examples/standalone-proxy.js index 9cab8c0..c5b9c27 100644 --- a/examples/standalone-proxy.js +++ b/examples/standalone-proxy.js @@ -25,10 +25,10 @@ */ var util = require('util'), - colors = require('colors') + colors = require('colors'), http = require('http'), httpProxy = require('./../lib/node-http-proxy'); - + // // Http Server with proxyRequest Handler and Latency // @@ -37,8 +37,8 @@ http.createServer(function (req, res) { var buffer = proxy.buffer(req); setTimeout(function() { proxy.proxyRequest(req, res, { - port: 9000, - host: 'localhost', + port: 9000, + host: 'localhost', buffer: buffer }); }, 200); @@ -54,4 +54,4 @@ http.createServer(function (req, res) { }).listen(9000); util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '8004 '.yellow + 'with proxyRequest handler'.cyan.underline + ' and latency'.magenta); -util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '9000 '.yellow); \ No newline at end of file +util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '9000 '.yellow); diff --git a/examples/web-socket-proxy.js b/examples/web-socket-proxy.js index f31f300..96678b5 100644 --- a/examples/web-socket-proxy.js +++ b/examples/web-socket-proxy.js @@ -32,7 +32,7 @@ var sys = require('sys'), try { var utils = require('socket.io/lib/socket.io/utils'), - io = require('socket.io'); + io = require('socket.io'); } catch (ex) { console.error('Socket.io is required for this example:'); @@ -55,11 +55,11 @@ server.listen(8080); var socket = io.listen(server); socket.on('connection', function (client) { sys.debug('Got websocket connection'); - + client.on('message', function (msg) { sys.debug('Got message from client: ' + msg); }); - + socket.broadcast('from server'); }); diff --git a/lib/node-http-proxy.js b/lib/node-http-proxy.js index 9f3521c..87de4f5 100644 --- a/lib/node-http-proxy.js +++ b/lib/node-http-proxy.js @@ -32,9 +32,9 @@ var util = require('util'), maxSockets = 100; // -// ### Version 0.5.0 +// ### Version 0.5.3 // 5/17/2011 // -exports.version = [0, 5, 0]; +exports.version = [0, 5, 3]; // // ### function _getAgent (host, port, secure) @@ -219,10 +219,11 @@ exports.createServer = function () { var HttpProxy = exports.HttpProxy = function (options) { events.EventEmitter.call(this); - var self = this; - options = options || {}; - this.forward = options.forward; - this.https = options.https; + var self = this; + options = options || {}; + this.forward = options.forward; + this.https = options.https; + this.changeOrigin = options.changeOrigin || false; if (options.router) { this.proxyTable = new ProxyTable(options.router, options.silent, options.hostnameOnly); @@ -523,31 +524,42 @@ HttpProxy.prototype.proxyWebSocketRequest = function (req, socket, head, options socket.setTimeout(0); socket.setNoDelay(true); if (keepAlive) { - socket.setKeepAlive(true, 0); + if (socket.setKeepAlive) { + socket.setKeepAlive(true, 0); + } + else if (socket.pair.cleartext.socket.setKeepAlive) { + socket.pair.cleartext.socket.setKeepAlive(true, 0); + } } else { socket.setEncoding('utf8'); } } - function onUpgrade(reverseProxy) { + function onUpgrade(out, reverseProxy) { + if (!out) { + reverseProxy.end(); + socket.end(); + return; + } + var listeners = {}; // We're now connected to the server, so lets change server socket reverseProxy.on('data', listeners._r_data = function(data) { // Pass data to client - if (socket.writable) { + if (out.incoming.socket.writable) { try { - socket.write(data); + out.incoming.socket.write(data); } catch (e) { - socket.end(); + out.incoming.socket.end(); reverseProxy.end(); } } }); - socket.on('data', listeners._data = function(data) { + out.incoming.socket.on('data', listeners._data = function(data) { // Pass data from client to server try { reverseProxy.write(data); @@ -562,13 +574,13 @@ HttpProxy.prototype.proxyWebSocketRequest = function (req, socket, head, options function detach() { reverseProxy.removeListener('close', listeners._r_close); reverseProxy.removeListener('data', listeners._r_data); - socket.removeListener('data', listeners._data); - socket.removeListener('close', listeners._close); + out.incoming.socket.removeListener('data', listeners._data); + out.incoming.socket.removeListener('close', listeners._close); } // Hook disconnections reverseProxy.on('end', listeners._r_close = function() { - socket.end(); + out.incoming.socket.end(); detach(); }); @@ -582,37 +594,60 @@ HttpProxy.prototype.proxyWebSocketRequest = function (req, socket, head, options _socket(socket); // Remote host address - var agent = _getAgent(options.host, options.port), - remoteHost = options.host + (options.port - 80 === 0 ? '' : ':' + options.port); + var protocolName = options.https || this.https ? 'https' : 'http', + agent = _getAgent(options.host, options.port, options.https || this.https), + remoteHost = options.host + (options.port - 80 === 0 ? '' : ':' + options.port); // Change headers - req.headers.host = remoteHost; - req.headers.origin = 'http://' + options.host; + if (this.changeOrigin) { + req.headers.host = remoteHost; + req.headers.origin = protocolName + '://' + remoteHost; + } outgoing = { host: options.host, port: options.port, - agent: agent, method: 'GET', path: req.url, - headers: req.headers + headers: req.headers, }; // Make the outgoing WebSocket request - var request = http.request(outgoing, function () { }); - - // Not disconnect on update - agent.on('upgrade', function(request, remoteSocket, head) { - // Prepare socket - _socket(remoteSocket, true); + var request = agent.appendMessage(outgoing); - // Emit event - onUpgrade(remoteSocket); - }); + // + // Here we set the incoming `req`, `socket` and `head` data to the outgoing + // request so that we can reuse this data later on in the closure scope + // available to the `upgrade` event. This bookkeeping is not tracked anywhere + // in nodejs core and is **very** specific to proxying WebSockets. + // + request.agent = agent; + request.incoming = { + request: req, + socket: socket, + head: head + }; + + // + // If the agent for this particular `host` and `port` combination + // is not already listening for the `upgrade` event, then do so once. + // This will force us not to disconnect. + // + // In addition, it's important to note the closure scope here. Since + // there is no mapping of the + // + if (!agent._events || agent._events['upgrade'].length === 0) { + agent.on('upgrade', function (out, remoteSocket, head) { + // Prepare socket + _socket(remoteSocket, true); + + // Emit event + onUpgrade(remoteSocket._httpMessage, remoteSocket); + }); + } - var handshake; if (typeof request.socket !== 'undefined') { - request.socket.on('data', handshake = function(data) { + request.socket.on('data', function handshake (data) { // Handshaking // Ok, kind of harmfull part of code diff --git a/package.json b/package.json index d362ace..493707a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "http-proxy", "description": "A full-featured http reverse proxy for node.js", - "version": "0.5.1", + "version": "0.5.3", "author": "Charlie Robbins ", "contributors": [ { "name": "Mikeal Rogers", "email": "mikeal.rogers@gmail.com" }, @@ -16,11 +16,15 @@ "dependencies": { "colors": ">= 0.5.x", "optimist": ">= 0.1.x", - "request": ">= 1.9.x", - "vows": ">= 0.5.x" + "request": ">= 1.9.x" + }, + "devDependencies": { + "vows": ">= 0.5.x", + "socket.io": ">= 0.6.x", + "docco": ">= 0.3.x" }, "main": "./lib/node-http-proxy", "bin": { "node-http-proxy": "./bin/node-http-proxy" }, "scripts": { "test": "vows test/*-test.js --spec" }, "engines": { "node": "= 0.4.7" } -} \ No newline at end of file +} diff --git a/test/helpers.js b/test/helpers.js index 598a426..42834da 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -37,7 +37,7 @@ var TestRunner = exports.TestRunner = function (protocol) { this.options = {}; this.protocol = protocol; this.testServers = []; - + if (protocol === 'https') { this.options.https = loadHttps(); } @@ -47,17 +47,17 @@ TestRunner.prototype.assertProxied = function (host, proxyPort, port, createProx var self = this, assertion = "should receive 'hello " + host + "'", output = 'hello ' + host; - + var test = { topic: function () { var that = this, options = { - method: 'GET', + method: 'GET', uri: self.protocol + '://localhost:' + proxyPort, headers: { host: host } }; - + function startTest () { if (port) { return self.startTargetServer(port, output, function () { @@ -67,47 +67,48 @@ TestRunner.prototype.assertProxied = function (host, proxyPort, port, createProx 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); }; - + return test; }; TestRunner.prototype.assertResponseCode = function (proxyPort, statusCode, createProxy) { - var assertion = "should receive " + statusCode + " responseCode"; - + var assertion = "should receive " + statusCode + " responseCode", + protocol = this.protocol; + var test = { topic: function () { var that = this, options = { - method: 'GET', - uri: 'http://localhost:' + proxyPort, + method: 'GET', + uri: protocol + '://localhost:' + proxyPort, headers: { host: 'unknown.com' } }; - + if (createProxy) { return createProxy(function () { - request(options, that.callback); + request(options, that.callback); }); } - + request(options, this.callback); } }; - + test[assertion] = function (err, res, body) { assert.isNull(err); assert.equal(res.statusCode, statusCode); }; - + return test; }; @@ -115,26 +116,28 @@ TestRunner.prototype.assertResponseCode = function (proxyPort, statusCode, creat // Creates the reverse proxy server // TestRunner.prototype.startProxyServer = function (port, targetPort, host, callback) { - var that = this, proxyServer = httpProxy.createServer(targetPort, host); - + var that = this, + options = that.options, + proxyServer = httpProxy.createServer(targetPort, host, options); + proxyServer.listen(port, function () { that.testServers.push(proxyServer); callback(null, proxyServer); - }); + }); }; -// +// // Creates the reverse proxy server with a specified latency // TestRunner.prototype.startLatentProxyServer = function (port, targetPort, host, latency, callback) { // Initialize the nodeProxy and start proxying the request var that = this, proxyServer = httpProxy.createServer(function (req, res, proxy) { var buffer = proxy.buffer(req); - + setTimeout(function () { proxy.proxyRequest(req, res, { - port: targetPort, - host: host, + port: targetPort, + host: host, buffer: buffer }); }, latency); @@ -150,12 +153,12 @@ TestRunner.prototype.startLatentProxyServer = function (port, targetPort, host, // Creates the reverse proxy server with a ProxyTable // TestRunner.prototype.startProxyServerWithTable = function (port, options, callback) { - var that = this, proxyServer = httpProxy.createServer(merge({}, options, this.options)); + var that = this, proxyServer = httpProxy.createServer(merge({}, options, this.options)); proxyServer.listen(port, function () { that.testServers.push(proxyServer); callback(); }); - + return proxyServer; }; @@ -164,21 +167,28 @@ TestRunner.prototype.startProxyServerWithTable = function (port, options, callba // TestRunner.prototype.startProxyServerWithTableAndLatency = function (port, latency, options, callback) { // Initialize the nodeProxy and start proxying the request - var proxyServer, that = this, proxy = new httpProxy.HttpProxy(merge({}, options, this.options)); - proxyServer = http.createServer(function (req, res) { + var proxyServer, + that = this, + proxy = new httpProxy.HttpProxy(merge({}, options, that.options)); + + var handler = function (req, res) { var buffer = proxy.buffer(req); setTimeout(function () { proxy.proxyRequest(req, res, { buffer: buffer }); }, latency); - }, this.options); - + }; + + proxyServer = that.options.https + ? https.createServer(that.options.https, handler, that.options) + : http.createServer(handler, that.options); + proxyServer.listen(port, function () { that.testServers.push(proxyServer); callback(); }); - + return proxyServer; }; @@ -186,7 +196,7 @@ TestRunner.prototype.startProxyServerWithTableAndLatency = function (port, laten // Creates proxy server forwarding to the specified options // TestRunner.prototype.startProxyServerWithForwarding = function (port, targetPort, host, options, callback) { - var that = this, proxyServer = httpProxy.createServer(targetPort, host, merge({}, options, this.options)); + var that = this, proxyServer = httpProxy.createServer(targetPort, host, merge({}, options, this.options)); proxyServer.listen(port, function () { that.testServers.push(proxyServer); callback(null, proxyServer); @@ -200,13 +210,13 @@ TestRunner.prototype.startTargetServer = function (port, output, callback) { var that = this, targetServer, handler = function (req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write(output); - res.end(); + res.end(); }; - - targetServer = this.options.https + + targetServer = this.options.https ? https.createServer(this.options.https, handler) : http.createServer(handler); - + targetServer.listen(port, function () { that.testServers.push(targetServer); callback(null, targetServer); @@ -222,4 +232,4 @@ TestRunner.prototype.closeServers = function () { }); return this.testServers; -}; \ No newline at end of file +}; diff --git a/test/node-http-proxy-test.js b/test/node-http-proxy-test.js index ba0866b..536b3ab 100644 --- a/test/node-http-proxy-test.js +++ b/test/node-http-proxy-test.js @@ -89,4 +89,4 @@ vows.describe('node-http-proxy/' + protocol).addBatch({ assert.isTrue(true); } } -}).export(module); \ No newline at end of file +}).export(module); diff --git a/test/proxy-table-test.js b/test/proxy-table-test.js index aaf2e79..ff83066 100644 --- a/test/proxy-table-test.js +++ b/test/proxy-table-test.js @@ -14,7 +14,7 @@ var fs = require('fs'), helpers = require('./helpers'), argv = require('optimist').argv, TestRunner = helpers.TestRunner; - + var protocol = argv.https ? 'https' : 'http', runner = new TestRunner(protocol), routeFile = path.join(__dirname, 'config.json'); @@ -78,13 +78,13 @@ vows.describe('node-http-proxy/proxy-table/' + protocol).addBatch({ data = fs.readFileSync(routeFile), config = JSON.parse(data); - config.router['dynamic.com'] = "127.0.0.1:8103" + config.router['dynamic.com'] = "127.0.0.1:8103"; fs.writeFileSync(routeFile, JSON.stringify(config)); - + this.server.on('routes', function () { var options = { - method: 'GET', - uri: 'http://localhost:8100', + method: 'GET', + uri: protocol + '://localhost:8100', headers: { host: 'dynamic.com' } @@ -126,4 +126,4 @@ vows.describe('node-http-proxy/proxy-table/' + protocol).addBatch({ assert.isTrue(true); } } -}).export(module); \ No newline at end of file +}).export(module); diff --git a/test/web-socket-proxy-test.js b/test/web-socket-proxy-test.js index 726f095..34ba113 100644 --- a/test/web-socket-proxy-test.js +++ b/test/web-socket-proxy-test.js @@ -23,18 +23,19 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - + var vows = require('vows'), util = require('util'), colors = require('colors'), request = require('request'), assert = require('assert'), + argv = require('optimist').argv, websocket = require('./../vendor/websocket'), helpers = require('./helpers'); try { var utils = require('socket.io/lib/socket.io/utils'), - io = require('socket.io'); + io = require('socket.io'); } catch (ex) { console.error('Socket.io is required for this test:'); @@ -42,7 +43,9 @@ catch (ex) { process.exit(1); } -var runner = new helpers.TestRunner(); +var protocol = argv.https ? 'https' : 'http', + wsprotocol = argv.https ? 'wss' : 'ws', + runner = new helpers.TestRunner(protocol); vows.describe('node-http-proxy/websocket').addBatch({ "When using server created by httpProxy.createServer()": { @@ -50,61 +53,83 @@ vows.describe('node-http-proxy/websocket').addBatch({ "when an inbound message is sent from a WebSocket client": { topic: function () { var that = this; - + runner.startTargetServer(8130, 'hello websocket', function (err, target) { - var socket = io.listen(target); - + var socket = io.listen(target), + headers = {}; + socket.on('connection', function (client) { client.on('message', function (msg) { - that.callback(null, msg); + that.callback(null, msg, headers); }); }); - + runner.startProxyServer(8131, 8130, 'localhost', function (err, proxy) { // // Setup the web socket against our proxy // - var ws = new websocket.WebSocket('ws://localhost:8131/socket.io/websocket/', 'borf'); + var ws = new websocket.WebSocket(wsprotocol + '://home.devjitsu.com:8131/socket.io/websocket/', 'borf', { + origin: protocol + '://home.devjitsu.com' + }); + + ws.on('wsupgrade', function (req, res) { + headers.request = req; + headers.response = res.headers; + }); ws.on('open', function () { ws.send(utils.encode('from client')); }); }); - }) + }); }, - "the target server should receive the message": function (err, msg) { + "the target server should receive the message": function (err, msg, headers) { assert.equal(msg, 'from client'); - } + }, + "the origin and sec-websocket-origin headers should match": function (err, msg, headers) { + assert.equal(headers.request.Origin, headers.response['sec-websocket-origin']); + } }, "when an outbound message is sent from the target server": { topic: function () { var that = this; - + runner.startTargetServer(8132, 'hello websocket', function (err, target) { - var socket = io.listen(target); - + var socket = io.listen(target), + headers = {}; + socket.on('connection', function (client) { socket.broadcast('from server'); }); - + runner.startProxyServer(8133, 8132, 'localhost', function (err, proxy) { // // Setup the web socket against our proxy // - var ws = new websocket.WebSocket('ws://localhost:8133/socket.io/websocket/', 'borf'); + var ws = new websocket.WebSocket(wsprotocol + '://home.devjitsu.com:8133/socket.io/websocket/', 'borf', { + origin: protocol + '://home.devjitsu.com' + }); + + ws.on('wsupgrade', function (req, res) { + headers.request = req; + headers.response = res.headers; + }); ws.on('message', function (msg) { msg = utils.decode(msg); if (!/\d+/.test(msg)) { - that.callback(null, msg); + that.callback(null, msg, headers); } }); }); - }) + }); }, - "the client should receive the message": function (err, msg) { + "the client should receive the message": function (err, msg, headers) { assert.equal(msg, 'from server'); - } + }, + "the origin and sec-websocket-origin headers should match": function (err, msg, headers) { + assert.equal(headers.request.Origin, headers.response['sec-websocket-origin']); + } } } } @@ -117,4 +142,4 @@ vows.describe('node-http-proxy/websocket').addBatch({ assert.isTrue(true); } } -}).export(module); \ No newline at end of file +}).export(module); diff --git a/vendor/websocket.js b/vendor/websocket.js index 82c0297..d8c5eeb 100644 --- a/vendor/websocket.js +++ b/vendor/websocket.js @@ -35,6 +35,7 @@ var buffer = require('buffer'); var crypto = require('crypto'); var events = require('events'); var http = require('http'); +var https = require('https'); var net = require('net'); var urllib = require('url'); var sys = require('sys'); @@ -178,16 +179,6 @@ var str2hex = function(str) { return out.trim(); }; -// Get the scheme for a URL, undefined if none is found -var getUrlScheme = function(url) { - var i = url.indexOf(':'); - if (i == -1) { - return undefined; - } - - return url.substring(0, i); -}; - // Set a constant on the given object var setConstant = function(obj, name, value) { Object.defineProperty(obj, name, { @@ -501,120 +492,136 @@ var WebSocket = function(url, proto, opts) { // that http.Client passes its constructor arguments through, // un-inspected to net.Stream.connect(). The latter accepts a // string as its first argument to connect to a UNIX socket. - var httpClient = undefined; - switch (getUrlScheme(url)) { - case 'ws': - var u = urllib.parse(url); - httpClient = http.createClient(u.port || 80, u.hostname); + var protocol, agent, port, u = urllib.parse(url); + if (u.protocol === 'ws:' || u.protocol === 'wss:') { + protocol = u.protocol === 'ws:' ? http : https; + port = u.protocol === 'ws:' ? 80 : 443; + agent = u.protocol === 'ws:' ? protocol.getAgent(u.hostname, u.port || port) : protocol.getAgent({ + host: u.hostname, + port: u.port || port + }); + httpPath = (u.pathname || '/') + (u.search || ''); httpHeaders.Host = u.hostname + (u.port ? (":" + u.port) : ""); - break; - - case 'ws+unix': - var sockPath = url.substring('ws+unix://'.length, url.length); - httpClient = http.createClient(sockPath); - httpHeaders.Host = 'localhost'; - break; - - default: + } + else if (urlScheme === 'ws+unix') { + throw new Error('ws+unix is not implemented'); + // var sockPath = url.substring('ws+unix://'.length, url.length); + // httpClient = http.createClient(sockPath); + // httpHeaders.Host = 'localhost'; + } + else { throw new Error('Invalid URL scheme \'' + urlScheme + '\' specified.'); } + + if (!agent._events || agent._events['upgrade'].length === 0) { + agent.on('upgrade', (function() { + var data = undefined; - httpClient.on('upgrade', (function() { - var data = undefined; + return function(res, s, head) { + stream = s; - return function(req, s, head) { - stream = s; + // + // Emit the `wsupgrade` event to inspect the raw + // arguments returned from the websocket request. + // + self.emit('wsupgrade', httpHeaders, res, s, head); + + stream.on('data', function(d) { + if (d.length <= 0) { + return; + } + + if (!data) { + data = d; + } else { + var data2 = new buffer.Buffer(data.length + d.length); - stream.on('data', function(d) { - if (d.length <= 0) { - return; - } + data.copy(data2, 0, 0, data.length); + d.copy(data2, data.length, 0, d.length); - if (!data) { - data = d; - } else { - var data2 = new buffer.Buffer(data.length + d.length); + data = data2; + } - data.copy(data2, 0, 0, data.length); - d.copy(data2, data.length, 0, d.length); + if (data.length >= 16) { + var expected = computeSecretKeySignature(key1, key2, challenge); + var actual = data.slice(0, 16).toString('binary'); - data = data2; - } - - if (data.length >= 16) { - var expected = computeSecretKeySignature(key1, key2, challenge); - var actual = data.slice(0, 16).toString('binary'); - - // Handshaking fails; we're donezo - if (actual != expected) { - debug( - 'expected=\'' + str2hex(expected) + '\'; ' + - 'actual=\'' + str2hex(actual) + '\'' - ); - - process.nextTick(function() { - // N.B. Emit 'wserror' here, as 'error' is a reserved word in the - // EventEmitter world, and gets thrown. - self.emit( - 'wserror', - new Error('Invalid handshake from server:' + - 'expected \'' + str2hex(expected) + '\', ' + - 'actual \'' + str2hex(actual) + '\'' - ) + // Handshaking fails; we're donezo + if (actual != expected) { + debug( + 'expected=\'' + str2hex(expected) + '\'; ' + + 'actual=\'' + str2hex(actual) + '\'' ); - if (self.onerror) { - self.onerror(); - } + process.nextTick(function() { + // N.B. Emit 'wserror' here, as 'error' is a reserved word in the + // EventEmitter world, and gets thrown. + self.emit( + 'wserror', + new Error('Invalid handshake from server:' + + 'expected \'' + str2hex(expected) + '\', ' + + 'actual \'' + str2hex(actual) + '\'' + ) + ); - finishClose(); - }); - } + if (self.onerror) { + self.onerror(); + } - // Un-register our data handler and add the one to be used - // for the normal, non-handshaking case. If we have extra - // data left over, manually fire off the handler on - // whatever remains. - // - // XXX: This is lame. We should only remove the listeners - // that we added. - httpClient.removeAllListeners('upgrade'); - stream.removeAllListeners('data'); - stream.on('data', dataListener); - - readyState = OPEN; - - process.nextTick(function() { - self.emit('open'); - - if (self.onopen) { - self.onopen(); + finishClose(); + }); } - }); - // Consume any leftover data - if (data.length > 16) { - stream.emit('data', data.slice(16, data.length)); + // + // Un-register our data handler and add the one to be used + // for the normal, non-handshaking case. If we have extra + // data left over, manually fire off the handler on + // whatever remains. + // + stream.removeAllListeners('data'); + stream.on('data', dataListener); + + readyState = OPEN; + + process.nextTick(function() { + self.emit('open'); + + if (self.onopen) { + self.onopen(); + } + }); + + // Consume any leftover data + if (data.length > 16) { + stream.emit('data', data.slice(16, data.length)); + } } - } - }); - stream.on('fd', fdListener); - stream.on('error', errorListener); - stream.on('close', function() { - errorListener(new Error('Stream closed unexpectedly.')); - }); + }); + stream.on('fd', fdListener); + stream.on('error', errorListener); + stream.on('close', function() { + errorListener(new Error('Stream closed unexpectedly.')); + }); - stream.emit('data', head); - }; - })()); - httpClient.on('error', function(e) { - httpClient.end(); + stream.emit('data', head); + }; + })()); + } + + agent.on('error', function (e) { errorListener(e); }); - var httpReq = httpClient.request(httpPath, httpHeaders); - + var httpReq = protocol.request({ + host: u.hostname, + method: 'GET', + agent: agent, + port: u.port, + path: httpPath, + headers: httpHeaders + }); + httpReq.write(challenge, 'binary'); httpReq.end(); })();