small api rework and bugfixes

This commit is contained in:
AJ ONeal 2019-10-28 01:06:43 -06:00
parent 8d464d6810
commit af7c75a0f7
8 changed files with 236 additions and 116 deletions

65
demo.js
View File

@ -1,37 +1,38 @@
"use strict"; "use strict";
var Greenlock = require("./"); require("./")
var greenlockOptions = { .init(initialize)
cluster: false, .serve(worker)
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!");
});
})
.master(function() { .master(function() {
console.log("Hello from master"); 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!");
});
}

View File

@ -1,14 +1,32 @@
"use strict"; "use strict";
require("./lib/compat"); require("./lib/compat");
var cluster = require("cluster");
// Greenlock Express // Greenlock Express
var GLE = module.exports; var GLE = module.exports;
// opts.approveDomains(options, certs, cb) // Node's cluster is awesome, because it encourages writing scalable services.
GLE.create = function(opts) { //
if (!opts) { // The point of this provide an API that is consistent between single-process
opts = {}; // 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 // 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 (opts.cluster) {
if (require("cluster").isMaster) { return require("./master.js").create(opts);
return require("./master.js").create(opts);
}
return require("./worker.js").create(opts);
} }
return require("./single.js").create(opts); return require("./single.js").create(opts);
}; };

View File

@ -1,18 +1,13 @@
"use strict"; "use strict";
var pkg = require("./package.json");
module.exports.create = function(opts) { module.exports.create = function(opts) {
var Greenlock = require("@root/greenlock"); opts = parsePackage(opts);
var packageAgent = pkg.name + "/" + pkg.version; opts.packageAgent = addGreenlockAgent(opts);
if ("string" === typeof opts.packageAgent) {
opts.packageAgent += " ";
} else {
opts.packageAgent = "";
}
opts.packageAgent += packageAgent;
var Greenlock = require("@root/greenlock");
var greenlock = Greenlock.create(opts); var greenlock = Greenlock.create(opts);
// TODO move to greenlock proper
greenlock.getAcmeHttp01ChallengeResponse = function(opts) { greenlock.getAcmeHttp01ChallengeResponse = function(opts) {
return greenlock.find({ servername: opts.servername }).then(function(sites) { return greenlock.find({ servername: opts.servername }).then(function(sites) {
if (!sites.length) { if (!sites.length) {
@ -45,3 +40,47 @@ module.exports.create = function(opts) {
return greenlock; 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 <john@example.com> (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;
}

View File

@ -6,9 +6,9 @@ var Master = module.exports;
var cluster = require("cluster"); var cluster = require("cluster");
var os = require("os"); var os = require("os");
var msgPrefix = "greenlock:";
Master.create = function(opts) { Master.create = function(opts) {
var workers = [];
var resolveCb; var resolveCb;
var readyCb; var readyCb;
var _kicked = false; var _kicked = false;
@ -27,19 +27,16 @@ Master.create = function(opts) {
} }
_kicked = true; _kicked = true;
console.log("TODO: start the workers and such..."); Master._spawnWorkers(opts, greenlock);
// handle messages from workers
workers.push(null);
ready.then(function(fn) { ready.then(function(fn) {
// not sure what this API should be yet // not sure what this API should be yet
fn({ fn();
//workers: workers.slice(0)
});
}); });
} }
var master = { var master = {
worker: function() { serve: function() {
kickoff(); kickoff();
return master; 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) { function range(n) {
n = parseInt(n, 10);
if (!n) {
return [];
}
return new Array(n).join(",").split(","); 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; var numCpus = parseInt(process.env.NUMBER_OF_PROCESSORS, 10) || os.cpus().length;
// process rpc messages
// start when dead
var numWorkers = parseInt(opts.numWorkers, 10); var numWorkers = parseInt(opts.numWorkers, 10);
if (!numWorkers) { if (!numWorkers) {
if (numCpus <= 2) { if (numCpus <= 2) {
@ -79,7 +73,68 @@ Master._spawnWorkers = function(opts) {
} }
} }
return range(numWorkers).map(function() { return range(numWorkers - 1).map(function() {
return cluster.fork(); 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);
};

View File

@ -6,6 +6,7 @@ var http = require("http");
var HttpMiddleware = require("./http-middleware.js"); var HttpMiddleware = require("./http-middleware.js");
var HttpsMiddleware = require("./https-middleware.js"); var HttpsMiddleware = require("./https-middleware.js");
var sni = require("./sni.js"); var sni = require("./sni.js");
var cluster = require("cluster");
Servers.create = function(greenlock, opts) { Servers.create = function(greenlock, opts) {
var servers = {}; var servers = {};
@ -22,14 +23,24 @@ Servers.create = function(greenlock, opts) {
return _httpServer; return _httpServer;
} }
_httpServer = http.createServer(HttpMiddleware.create(opts.greenlock, defaultApp)); _httpServer = http.createServer(HttpMiddleware.create(greenlock, defaultApp));
_httpServer.once("error", startError); _httpServer.once("error", startError);
return _httpServer; return _httpServer;
}; };
var _middlewareApp;
servers.httpsServer = function(secureOpts, defaultApp) { servers.httpsServer = function(secureOpts, defaultApp) {
if (defaultApp) {
// TODO guard against being set twice?
_middlewareApp = defaultApp;
}
if (_httpsServer) { if (_httpsServer) {
if (secureOpts && Object.keys(secureOpts)) {
throw new Error("Call glx.httpsServer(tlsOptions) before calling glx.serveApp(app)");
}
return _httpsServer; return _httpsServer;
} }
@ -39,7 +50,12 @@ Servers.create = function(greenlock, opts) {
_httpsServer = createSecureServer( _httpsServer = createSecureServer(
wrapDefaultSniCallback(opts, greenlock, secureOpts), 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); _httpsServer.once("error", startError);
@ -53,18 +69,25 @@ Servers.create = function(greenlock, opts) {
return; return;
} }
var id = cluster.isWorker && cluster.worker.id;
var idstr = (id && "$" + id + " ") || "";
var plainServer = servers.httpServer(require("redirect-https")()); var plainServer = servers.httpServer(require("redirect-https")());
var plainAddr = "0.0.0.0"; var plainAddr = "0.0.0.0";
var plainPort = 80; var plainPort = 80;
plainServer.listen(plainPort, plainAddr, function() { 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 // TODO fetch greenlock.servername
_middlewareApp = app || _middlewareApp;
var secureServer = servers.httpsServer({}, app); var secureServer = servers.httpsServer({}, app);
var secureAddr = "0.0.0.0"; var secureAddr = "0.0.0.0";
var securePort = 443; var securePort = 443;
secureServer.listen(securePort, secureAddr, function() { 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); plainServer.removeListener("error", startError);
secureServer.removeListener("error", startError); secureServer.removeListener("error", startError);
@ -73,6 +96,7 @@ Servers.create = function(greenlock, opts) {
}); });
}); });
}; };
return servers; return servers;
}; };

View File

@ -11,7 +11,7 @@ Single.create = function(opts) {
var servers = Servers.create(greenlock, opts); var servers = Servers.create(greenlock, opts);
var single = { var single = {
worker: function(fn) { serve: function(fn) {
fn(servers); fn(servers);
return single; return single;
}, },

4
sni.js
View File

@ -14,7 +14,7 @@ var smallStagger = Math.round(Math.PI * (30 * 1000));
//secureOpts.SNICallback = sni.create(opts, greenlock, secureOpts); //secureOpts.SNICallback = sni.create(opts, greenlock, secureOpts);
sni.create = function(opts, greenlock, secureOpts) { sni.create = function(opts, greenlock, secureOpts) {
var _cache = {}; var _cache = {};
var defaultServername = opts.servername || greenlock.servername; var defaultServername = opts.servername || greenlock.servername || "";
if (secureOpts.cert) { if (secureOpts.cert) {
// Note: it's fine if greenlock.servername is undefined, // 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) { [match.altnames || site.altnames || [match.subject || site.subject]].forEach(function(altname) {
_cache[altname] = meta; _cache[altname] = meta;
}); });
return meta.secureContext;
}); });
} }

View File

@ -1,18 +1,21 @@
"use strict"; "use strict";
var Worker = module.exports; 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) { Worker.create = function() {
var greenlock = { var greenlock = {};
// rename presentChallenge? ["getAcmeHttp01ChallengeResponse", "renew", "notify"].forEach(function(k) {
getAcmeHttp01ChallengeResponse: presentChallenge, greenlock[k] = function(args) {
notify: notifyMaster, return rpc(k, args);
get: greenlockRenew };
}; });
var worker = { var worker = {
worker: function(fn) { serve: function(fn) {
var servers = require("./servers.js").create(greenlock, opts); var servers = require("./servers.js").create(greenlock);
fn(servers); fn(servers);
return worker; return worker;
}, },
@ -24,51 +27,32 @@ Worker.create = function(opts) {
return worker; return worker;
}; };
function greenlockRenew(args) { function rpc(funcname, msg) {
return request("renew", {
servername: args.servername
});
}
function presentChallenge(args) {
return request("challenge-response", {
servername: args.servername,
token: args.token
});
}
function request(typename, msg) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var rnd = Math.random() var rnd = Math.random()
.slice(2) .slice(2)
.toString(16); .toString(16);
var id = "greenlock:" + rnd; var id = msgPrefix + rnd;
var timeout; var timeout;
function getResponse(msg) { function getResponse(msg) {
if (msg.id !== id) { if (msg._id !== id) {
return; return;
} }
clearTimeout(timeout); clearTimeout(timeout);
resolve(msg); resolve(msg._result);
} }
process.on("message", getResponse); process.on("message", getResponse);
msg.id = msg; process.send({
msg.type = typename; _id: id,
process.send(msg); _funcname: funcname,
_input: msg
});
timeout = setTimeout(function() { timeout = setTimeout(function() {
process.removeListener("message", getResponse); process.removeListener("message", getResponse);
reject(new Error("process message timeout")); reject(new Error("worker rpc request timeout"));
}, 30 * 1000); }, messageTimeout);
});
}
function notifyMaster(ev, args) {
process.on("message", {
type: "notification",
event: ev,
parameters: args
}); });
} }