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

43
demo.js
View File

@ -1,37 +1,38 @@
"use strict";
var Greenlock = require("./");
var greenlockOptions = {
cluster: false,
require("./")
.init(initialize)
.serve(worker)
.master(function() {
console.log("Hello from master");
});
serverId: "bowie.local",
servername: "foo-gl.test.utahrust.com",
maintainerEmail: "greenlock-test@rootprojects.org",
function initialize() {
var pkg = require("./package.json");
var config = {
package: pkg,
//serverId: "bowie.local",
//servername: "foo-gl.test.utahrust.com",
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) {
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!");
});
})
.master(function() {
console.log("Hello from master");
});
}

View File

@ -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("./single.js").create(opts);
};

View File

@ -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 <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 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);
};

View File

@ -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;
};

View File

@ -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;
},

4
sni.js
View File

@ -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;
});
}

View File

@ -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);
});
}