Node.js app combining virtual hosts, proxies, mapped subdirectores, and static sites, with WebSockets support.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Companion Blog Post about architecture: | |
// http://jimbesser.wordpress.com/2014/10/20/its-node-js-all-the-way-down/ | |
// Companion Blog Post about dealing with legacy requests: | |
// http://jimbesser.wordpress.com/2014/11/29/the-horrible-things-peoples-routers-do-to-my-packets/ | |
// | |
// Routing handled by this app: | |
// [www.]bigscreensmallgames.com -> static site: /var/data/smb_web/bigscreensmallgames.com/ | |
// fanime.info -> node app running entire site on port 4001 | |
// [default site]/app1: replace URL and redirect to single page app on 192.168.0.127:21022 | |
// [default site] -> static site: /var/data/smb_web/dashingstrike.com/ | |
var express = require('express'); | |
var fs = require('fs'); | |
var http = require('http'); | |
var httpProxy = require('http-proxy'); | |
var morgan = require('morgan'); | |
var serveIndex = require('serve-index'); | |
var serveStatic = require('serve-static'); | |
var through = require('through'); | |
var vhost = require('vhost'); | |
var out_stream = through(); | |
out_stream.pipe(process.stdout); | |
function log(msg) { | |
out_stream.write('**LOG: ' + new Date().toISOString() + ' ' + msg + '\n'); | |
} | |
var last_log_file; | |
function openNewLogFile() { | |
var filename = '/var/log/webroot.' + new Date().toISOString().slice(0, 13).replace(/\:/g, '_').replace('T', '_') + '.log'; | |
last_log_file && last_log_file.close(); | |
last_log_file = fs.createWriteStream(filename, { flags: 'a', mode: 0664 }); | |
out_stream.pipe(last_log_file); | |
log('Opened new log file: ' + filename); | |
} | |
openNewLogFile(); | |
setInterval(openNewLogFile, 12*60*60*1000); | |
function handleError(e) { | |
log('ERROR: ' + (e.stack || e)); | |
console.error('ERROR', new Date(), e); | |
} | |
// Absolutely required, httpProxy throws uncaught socket hang-up exceptions | |
// whenever proxying to another server which disconnects a socket (e.g. if you | |
// you make a socket.io connection to the wrong endpoint). | |
process.on('uncaughtException', handleError); | |
function staticSite(app, dir) { | |
app = app || express(); | |
app.use(serveStatic('/var/data/smb_web/' + dir + '/')); | |
app.use(serveIndex('/var/data/smb_web/' + dir + '/', { icons: true, view: 'details' })); | |
return app; | |
} | |
function wsProxy(proxy) { | |
return function (req, data) { | |
log('Proxying ws upgrade request for ' + req.headers.host + ' ' + (req.originalUrl || req.url)); | |
proxy.ws(req, data.socket, data.head); | |
}; | |
} | |
function webProxy(proxy) { | |
return proxy.web.bind(proxy); | |
} | |
function directoryize(app, dir) { | |
// redirect requests for /foo to /foo/ so that relative paths don't get messed up | |
app.get('/' + dir, function (req, res, next) { | |
if (req.url !== '/' + dir) { | |
return next(); | |
} | |
res.redirect('/' + dir + '/'); | |
}); | |
} | |
// Sub sites running as separate processes | |
var proxy_fanimeinfo = httpProxy.createProxyServer({ | |
target: { host: 'localhost', port: 4001 }, | |
agent: http.globalAgent, // passing agent to prevent connection: close from being added | |
xfwd: true, // add x-forwarded-for header so we get the real IP | |
}); | |
var proxy_app1 = httpProxy.createProxyServer({ | |
target: { host: '192.168.0.127', port: 21022 }, | |
agent: http.globalAgent, // passing agent to prevent connection: close from being added | |
xfwd: true, // add x-forwarded-for header so we get the real IP | |
}); | |
var main = express(); | |
var main_ws = express.Router(); | |
directoryize(main, 'app1'); | |
main.use('/app1', webProxy(proxy_app1)); | |
main_ws.use('/app1', wsProxy(proxy_app1)); | |
staticSite(main, 'dashingstrike.com'); | |
// Root vhost app | |
var vhost_app = express(); | |
var vhost_ws = express.Router(); | |
// Logging | |
vhost_app.use(morgan(':remote-addr [:date] ":method :req[host]:url" START ":referrer" ":user-agent"', { stream: out_stream })); | |
vhost_app.use(morgan(':remote-addr [:date] ":method :req[host]:url" FINISH :status :res[content-length] :response-time ms', { stream: out_stream })); | |
// Directory mapped virtual hosts | |
vhost_app.use(vhost(/(?:www\.)?bigscreensmallgames\.com/, staticSite(null, 'bigscreensmallgames.com'))); | |
// Virtual hosts mapping to other node apps running as separate processes | |
vhost_app.use(vhost(/(?:www\.)?fanime\.info/, webProxy(proxy_fanimeinfo))); | |
vhost_ws.use(vhost(/(?:www\.)?fanime\.info/, wsProxy(proxy_fanimeinfo))); | |
// Default - dashingstrike.com and variants, anything unknown, etc | |
vhost_app.use(main); | |
vhost_ws.use(main_ws); | |
var server = http.createServer(function (req, res) { | |
// Re-map a url with a host into the format express/vhost logic needs | |
if (!req.headers.host && req.url.indexOf('://') !== -1) { | |
log('Remapping legacy request with no host for ' + req.url); | |
var parsed = url.parse(req.url); | |
req.headers.host = parsed.host; | |
req.url = parsed.path + (parsed.hash || ''); | |
} | |
// Need to attach this error handler because morgan implicitly adds an error | |
// handler which squashes this error from ever being seen! | |
req.socket.on('error', handleError); | |
vhost_app(req, res); | |
}); | |
server.on('upgrade', function (req, socket, head) { | |
// Use the same express.Router logic for vhost mapping of the upgrade request | |
vhost_ws(req, { socket: socket, head: head }, function () { | |
log('No one to proxy websocket to for ' + req.headers.host + ' ' + req.url); | |
}); | |
}); | |
// Add intercept to remove null-termination from requests from libGlov's fetch.cpp | |
server.on('connection', function (s) { | |
var orig_ondata = s.ondata; | |
var is_legacy = false; | |
var call_count = 0; | |
s.ondata = function (d, start, end) { | |
++call_count; | |
if (call_count === 1) { | |
var head = d.slice(start, Math.min(end, start + 11)).toString(); | |
is_legacy = (head === 'GET http://'); | |
if (is_legacy && d.slice(end-2, end).toString()==='\n\0') { | |
// Remove null termination that will cause a HTTP parser error | |
log('Stripping null from null terimianted request buffer'); | |
end = end - 1; | |
s.ondata = orig_ondata; // Remove hook | |
} | |
} else if (call_count === 2) { | |
// assert.ok(is_legacy); | |
if (end - start <= 2 && d[end-1] === 0) { | |
log('Stripping null from now empty request buffer'); | |
end = end - 1; | |
} | |
s.ondata = orig_ondata; // Remove hook | |
} | |
if (!is_legacy) { | |
s.ondata = orig_ondata; // Remove hook | |
} | |
orig_ondata(d, start, end); | |
}; | |
}); | |
// I gave the node binary privileges to listen on this port by running: | |
// $ sudo setcap cap_net_bind_service=+ep `which node` | |
var port = 80; | |
server.listen(port); | |
log('Started webroot on port ' + port + ', process id ' + process.pid); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment