diff --git a/lib/local-server.js b/lib/local-server.js new file mode 100644 index 0000000..9c6fa7b --- /dev/null +++ b/lib/local-server.js @@ -0,0 +1,30 @@ +'use strict'; + +module.exports.create = function (port, promiseApp) { + var PromiseA = require('bluebird'); + + return new PromiseA(function (resolve, reject) { + var server = require('http').createServer(); + + server.on('error', reject); + server.listen(port, 'localhost', function () { + console.log("Listening", server.address()); + resolve(server); + }); + + // Get up and listening as absolutely quickly as possible + server.on('request', function (req, res) { + // TODO move to caddy parser? + if (/(^|\.)proxyable\./.test(req.headers.host)) { + // device-id-12345678.proxyable.myapp.mydomain.com => myapp.mydomain.com + // proxyable.myapp.mydomain.com => myapp.mydomain.com + // TODO myapp.mydomain.com.proxyable.com => myapp.mydomain.com + req.headers.host = req.headers.host.replace(/.*\.?proxyable\./, ''); + } + + promiseApp().then(function (app) { + app(req, res); + }); + }); + }); +}; diff --git a/lib/spawn-caddy.js b/lib/spawn-caddy.js new file mode 100644 index 0000000..a0d0d2a --- /dev/null +++ b/lib/spawn-caddy.js @@ -0,0 +1,127 @@ +'use strict'; + +module.exports.create = function (/*config*/) { + var PromiseA = require('bluebird'); + var spawn = require('child_process').spawn; + var path = require('path'); + var caddypath = '/usr/local/bin/caddy'; + var caddyfilepath = path.join(__dirname, '..', 'Caddyfile'); + var sitespath = path.join(__dirname, '..', 'sites-enabled'); + var caddy; + var fs = require('fs'); + + + // TODO this should be expanded to include proxies a la proxydyn + function writeCaddyfile(conf) { + return new PromiseA(function (resolve, reject) { + fs.readdir(sitespath, function (err, nodes) { + if (err) { + reject(err); + return; + } + + conf.domains = nodes.filter(function (node) { + return /\./.test(node) && !/(^\.)|([\/\:\\])/.test(node); + }); + + var contents = tplCaddyfile(conf); + fs.writeFile(caddyfilepath, contents, 'utf8', function (err) { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }); + }); + } + + function tplCaddyfile(conf) { + var contents = []; + + conf.domains.forEach(function (hostname) { + var content = ""; + + content+= "https://" + hostname + " {\n" + + " gzip\n" + + " tls " + + "/srv/walnut/certs/live/" + hostname + "/fullchain.pem " + + "/srv/walnut/certs/live/" + hostname + "/privkey.pem\n" + ; + + if (conf.locked) { + content += " root /srv/walnut/init.public/\n"; + } else { + content += " root /srv/walnut/sites-enabled/" + hostname + "/\n"; + } + + content += + " proxy /api http://localhost:" + conf.localPort.toString() + "\n" + // # TODO internal + + "}"; + + contents.push(content); + }); + + return contents.join('\n\n'); + } + + function spawnCaddy(conf) { + console.log('[CADDY] start'); + return writeCaddyfile(conf).then(function () { + if (caddy) { + caddy.kill('SIGUSR1'); + return; + + // TODO caddy.kill('SIGKILL'); if SIGTERM fails + // https://github.com/mholt/caddy/issues/107 + // SIGUSR1 + + //caddy.kill('SIGTERM'); + } + + caddy = spawn(caddypath, ['-conf', caddyfilepath], { stdio: ['ignore', 'pipe', 'pipe'] }); + caddy.stdout.on('data', function (str) { + console.error('[Caddy]', str.toString('utf8')); + }); + + caddy.stderr.on('data', function (errstr) { + console.error('[Caddy]', errstr.toString('utf8')); + }); + + caddy.on('close', function (code, signal) { + // TODO catch if caddy doesn't exist + console.log('[Caddy]'); + console.log(code, signal); + caddy = null; + setTimeout(function () { + spawnCaddy(conf); + }, 1 * 1000); + }); + + return caddy; + }); + } + + function sighup() { + if (caddy) { + caddy.kill('SIGUSR1'); + return; + } + + // sudo kill -s SIGUSR1 `cat caddy.pid` + fs.readFileAsync('/srv/walnut/caddy.pid', 'utf8').then(function (pid) { + console.log('[caddy] pid', pid); + caddy = spawn('/bin/kill', ['-s', 'SIGUSR1', pid]); + }); + } + + return { + spawn: spawnCaddy + , update: function (conf) { + return writeCaddyfile(conf).then(sighup); + } + , sighup: sighup + }; +}; diff --git a/master.js b/master.js index dbd8127..664a41c 100644 --- a/master.js +++ b/master.js @@ -3,11 +3,15 @@ console.log('\n\n\n[MASTER] Welcome to WALNUT!'); var PromiseA = require('bluebird'); +var fs = PromiseA.promisifyAll(require('fs')); var cluster = require('cluster'); -var numCores = require('os').cpus().length; -var securePort = process.argv[2] || 443; -var insecurePort = process.argv[3] || 80; -var secureServer; +var numForks = 0; +var numCores = Math.min(2, require('os').cpus().length); +var securePort = process.argv[2] || 443; // 443 +var insecurePort = process.argv[3] || 80; // 80 +var localPort = securePort; +var caddy; +var masterServer; var rootMasterKey; var redirects = require('./redirects.json'); @@ -17,9 +21,17 @@ var path = require('path'); var certPaths = [path.join(__dirname, 'certs', 'live')]; var promiseServer; var masterApp; +var caddyConf = { localPort: 4080, locked: true }; //console.log('\n.'); +function fork() { + if (numForks < numCores) { + numForks += 1; + cluster.fork(); + } +} + // Note that this function will be called async, after promiseServer is returned // it seems like a circular dependency, but it isn't... not exactly anyway function promiseApps() { @@ -27,17 +39,27 @@ function promiseApps() { return PromiseA.resolve(masterApp); } - masterApp = promiseServer.then(function (_secureServer) { - secureServer = _secureServer; - console.log("[MASTER] Listening on https://localhost:" + secureServer.address().port, '\n'); + masterApp = promiseServer.then(function (_masterServer) { + masterServer = _masterServer; + console.log("[MASTER] Listening on https://localhost:" + masterServer.address().port, '\n'); return require('./lib/unlock-device').create().then(function (result) { result.promise.then(function (_rootMasterKey) { var i; + caddyConf.locked = false; + if (caddy) { + caddy.update(caddyConf); + } rootMasterKey = _rootMasterKey; + if (numCores <= 2) { + // we're on one core, stagger the remaning + fork(); + return; + } + for (i = 0; i < numCores; i += 1) { - cluster.fork(); + fork(); } }); @@ -52,30 +74,61 @@ function promiseApps() { // TODO have a fallback server than can download and apply an update? require('./lib/insecure-server').create(securePort, insecurePort, redirects); //console.log('\n.'); -promiseServer = require('./lib/sni-server').create(certPaths, securePort, promiseApps); +promiseServer = fs.existsAsync('/usr/local/bin/caddy').then(function () { + console.log("Caddy is not present"); + // Caddy DOES NOT exist, use our node sni-server + return require('./lib/sni-server').create(certPaths, localPort, promiseApps); +}, function () { + console.log("Caddy is present (assumed running)"); + // Caddy DOES exist, use our http server without sni + localPort = caddyConf.localPort; + caddy = require('./lib/spawn-caddy').create(); + + return caddy.spawn(caddyConf).then(function () { + console.log("caddy has spawned"); + //return caddy.update(caddyConf).then(function () { + // console.log("caddy is updating"); + + setInterval(function () { + console.log('SIGUSR1 to caddy'); + return caddy.update(caddyConf); + }, 60 * 1000); + + return require('./lib/local-server').create(localPort, promiseApps); + //}); + }); +}); + //console.log('\n.'); cluster.on('online', function (worker) { console.log('[MASTER] Worker ' + worker.process.pid + ' is online'); - if (secureServer) { + fork(); + + if (masterServer) { // NOTE: it's possible that this could survive idle for a while through keep-alive // should default to connection: close - secureServer.close(); - secureServer = null; + masterServer.close(); + masterServer = null; setTimeout(function () { // TODO use `id' to find user's uid / gid and set to file // TODO set immediately? - process.setgid(1000); - process.setuid(1000); + if (!caddy) { + // TODO what about caddy + process.setgid(1000); + process.setuid(1000); + } }, 1000); } + console.log("securePort", securePort); worker.send({ type: 'init' - , securePort: securePort - , certPaths: certPaths + , securePort: localPort + , certPaths: caddy ? null : certPaths }); + worker.on('message', function (msg) { console.log('message from worker'); console.log(msg); @@ -83,8 +136,9 @@ cluster.on('online', function (worker) { }); cluster.on('exit', function (worker, code, signal) { + numForks -= 1; console.log('[MASTER] Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal); - cluster.fork(); + fork(); }); // TODO delegate to workers diff --git a/worker.js b/worker.js index c1dadd3..22c8ffc 100644 --- a/worker.js +++ b/worker.js @@ -188,7 +188,11 @@ function init(info) { return workerApp; } - promiseServer = require('./lib/sni-server').create(info.certPaths, info.securePort, promiseApps); + if (info.certPaths) { + promiseServer = require('./lib/sni-server').create(info.certPaths, info.securePort, promiseApps); + } else { + promiseServer = require('./lib/local-server').create(info.securePort, promiseApps); + } } process.on('message', function (msg) {