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

229 lines
7.3 KiB
JavaScript
Raw Permalink Normal View History

2015-12-17 03:03:07 +00:00
'use strict';
var crypto = require('crypto');
var tls = require('tls');
2015-12-17 05:08:14 +00:00
module.exports.create = function (opts) {
if (opts.debug) {
2015-12-17 08:44:55 +00:00
console.debug("[LEX] creating sniCallback", JSON.stringify(opts, null, ' '));
2015-12-17 05:08:14 +00:00
}
2015-12-17 03:03:07 +00:00
var ipc = {}; // in-process cache
2015-12-17 05:08:14 +00:00
if (!opts) { throw new Error("requires opts to be an object"); }
if (!opts.letsencrypt) { throw new Error("requires opts.letsencrypt to be a letsencrypt instance"); }
2015-12-17 03:03:07 +00:00
2015-12-17 05:08:14 +00:00
if (!opts.lifetime) { opts.lifetime = 90 * 24 * 60 * 60 * 1000; }
if (!opts.failedWait) { opts.failedWait = 5 * 60 * 1000; }
if (!opts.renewWithin) { opts.renewWithin = 3 * 24 * 60 * 60 * 1000; }
if (!opts.memorizeFor) { opts.memorizeFor = 1 * 24 * 60 * 60 * 1000; }
2015-12-17 03:03:07 +00:00
2015-12-17 05:08:14 +00:00
if (!opts.approveRegistration) { opts.approveRegistration = function (hostname, cb) { cb(null, null); }; }
if (!opts.handleRenewFailure) { opts.handleRenewFailure = function (/*err, hostname, certInfo*/) {}; }
2015-12-17 03:03:07 +00:00
function assignBestByDates(now, certInfo) {
certInfo = certInfo || { loadedAt: now, expiresAt: 0, issuedAt: 0, lifetime: 0 };
2015-12-17 08:44:55 +00:00
var rnds = crypto.randomBytes(3);
2015-12-17 03:03:07 +00:00
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
2015-12-17 05:08:14 +00:00
var memorizeFor = Math.floor(opts.memorizeFor + ((opts.memorizeFor / 4) * rnd1));
2015-12-17 03:03:07 +00:00
// 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
2015-12-17 05:08:14 +00:00
var bestIfUsedBy = certInfo.expiresAt - (opts.renewWithin + Math.floor(opts.renewWithin * rnd2));
2015-12-17 03:03:07 +00:00
// 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;
2015-12-17 08:44:55 +00:00
return certInfo;
2015-12-17 03:03:07 +00:00
}
function renewInBackground(now, hostname, certInfo) {
2015-12-17 05:08:14 +00:00
if ((now - certInfo.loadedAt) < opts.failedWait) {
2015-12-17 03:03:07 +00:00
// wait a few minutes
return;
}
if (now > certInfo.bestIfUsedBy && !certInfo.timeout) {
// EXPIRING
if (now > certInfo.expiresAt) {
// EXPIRED
certInfo.renewTimeout = Math.floor(certInfo.renewTimeout / 2);
}
2015-12-17 05:08:14 +00:00
if (opts.debug) {
2015-12-17 08:44:55 +00:00
console.debug("[LEX] skipping stagger '" + certInfo.renewTimeout + "' and renewing '" + hostname + "' now");
2015-12-17 05:08:14 +00:00
certInfo.renewTimeout = 500;
}
2015-12-17 03:03:07 +00:00
certInfo.timeout = setTimeout(function () {
2015-12-17 05:08:14 +00:00
var args = { domains: [ hostname ], duplicate: false };
opts.letsencrypt.renew(args, function (err, certInfo) {
2015-12-17 03:03:07 +00:00
if (err || !certInfo) {
2015-12-17 05:08:14 +00:00
opts.handleRenewFailure(err, hostname, certInfo);
2015-12-17 03:03:07 +00:00
}
ipc[hostname] = assignBestByDates(now, certInfo);
});
}, certInfo.renewTimeout);
}
}
2015-12-17 08:44:55 +00:00
function cacheResult(err, hostname, certInfo, sniCb) {
if (certInfo && certInfo.fullchain && certInfo.privkey) {
2015-12-17 05:08:14 +00:00
if (opts.debug) {
2015-12-17 08:44:55 +00:00
console.debug('cert is looking good');
2015-12-17 03:03:07 +00:00
}
2015-12-17 08:44:55 +00:00
try {
certInfo.tlsContext = tls.createSecureContext({
key: certInfo.privkey || certInfo.key // privkey.pem
, cert: certInfo.fullchain || 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");
console.warn("(either missing or malformed certInfo.key and / or certInfo.fullchain)");
err = e;
2015-12-17 03:03:07 +00:00
}
2015-12-17 08:44:55 +00:00
sniCb(err, certInfo.tlsContext);
} else {
if (opts.debug) {
console.debug('cert is NOT looking good');
}
sniCb(err || new Error("couldn't get certInfo: unknown"), null);
}
2015-12-17 05:08:14 +00:00
2015-12-17 08:44:55 +00:00
var now = Date.now();
certInfo = ipc[hostname] = assignBestByDates(now, certInfo);
renewInBackground(now, hostname, certInfo);
}
function registerCert(hostname, sniCb) {
if (opts.debug) {
console.debug("[LEX] '" + hostname + "' is not registered, requesting approval");
}
opts.approveRegistration(hostname, function (err, args) {
if (opts.debug) {
console.debug("[LEX] '" + hostname + "' registration approved, attempting register");
}
if (err) {
cacheResult(err, hostname, null, sniCb);
2015-12-17 03:03:07 +00:00
return;
}
2015-12-17 08:44:55 +00:00
if (!(args && args.agreeTos && args.email && args.domains)) {
cacheResult(new Error("not approved or approval is missing arguments - such as agreeTos, email, domains"), hostname, null, sniCb);
return;
}
2015-12-17 05:08:14 +00:00
2015-12-17 08:44:55 +00:00
opts.letsencrypt.register(args, function (err, certInfo) {
if (opts.debug) {
console.debug("[LEX] '" + hostname + "' register completed", err && err.stack || null, certInfo);
}
2015-12-17 05:08:14 +00:00
2015-12-17 08:44:55 +00:00
cacheResult(err, hostname, certInfo, sniCb);
});
});
}
2015-12-17 05:08:14 +00:00
2015-12-17 08:44:55 +00:00
function fetch(hostname, sniCb) {
opts.letsencrypt.fetch({ domains: [hostname] }, function (err, certInfo) {
if (opts.debug) {
console.debug("[LEX] fetch from disk result '" + hostname + "':");
console.debug(certInfo && Object.keys(certInfo));
2015-12-17 05:08:14 +00:00
if (err) {
2015-12-17 08:44:55 +00:00
console.error(err.stack || err);
2015-12-17 05:08:14 +00:00
}
2015-12-17 08:44:55 +00:00
}
2015-12-17 05:08:14 +00:00
2015-12-17 08:44:55 +00:00
if (err) {
sniCb(err, null);
return;
}
2015-12-17 05:08:14 +00:00
2015-12-17 08:44:55 +00:00
if (certInfo) {
cacheResult(err, hostname, certInfo, sniCb);
return;
2015-12-17 05:08:14 +00:00
}
2015-12-17 08:44:55 +00:00
registerCert(hostname, sniCb);
2015-12-17 03:03:07 +00:00
});
}
return function sniCallback(hostname, cb) {
var now = Date.now();
var certInfo = ipc[hostname];
2015-12-17 08:44:55 +00:00
//
// No cert is available in cache.
// try to fetch it from disk quickly
// and return to the browser
//
2015-12-17 03:03:07 +00:00
if (!certInfo) {
2015-12-17 05:08:14 +00:00
if (opts.debug) {
2015-12-17 08:44:55 +00:00
console.debug("[LEX] no certs loaded for '" + hostname + "'");
2015-12-17 05:08:14 +00:00
}
2015-12-17 03:03:07 +00:00
fetch(hostname, cb);
return;
}
2015-12-17 08:44:55 +00:00
//
// A cert is available
// See if it's old enough that
// we should refresh it from disk
// (in the background)
//
// TODO once ECDSA is available, wait for cert renewal if its due (maybe?)
2015-12-17 05:08:14 +00:00
if (certInfo.tlsContext) {
cb(null, certInfo.tlsContext);
2015-12-17 03:03:07 +00:00
if ((now - certInfo.loadedAt) < (certInfo.memorizeFor)) {
// these aren't stale, so don't fall through
2015-12-17 05:08:14 +00:00
if (opts.debug) {
2015-12-17 08:44:55 +00:00
console.debug("[LEX] certs for '" + hostname + "' are fresh from disk");
2015-12-17 05:08:14 +00:00
}
2015-12-17 03:03:07 +00:00
return;
}
}
2015-12-17 05:08:14 +00:00
else if ((now - certInfo.loadedAt) < opts.failedWait) {
if (opts.debug) {
2015-12-17 08:44:55 +00:00
console.debug("[LEX] certs for '" + hostname + "' recently failed and are still in cool down");
2015-12-17 05:08:14 +00:00
}
2015-12-17 03:03:07 +00:00
// this was just fetched and failed, wait a few minutes
cb(null, null);
return;
}
2015-12-17 05:08:14 +00:00
if (opts.debug) {
2015-12-17 08:44:55 +00:00
console.debug("[LEX] certs for '" + hostname + "' are stale on disk and should be will be fetched again");
console.debug({
age: now - certInfo.loadedAt
, loadedAt: certInfo.loadedAt
, issuedAt: certInfo.issuedAt
, expiresAt: certInfo.expiresAt
, privkey: !!certInfo.privkey
, chain: !!certInfo.chain
, fullchain: !!certInfo.fullchain
, cert: !!certInfo.cert
, memorizeFor: certInfo.memorizeFor
, failedWait: opts.failedWait
});
2015-12-17 05:08:14 +00:00
}
fetch(hostname, cb);
2015-12-17 03:03:07 +00:00
};
};