sni callback stuff
This commit is contained in:
parent
3132e7a592
commit
75d259dbb1
23
README.md
23
README.md
|
@ -24,7 +24,7 @@ npm install --save letsencrypt-express
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Note: using staging server url, remove .testing() for production
|
// Note: using staging server url, remove .testing() for production
|
||||||
var le = require('letsencrypt-express').testing();
|
var lex = require('letsencrypt-express').testing();
|
||||||
var express = require('express');
|
var express = require('express');
|
||||||
var app = express();
|
var app = express();
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ app.use('/', function (req, res) {
|
||||||
res.send({ success: true });
|
res.send({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
le.create('/etc/letsencrypt', app).listen([80], [443, 5001], function () {
|
lex.create('/etc/letsencrypt', app).listen([80], [443, 5001], function () {
|
||||||
console.log("ENCRYPT __ALL__ THE DOMAINS!");
|
console.log("ENCRYPT __ALL__ THE DOMAINS!");
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
@ -42,7 +42,7 @@ le.create('/etc/letsencrypt', app).listen([80], [443, 5001], function () {
|
||||||
```javascript
|
```javascript
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var le = require('letsencrypt-express');
|
var lex = require('letsencrypt-express');
|
||||||
var express = require('express');
|
var express = require('express');
|
||||||
var app = express();
|
var app = express();
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ app.use('/', function (req, res) {
|
||||||
res.send({ success: true });
|
res.send({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
var results = le.create({
|
var results = lex.create({
|
||||||
configDir: '/etc/letsencrypt'
|
configDir: '/etc/letsencrypt'
|
||||||
, onRequest: app
|
, onRequest: app
|
||||||
, server: require('letsencrypt').productionServerUrl
|
, server: require('letsencrypt').productionServerUrl
|
||||||
|
@ -84,6 +84,21 @@ results.tlsServers.forEach(function (server) {
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```
|
||||||
|
LEX.create(options) // checks options and sets up defaults. returns object with `listen`
|
||||||
|
// (it was really just done this way to appeal to what people are used to seeing)
|
||||||
|
|
||||||
|
lex.listen(plain, tls, fn) // actually creates the servers and causes them to listen
|
||||||
|
|
||||||
|
LEX.createSniCallback(le) // receives an instance of letsencrypt, returns an SNICallback handler for https.createServer()
|
||||||
|
|
||||||
|
|
||||||
|
LEX.getChallenge(opts, hostname, key cb) // uses `opts.webrootPath` to read from the filesystem
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
If any of these values are `undefined` or `null` the will assume use reasonable defaults.
|
If any of these values are `undefined` or `null` the will assume use reasonable defaults.
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
'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);
|
||||||
|
};
|
||||||
|
};
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var challengeStore = require('./lib/challange-handlers');
|
var challengeStore = require('./lib/challange-handlers');
|
||||||
|
var createSniCallback = require('./lib/sni-callback').create;
|
||||||
var LE = require('letsencrypt');
|
var LE = require('letsencrypt');
|
||||||
|
|
||||||
function LEX(obj, app) {
|
function LEX(obj, app) {
|
||||||
|
@ -105,6 +106,7 @@ function LEX(obj, app) {
|
||||||
httpsOptions.SNICallback = obj.sniCallback;
|
httpsOptions.SNICallback = obj.sniCallback;
|
||||||
}
|
}
|
||||||
else if (sniCallback) {
|
else if (sniCallback) {
|
||||||
|
obj._sniCallback = createSniCallback(obj);
|
||||||
httpsOptions.SNICallback = function (domain, cb) {
|
httpsOptions.SNICallback = function (domain, cb) {
|
||||||
sniCallback(domain, function (err, context) {
|
sniCallback(domain, function (err, context) {
|
||||||
if (context) {
|
if (context) {
|
||||||
|
@ -112,12 +114,12 @@ function LEX(obj, app) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.letsencrypt.sniCallback(domain, cb);
|
obj._sniCallback(domain, cb);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
httpsOptions.SNICallback = obj.letsencrypt.sniCallback;
|
httpsOptions.SNICallback = createSniCallback(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
function listen(plainPorts, tlsPorts, onListening) {
|
function listen(plainPorts, tlsPorts, onListening) {
|
||||||
|
@ -190,6 +192,7 @@ function LEX(obj, app) {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = LEX;
|
module.exports = LEX;
|
||||||
|
|
||||||
LEX.create = LEX;
|
LEX.create = LEX;
|
||||||
LEX.setChallenge = challengeStore.set;
|
LEX.setChallenge = challengeStore.set;
|
||||||
LEX.getChallenge = challengeStore.get;
|
LEX.getChallenge = challengeStore.get;
|
||||||
|
|
Loading…
Reference in New Issue