UPDATE: There is a followup to this post.
Various people have asked me about my reverse proxy server, which I alluded to previously. I’ve used Apache mod_proxy, and perlbal, but now I do it with a 50-odd SLOC node.js program, which leans heavily on node-http-proxy. It’s too ad-hoc to be worth posting to GitHub, but its possibly worth describing for any other node.js types out there.
Mainly it just consists of loading the dependencies and creating the appropriate proxy objects. The only remotely clever thing I did was to create an interface for reloading the routes on-the-fly, which allows me to add new applications without having to stop the existing service. (A ‘route’, in this context, means a mapping between a URL pattern and a backend application, usually described by some private port on localhost.)
Considering that some of my applications use WebSockets, this means I can muck around with routes without disrupting users of the other applications. If I did the simpleminded thing (i.e. restarting the proxy server whenever I changed the routes) this would break the WebSocket connections each time the proxy server was restarted.
Here’s what it looks like (proxy.js):
"use strict";
//
// proxy.js
//
var fs = require('fs'),
http = require('http'),
https = require('https'),
util = require('util'),
httpProxy = require('http-proxy');
//
// Create a HTTP proxy server
//
var regular_proxy = httpProxy.createServer(81, 'localhost').listen(80);
var routes_file = fs.readFileSync("routes.json");
var routes_json;
try {
routes_json = JSON.parse(routes_file);
} catch (err) {
console.log("error parsing json file\n");
console.log(routes_file + "\n");
console.log(routes_json + "\n");
console.log("error was "+ err.message + "\n");
}
//
// Create a HTTPS proxy server
//
var ssl_proxy = httpProxy.createServer({
router: routes_json,
https: {
key: fs.readFileSync('/etc/apache2/ssl/risacher.org.key', 'utf8'),
cert: fs.readFileSync('/etc/apache2/ssl/risacher.org.crt', 'utf8'),
ca: fs.readFileSync('/etc/apache2/ssl/gd_bundle.crt', 'utf8'),
ciphers: 'ECDHE-RSA-AES256-SHA:AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM',
honorCipherOrder: true
}
}).listen(443);
http.createServer(function (req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('hello\n');
var routes_file = fs.readFileSync("routes.json");
var routes_json;
try {
routes_json = JSON.parse(routes_file);
ssl_proxy.proxy.proxyTable.setRoutes(routes_json);
} catch (err) {
res.write("error parsing json file\n");
res.write("error was "+ err.message + "\n");
}
res.write(util.inspect(regular_proxy.proxy.proxyTable, false, 4));
res.end();
}).listen(8000);
I then have a routes.json file which contains something like this:
{
"risacher.org/ajaxterm": "127.0.0.1:8022",
"risacher.org/term/": "127.0.0.1:8001",
"risacher.org/app2/": "127.0.0.1:3000",
"risacher.org/app3/": "127.0.0.1:9001",
"risacher.org/app3/": "127.0.0.1:8094",
"risacher.org/app4/": "127.0.0.1:3001",
"risacher.org/ghost/": "127.0.0.1:2368",
"www.risacher.org/app3/": "127.0.0.1:9001",
".*": "127.0.0.1:81"
}
So each app listens to some port on localhost, and proxy.js proxies them appropriately – ajaxterm listens on localhost:8022, and apache2 listens on localhost:81. I can reload the routes on-the-fly by doing wget http://localhost:8000/
(which is clumsy, but effective). I don’t do routing on unencrypted http traffic, since I don’t want to access any of my non-apache2 content unencrypted, but it would be pretty trivial to do so if you wanted.