Skip to content

Instantly share code, notes, and snippets.

@Jimbly
Last active March 13, 2019 17:38
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Jimbly/d996bd8c80ae1a376a0b to your computer and use it in GitHub Desktop.
Save Jimbly/d996bd8c80ae1a376a0b to your computer and use it in GitHub Desktop.
Node.js app combining virtual hosts, proxies, mapped subdirectores, and static sites, with WebSockets support.
// 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