more passing
This commit is contained in:
parent
3f1c669ef7
commit
f227a3ea6c
18
README.md
18
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) {}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue