diff --git a/demo.js b/demo.js index 2339b71..a73fe48 100644 --- a/demo.js +++ b/demo.js @@ -1,37 +1,38 @@ "use strict"; -var Greenlock = require("./"); -var greenlockOptions = { - cluster: false, - - serverId: "bowie.local", - servername: "foo-gl.test.utahrust.com", - maintainerEmail: "greenlock-test@rootprojects.org", - staging: true, - - /* - manager: { - module: "greenlock-manager-sequelize", - dbUrl: "postgres://foo@bar:baz/quux" - } - */ - - challenges: { - "dns-01": { - module: "acme-dns-01-digitalocean" - } - } -}; - -Greenlock.create(greenlockOptions) - .worker(function(glx) { - console.info(); - console.info("Hello from worker"); - - glx.serveApp(function(req, res) { - res.end("Hello, Encrypted World!"); - }); - }) +require("./") + .init(initialize) + .serve(worker) .master(function() { console.log("Hello from master"); }); + +function initialize() { + var pkg = require("./package.json"); + var config = { + package: pkg, + //serverId: "bowie.local", + //servername: "foo-gl.test.utahrust.com", + staging: true, + + challenges: { + "dns-01": { + module: "acme-dns-01-digitalocean" + } + }, + + notify: function (ev, params) { + console.log(ev, params); + } + }; + return config; +} + +function worker(glx) { + console.info(); + console.info("Hello from worker"); + + glx.serveApp(function(req, res) { + res.end("Hello, Encrypted World!"); + }); +} diff --git a/greenlock-express.js b/greenlock-express.js index 50ca472..2000c36 100644 --- a/greenlock-express.js +++ b/greenlock-express.js @@ -1,14 +1,32 @@ "use strict"; require("./lib/compat"); +var cluster = require("cluster"); // Greenlock Express var GLE = module.exports; -// opts.approveDomains(options, certs, cb) -GLE.create = function(opts) { - if (!opts) { - opts = {}; +// Node's cluster is awesome, because it encourages writing scalable services. +// +// The point of this provide an API that is consistent between single-process +// and multi-process services so that beginners can more easily take advantage +// of what cluster has to offer. +// +// This API provides just enough abstraction to make it easy, but leaves just +// enough hoopla so that there's not a large gap in understanding what happens +// under the hood. That's the hope, anyway. + +GLE.init = function(fn) { + if (cluster.isWorker) { + // ignore the init function and launch the worker + return require("./worker.js").create(); + } + + var opts = fn(); + if (!opts || "object" !== typeof opts) { + throw new Error( + "the `Greenlock.init(fn)` function should return an object `{ maintainerEmail, packageAgent, notify }`" + ); } // just for ironic humor @@ -18,12 +36,9 @@ GLE.create = function(opts) { } }); - // we want to be minimal, and only load the code that's necessary to load if (opts.cluster) { - if (require("cluster").isMaster) { - return require("./master.js").create(opts); - } - return require("./worker.js").create(opts); + return require("./master.js").create(opts); } + return require("./single.js").create(opts); }; diff --git a/greenlock.js b/greenlock.js index 713ab23..e2eac2e 100644 --- a/greenlock.js +++ b/greenlock.js @@ -1,18 +1,13 @@ "use strict"; -var pkg = require("./package.json"); - module.exports.create = function(opts) { - var Greenlock = require("@root/greenlock"); - var packageAgent = pkg.name + "/" + pkg.version; - if ("string" === typeof opts.packageAgent) { - opts.packageAgent += " "; - } else { - opts.packageAgent = ""; - } - opts.packageAgent += packageAgent; + opts = parsePackage(opts); + opts.packageAgent = addGreenlockAgent(opts); + var Greenlock = require("@root/greenlock"); var greenlock = Greenlock.create(opts); + + // TODO move to greenlock proper greenlock.getAcmeHttp01ChallengeResponse = function(opts) { return greenlock.find({ servername: opts.servername }).then(function(sites) { if (!sites.length) { @@ -45,3 +40,47 @@ module.exports.create = function(opts) { return greenlock; }; + +function addGreenlockAgent(opts) { + // Add greenlock as part of Agent, unless this is greenlock + if (!/^greenlock(-express|-pro)?/.test(opts.packageAgent)) { + var pkg = require("./package.json"); + var packageAgent = pkg.name + "/" + pkg.version; + opts.packageAgent += " " + packageAgent; + } + + return opts.packageAgent; +} + +// ex: John Doe (https://john.doe) +var looseEmailRe = /.* <([^'" <>:;`]+@[^'" <>:;`]+\.[^'" <>:;`]+)> .*/; +function parsePackage(opts) { + // 'package' is sometimes a reserved word + var pkg = opts.package || opts.pkg; + if (!pkg) { + return opts; + } + + if (!opts.packageAgent) { + var err = "missing `package.THING`, which is used for the ACME client user agent string"; + if (!pkg.name) { + throw new Error(err.replace("THING", "name")); + } + if (!pkg.version) { + throw new Error(err.replace("THING", "version")); + } + opts.packageAgent = pkg.name + "/" + pkg.version; + } + + if (!opts.maintainerEmail) { + try { + opts.maintainerEmail = pkg.author.email || pkg.author.match(looseEmailRe)[1]; + } catch (e) {} + } + if (!opts.maintainerEmail) { + throw new Error("missing or malformed `package.author`, which is used as the contact for support notices"); + } + opts.package = undefined; + + return opts; +} diff --git a/master.js b/master.js index 819b39f..abe2096 100644 --- a/master.js +++ b/master.js @@ -6,9 +6,9 @@ var Master = module.exports; var cluster = require("cluster"); var os = require("os"); +var msgPrefix = "greenlock:"; Master.create = function(opts) { - var workers = []; var resolveCb; var readyCb; var _kicked = false; @@ -27,19 +27,16 @@ Master.create = function(opts) { } _kicked = true; - console.log("TODO: start the workers and such..."); - // handle messages from workers - workers.push(null); + Master._spawnWorkers(opts, greenlock); + ready.then(function(fn) { // not sure what this API should be yet - fn({ - //workers: workers.slice(0) - }); + fn(); }); } var master = { - worker: function() { + serve: function() { kickoff(); return master; }, @@ -54,22 +51,19 @@ Master.create = function(opts) { }; }; -// opts.approveDomains(options, certs, cb) -GLE.create = function(opts) { - GLE._spawnWorkers(opts); - - gl.tlsOptions = {}; - - return master; -}; - function range(n) { + n = parseInt(n, 10); + if (!n) { + return []; + } return new Array(n).join(",").split(","); } -Master._spawnWorkers = function(opts) { +Master._spawnWorkers = function(opts, greenlock) { var numCpus = parseInt(process.env.NUMBER_OF_PROCESSORS, 10) || os.cpus().length; + // process rpc messages + // start when dead var numWorkers = parseInt(opts.numWorkers, 10); if (!numWorkers) { if (numCpus <= 2) { @@ -79,7 +73,68 @@ Master._spawnWorkers = function(opts) { } } - return range(numWorkers).map(function() { - return cluster.fork(); + return range(numWorkers - 1).map(function() { + Master._spawnWorker(opts, greenlock); }); }; + +Master._spawnWorker = function(opts, greenlock) { + var w = cluster.fork(); + // automatically added to master's `cluster.workers` + w.on("exit", function(code, signal) { + // TODO handle failures + // Should test if the first starts successfully + // Should exit if failures happen too quickly + + // For now just kill all when any die + if (signal) { + console.error("worker was killed by signal:", signal); + } else if (code !== 0) { + console.error("worker exited with error code:", code); + } else { + console.error("worker unexpectedly quit without exit code or signal"); + } + process.exit(2); + + //addWorker(); + }); + + function handleMessage(msg) { + if (0 !== (msg._id || "").indexOf(msgPrefix)) { + return; + } + if ("string" !== typeof msg._funcname) { + // TODO developer error + return; + } + + function rpc() { + return greenlock[msg._funcname](msg._input) + .then(function(result) { + w.send({ + _id: msg._id, + _result: result + }); + }) + .catch(function(e) { + var error = new Error(e.message); + Object.getOwnPropertyNames(e).forEach(function(k) { + error[k] = e[k]; + }); + w.send({ + _id: msg._id, + _error: error + }); + }); + } + + try { + rpc(); + } catch (e) { + console.error("Unexpected and uncaught greenlock." + msg._funcname + " error:"); + console.error(e); + } + } + + w.on("message", handleMessage); +}; diff --git a/servers.js b/servers.js index 415b2c9..637108b 100644 --- a/servers.js +++ b/servers.js @@ -6,6 +6,7 @@ var http = require("http"); var HttpMiddleware = require("./http-middleware.js"); var HttpsMiddleware = require("./https-middleware.js"); var sni = require("./sni.js"); +var cluster = require("cluster"); Servers.create = function(greenlock, opts) { var servers = {}; @@ -22,14 +23,24 @@ Servers.create = function(greenlock, opts) { return _httpServer; } - _httpServer = http.createServer(HttpMiddleware.create(opts.greenlock, defaultApp)); + _httpServer = http.createServer(HttpMiddleware.create(greenlock, defaultApp)); _httpServer.once("error", startError); return _httpServer; }; + var _middlewareApp; + servers.httpsServer = function(secureOpts, defaultApp) { + if (defaultApp) { + // TODO guard against being set twice? + _middlewareApp = defaultApp; + } + if (_httpsServer) { + if (secureOpts && Object.keys(secureOpts)) { + throw new Error("Call glx.httpsServer(tlsOptions) before calling glx.serveApp(app)"); + } return _httpsServer; } @@ -39,7 +50,12 @@ Servers.create = function(greenlock, opts) { _httpsServer = createSecureServer( wrapDefaultSniCallback(opts, greenlock, secureOpts), - HttpsMiddleware.create(greenlock, defaultApp) + HttpsMiddleware.create(greenlock, function(req, res) { + if (!_middlewareApp) { + throw new Error("Set app with `glx.serveApp(app)` or `glx.httpsServer(tlsOptions, app)`"); + } + _middlewareApp(req, res); + }) ); _httpsServer.once("error", startError); @@ -53,18 +69,25 @@ Servers.create = function(greenlock, opts) { return; } + var id = cluster.isWorker && cluster.worker.id; + var idstr = (id && "$" + id + " ") || ""; var plainServer = servers.httpServer(require("redirect-https")()); var plainAddr = "0.0.0.0"; var plainPort = 80; plainServer.listen(plainPort, plainAddr, function() { - console.info("Listening on", plainAddr + ":" + plainPort, "for ACME challenges, and redirecting to HTTPS"); + console.info( + idstr + "Listening on", + plainAddr + ":" + plainPort, + "for ACME challenges, and redirecting to HTTPS" + ); // TODO fetch greenlock.servername + _middlewareApp = app || _middlewareApp; var secureServer = servers.httpsServer({}, app); var secureAddr = "0.0.0.0"; var securePort = 443; secureServer.listen(securePort, secureAddr, function() { - console.info("Listening on", secureAddr + ":" + securePort, "for secure traffic"); + console.info(idstr + "Listening on", secureAddr + ":" + securePort, "for secure traffic"); plainServer.removeListener("error", startError); secureServer.removeListener("error", startError); @@ -73,6 +96,7 @@ Servers.create = function(greenlock, opts) { }); }); }; + return servers; }; diff --git a/single.js b/single.js index 61c8c35..28fa190 100644 --- a/single.js +++ b/single.js @@ -11,7 +11,7 @@ Single.create = function(opts) { var servers = Servers.create(greenlock, opts); var single = { - worker: function(fn) { + serve: function(fn) { fn(servers); return single; }, diff --git a/sni.js b/sni.js index 975f32d..9d2b5a6 100644 --- a/sni.js +++ b/sni.js @@ -14,7 +14,7 @@ var smallStagger = Math.round(Math.PI * (30 * 1000)); //secureOpts.SNICallback = sni.create(opts, greenlock, secureOpts); sni.create = function(opts, greenlock, secureOpts) { var _cache = {}; - var defaultServername = opts.servername || greenlock.servername; + var defaultServername = opts.servername || greenlock.servername || ""; if (secureOpts.cert) { // Note: it's fine if greenlock.servername is undefined, @@ -157,6 +157,8 @@ sni.create = function(opts, greenlock, secureOpts) { [match.altnames || site.altnames || [match.subject || site.subject]].forEach(function(altname) { _cache[altname] = meta; }); + + return meta.secureContext; }); } diff --git a/worker.js b/worker.js index 37b6425..22697d8 100644 --- a/worker.js +++ b/worker.js @@ -1,18 +1,21 @@ "use strict"; var Worker = module.exports; +// *very* generous, but well below the http norm of 120 +var messageTimeout = 30 * 1000; +var msgPrefix = 'greenlock:'; -Worker.create = function(opts) { - var greenlock = { - // rename presentChallenge? - getAcmeHttp01ChallengeResponse: presentChallenge, - notify: notifyMaster, - get: greenlockRenew - }; +Worker.create = function() { + var greenlock = {}; + ["getAcmeHttp01ChallengeResponse", "renew", "notify"].forEach(function(k) { + greenlock[k] = function(args) { + return rpc(k, args); + }; + }); var worker = { - worker: function(fn) { - var servers = require("./servers.js").create(greenlock, opts); + serve: function(fn) { + var servers = require("./servers.js").create(greenlock); fn(servers); return worker; }, @@ -24,51 +27,32 @@ Worker.create = function(opts) { return worker; }; -function greenlockRenew(args) { - return request("renew", { - servername: args.servername - }); -} - -function presentChallenge(args) { - return request("challenge-response", { - servername: args.servername, - token: args.token - }); -} - -function request(typename, msg) { +function rpc(funcname, msg) { return new Promise(function(resolve, reject) { var rnd = Math.random() .slice(2) .toString(16); - var id = "greenlock:" + rnd; + var id = msgPrefix + rnd; var timeout; function getResponse(msg) { - if (msg.id !== id) { + if (msg._id !== id) { return; } clearTimeout(timeout); - resolve(msg); + resolve(msg._result); } process.on("message", getResponse); - msg.id = msg; - msg.type = typename; - process.send(msg); + process.send({ + _id: id, + _funcname: funcname, + _input: msg + }); timeout = setTimeout(function() { process.removeListener("message", getResponse); - reject(new Error("process message timeout")); - }, 30 * 1000); - }); -} - -function notifyMaster(ev, args) { - process.on("message", { - type: "notification", - event: ev, - parameters: args + reject(new Error("worker rpc request timeout")); + }, messageTimeout); }); }