greenlock-express.js/lib/sni-callback.js

129 lines
4.2 KiB
JavaScript

'use strict';
var crypto = require('crypto');
var tls = require('tls');
module.exports.create = function (memos) {
var ipc = {}; // in-process cache
if (!memos) { throw new Error("requires opts to be an object"); }
if (!memos.letsencrypt) { throw new Error("requires opts.letsencrypt to be a letsencrypt instance"); }
if (!memos.lifetime) { memos.lifetime = 90 * 24 * 60 * 60 * 1000; }
if (!memos.failedWait) { memos.failedWait = 5 * 60 * 1000; }
if (!memos.renewWithin) { memos.renewWithin = 3 * 24 * 60 * 60 * 1000; }
if (!memos.memorizeFor) { memos.memorizeFor = 1 * 24 * 60 * 60 * 1000; }
if (!memos.handleRegistration) { memos.handleRegistration = function (args, cb) { cb(null, null); }; }
if (!memos.handleRenewFailure) { memos.handleRenewFailure = function () {}; }
function assignBestByDates(now, certInfo) {
certInfo = certInfo || { loadedAt: now, expiresAt: 0, issuedAt: 0, lifetime: 0 };
var rnds = crypto.randomBytes(3)[0];
var rnd1 = ((rnds[0] + 1) / 257);
var rnd2 = ((rnds[1] + 1) / 257);
var rnd3 = ((rnds[2] + 1) / 257);
// Stagger randomly by plus 0% to 25% to prevent all caches expiring at once
var memorizeFor = Math.floor(memos.memorizeFor + ((memos.memorizeFor / 4) * rnd1));
// Stagger randomly to renew between n and 2n days before renewal is due
// this *greatly* reduces the risk of multiple cluster processes renewing the same domain at once
var bestIfUsedBy = certInfo.expiresAt - (memos.renewWithin + Math.floor(memos.renewWithin * rnd2));
// Stagger randomly by plus 0 to 5 min to reduce risk of multiple cluster processes
// renewing at once on boot when the certs have expired
var renewTimeout = Math.floor((5 * 60 * 1000) * rnd3);
certInfo.loadedAt = now;
certInfo.memorizeFor = memorizeFor;
certInfo.bestIfUsedBy = bestIfUsedBy;
certInfo.renewTimeout = renewTimeout;
}
function renewInBackground(now, hostname, certInfo) {
if ((now - certInfo.loadedAt) < memos.failedWait) {
// wait a few minutes
return;
}
if (now > certInfo.bestIfUsedBy && !certInfo.timeout) {
// EXPIRING
if (now > certInfo.expiresAt) {
// EXPIRED
certInfo.renewTimeout = Math.floor(certInfo.renewTimeout / 2);
}
certInfo.timeout = setTimeout(function () {
var opts = { domains: [ hostname ], duplicate: false };
le.renew(opts, function (err, certInfo) {
if (err || !certInfo) {
memos.handleRenewFailure(err, certInfo, opts);
}
ipc[hostname] = assignBestByDates(now, certInfo);
});
}, certInfo.renewTimeout);
}
}
function fetch(hostname, cb) {
le.fetch({ domains: [hostname] }, function (err, certInfo) {
var now = Date.now();
ipc[hostname] = assignBestByDates(now, certInfo);
if (!certInfo) {
// handles registration
memos.handleRegistration(hostname, cb);
return;
}
// handles renewals
renewInBackground(now, hostname, certInfo);
if (err) {
cb(err);
return;
}
try {
certInfo.tlsContext = tls.createSecureContext({
key: certInfo.key // privkey.pem
, cert: certInfo.cert // fullchain.pem (cert.pem + '\n' + chain.pem)
});
} catch(e) {
console.warn("[Sanity Check Fail]: a weird object was passed back through le.fetch to lex.fetch");
cb(e);
return;
}
cb(null, certInfo.tlsContext);
});
}
return function sniCallback(hostname, cb) {
var now = Date.now();
var certInfo = ipc[hostname];
// TODO once ECDSA is available, wait for cert renewal if its due
if (!certInfo) {
fetch(hostname, cb);
return;
}
if (certInfo.context) {
cb(null, certInfo.context);
if ((now - certInfo.loadedAt) < (certInfo.memorizeFor)) {
// these aren't stale, so don't fall through
return;
}
}
else if ((now - certInfo.loadedAt) < memos.failedWait) {
// this was just fetched and failed, wait a few minutes
cb(null, null);
return;
}
fetch({ domains: [hostname] }, cb);
};
};