216 lines
7.8 KiB
JavaScript
216 lines
7.8 KiB
JavaScript
"use strict";
|
|
|
|
var sni = module.exports;
|
|
var tls = require("tls");
|
|
var servernameRe = /^[a-z0-9\.\-]+$/i;
|
|
|
|
// a nice, round, irrational number - about every 6¼ hours
|
|
var refreshOffset = Math.round(Math.PI * 2 * (60 * 60 * 1000));
|
|
// and another, about 15 minutes
|
|
var refreshStagger = Math.round(Math.PI * 5 * (60 * 1000));
|
|
// and another, about 30 seconds
|
|
var smallStagger = Math.round(Math.PI * (30 * 1000));
|
|
|
|
//secureOpts.SNICallback = sni.create(greenlock, secureOpts);
|
|
sni.create = function(greenlock, secureOpts) {
|
|
var _cache = {};
|
|
var defaultServername = greenlock.servername || "";
|
|
|
|
if (secureOpts.cert) {
|
|
// Note: it's fine if greenlock.servername is undefined,
|
|
// but if the caller wants this to auto-renew, they should define it
|
|
_cache[defaultServername] = {
|
|
refreshAt: 0,
|
|
secureContext: tls.createSecureContext(secureOpts)
|
|
};
|
|
}
|
|
|
|
return getSecureContext;
|
|
|
|
function notify(ev, args) {
|
|
try {
|
|
// TODO _notify() or notify()?
|
|
(greenlock.notify || greenlock._notify)(ev, args);
|
|
} catch (e) {
|
|
console.error(e);
|
|
console.error(ev, args);
|
|
}
|
|
}
|
|
|
|
function getSecureContext(servername, cb) {
|
|
//console.log("debug sni", servername);
|
|
if ("string" !== typeof servername) {
|
|
// this will never happen... right? but stranger things have...
|
|
console.error("[sanity fail] non-string servername:", servername);
|
|
cb(new Error("invalid servername"), null);
|
|
return;
|
|
}
|
|
|
|
var secureContext = getCachedContext(servername);
|
|
if (secureContext) {
|
|
//console.log("debug sni got cached context", servername, getCachedMeta(servername));
|
|
cb(null, secureContext);
|
|
return;
|
|
}
|
|
|
|
getFreshContext(servername)
|
|
.then(function(secureContext) {
|
|
if (secureContext) {
|
|
//console.log("debug sni got fresh context", servername, getCachedMeta(servername));
|
|
cb(null, secureContext);
|
|
return;
|
|
}
|
|
|
|
// Note: this does not replace tlsSocket.setSecureContext()
|
|
// as it only works when SNI has been sent
|
|
//console.log("debug sni got default context", servername, getCachedMeta(servername));
|
|
if (!/PROD/.test(process.env.ENV) || /DEV|STAG/.test(process.env.ENV)) {
|
|
// Change this once
|
|
// A) the 'notify' message passing is verified fixed in cluster mode
|
|
// B) we have a good way to let people know their server isn't configured
|
|
console.debug("debug: ignoring servername " + JSON.stringify(servername));
|
|
console.debug(" (it's probably either missing from your config, or a bot)");
|
|
notify("servername_unknown", {
|
|
servername: servername
|
|
});
|
|
}
|
|
cb(null, getDefaultContext());
|
|
})
|
|
.catch(function(err) {
|
|
if (!err.context) {
|
|
err.context = "sni_callback";
|
|
}
|
|
notify("error", err);
|
|
//console.log("debug sni error", servername, err);
|
|
cb(err);
|
|
});
|
|
}
|
|
|
|
function getCachedMeta(servername) {
|
|
var meta = _cache[servername];
|
|
if (!meta) {
|
|
if (!_cache[wildname(servername)]) {
|
|
return null;
|
|
}
|
|
}
|
|
return meta;
|
|
}
|
|
|
|
function getCachedContext(servername) {
|
|
var meta = getCachedMeta(servername);
|
|
if (!meta) {
|
|
return null;
|
|
}
|
|
|
|
// always renew in background
|
|
if (!meta.refreshAt || Date.now() >= meta.refreshAt) {
|
|
getFreshContext(servername).catch(function(e) {
|
|
if (!e.context) {
|
|
e.context = "sni_background_refresh";
|
|
}
|
|
notify("error", e);
|
|
});
|
|
}
|
|
|
|
// under normal circumstances this would never be expired
|
|
// and, if it is expired, something is so wrong it's probably
|
|
// not worth wating for the renewal - it has probably failed
|
|
return meta.secureContext;
|
|
}
|
|
|
|
function getFreshContext(servername) {
|
|
var meta = getCachedMeta(servername);
|
|
if (!meta && !validServername(servername)) {
|
|
if ((servername && !/PROD/.test(process.env.ENV)) || /DEV|STAG/.test(process.env.ENV)) {
|
|
// Change this once
|
|
// A) the 'notify' message passing is verified fixed in cluster mode
|
|
// B) we have a good way to let people know their server isn't configured
|
|
console.debug("debug: invalid servername " + JSON.stringify(servername));
|
|
console.debug(" (it's probably just a bot trolling for vulnerable servers)");
|
|
notify("servername_invalid", {
|
|
servername: servername
|
|
});
|
|
}
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
if (meta) {
|
|
// prevent stampedes
|
|
meta.refreshAt = Date.now() + randomRefreshOffset();
|
|
}
|
|
|
|
// TODO don't get unknown certs at all, rely on auto-updates from greenlock
|
|
// Note: greenlock.get() will return an existing fresh cert or issue a new one
|
|
return greenlock.get({ servername: servername }).then(function(result) {
|
|
var meta = getCachedMeta(servername);
|
|
if (!meta) {
|
|
meta = _cache[servername] = { secureContext: { _valid: false } };
|
|
}
|
|
// prevent from being punked by bot trolls
|
|
meta.refreshAt = Date.now() + smallStagger;
|
|
|
|
// nothing to do
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
|
|
// we only care about the first one
|
|
var pems = result.pems;
|
|
var site = result.site;
|
|
if (!pems || !pems.cert) {
|
|
// nothing to do
|
|
// (and the error should have been reported already)
|
|
return null;
|
|
}
|
|
|
|
meta = {
|
|
refreshAt: Date.now() + randomRefreshOffset(),
|
|
secureContext: tls.createSecureContext({
|
|
// TODO support passphrase-protected privkeys
|
|
key: pems.privkey,
|
|
cert: pems.cert + "\n" + pems.chain + "\n"
|
|
})
|
|
};
|
|
meta.secureContext._valid = true;
|
|
|
|
// copy this same object into every place
|
|
(result.altnames || site.altnames || [result.subject || site.subject]).forEach(function(altname) {
|
|
_cache[altname] = meta;
|
|
});
|
|
|
|
return meta.secureContext;
|
|
});
|
|
}
|
|
|
|
function getDefaultContext() {
|
|
return getCachedContext(defaultServername);
|
|
}
|
|
};
|
|
|
|
// whenever we need to know when to refresh next
|
|
function randomRefreshOffset() {
|
|
var stagger = Math.round(refreshStagger / 2) - Math.round(Math.random() * refreshStagger);
|
|
return refreshOffset + stagger;
|
|
}
|
|
|
|
function validServername(servername) {
|
|
// format and (lightly) sanitize sni so that users can be naive
|
|
// and not have to worry about SQL injection or fs discovery
|
|
|
|
servername = (servername || "").toLowerCase();
|
|
// hostname labels allow a-z, 0-9, -, and are separated by dots
|
|
// _ is sometimes allowed, but not as a "hostname", and not by Let's Encrypt ACME
|
|
// REGEX // https://www.codeproject.com/Questions/1063023/alphanumeric-validation-javascript-without-regex
|
|
return servernameRe.test(servername) && -1 === servername.indexOf("..");
|
|
}
|
|
|
|
function wildname(servername) {
|
|
return (
|
|
"*." +
|
|
servername
|
|
.split(".")
|
|
.slice(1)
|
|
.join(".")
|
|
);
|
|
}
|