update README and code
This commit is contained in:
parent
ef26fe59dd
commit
feacdbaa74
112
README.md
112
README.md
|
@ -1,8 +1,6 @@
|
||||||
le-sni-auto
|
le-sni-auto
|
||||||
===========
|
===========
|
||||||
|
|
||||||
**DRAFT** this is not yet published to npm
|
|
||||||
|
|
||||||
An auto-sni strategy for registering and renewing letsencrypt certificates using SNICallback.
|
An auto-sni strategy for registering and renewing letsencrypt certificates using SNICallback.
|
||||||
|
|
||||||
This does a couple of rather simple things:
|
This does a couple of rather simple things:
|
||||||
|
@ -31,10 +29,10 @@ With node-letsencrypt
|
||||||
|
|
||||||
var leSni = require('le-sni-auto').create({
|
var leSni = require('le-sni-auto').create({
|
||||||
|
|
||||||
notBefore: 10 * 24 * 60 * 60 1000 // do not renew more than 10 days before expiration
|
renewWithin: 10 * 24 * 60 * 60 1000 // do not renew more than 10 days before expiration
|
||||||
, notAfter: 5 * 24 * 60 * 60 1000 // do not wait more than 5 days before expiration
|
, renewBy: 5 * 24 * 60 * 60 1000 // do not wait more than 5 days before expiration
|
||||||
|
|
||||||
, httpsOptions: {
|
, tlsOptions: {
|
||||||
rejectUnauthorized: true // These options will be used with tls.createSecureContext()
|
rejectUnauthorized: true // These options will be used with tls.createSecureContext()
|
||||||
, requestCert: false // in addition to key (privkey.pem) and cert (cert.pem + chain.pem),
|
, requestCert: false // in addition to key (privkey.pem) and cert (cert.pem + chain.pem),
|
||||||
, ca: null // which are provided by letsencrypt
|
, ca: null // which are provided by letsencrypt
|
||||||
|
@ -58,14 +56,15 @@ var le = require('letsencrypt').create({
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var redirectHttps = require('redirect-https').create();
|
||||||
|
http.createServer(le.middleware(redirectHttps));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var app = require('express')();
|
var app = require('express')();
|
||||||
var httpsOptions = { SNICallback: le.sni.callback };
|
var httpsOptions = { SNICallback: le.sni.callback };
|
||||||
|
|
||||||
httpsOptions = require('localhost.daplie.com-certificates').merge(httpsOptions);
|
httpsOptions = require('localhost.daplie.com-certificates').merge(httpsOptions);
|
||||||
|
https.createServer(dummyCerts, le.middleware(app)).listen(443);
|
||||||
|
|
||||||
http.createServer(le.handleAcmeOrRedirectToHttps());
|
|
||||||
https.createServer(dummyCerts, le.handleAcmeOrUse(app)).listen(443);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also provide a thunk-style `getCertificates(domain, certs, cb)`.
|
You can also provide a thunk-style `getCertificates(domain, certs, cb)`.
|
||||||
|
@ -77,12 +76,13 @@ Standalone
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
var le = require('letsencrypt').create({
|
|
||||||
notBefore: 10 * 24 * 60 * 60 1000 // do not renew prior to 10 days before expiration
|
var leSni = require('le-sni-auto').create({
|
||||||
, notAfter: 5 * 24 * 60 * 60 1000 // do not wait more than 5 days before expiration
|
renewWithin: 10 * 24 * 60 * 60 1000 // do not renew prior to 10 days before expiration
|
||||||
|
, renewBy: 5 * 24 * 60 * 60 1000 // do not wait more than 5 days before expiration
|
||||||
|
|
||||||
// key (privkey.pem) and cert (cert.pem + chain.pem) will be provided by letsencrypt
|
// key (privkey.pem) and cert (cert.pem + chain.pem) will be provided by letsencrypt
|
||||||
, httpsOptions: { rejectUnauthorized: true, requestCert: false, ca: null, crl: null }
|
, tlsOptions: { rejectUnauthorized: true, requestCert: false, ca: null, crl: null }
|
||||||
|
|
||||||
, getCertificatesAsync: function (domain, certs) {
|
, getCertificatesAsync: function (domain, certs) {
|
||||||
// return a promise with an object with the following keys:
|
// return a promise with an object with the following keys:
|
||||||
|
@ -92,11 +92,87 @@ var le = require('letsencrypt').create({
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// some default certificates that work with localhost
|
||||||
|
// (because default certificates are required as a fallback)
|
||||||
|
var httpsOptions = require('localhost.daplie.com-certificates').merge({
|
||||||
|
SNICallback: leSni.sniCallback
|
||||||
|
});
|
||||||
|
|
||||||
var dummyCerts = require('localhost.daplie.com-certificates');
|
https.createServer(httpsOptions, app);
|
||||||
dummyCerts.SNICallback = le.sni.sniCallback;
|
|
||||||
|
|
||||||
https.createServer(dummyCerts, );
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also provide a thunk-style `getCertificates(domain, certs, cb)`.
|
You can also provide a thunk-style `getCertificates(domain, certs, cb)`.
|
||||||
|
|
||||||
|
API
|
||||||
|
===
|
||||||
|
|
||||||
|
* create(options)
|
||||||
|
* `getCertificates(domain, certs, cb)` or `getCertificatesAsync(domain, certs)`
|
||||||
|
* `renewWithin` (default 7 days, min 3 days)
|
||||||
|
* `renewBy` (default 2 days, min 12 hours)
|
||||||
|
* `sniCallback(domain, cb)`
|
||||||
|
* `cacheCerts(certs)`
|
||||||
|
|
||||||
|
.renewWithin
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Specifies the maximum amount of time (in ms) before
|
||||||
|
the certificate expires to renew it.
|
||||||
|
|
||||||
|
Say the cert expires in 90 days and you would like
|
||||||
|
to renew, **at earliest** 10 days before it expires.
|
||||||
|
|
||||||
|
You would set this to `10 * 24 * 60 * 60 * 1000`.
|
||||||
|
|
||||||
|
.renewBy
|
||||||
|
--------
|
||||||
|
|
||||||
|
Specifies the maximum amount of time (in ms) before
|
||||||
|
the certificate expires to renew it.
|
||||||
|
|
||||||
|
Say the cert expires in 90 days and you would like
|
||||||
|
to renew, **at latest** 10 days before it expires.
|
||||||
|
|
||||||
|
You would set this to `10 * 24 * 60 * 60 * 1000`.
|
||||||
|
|
||||||
|
**MUST** be **less than** `renewWithin`.
|
||||||
|
|
||||||
|
.sniCallback()
|
||||||
|
-----------
|
||||||
|
|
||||||
|
This gets passed to `https.createServer(httpsOptions, app)` as `httpsOptions.SNICallback`.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var leSni = require('le-sni-auto').create({
|
||||||
|
renewWithin: 10 * 24 * 60 * 60 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
var httpsOptions = require('localhost.daplie.com-certificates').merge({
|
||||||
|
SNICallback: leSni.sniCallback
|
||||||
|
});
|
||||||
|
|
||||||
|
function app(req, res) {
|
||||||
|
res.end("Hello, World!");
|
||||||
|
}
|
||||||
|
|
||||||
|
https.createServer(httpsOptions, app);
|
||||||
|
```
|
||||||
|
|
||||||
|
.cacheCerts()
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Manually load a certificate into the cache.
|
||||||
|
|
||||||
|
This is useful in a cluster environment where the master
|
||||||
|
may wish to inform multiple workers of a new or renewed certificate.
|
||||||
|
|
||||||
|
```
|
||||||
|
leSni.cacheCerts({
|
||||||
|
, privkey: '<<privkey.pem>>'
|
||||||
|
, cert: '<<cert.pem + chain.pem>>'
|
||||||
|
, subject: 'example.com'
|
||||||
|
, altnames: [ 'example.com', 'www.example.com' ]
|
||||||
|
, issuedAt: 1470975565000
|
||||||
|
, expiresAt: 1478751565000
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
57
index.js
57
index.js
|
@ -1,21 +1,39 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// autoSni = { notBefore, notAfter, getCertificates, httpsOptions, _dbg_now }
|
var DAY = 24 * 60 * 60 * 1000;
|
||||||
|
var MIN = 60 * 1000;
|
||||||
|
var defaults = {
|
||||||
|
// don't renew before the renewWithin period
|
||||||
|
renewWithin: 7 * DAY
|
||||||
|
, _renewWithinMin: 3 * DAY
|
||||||
|
// renew before the renewBy period
|
||||||
|
, renewBy: 2 * DAY
|
||||||
|
, _renewByMin: Math.floor(DAY / 2)
|
||||||
|
// just to account for clock skew really
|
||||||
|
, _dropDead: 5 * MIN
|
||||||
|
};
|
||||||
|
|
||||||
|
// autoSni = { renewWithin, renewBy, getCertificates, tlsOptions, _dbg_now }
|
||||||
module.exports.create = function (autoSni) {
|
module.exports.create = function (autoSni) {
|
||||||
|
|
||||||
var DAY = 24 * 60 * 60 * 1000;
|
|
||||||
var MIN = 60 * 1000;
|
|
||||||
if (!autoSni.getCertificatesAsync) { autoSni.getCertificatesAsync = require('bluebird').promisify(autoSni.getCertificates); }
|
if (!autoSni.getCertificatesAsync) { autoSni.getCertificatesAsync = require('bluebird').promisify(autoSni.getCertificates); }
|
||||||
if (!autoSni.notBefore) { throw new Error("must supply options.notBefore (and options.notAfter)"); }
|
if (!autoSni.renewWithin) { autoSni.renewWithin = autoSni.notBefore || defaults.renewWithin; }
|
||||||
if (!autoSni.notAfter) { autoSni.notAfter = autoSni.notBefore - (3 * DAY); }
|
if (autoSni.renewWithin < defaults._renewWithinMin) {
|
||||||
if (!autoSni.httpsOptions) { autoSni.httpsOptions = {}; }
|
throw new Error("options.renewWithin should be at least 3 days");
|
||||||
|
}
|
||||||
|
if (!autoSni.renewBy) { autoSni.renewBy = autoSni.notBefore || defaults.renewBy; }
|
||||||
|
if (autoSni.renewBy < defaults._renewByMin) {
|
||||||
|
throw new Error("options.renewBy should be at least 12 hours");
|
||||||
|
}
|
||||||
|
if (!autoSni.tlsOptions) { autoSni.tlsOptions = autoSni.httpsOptions || {}; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
autoSni._dropDead = defaults._dropDead;
|
||||||
//autoSni.renewWithin = autoSni.notBefore; // i.e. 15 days
|
//autoSni.renewWithin = autoSni.notBefore; // i.e. 15 days
|
||||||
autoSni.renewWindow = autoSni.notBefore - autoSni.notAfter; // i.e. 1 day
|
autoSni._renewWindow = autoSni.renewWithin - autoSni.renewBy; // i.e. 1 day
|
||||||
//autoSni.renewRatio = autoSni.notBefore = autoSni.renewWindow; // i.e. 1/15 (6.67%)
|
//autoSni.renewRatio = autoSni.notBefore = autoSni._renewWindow; // i.e. 1/15 (6.67%)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,31 +50,32 @@ module.exports.create = function (autoSni) {
|
||||||
|
|
||||||
// in-process cache
|
// in-process cache
|
||||||
_ipc: {}
|
_ipc: {}
|
||||||
// just to account for clock skew
|
, getOptions: function () {
|
||||||
, _fiveMin: 5 * MIN
|
return JSON.parse(JSON.stringify(defaults));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// cache and format incoming certs
|
// cache and format incoming certs
|
||||||
, _cacheCerts: function (certs) {
|
, cacheCerts: function (certs) {
|
||||||
var meta = {
|
var meta = {
|
||||||
certs: certs
|
certs: certs
|
||||||
, tlsContext: 'string' === typeof certs.cert && tls.createSecureContext({
|
, tlsContext: 'string' === typeof certs.cert && tls.createSecureContext({
|
||||||
key: certs.privkey
|
key: certs.privkey
|
||||||
, cert: certs.cert + certs.chain
|
, cert: certs.cert + certs.chain
|
||||||
, rejectUnauthorized: autoSni.httpsOptions.rejectUnauthorized
|
, rejectUnauthorized: autoSni.tlsOptions.rejectUnauthorized
|
||||||
|
|
||||||
, requestCert: autoSni.httpsOptions.requestCert // request peer verification
|
, requestCert: autoSni.tlsOptions.requestCert // request peer verification
|
||||||
, ca: autoSni.httpsOptions.ca // this chain is for incoming peer connctions
|
, ca: autoSni.tlsOptions.ca // this chain is for incoming peer connctions
|
||||||
, crl: autoSni.httpsOptions.crl // this crl is for incoming peer connections
|
, crl: autoSni.tlsOptions.crl // this crl is for incoming peer connections
|
||||||
}) || { '_fake_tls_context_': true }
|
}) || { '_fake_tls_context_': true }
|
||||||
|
|
||||||
, subject: certs.subject
|
, subject: certs.subject
|
||||||
// stagger renewal time by a little bit of randomness
|
// stagger renewal time by a little bit of randomness
|
||||||
, renewAt: (certs.expiresAt - (autoSni.notBefore - (autoSni.renewWindow * Math.random())))
|
, renewAt: (certs.expiresAt - (autoSni.renewWithin - (autoSni._renewWindow * Math.random())))
|
||||||
// err just barely on the side of safety
|
// err just barely on the side of safety
|
||||||
, expiresNear: certs.expiresAt - autoSni._fiveMin
|
, expiresNear: certs.expiresAt - autoSni._dropDead
|
||||||
};
|
};
|
||||||
var link = { subject: certs.subject };
|
var link = { subject: certs.subject };
|
||||||
|
|
||||||
|
@ -99,7 +118,7 @@ module.exports.create = function (autoSni) {
|
||||||
// give the cert some time (2-5 min) to be validated and replaced before trying again
|
// give the cert some time (2-5 min) to be validated and replaced before trying again
|
||||||
certMeta.renewAt = (autoSni._dbg_now || Date.now()) + (2 * MIN) + (3 * MIN * Math.random());
|
certMeta.renewAt = (autoSni._dbg_now || Date.now()) + (2 * MIN) + (3 * MIN * Math.random());
|
||||||
// let the update happen in the background
|
// let the update happen in the background
|
||||||
autoSni.getCertificatesAsync(domain, certMeta.certs).then(autoSni._cacheCerts);
|
autoSni.getCertificatesAsync(domain, certMeta.certs).then(autoSni.cacheCerts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the valid cert right away
|
// return the valid cert right away
|
||||||
|
@ -108,7 +127,7 @@ module.exports.create = function (autoSni) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// promise the non-existent or expired cert
|
// promise the non-existent or expired cert
|
||||||
promise.then(autoSni._cacheCerts).then(function (certMeta) {
|
promise.then(autoSni.cacheCerts).then(function (certMeta) {
|
||||||
cb(null, certMeta.tlsContext);
|
cb(null, certMeta.tlsContext);
|
||||||
}, function (err) {
|
}, function (err) {
|
||||||
console.error('ERROR in le-sni-auto:');
|
console.error('ERROR in le-sni-auto:');
|
||||||
|
|
Loading…
Reference in New Issue