w00t!
This commit is contained in:
parent
f227a3ea6c
commit
765293de3f
19
README.md
19
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!");
|
||||
});
|
||||
```
|
||||
|
|
|
@ -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();
|
|
@ -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!");
|
||||
});
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
116.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); }
|
||||
|
|
|
@ -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) {
|
||||
opts.letsencrypt.fetch({ domains: [hostname] }, function (err, certInfo) {
|
||||
function cacheResult(err, hostname, certInfo, sniCb) {
|
||||
if (certInfo && certInfo.fullchain && certInfo.privkey) {
|
||||
if (opts.debug) {
|
||||
console.log("[LEX] fetch result '" + hostname + "':");
|
||||
console.log(err, certInfo);
|
||||
console.debug('cert is looking good');
|
||||
}
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
|
||||
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();
|
||||
|
||||
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
|
||||
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) {
|
||||
cb(err);
|
||||
cacheResult(err, hostname, null, sniCb);
|
||||
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)
|
||||
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);
|
||||
});
|
||||
} 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;
|
||||
});
|
||||
}
|
||||
|
||||
function fetch(hostname, sniCb) {
|
||||
opts.letsencrypt.fetch({ domains: [hostname] }, function (err, certInfo) {
|
||||
if (opts.debug) {
|
||||
console.debug("[LEX] fetch from disk result '" + hostname + "':");
|
||||
console.debug(certInfo && Object.keys(certInfo));
|
||||
if (err) {
|
||||
console.error(err.stack || err);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue