From f227a3ea6c9115e7346550a1b6854df8c95bd880 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 17 Dec 2015 05:08:14 +0000 Subject: [PATCH] more passing --- README.md | 18 ++++++ examples/serve.js | 4 +- lib/challenge-handlers.js | 36 +++++++++-- lib/sni-callback.js | 132 +++++++++++++++++++++++++++----------- lib/standalone.js | 36 +++++++++-- 5 files changed, 177 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index c40b207..bca464e 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,24 @@ LEX.createSniCallback(opts) // this will call letsencrypt.renew and letsencr , memorizeFor: <1 day> // how long to wait before checking the disk for updated certificates , renewWithin: <3 days> // the first possible moment the certificate staggering should begin , failedWait: <5 minutes> // how long to wait before trying again if the certificate registration failed + + + // registrations are NOT approved automatically by default due to security concerns + , approveRegistration: func // (someone can spoof servername indication to your server and cause you to be rate-limited) + // but you can implement handling of them if you wish + // (note that you should probably call the callback immediately with a tlsContext) + // + // default function (hostname, cb) { cb(null, null); } + // + // example function (hostname, cb) { + // cb(null, { domains: [hostname], agreeTos: true, email: 'user@example.com' }); + // } + + + , handleRenewFailure: func // renewals are automatic, but sometimes they may fail. If that happens, you should handle it + // (note that renewals happen in the background) + // + // default function (err, letsencrypt, hostname, certInfo) {} } diff --git a/examples/serve.js b/examples/serve.js index 0b6558c..8b40499 100644 --- a/examples/serve.js +++ b/examples/serve.js @@ -1,7 +1,7 @@ 'use strict'; //var le = require('letsencrypt-express'); -var le = require('../'); +var le = require('../').testing(); var express = require('express'); var app = express(); @@ -9,4 +9,4 @@ app.use(function (req, res) { res.send({ success: true }); }); -le.create(app).listen([80], [443, 5001]); +le.create('./letsencrypt.config', app).listen([80], [443, 5001]); diff --git a/lib/challenge-handlers.js b/lib/challenge-handlers.js index 1196c1f..5777c81 100644 --- a/lib/challenge-handlers.js +++ b/lib/challenge-handlers.js @@ -2,6 +2,7 @@ var fs = require('fs'); var path = require('path'); +var mkdirp = require('mkdirp'); // TODO handle templating :hostname in letsencrypt proper @@ -10,15 +11,36 @@ var path = require('path'); module.exports = { set: function setChallenge(args, hostname, key, value, cb) { - var keyfile = path.join((args.webrootPath || args.webrootTpl).replace(':hostname', hostname), key); + var webrootPath = (args.webrootPath || args.webrootTpl).replace(':hostname', hostname); + var keyfile = path.join(webrootPath, key); - fs.writeFile(keyfile, value, 'utf8', cb); + if (args.debug) { + console.log('[LEX] write file', hostname, webrootPath, key); + } + fs.writeFile(keyfile, value, 'utf8', function (err) { + if (!err) { cb(null); return; } + + + if (args.debug) { + console.log('[LEX] mkdirp', webrootPath); + } + mkdirp(webrootPath, function () { + if (err) { cb(err); return; } + + fs.writeFile(keyfile, value, 'utf8', cb); + }); + }); } , get: function getChallenge(args, hostname, key, cb) { var keyfile = path.join((args.webrootPath || args.webrootTpl).replace(':hostname', hostname), key); - fs.readFile(keyfile, 'utf8', cb); + if (args.debug) { + console.log('[LEX] getChallenge', hostname, key); + } + fs.readFile(keyfile, 'utf8', function (err, text) { + cb(null, text); + }); } , remove: function removeChallenge(args, hostname, key, cb) { @@ -26,6 +48,12 @@ module.exports = { // Note: it's not actually terribly important that we wait for the unlink callback // but it's a polite thing to do - and we're polite people! - fs.unlink(keyfile, cb); + if (args.debug) { + console.log('[LEX] removeChallenge', hostname, key); + } + fs.unlink(keyfile, function (err) { + if (err) { console.warn(err.stack); } + cb(null); + }); } }; diff --git a/lib/sni-callback.js b/lib/sni-callback.js index ab599cf..e9c9b03 100644 --- a/lib/sni-callback.js +++ b/lib/sni-callback.js @@ -3,19 +3,22 @@ var crypto = require('crypto'); var tls = require('tls'); -module.exports.create = function (memos) { +module.exports.create = function (opts) { + if (opts.debug) { + console.log("[LEX] creating sniCallback", JSON.stringify(opts, null, ' ')); + } 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 (!opts) { throw new Error("requires opts to be an object"); } + if (!opts.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 (!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; } - if (!memos.handleRegistration) { memos.handleRegistration = function (args, cb) { cb(null, null); }; } - if (!memos.handleRenewFailure) { memos.handleRenewFailure = function () {}; } + if (!opts.approveRegistration) { opts.approveRegistration = function (hostname, cb) { cb(null, null); }; } + if (!opts.handleRenewFailure) { opts.handleRenewFailure = function (/*err, hostname, certInfo*/) {}; } function assignBestByDates(now, certInfo) { certInfo = certInfo || { loadedAt: now, expiresAt: 0, issuedAt: 0, lifetime: 0 }; @@ -26,10 +29,10 @@ module.exports.create = function (memos) { 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)); + var memorizeFor = Math.floor(opts.memorizeFor + ((opts.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)); + var bestIfUsedBy = certInfo.expiresAt - (opts.renewWithin + Math.floor(opts.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); @@ -41,7 +44,7 @@ module.exports.create = function (memos) { } function renewInBackground(now, hostname, certInfo) { - if ((now - certInfo.loadedAt) < memos.failedWait) { + if ((now - certInfo.loadedAt) < opts.failedWait) { // wait a few minutes return; } @@ -53,11 +56,16 @@ module.exports.create = function (memos) { certInfo.renewTimeout = Math.floor(certInfo.renewTimeout / 2); } + if (opts.debug) { + console.log("[LEX] skipping stagger '" + certInfo.renewTimeout + "' and renewing '" + hostname + "' now"); + certInfo.renewTimeout = 500; + } + certInfo.timeout = setTimeout(function () { - var opts = { domains: [ hostname ], duplicate: false }; - le.renew(opts, function (err, certInfo) { + var args = { domains: [ hostname ], duplicate: false }; + opts.letsencrypt.renew(args, function (err, certInfo) { if (err || !certInfo) { - memos.handleRenewFailure(err, certInfo, opts); + opts.handleRenewFailure(err, hostname, certInfo); } ipc[hostname] = assignBestByDates(now, certInfo); }); @@ -66,36 +74,70 @@ module.exports.create = function (memos) { } 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; + opts.letsencrypt.fetch({ domains: [hostname] }, function (err, certInfo) { + if (opts.debug) { + console.log("[LEX] fetch result '" + hostname + "':"); + console.log(err, certInfo); } - - // 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) + var now = Date.now(); + + if (!certInfo) { + // handles registration + if (opts.debug) { + console.log("[LEX] '" + hostname + "' is not registered, requesting approval"); + } + opts.approveRegistration(hostname, function (err, args) { + if (opts.debug) { + console.log("[LEX] '" + hostname + "' registration approved, attempting register"); + } + if (err || !(args && args.agreeTos)) { + done(err, certInfo); + return; + } + opts.letsencrypt.register(args, function (err, certInfo) { + if (opts.debug) { + console.log("[LEX] '" + hostname + "' register completed", err, certInfo); + } + done(err, certInfo); + }); }); - } 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); + done(err, certInfo); + + function done(err, certInfo) { + ipc[hostname] = assignBestByDates(now, certInfo); + + // handles renewals + renewInBackground(now, hostname, certInfo); + + if (err) { + cb(err); + return; + } + + if (!certInfo.tlsContext && null !== certInfo.tlsContext) { + try { + certInfo.tlsContext = tls.createSecureContext({ + key: certInfo.key // privkey.pem + , cert: certInfo.cert // fullchain.pem (cert.pem + '\n' + chain.pem) + }); + } catch(e) { + certInfo.tlsContext = null; + console.warn("[Sanity Check Fail]: a weird object was passed back through le.fetch to lex.fetch"); + cb(e); + return; + } + } + + cb(null, certInfo.tlsContext); + } }); } @@ -105,24 +147,36 @@ module.exports.create = function (memos) { // TODO once ECDSA is available, wait for cert renewal if its due if (!certInfo) { + if (opts.debug) { + console.log("[LEX] no certs loaded for '" + hostname + "'"); + } fetch(hostname, cb); return; } - if (certInfo.context) { - cb(null, certInfo.context); + if (certInfo.tlsContext) { + cb(null, certInfo.tlsContext); if ((now - certInfo.loadedAt) < (certInfo.memorizeFor)) { // these aren't stale, so don't fall through + if (opts.debug) { + console.log("[LEX] certs for '" + hostname + "' are fresh from disk"); + } return; } } - else if ((now - certInfo.loadedAt) < memos.failedWait) { + else if ((now - certInfo.loadedAt) < opts.failedWait) { + if (opts.debug) { + console.log("[LEX] certs for '" + hostname + "' recently failed and are still in cool down"); + } // this was just fetched and failed, wait a few minutes cb(null, null); return; } - fetch({ domains: [hostname] }, cb); + if (opts.debug) { + console.log("[LEX] certs for '" + hostname + "' are stale on disk and should be will be fetched again"); + } + fetch(hostname, cb); }; }; diff --git a/lib/standalone.js b/lib/standalone.js index 54d0a01..be4601f 100644 --- a/lib/standalone.js +++ b/lib/standalone.js @@ -1,8 +1,8 @@ 'use strict'; var path = require('path'); -var challengeStore = require('./lib/challange-handlers'); -var createSniCallback = require('./lib/sni-callback').create; +var challengeStore = require('./challenge-handlers'); +var createSniCallback = require('./sni-callback').create; var LE = require('letsencrypt'); function LEX(obj, app) { @@ -26,6 +26,8 @@ function LEX(obj, app) { }; } + obj.debug = LEX.debug; + if ('function' === typeof app) { obj.onRequest = obj.onRequest || app; } @@ -63,6 +65,9 @@ function LEX(obj, app) { } function acmeResponder(req, res) { + if (LEX.debug) { + console.log('[LEX] ', req.method, req.headers.host, req.url); + } var acmeChallengePrefix = '/.well-known/acme-challenge/'; if (0 !== req.url.indexOf(acmeChallengePrefix)) { @@ -73,6 +78,9 @@ function LEX(obj, app) { var key = req.url.slice(acmeChallengePrefix.length); obj.getChallenge(obj, req.headers.host, key, function (err, val) { + if (LEX.debug) { + console.log('[LEX] challenge response:', key, err, val); + } res.end(val || '_'); }); } @@ -98,6 +106,20 @@ function LEX(obj, app) { } } + if (!obj.approveRegistration && LEX.defaultApproveRegistration) { + obj.approveRegistration = function (domain, cb) { + if (LEX.debug) { + console.log('[LEX] auto register against staging server'); + } + cb(null, { + email: 'example@gmail.com' + , domains: [domain] + , agreeTos: true + , server: LEX.stagingServerUrl + }); + }; + } + if (obj.sniCallback) { if (sniCallback) { console.warn("You specified both args.sniCallback and args.httpsOptions.SNICallback," @@ -108,6 +130,7 @@ function LEX(obj, app) { else if (sniCallback) { obj._sniCallback = createSniCallback(obj); httpsOptions.SNICallback = function (domain, cb) { + console.log('[LEX] auto register against staging server'); sniCallback(domain, function (err, context) { if (context) { cb(err, context); @@ -146,7 +169,6 @@ function LEX(obj, app) { console.info('Listening ' + protocol + '://' + addr.address + port + '/'); } - console.log(plainPorts); plainPorts.forEach(function (addr) { var port = addr.port || addr; var address = addr.address || ''; @@ -203,6 +225,12 @@ LEX.stagingServerUrl = LE.stagingServerUrl; LEX.productionServerUrl = LE.productionServerUrl || LE.liveServerUrl; LEX.defaultServerUrl = LEX.productionServerUrl; LEX.testing = function () { + LEX.debug = true; LEX.defaultServerUrl = LEX.stagingServerUrl; - return module.expotrs; + LEX.defaultApproveRegistration = true; + console.log('[LEX] testing mode turned on'); + console.log('[LEX] default server: ' + LEX.defaultServerUrl); + console.log('[LEX] automatic registration handling turned on for testing.'); + + return module.exports; };