Node.js for Round-robin HTTPS to HTTP Proxying

I am mostly familiar with Apache httpd and using mod_jk to do proxying to Tomcat. Having done a bit of research on Node.js, I wanted to see how to build an HTTPS to HTTP proxy for similar ends.

It is very likely that I am breaking all kinds of style guidelines and best practices. I am very new to Node.js. I appreciate any feedback or guidance you may have for me.

First I need an enviroment to test this on. So I fire up four new Node.js SmartMachines on my SmartOS host (two for load balancing and two for the hello world app):

vmadm create <<DOC
{
  "alias": "nodebalancer",
  "brand": "joyent",
  "dataset_uuid": "1fc068b0-13b0-11e2-9f4e-2f3f6a96d9bc",
  "nics": [
    {
      "nic_tag": "admin",
      "ip": "192.168.2.101",
      "netmask": "255.255.255.0",
      "gateway": "192.168.2.1"
    }
  ]
}
DOC
vmadm create <<DOC
{
  "alias": "nodebalancer",
  "brand": "joyent",
  "dataset_uuid": "1fc068b0-13b0-11e2-9f4e-2f3f6a96d9bc",
  "nics": [
    {
      "nic_tag": "admin",
      "ip": "192.168.2.102",
      "netmask": "255.255.255.0",
      "gateway": "192.168.2.1"
    }
  ]
}
DOC
vmadm create <<DOC
{
  "alias": "nodeapp",
  "brand": "joyent",
  "dataset_uuid": "1fc068b0-13b0-11e2-9f4e-2f3f6a96d9bc",
  "nics": [
    {
      "nic_tag": "admin",
      "ip": "192.168.2.103",
      "netmask": "255.255.255.0",
      "gateway": "192.168.2.1"
    }
  ]
}
DOC
vmadm create <<DOC
{
  "alias": "nodeapp",
  "brand": "joyent",
  "dataset_uuid": "1fc068b0-13b0-11e2-9f4e-2f3f6a96d9bc",
  "nics": [
    {
      "nic_tag": "admin",
      "ip": "192.168.2.104",
      "netmask": "255.255.255.0",
      "gateway": "192.168.2.1"
    }
  ]
}
DOC

Next I prep my nodeapp servers with a simple Hello World application by creating nodeapp.js and launching it with node nodeapp.js:

var http = require('http');

http.createServer(function(request, response) {
        response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello World");
  response.end();
}).listen(80);

Next I need to create my nodebalancers. It turns out that Nodejitsu has already build an HTTP proxy module for Node.js. Obtaining it is easy thanks to NPM with a simple npm install http-proxy.

The canonical Round-robin http-proxy load balancer is written as follows as nodebalancer.js and launched with node nodebalancer.js:

var http = require('http'),
    httpProxy = require('http-proxy');
var addresses = [
  {
    host: '192.168.2.103',
    port: 80
  },
  {
    host: '192.168.2.104',
    port: 80
  }
];
httpProxy.createServer(function (req, res, proxy) {
  var target = addresses.shift();
  proxy.proxyRequest(req, res, target);
  addresses.push(target);
}).listen(80);

Regrettably, it is significantly trickier to get SSL Round-robin load balancing working. It turns out that the Round-robin server above relies on an internal default to constructing a RoutingProxy with an empty object as a target. I use that internal knowledge in my implementation which is very ugly, but it works. Nodejitsu could easily change this in future releases, so YMMV.

I extracted the Round-robin target selection:

var nextTarget = (function(){
  var addresses = [{
      host: '192.168.2.103',
      port: 80
    },{
      host: '192.168.2.104',
      port: 80
  }];
  var target = 0;
  return function() {
    target = (target + 1) % addresses.length;
    return addresses[target];
  }
})();

And the request proxying:

var handleRequest = (function(){
  var httpProxy = require('http-proxy');
  var proxy = new httpProxy.RoutingProxy({target:{}});
  return function(req, res) {
    proxy.proxyRequest(req, res, nextTarget());
  }
})();

Getting HTTPS to work involves TLS and it can be a bear.  I am running with a cert, its private key and its chain that I have already working in Apache httpd.  I convert these to a single PKCS#12 PFX file to simplify configuration in Node.js:

openssl pkcs12 -export -out example.com.pfx -inkey example.com.key \
               -in example.com.crt -certfile example.com.chain

The building of HTTPS server options:

var httpsOptions = (function(){
    var fs = require('fs');
    return {
      pfx: fs.readFileSync('example.com.pfx')
    };
})();

And finally the construction of the HTTPS server itself:

var httpsServer = (function(){
  var https = require('https');
  return https.createServer(httpsOptions, handleRequest);
})();

httpsServer.listen(443);

Putting all that together, I ended up with code like the following in sslnodebalancer.js:

var nextTarget = (function(){
  var addresses = [{
      host: '192.168.2.103',
      port: 80
    },{
      host: '192.168.2.104',
      port: 80
  }];
  var target = 0;
  return function() {
    target = (target + 1) % addresses.length;
    return addresses[target];
  }
})();

var handleRequest = (function(){
  var httpProxy = require('http-proxy');
  var proxy = new httpProxy.RoutingProxy({target:{}});
  return function(req, res) {
    proxy.proxyRequest(req, res, nextTarget());
  }
})();

var httpsOptions = (function(){
    var fs = require('fs');
    return {
      pfx: fs.readFileSync('example.com.pfx')
    };
})();

var httpsServer = (function(){
  var https = require('https');
  return https.createServer(httpsOptions, handleRequest);
})();

httpsServer.listen(443);

It is very likely that I am breaking all kinds of style guidelines and best practices. I am very new to Node.js. I appreciate any feedback or guidance you may have for me.

UPDATE (6 Jan 2013): The big caveat here is that I have implemented no intelligence for handling downed back-end servers.  In production you will need to handle that or use something like HAProxy which builds it in.  The challenge for me with HAProxy is that SSL is only directly supported in 1.5-dev and the added complexity of stunnel doesn't help.  I am looking for to HAProxy 1.6-stable that will have this capability.  Until then I will likely recommend something like Exceliance or F5 for production usage.

UPDATE (7 Jan 2013): Tightened up the Node.js code significantly by using a PKCS#12 PFX file instead of using separate CRT, KEY and CHAIN files.  This removes the need for the ugly loop logic in httpOptions.