walnut.js/boot/worker.js

263 lines
7.8 KiB
JavaScript
Raw Normal View History

'use strict';
module.exports.create = function (opts) {
var id = '0';
var promiseApp;
2016-04-09 23:14:00 +00:00
function createAndBindInsecure(lex, conf, getOrCreateHttpApp) {
// TODO conditional if 80 is being served by caddy
2016-04-09 23:14:00 +00:00
var appPromise = null;
var app = null;
var http = require('http');
var insecureServer = http.createServer();
function onRequest(req, res) {
if (app) {
app(req, res);
return;
}
if (!appPromise) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end('{ "error": { "code": "E_SANITY_FAIL", "message": "should have an express app, but didn\'t" } }');
return;
}
appPromise.then(function (_app) {
appPromise = null;
app = _app;
app(req, res);
});
}
insecureServer.listen(conf.insecurePort, function () {
console.info("#" + id + " Listening on http://"
+ insecureServer.address().address + ":" + insecureServer.address().port, '\n');
appPromise = getOrCreateHttpApp(null, insecureServer);
if (!appPromise) {
throw new Error('appPromise returned nothing');
}
});
insecureServer.on('request', onRequest);
}
function walkLe(domainname) {
var PromiseA = require('bluebird');
if (!domainname) {
return PromiseA.reject(new Error('no domainname given for walkLe'));
}
2016-04-09 23:14:00 +00:00
var fs = PromiseA.promisifyAll(require('fs'));
var path = require('path');
var parts = domainname.split('.'); //.replace(/^www\./, '').split('.');
var configname = parts.join('.') + '.json';
var configpath = path.join(__dirname, '..', '..', 'config', configname);
if (parts.length < 2) {
return PromiseA.resolve(null);
}
// TODO configpath a la varpath
return fs.readFileAsync(configpath, 'utf8').then(function (text) {
var data = JSON.parse(text);
data.name = configname;
return data;
}, function (/*err*/) {
parts.shift();
return walkLe(parts.join('.'));
});
}
2016-04-09 23:14:00 +00:00
function createLe(lexConf, conf) {
2016-03-29 19:03:09 +00:00
var LEX = require('letsencrypt-express');
var lex = LEX.create({
2016-04-09 23:14:00 +00:00
configDir: lexConf.configDir // i.e. __dirname + '/letsencrypt.config'
2016-03-29 19:03:09 +00:00
, approveRegistration: function (hostname, cb) {
2016-04-09 23:14:00 +00:00
// TODO cache/report unauthorized
if (!hostname) {
cb(new Error("[lex.approveRegistration] undefined hostname"), null);
return;
}
walkLe(hostname).then(function (leAuth) {
// TODO should still check dns for hostname (and mx for email)
if (leAuth && leAuth.email && leAuth.agreeTos) {
cb(null, {
domains: [hostname] // TODO handle www and bare on the same cert
, email: leAuth.email
, agreeTos: leAuth.agreeTos
});
}
else {
// TODO report unauthorized
cb(new Error("Valid LetsEncrypt config with email and agreeTos not found for '" + hostname + "'"), null);
}
2016-03-29 19:03:09 +00:00
});
/*
letsencrypt.getConfig({ domains: [domain] }, function (err, config) {
if (!(config && config.checkpoints >= 0)) {
cb(err, null);
return;
}
cb(null, {
email: config.email
// can't remember which it is, but the pyconf is different that the regular variable
, agreeTos: config.tos || config.agree || config.agreeTos
, server: config.server || LE.productionServerUrl
, domains: config.domains || [domain]
});
});
*/
}
});
2016-04-09 23:14:00 +00:00
conf.letsencrypt = lex.letsencrypt;
conf.lex = lex;
conf.walkLe = walkLe;
2016-03-29 19:03:09 +00:00
return lex;
}
2016-04-09 23:14:00 +00:00
function createAndBindServers(conf, getOrCreateHttpApp) {
2016-03-29 19:03:09 +00:00
var lex;
2016-04-09 23:14:00 +00:00
if (conf.lexConf) {
lex = createLe(conf.lexConf, conf);
2016-03-29 19:03:09 +00:00
}
// NOTE that message.conf[x] will be overwritten when the next message comes in
2016-04-09 23:14:00 +00:00
require('./local-server').create(lex, conf.certPaths, conf.localPort, conf, function (err, webserver) {
if (err) {
console.error('[ERROR] worker.js');
console.error(err.stack);
throw err;
}
2016-04-09 23:14:00 +00:00
console.info("#" + id + " Listening on " + conf.protocol + "://" + webserver.address().address + ":" + webserver.address().port, '\n');
// we don't need time to pass, just to be able to return
process.nextTick(function () {
2016-04-09 23:14:00 +00:00
createAndBindInsecure(lex, conf, getOrCreateHttpApp);
});
// we are returning the promise result to the caller
2016-04-09 23:14:00 +00:00
return getOrCreateHttpApp(null, null, webserver, conf);
});
}
//
// Worker Mode
//
2016-04-09 23:14:00 +00:00
function waitForConfig(realMessage) {
if ('walnut.init' !== realMessage.type) {
console.warn('[Worker] 0 got unexpected message:');
2016-04-09 23:14:00 +00:00
console.warn(realMessage);
return;
}
2016-04-09 23:14:00 +00:00
var conf = realMessage.conf;
process.removeListener('message', waitForConfig);
// NOTE: this callback must return a promise for an express app
2016-04-09 23:14:00 +00:00
function getExpressApp(err, insecserver, webserver/*, newMessage*/) {
var PromiseA = require('bluebird');
2016-04-09 23:14:00 +00:00
if (promiseApp) {
return promiseApp;
}
2016-04-09 23:14:00 +00:00
promiseApp = new PromiseA(function (resolve) {
2016-04-09 23:14:00 +00:00
function initHttpApp(srvmsg) {
2015-11-28 05:57:07 +00:00
if ('walnut.webserver.onrequest' !== srvmsg.type) {
2016-04-09 23:14:00 +00:00
console.warn('[Worker] [onrequest] unexpected message:');
console.warn(srvmsg);
return;
}
2016-04-09 23:14:00 +00:00
process.removeListener('message', initHttpApp);
2016-04-09 23:14:00 +00:00
if (srvmsg.conf) {
Object.keys(srvmsg.conf).forEach(function (key) {
conf[key] = srvmsg.conf[key];
});
}
resolve(require('../lib/worker').create(webserver, conf));
}
2015-11-28 05:57:07 +00:00
process.send({ type: 'walnut.webserver.listening' });
2016-04-09 23:14:00 +00:00
process.on('message', initHttpApp);
}).then(function (app) {
console.info('[Worker Ready]');
return app;
});
2016-04-09 23:14:00 +00:00
return promiseApp;
2016-04-09 23:14:00 +00:00
}
createAndBindServers(conf, getExpressApp);
}
//
// Standalone Mode
//
if (opts) {
// NOTE: this callback must return a promise for an express app
2016-04-09 23:14:00 +00:00
createAndBindServers(opts, function (err, insecserver, webserver/*, conf*/) {
var PromiseA = require('bluebird');
2016-04-09 23:14:00 +00:00
if (promiseApp) {
return promiseApp;
}
2016-04-09 23:14:00 +00:00
promiseApp = new PromiseA(function (resolve) {
opts.getConfig(function (srvmsg) {
resolve(require('../lib/worker').create(webserver, srvmsg));
});
}).then(function (app) {
console.info('[Standalone Ready]');
return app;
});
2016-04-09 23:14:00 +00:00
return promiseApp;
});
} else {
// we are in cluster mode, as opposed to standalone mode
id = require('cluster').worker.id.toString();
// We have to wait to get the configuration from the master process
// before we can start our webserver
console.info('[Worker #' + id + '] online!');
process.on('message', waitForConfig);
}
//
// Debugging
//
process.on('exit', function (code) {
// only sync code can run here
console.info('uptime:', process.uptime());
console.info(process.memoryUsage());
console.info('[exit] process.exit() has been called (or master has killed us).');
console.info(code);
});
process.on('beforeExit', function () {
// async can be scheduled here
console.info('[beforeExit] Event Loop is empty. Process will end.');
});
process.on('unhandledRejection', function (err) {
// this should always throw
// (it means somewhere we're not using bluebird by accident)
console.error('[caught] [unhandledRejection]');
console.error(Object.keys(err));
console.error(err);
console.error(err.stack);
});
process.on('rejectionHandled', function (msg) {
console.error('[rejectionHandled]');
console.error(msg);
});
};