diff --git a/README.md b/README.md index bca464e..c80fc34 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,24 @@ app.use('/', function (req, res) { res.send({ success: true }); }); -lex.create('/etc/letsencrypt', app).listen([80], [443, 5001], function () { +lex.create(app).listen(); +``` + +### Slightly more verbose + +```javascript +'use strict'; + +// Note: using staging server url, remove .testing() for production +var lex = require('letsencrypt-express').testing(); +var express = require('express'); +var app = express(); + +app.use('/', function (req, res) { + res.send({ success: true }); +}); + +lex.create('./letsencrypt.config', app).listen([80], [443, 5001], function () { console.log("ENCRYPT __ALL__ THE DOMAINS!"); }); ``` diff --git a/examples/116.js b/examples/116.js new file mode 100644 index 0000000..40a4099 --- /dev/null +++ b/examples/116.js @@ -0,0 +1,9 @@ +'use strict'; + +// Don't try this at home kids, it's just for fun +// + +// require('letsencrypt-express') +require('../').testing().create(require('express')().use(function (_, r) { + r.end('Hi!'); +})).listen(); diff --git a/examples/serve.js b/examples/serve.js index 8b40499..bba3538 100644 --- a/examples/serve.js +++ b/examples/serve.js @@ -1,12 +1,14 @@ 'use strict'; -//var le = require('letsencrypt-express'); -var le = require('../').testing(); +// Note: using staging server url, remove .testing() for production +var lex = require('letsencrypt-express').testing(); var express = require('express'); var app = express(); -app.use(function (req, res) { +app.use('/', function (req, res) { res.send({ success: true }); }); -le.create('./letsencrypt.config', app).listen([80], [443, 5001]); +lex.create('./letsencrypt.config', app).listen([80], [443, 5001], function () { + console.log("ENCRYPT __ALL__ THE DOMAINS!"); +}); diff --git a/examples/tweet.js b/examples/tweet.js new file mode 120000 index 0000000..b390447 --- /dev/null +++ b/examples/tweet.js @@ -0,0 +1 @@ +116.js \ No newline at end of file diff --git a/lib/challenge-handlers.js b/lib/challenge-handlers.js index 5777c81..eb08000 100644 --- a/lib/challenge-handlers.js +++ b/lib/challenge-handlers.js @@ -11,18 +11,20 @@ var mkdirp = require('mkdirp'); module.exports = { set: function setChallenge(args, hostname, key, value, cb) { - var webrootPath = (args.webrootPath || args.webrootTpl).replace(':hostname', hostname); + var webrootPath = args.webrootPath; var keyfile = path.join(webrootPath, key); if (args.debug) { - console.log('[LEX] write file', hostname, webrootPath, key); + console.debug('[LEX] write file', hostname, webrootPath); + console.debug('challenge:', key); + console.debug('response:', value); } fs.writeFile(keyfile, value, 'utf8', function (err) { if (!err) { cb(null); return; } if (args.debug) { - console.log('[LEX] mkdirp', webrootPath); + console.debug('[LEX] mkdirp', webrootPath); } mkdirp(webrootPath, function () { if (err) { cb(err); return; } @@ -33,10 +35,10 @@ module.exports = { } , get: function getChallenge(args, hostname, key, cb) { - var keyfile = path.join((args.webrootPath || args.webrootTpl).replace(':hostname', hostname), key); + var keyfile = path.join(args.webrootPath, key); if (args.debug) { - console.log('[LEX] getChallenge', hostname, key); + console.debug('[LEX] getChallenge', keyfile, hostname, key); } fs.readFile(keyfile, 'utf8', function (err, text) { cb(null, text); @@ -44,12 +46,12 @@ module.exports = { } , remove: function removeChallenge(args, hostname, key, cb) { - var keyfile = path.join((args.webrootPath || args.webrootTpl).replace(':hostname', hostname), key); + var keyfile = path.join(args.webrootPath, key); // 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! if (args.debug) { - console.log('[LEX] removeChallenge', hostname, key); + console.debug('[LEX] removeChallenge', keyfile, hostname, key); } fs.unlink(keyfile, function (err) { if (err) { console.warn(err.stack); } diff --git a/lib/sni-callback.js b/lib/sni-callback.js index e9c9b03..02eb07e 100644 --- a/lib/sni-callback.js +++ b/lib/sni-callback.js @@ -5,7 +5,7 @@ var tls = require('tls'); module.exports.create = function (opts) { if (opts.debug) { - console.log("[LEX] creating sniCallback", JSON.stringify(opts, null, ' ')); + console.debug("[LEX] creating sniCallback", JSON.stringify(opts, null, ' ')); } var ipc = {}; // in-process cache @@ -23,7 +23,7 @@ module.exports.create = function (opts) { function assignBestByDates(now, certInfo) { certInfo = certInfo || { loadedAt: now, expiresAt: 0, issuedAt: 0, lifetime: 0 }; - var rnds = crypto.randomBytes(3)[0]; + var rnds = crypto.randomBytes(3); var rnd1 = ((rnds[0] + 1) / 257); var rnd2 = ((rnds[1] + 1) / 257); var rnd3 = ((rnds[2] + 1) / 257); @@ -41,6 +41,8 @@ module.exports.create = function (opts) { certInfo.memorizeFor = memorizeFor; certInfo.bestIfUsedBy = bestIfUsedBy; certInfo.renewTimeout = renewTimeout; + + return certInfo; } function renewInBackground(now, hostname, certInfo) { @@ -57,7 +59,7 @@ module.exports.create = function (opts) { } if (opts.debug) { - console.log("[LEX] skipping stagger '" + certInfo.renewTimeout + "' and renewing '" + hostname + "' now"); + console.debug("[LEX] skipping stagger '" + certInfo.renewTimeout + "' and renewing '" + hostname + "' now"); certInfo.renewTimeout = 500; } @@ -73,71 +75,88 @@ module.exports.create = function (opts) { } } - function fetch(hostname, cb) { + function cacheResult(err, hostname, certInfo, sniCb) { + if (certInfo && certInfo.fullchain && certInfo.privkey) { + if (opts.debug) { + console.debug('cert is looking good'); + } + + try { + certInfo.tlsContext = tls.createSecureContext({ + key: certInfo.privkey || certInfo.key // privkey.pem + , cert: certInfo.fullchain || 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"); + console.warn("(either missing or malformed certInfo.key and / or certInfo.fullchain)"); + err = e; + } + + sniCb(err, certInfo.tlsContext); + } else { + if (opts.debug) { + console.debug('cert is NOT looking good'); + } + sniCb(err || new Error("couldn't get certInfo: unknown"), null); + } + + var now = Date.now(); + certInfo = ipc[hostname] = assignBestByDates(now, certInfo); + renewInBackground(now, hostname, certInfo); + } + + function registerCert(hostname, sniCb) { + if (opts.debug) { + console.debug("[LEX] '" + hostname + "' is not registered, requesting approval"); + } + + opts.approveRegistration(hostname, function (err, args) { + + if (opts.debug) { + console.debug("[LEX] '" + hostname + "' registration approved, attempting register"); + } + + if (err) { + cacheResult(err, hostname, null, sniCb); + return; + } + + if (!(args && args.agreeTos && args.email && args.domains)) { + cacheResult(new Error("not approved or approval is missing arguments - such as agreeTos, email, domains"), hostname, null, sniCb); + return; + } + + opts.letsencrypt.register(args, function (err, certInfo) { + if (opts.debug) { + console.debug("[LEX] '" + hostname + "' register completed", err && err.stack || null, certInfo); + } + + cacheResult(err, hostname, certInfo, sniCb); + }); + }); + } + + function fetch(hostname, sniCb) { opts.letsencrypt.fetch({ domains: [hostname] }, function (err, certInfo) { if (opts.debug) { - console.log("[LEX] fetch result '" + hostname + "':"); - console.log(err, certInfo); - } - if (err) { - cb(err); - return; - } - - 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); - }); - }); - return; - } - - done(err, certInfo); - - function done(err, certInfo) { - ipc[hostname] = assignBestByDates(now, certInfo); - - // handles renewals - renewInBackground(now, hostname, certInfo); - + console.debug("[LEX] fetch from disk result '" + hostname + "':"); + console.debug(certInfo && Object.keys(certInfo)); if (err) { - cb(err); - return; + console.error(err.stack || err); } - - 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); } + + if (err) { + sniCb(err, null); + return; + } + + if (certInfo) { + cacheResult(err, hostname, certInfo, sniCb); + return; + } + + registerCert(hostname, sniCb); }); } @@ -145,29 +164,44 @@ module.exports.create = function (opts) { var now = Date.now(); var certInfo = ipc[hostname]; - // TODO once ECDSA is available, wait for cert renewal if its due + + + // + // No cert is available in cache. + // try to fetch it from disk quickly + // and return to the browser + // if (!certInfo) { if (opts.debug) { - console.log("[LEX] no certs loaded for '" + hostname + "'"); + console.debug("[LEX] no certs loaded for '" + hostname + "'"); } fetch(hostname, cb); return; } + + + // + // A cert is available + // See if it's old enough that + // we should refresh it from disk + // (in the background) + // + // TODO once ECDSA is available, wait for cert renewal if its due (maybe?) 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"); + console.debug("[LEX] certs for '" + hostname + "' are fresh from disk"); } return; } } else if ((now - certInfo.loadedAt) < opts.failedWait) { if (opts.debug) { - console.log("[LEX] certs for '" + hostname + "' recently failed and are still in cool down"); + console.debug("[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); @@ -175,7 +209,19 @@ module.exports.create = function (opts) { } if (opts.debug) { - console.log("[LEX] certs for '" + hostname + "' are stale on disk and should be will be fetched again"); + console.debug("[LEX] certs for '" + hostname + "' are stale on disk and should be will be fetched again"); + console.debug({ + age: now - certInfo.loadedAt + , loadedAt: certInfo.loadedAt + , issuedAt: certInfo.issuedAt + , expiresAt: certInfo.expiresAt + , privkey: !!certInfo.privkey + , chain: !!certInfo.chain + , fullchain: !!certInfo.fullchain + , cert: !!certInfo.cert + , memorizeFor: certInfo.memorizeFor + , failedWait: opts.failedWait + }); } fetch(hostname, cb); }; diff --git a/lib/standalone.js b/lib/standalone.js index be4601f..665c907 100644 --- a/lib/standalone.js +++ b/lib/standalone.js @@ -48,7 +48,19 @@ function LEX(obj, app) { } if (!obj.configDir) { - obj.configDir = require('os').homedir() + '/letsencrypt/etc'; + obj.configDir = path.join(require('homedir')(), '/letsencrypt/etc'); + } + if (!obj.privkeyPath) { + obj.privkeyPath = ':config/live/:hostname/privkey.pem'; + } + if (!obj.fullchainPath) { + obj.fullchainPath = ':config/live/:hostname/fullchain.pem'; + } + if (!obj.certPath) { + obj.certPath = ':config/live/:hostname/cert.pem'; + } + if (!obj.chainPath) { + obj.chainPath = ':config/live/:hostname/chain.pem'; } if (!obj.server) { @@ -66,7 +78,7 @@ function LEX(obj, app) { function acmeResponder(req, res) { if (LEX.debug) { - console.log('[LEX] ', req.method, req.headers.host, req.url); + console.debug('[LEX] ', req.method, req.headers.host, req.url); } var acmeChallengePrefix = '/.well-known/acme-challenge/'; @@ -79,7 +91,12 @@ function LEX(obj, app) { obj.getChallenge(obj, req.headers.host, key, function (err, val) { if (LEX.debug) { - console.log('[LEX] challenge response:', key, err, val); + console.debug('[LEX] GET challenge, response:'); + console.debug('challenge:', key); + console.debug('response:', val); + if (err) { + console.debug(err.stack); + } } res.end(val || '_'); }); @@ -109,7 +126,7 @@ 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'); + console.debug('[LEX] auto register against staging server'); } cb(null, { email: 'example@gmail.com' @@ -130,7 +147,6 @@ 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); @@ -220,6 +236,41 @@ LEX.setChallenge = challengeStore.set; LEX.getChallenge = challengeStore.get; LEX.removeChallenge = challengeStore.remove; LEX.createSniCallback = createSniCallback; +// TODO not sure how well this works +LEX.middleware = function (defaults) { + var leCore = require('letiny-core'); + var merge = require('letsencrypt/common').merge; + var tplConfigDir = require('letsencrypt/common').tplConfigDir; + var tplHostname = require('letsencrypt/common').tplHostname; + var prefix = leCore.acmeChallengePrefix; + + tplConfigDir(defaults.configDir || '', defaults); + + return function (req, res, next) { + if (LEX.debug) { + console.debug('[LEX middleware]:', req.hostname, req.url, req.url.slice(prefix.length)); + } + + if (0 !== req.url.indexOf(prefix)) { + next(); + return; + } + + function done(err, token) { + if (err) { + res.send("Error: These aren't the tokens you're looking for. Move along."); + return; + } + + res.send(token); + } + + var copy = merge(defaults, { domains: [req.hostname] }); + tplHostname(req.hostname, copy); + + LEX.getChallenge(copy, req.hostname, req.url.slice(prefix.length), done); + }; +}; LEX.stagingServerUrl = LE.stagingServerUrl; LEX.productionServerUrl = LE.productionServerUrl || LE.liveServerUrl; @@ -228,9 +279,24 @@ LEX.testing = function () { LEX.debug = true; LEX.defaultServerUrl = LEX.stagingServerUrl; 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.'); + console.debug = console.log; + console.debug('[LEX] testing mode turned on'); + console.debug('[LEX] default server: ' + LEX.defaultServerUrl); + console.debug('\n'); + console.debug('###################################################'); + console.debug('# #'); + console.debug('# Open up a browser and visit this server #'); + console.debug('# at its domain name. #'); + console.debug('# #'); + console.debug('# ENJOY! #'); + console.debug('# #'); + console.debug('###################################################'); + console.debug('\n'); + console.debug('Note: testing certs will be installed because .testing() was called.'); + console.debug(' remove .testing() to get live certs.'); + console.debug('\n'); + console.debug('[LEX] automatic registration handling turned on for testing.'); + console.debug('\n'); return module.exports; }; diff --git a/package.json b/package.json index 46f7c8c..c8f0ef1 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ }, "homepage": "https://github.com/Daplie/letsencrypt-express#readme", "dependencies": { + "homedir": "^0.6.0", "letsencrypt": "^1.1.0", "mkdirp": "^0.5.1" }