From feacdbaa74fac62f363a35be0a630f45d6760c2e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 12 Aug 2016 01:59:19 -0400 Subject: [PATCH] update README and code --- README.md | 112 +++++++++++++++++++++++++++++++++++++++++++++--------- index.js | 59 ++++++++++++++++++---------- 2 files changed, 133 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index c3d0584..8db3570 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ le-sni-auto =========== -**DRAFT** this is not yet published to npm - An auto-sni strategy for registering and renewing letsencrypt certificates using SNICallback. This does a couple of rather simple things: @@ -31,10 +29,10 @@ With node-letsencrypt var leSni = require('le-sni-auto').create({ - notBefore: 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 + renewWithin: 10 * 24 * 60 * 60 1000 // do not renew more than 10 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() , requestCert: false // in addition to key (privkey.pem) and cert (cert.pem + chain.pem), , 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 httpsOptions = { SNICallback: le.sni.callback }; - httpsOptions = require('localhost.daplie.com-certificates').merge(httpsOptions); - - -http.createServer(le.handleAcmeOrRedirectToHttps()); -https.createServer(dummyCerts, le.handleAcmeOrUse(app)).listen(443); +https.createServer(dummyCerts, le.middleware(app)).listen(443); ``` You can also provide a thunk-style `getCertificates(domain, certs, cb)`. @@ -77,12 +76,13 @@ Standalone 'use strict'; -var le = require('letsencrypt').create({ - notBefore: 10 * 24 * 60 * 60 1000 // do not renew prior to 10 days before expiration -, notAfter: 5 * 24 * 60 * 60 1000 // do not wait more than 5 days before expiration + +var leSni = require('le-sni-auto').create({ + 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 -, httpsOptions: { rejectUnauthorized: true, requestCert: false, ca: null, crl: null } +, tlsOptions: { rejectUnauthorized: true, requestCert: false, ca: null, crl: null } , getCertificatesAsync: function (domain, certs) { // 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'); -dummyCerts.SNICallback = le.sni.sniCallback; - -https.createServer(dummyCerts, ); +https.createServer(httpsOptions, app); ``` 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: '<>' +, cert: '<>' +, subject: 'example.com' +, altnames: [ 'example.com', 'www.example.com' ] +, issuedAt: 1470975565000 +, expiresAt: 1478751565000 +}); +``` diff --git a/index.js b/index.js index 243c1f5..5f2b9c9 100644 --- a/index.js +++ b/index.js @@ -1,21 +1,39 @@ '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) { - var DAY = 24 * 60 * 60 * 1000; - var MIN = 60 * 1000; 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.notAfter) { autoSni.notAfter = autoSni.notBefore - (3 * DAY); } - if (!autoSni.httpsOptions) { autoSni.httpsOptions = {}; } + if (!autoSni.renewWithin) { autoSni.renewWithin = autoSni.notBefore || defaults.renewWithin; } + if (autoSni.renewWithin < defaults._renewWithinMin) { + 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.renewWithin = autoSni.notBefore; // i.e. 15 days - autoSni.renewWindow = autoSni.notBefore - autoSni.notAfter; // i.e. 1 day - //autoSni.renewRatio = autoSni.notBefore = autoSni.renewWindow; // i.e. 1/15 (6.67%) + autoSni._dropDead = defaults._dropDead; + //autoSni.renewWithin = autoSni.notBefore; // i.e. 15 days + autoSni._renewWindow = autoSni.renewWithin - autoSni.renewBy; // i.e. 1 day + //autoSni.renewRatio = autoSni.notBefore = autoSni._renewWindow; // i.e. 1/15 (6.67%) @@ -32,31 +50,32 @@ module.exports.create = function (autoSni) { // in-process cache _ipc: {} - // just to account for clock skew - , _fiveMin: 5 * MIN + , getOptions: function () { + return JSON.parse(JSON.stringify(defaults)); + } // cache and format incoming certs - , _cacheCerts: function (certs) { + , cacheCerts: function (certs) { var meta = { certs: certs , tlsContext: 'string' === typeof certs.cert && tls.createSecureContext({ key: certs.privkey , cert: certs.cert + certs.chain - , rejectUnauthorized: autoSni.httpsOptions.rejectUnauthorized + , rejectUnauthorized: autoSni.tlsOptions.rejectUnauthorized - , requestCert: autoSni.httpsOptions.requestCert // request peer verification - , ca: autoSni.httpsOptions.ca // this chain is for incoming peer connctions - , crl: autoSni.httpsOptions.crl // this crl is for incoming peer connections + , requestCert: autoSni.tlsOptions.requestCert // request peer verification + , ca: autoSni.tlsOptions.ca // this chain is for incoming peer connctions + , crl: autoSni.tlsOptions.crl // this crl is for incoming peer connections }) || { '_fake_tls_context_': true } , subject: certs.subject // 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 - , expiresNear: certs.expiresAt - autoSni._fiveMin + , expiresNear: certs.expiresAt - autoSni._dropDead }; 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 certMeta.renewAt = (autoSni._dbg_now || Date.now()) + (2 * MIN) + (3 * MIN * Math.random()); // 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 @@ -108,7 +127,7 @@ module.exports.create = function (autoSni) { } // 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); }, function (err) { console.error('ERROR in le-sni-auto:');