From ed49e5556c72096593fcc8766cf3c115ad395cc6 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 12 Aug 2016 03:02:33 -0400 Subject: [PATCH] v2.0.0 --- README.md | 180 ++++++++++++++++++++++++++++++++++++- examples/worker.js | 4 +- index.js | 12 +++ lib/master.js => master.js | 9 +- lib/worker.js => worker.js | 0 5 files changed, 196 insertions(+), 9 deletions(-) create mode 100644 index.js rename lib/master.js => master.js (92%) rename lib/worker.js => worker.js (100%) diff --git a/README.md b/README.md index ec04982..13b8a97 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,178 @@ -# letsencrypt-cluster -Use automatic letsencrypt with node cluster. +letsencrypt-cluster +=================== -(working on letsencrypt-express first) +Use automatic letsencrypt with node on multiple cores or even multiple machines. + +* Take advantage of multi-core computing +* Process certificates in master +* Serve https from multiple workers +* Can work with any clustering strategy [#1](https://github.com/Daplie/letsencrypt-cluster/issues/1) + +Install +======= + +```bash +npm install --save letsencrypt-cluster@2.x +``` + +Usage +===== + +In a cluster environment you have some main file that boots your app +and then conditionally loads certain code based on whether that fork +is the master or just a worker. + +In such a file you might want to define some of the options that need +to be shared between both the master and the worker, like this: + +`boot.js`: +```javascript +'use strict'; + +var cluster = require('cluster'); +var path = require('path'); +var os = require('os'); + +var main; +var sharedOptions = { + webrootPath: path.join(os.tmpdir(), 'acme-challenge') // /tmp/acme-challenge + // used by le-challenge-fs, the default plugin + +, renewWithin: 10 * 24 * 60 * 60 * 1000 // 10 days before expiration + +, debug: true +}; + +if (cluster.isMaster) { + main = require('./master'); +} +else { + main = require('./worker'); +} + +main.init(sharedOptions); +``` + +Master +------ + +We think it makes the most sense to load letsencrypt in master. +This can prevent race conditions (see [node-letsencrypt#45](https://github.com/Daplie/node-letsencrypt/issues/45)) +as only one process is writing the to file system or database at a time. + +The main implementation detail here is `approveDomains(options, certs, cb)` for new domain certificates +and potentially `agreeToTerms(opts, cb)` for new accounts. + +The master takes **the same arguments** as `node-letsencrypt`, plus a few extra: + +`master.js`: +``` +'use strict'; + +var cluster = require('cluster'); + +module.exports.init = function (sharedOpts) { + var cores = require('os').cpus(); + var master = require('letsencrypt-cluster/master').create({ + debug: sharedOpts.debug + + , server: 'staging' // CHANGE TO PRODUCTION + + , renewWithin: sharedOpts.renewWithin + + , webrootPath: sharedOpts.webrootPath + + , approveDomains: function (masterOptions, certs, cb) { + // Do any work that must be done by master to approve this domain + // (in this example, it's assumed to be done by the worker) + + var results = { domain: masterOptions.domain // required + , options: masterOptions // domains, email, agreeTos + , certs: certs }; // altnames, privkey, cert + cb(null, results); + } + }); + + cores.forEach(function () { + var worker = cluster.fork(); + master.addWorker(worker); + }); +}; +``` + +Worker +------ + +The worker takes *similar* arguments to `node-letsencrypt`, +but only ones that are useful for determining certificate +renewal and for `le.challenge.get`. + +If you want to a non-default `le.challenge` + +`worker.js`: +``` +'use strict'; + +module.exports.init = function (sharedOpts) { + var worker = require('../worker').create({ + debug: sharedOpts.debug + + , renewWithin: sharedOpts.renewWithin + + , webrootPath: sharedOpts.webrootPath + + // , challenge: require('le-challenge-fs').create({ webrootPath: '...', ... }) + + , approveDomains: function (workerOptions, certs, cb) { + // opts = { domains, email, agreeTos, tosUrl } + // certs = { subject, altnames, expiresAt, issuedAt } + + var results = { + domain: workerOptions.domains[0] + , options: { + domains: workerOptions.domains + } + , certs: certs + }; + + if (certs) { + // modify opts.domains to match the original request + // email is not necessary, because the account already exists + // this will only fail if the account has become corrupt + results.options.domains = certs.altnames; + cb(null, results); + return; + } + + // This is where one would check one's application-specific database: + // 1. Lookup the domain to see which email it belongs to + // 2. Assign a default email if it isn't in the system + // 3. If the email has no le account, `agreeToTerms` will fire unless `agreeTos` is preset + + results.options.email = 'john.doe@example.com' + results.options.agreeTos = true // causes agreeToTerms to be skipped + cb(null, results); + } + }); + + function app(req, res) { + res.end("Hello, World!"); + } + + var redirectHttps = require('redirect-https')(); + var plainServer = require('http').createServer(worker.middleware(redirectHttps)); + plainServer.listen(80); + + var server = require('https').createServer(worker.httpsOptions, worker.middleware(app)); + server.listen(443); +}; +``` + +Message Passing +--------------- + +The master and workers will communicate through `process.on('message', fn)`, `process.send({})`, +`worker.on('message', fn)`and `worker.send({})`. + +All messages have a `type` property which is a string and begins with `LE_`. +All other messages are ignored. diff --git a/examples/worker.js b/examples/worker.js index 8758654..08b2643 100644 --- a/examples/worker.js +++ b/examples/worker.js @@ -9,8 +9,8 @@ module.exports.init = function (sharedOpts) { // We want both to renew well before the expiration date // and also to stagger the renewals, just a touch // here we specify to renew between 10 and 15 days - , notBefore: 15 * 24 * 60 * 60 * 1000 - , notAfter: 10 * 24 * 60 * 60 * 1000 // optional + , renewWithin: sharedOpts.renewWithin + , renewBy: 10 * 24 * 60 * 60 * 1000 // optional diff --git a/index.js b/index.js new file mode 100644 index 0000000..b5af499 --- /dev/null +++ b/index.js @@ -0,0 +1,12 @@ +'use strict'; + +console.error(""); +console.error("One does not simply require('letsencrypt-cluster');"); +console.error(""); +console.error("Usage:"); +console.error("\trequire('letsencrypt-cluster/master').create({ ... });"); +console.error("\trequire('letsencrypt-cluster/worker').create({ ... });"); +console.error(""); +console.error(""); + +process.exit(1); diff --git a/lib/master.js b/master.js similarity index 92% rename from lib/master.js rename to master.js index 92e25a8..d504eed 100644 --- a/lib/master.js +++ b/master.js @@ -1,10 +1,11 @@ 'use strict'; +// opts.addWorker(worker) +// opts.approveDomains(options, certs, cb) module.exports.create = function (opts) { - if (!opts.letsencrypt) { opts.letsencrypt = require('letsencrypt').create({ - server: opts.server - , webrootPath: require('os').tmpdir() + require('path').sep + 'acme-challenge' - }); } + opts = opts || { }; + opts.webrootPath = opts.webrootPath || require('os').tmpdir() + require('path').sep + 'acme-challenge'; + if (!opts.letsencrypt) { opts.letsencrypt = require('letsencrypt').create(opts); } if ('function' !== typeof opts.approveDomains) { throw new Error("You must provide opts.approveDomains(domain, certs, callback) to approve certificates"); } diff --git a/lib/worker.js b/worker.js similarity index 100% rename from lib/worker.js rename to worker.js