This commit is contained in:
AJ ONeal 2016-08-15 21:15:16 -04:00
parent fe039dc137
commit 26eb38fb25
4 changed files with 60 additions and 193 deletions

View File

@ -59,6 +59,8 @@ require('letsencrypt-express').create({
, agreeTos: true
, approvedDomains: [ 'example.com' ]
, app: require('express')().use('/', function (req, res) {
res.end('Hello, World!');
})
@ -134,7 +136,7 @@ var lex = require('letsencrypt-express').create({
// handles acme-challenge and redirects to https
require('http').createServer(lex.middleware()).listen(80, function () {
require('http').createServer(le.middleware()).listen(80, function () {
console.log("Listening for ACME http-01 challenges on", this.address());
});
@ -146,7 +148,7 @@ app.use('/', function (req, res) {
});
// handles your app
require('https').createServer(lex.httpsOptions, lex.middleware(app)).listen(443, function () {
require('https').createServer(le.httpsOptions, le.middleware(app)).listen(443, function () {
console.log("Listening for ACME tls-sni-01 challenges and serve app on", this.address());
});
```
@ -154,22 +156,22 @@ require('https').createServer(lex.httpsOptions, lex.middleware(app)).listen(443,
API
===
All options are passed directly to `node-letsencrypt`,
so `lex` is an instance of `letsencrypt`, but has a few
extra helper methods and options.
This module is an elaborate ruse (to provide an oversimplified example and to nab some SEO).
See [node-letsencrypt options](https://github.com/Daplie/node-letsencrypt)
The API is actually located at [node-letsencrypt options](https://github.com/Daplie/node-letsencrypt)
(because all options are simply passed through to `node-letsencrypt` proper without modification).
* `lexOptions.approveDomains(options, certs, cb)` is special for `letsencrypt-express`, but will probably be included in `node-letsencrypt` in the future (no API change).
The only "API" consists of two options, the rest is just a wrapper around `node-letsencrypt` to take LOC from 15 to 5:
* `lexOptions.app` is just an elaborate ruse used for the Quickstart. It's sole purpose is to trim out 5 lines of code for setting http and https servers so that whiners won't whine. Real programmers don't use this.
* `leOptions.server` set to https://acme-v01.api.letsencrypt.org/directory in production
* `leOptions.email` useful for simple sites where there is only one owner. Leave this `null` and use `approveDomains` otherwise.
* `leOptions.agreeTos` useful for simple sites where there is only one owner. Leave this `null` and use `approveDomains` otherwise.
* `leOptions.renewWithin` is shared so that the worker knows how earlier to request a new cert
* `leOptions.renewBy` is passed to `le-sni-auto` so that it staggers renewals between `renewWithin` (latest) and `renewBy` (earlier)
* `lex.middleware(nextApp)` uses `letsencrypt/middleware` for GET-ing `http-01`, hence `sharedOptions.webrootPath`
* `lex.httpsOptions` has a default localhost certificate and the `SNICallback`.
* `opts.app` An express app in the format `function (req, res) { ... }` (no `next`).
* `lex.listen(plainPort, tlsPort)` Accepts port numbers (or arrays of port numbers) to listen on.
There are a few options that aren't shown in these examples, so if you need to change something
that isn't shown here, look at the code (it's not that much) or open an issue.
Brief overview of some simple options for `node-letsencrypt`:
* `opts.server` set to https://acme-v01.api.letsencrypt.org/directory in production
* `opts.email` The default email to use to accept agreements.
* `opts.agreeTos` When set to `true`, this always accepts the LetsEncrypt TOS. When a string it checks the agreement url first.
* `opts.approvedDomains` An explicit array of The allowed domains (can be used instead of `approveDomains`).
* `opts.approveDomains` A callback for checking your database before allowing a domain `function (opts, certs, cb) { }`
* `opts.renewWithin` is the **maximum** number of days (in ms) before expiration to renew a certificate.
* `opts.renewBy` is the **minimum** number of days (in ms) before expiration to renew a certificate.

View File

@ -1,12 +0,0 @@
'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);

118
master.js
View File

@ -1,91 +1,55 @@
'use strict';
// opts.addWorker(worker)
// opts.approveDomains(options, certs, cb)
module.exports.create = function (opts) {
opts = opts || { };
opts._workers = [];
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");
}
// accept all defaults for le.challenges, le.store, le.middleware
var le = require('letsencrypt').create(opts);
function log(debug) {
if (!debug) {
return;
opts.app = opts.app || require('express')().use('/', function (req, res) {
res.end("Hello, World!\nWith Love,\nLet's Encrypt Express");
});
opts.listen = function (plainPort, port) {
var PromiseA = require('bluebird');
var promises = [];
var plainPorts = plainPort;
var ports = port;
var servers = [];
if (!plainPorts || !ports) {
plainPorts = 80;
ports = 443;
}
var args = Array.prototype.slice.call(arguments);
args.shift();
args.unshift("[le/lib/core.js]");
console.log.apply(console, args);
}
if (!Array.isArray(plainPorts)) {
plainPorts = [ plainPorts ];
ports = [ ports ];
}
opts.addWorker = function (worker) {
opts._workers.push(worker);
worker.on('online', function () {
log(opts.debug, 'worker is up');
plainPorts.forEach(function (p) {
promises.push(new PromiseA(function (resolve, reject) {
require('http').createServer(le.middleware(require('https-redirect').create())).listen(p, function () {
resolve();
}).on('error', reject);
}));
});
worker.on('message', function (msg) {
log(opts.debug, 'Message from worker ' + worker.id);
if ('LE_REQUEST' !== (msg && msg.type)) {
log(opts.debug, 'Ignoring irrelevant message');
log(opts.debug, msg);
return;
}
log(opts.debug, 'about to approveDomains');
opts.approveDomains(msg.options, msg.certs, function (err, results) {
if (err) {
log(opts.debug, 'Approval got ERROR', err.stack || err);
worker.send({
type: 'LE_RESPONSE'
, domain: msg.domain
, error: { message: err.message, code: err.code, stack: err.stack }
});
return;
}
var promise;
//
/*
var certs = require('localhost.daplie.com-certificates').merge({
subject: msg.domain
, altnames: [ msg.domain ]
, issuedAt: Date.now()
, expiresAt: Date.now() + (90 * 24 * 60 * 60 * 1000)
});
certs.privkey = certs.key.toString('ascii');
certs.cert = certs.cert.toString('ascii');
certs.chain = '';
worker.send({ type: 'LE_RESPONSE', domain: msg.domain, certs: certs });
return;
// */
if (results.certs) {
promise = opts.letsencrypt.renew(results.options, results.certs);
}
else {
promise = opts.letsencrypt.register(results.options);
}
promise.then(function (certs) {
log(opts.debug, 'Approval got certs', certs);
// certs = { subject, domains, issuedAt, expiresAt, privkey, cert, chain };
opts._workers.forEach(function (w) {
w.send({ type: 'LE_RESPONSE', domain: msg.domain, certs: certs });
});
}, function (err) {
log(opts.debug, 'Approval got ERROR', err.stack || err);
worker.send({ type: 'LE_RESPONSE', domain: msg.domain, error: err });
});
});
ports.forEach(function (p) {
promises.push(new PromiseA(function (resolve, reject) {
var server = require('https').createServer(le.httpsOptions, le.middleware(le.app)).listen(p, function () {
resolve();
}).on('error', reject);
servers.push(server);
}));
});
if (!Array.isArray(port)) {
servers = servers[0];
}
return servers;
};
return opts;
return le;
};

View File

@ -1,87 +0,0 @@
'use strict';
function log(debug) {
if (!debug) {
return;
}
var args = Array.prototype.slice.call(arguments);
args.shift();
args.unshift("[le/lib/core.js]");
console.log.apply(console, args);
}
module.exports.create = function (opts) {
// if another worker updates the certs,
// receive a copy from master here as well
// and update the sni cache manually
process.on('message', function (msg) {
if ('LE_RESPONSE' === msg.type && msg.certs) {
opts.sni.cacheCerts(msg.certs);
}
});
opts.sni = require('le-sni-auto').create({
renewWithin: opts.renewWithin || (10 * 24 * 60 * 60 * 1000)
, renewBy: opts.renewBy || (5 * 24 * 60 * 60 * 1000)
, getCertificates: function (domain, certs, cb) {
var workerOptions = { domains: [ domain ] };
opts.approveDomains(workerOptions, certs, function (_err, results) {
if (_err) {
cb(_err);
return;
}
var err = new Error("___MESSAGE___");
process.send({ type: 'LE_REQUEST', domain: domain, options: results.options, certs: results.certs });
process.on('message', function (msg) {
log(opts.debug, 'Message from master');
log(opts.debug, msg);
if (msg.domain !== domain) {
return;
}
if (msg.error) {
err.message = msg.error.message || "unknown error sent from cluster master to worker";
err.stack.replace("___MESSAGE___", err.message);
err = {
message: err.message
, stack: err.stack
, data: { options: workerOptions, certs: certs }
};
} else {
err = null;
}
cb(err, msg.certs);
});
});
}
});
opts.httpsOptions = require('localhost.daplie.com-certificates').merge({ SNICallback: opts.sni.sniCallback });
opts.challenge = {
get: opts.getChallenge
|| (opts.challenge && opts.challenge.get)
|| require('le-challenge-fs').create({ webrootPath: opts.webrootPath }).get
};
// opts.challenge.get, opts.acmeChallengePrefix
opts.middleware = require('letsencrypt/lib/middleware').create(opts);
return opts;
};