updates for letsencrypt-express

This commit is contained in:
AJ ONeal 2015-12-17 08:46:40 +00:00
parent 60f9968177
commit 3b71f14c9e
5 changed files with 148 additions and 123 deletions

111
index.js
View File

@ -3,9 +3,9 @@
// TODO handle www and no-www together somehow?
var PromiseA = require('bluebird');
var crypto = require('crypto');
var tls = require('tls');
var leCore = require('letiny-core');
var merge = require('./lib/common').merge;
var tplHostname = require('./lib/common').tplHostname;
var LE = module.exports;
LE.productionServerUrl = leCore.productionServerUrl;
@ -21,18 +21,8 @@ LE.stagingServer = leCore.stagingServerUrl;
LE.liveServer = leCore.productionServerUrl;
LE.knownUrls = leCore.knownEndpoints;
LE.merge = function merge(defaults, args) {
var copy = {};
Object.keys(defaults).forEach(function (key) {
copy[key] = defaults[key];
});
Object.keys(args).forEach(function (key) {
copy[key] = args[key];
});
return copy;
};
LE.merge = require('./lib/common').merge;
LE.tplConfigDir = require('./lib/common').tplConfigDir;
// backend, defaults, handlers
LE.create = function (defaults, handlers, backend) {
@ -66,7 +56,11 @@ LE.create = function (defaults, handlers, backend) {
// "directory" for this. It's not that big of a deal.
var defaultos = LE.merge(defaults, {});
var getChallenge = require('./lib/default-handlers').getChallenge;
var copy = merge(defaults, { domains: [hostname] });
tplHostname(hostname, copy);
defaultos.domains = [hostname];
if (3 === getChallenge.length) {
getChallenge(defaultos, key, done);
}
@ -105,11 +99,16 @@ LE.create = function (defaults, handlers, backend) {
// ignore
// this backend was created the v1.0.0 way
}
// replaces strings of workDir, certPath, etc
// if they have :config/etc/live or :conf/etc/archive
// to instead have the path of the configDir
LE.tplConfigDir(defaults.configDir, defaults);
backend = PromiseA.promisifyAll(backend);
var utils = require('./utils');
var utils = require('./lib/utils');
//var attempts = {}; // should exist in master process only
var ipc = {}; // in-process cache
var le;
// TODO check certs on initial load
@ -141,39 +140,6 @@ LE.create = function (defaults, handlers, backend) {
console.warn("[SECURITY WARNING]: node-letsencrypt: validate(hostnames, cb) NOT IMPLEMENTED");
cb(null, true);
}
, middleware: function () {
var prefix = leCore.acmeChallengePrefix;
return function (req, res, next) {
if (0 !== req.url.indexOf(prefix)) {
//console.log('[LE middleware]: pass');
next();
return;
}
//args.domains = [req.hostname];
//console.log('[LE middleware]:', req.hostname, req.url, req.url.slice(prefix.length));
function done(err, token) {
if (err) {
res.send("Error: These aren't the tokens you're looking for. Move along.");
return;
}
res.send(token);
}
if (3 === handlers.getChallenge.length) {
handlers.getChallenge(req.hostname, req.url.slice(prefix.length), done);
}
else if (4 === handlers.getChallenge.length) {
handlers.getChallenge(defaults, req.hostname, req.url.slice(prefix.length), done);
}
else {
console.error("handlers.getChallenge [2] receives the wrong number of arguments");
done(new Error("handlers.getChallenge [2] receives the wrong number of arguments"));
}
};
}
, _registerHelper: function (args, cb) {
var copy = LE.merge(defaults, args);
var err;
@ -202,10 +168,7 @@ LE.create = function (defaults, handlers, backend) {
, _fetchHelper: function (args, cb) {
return backend.fetchAsync(args).then(function (certInfo) {
if (args.debug) {
console.log('[LE] debug is on');
}
if (true || args.debug) {
console.log('[LE] raw fetch certs', certInfo);
console.log('[LE] raw fetch certs', certInfo && Object.keys(certInfo));
}
if (!certInfo) { cb(null, null); return; }
@ -239,42 +202,38 @@ LE.create = function (defaults, handlers, backend) {
// but ensure that it has the most fresh copy
// before attempting a renew
le._fetchHelper(args, function (err, hit) {
var hostname = args.domains[0];
var now = Date.now();
if (err) { cb(err); return; }
else if (hit) { cb(null, hit); return; }
if (err) {
// had a bad day
cb(err);
return;
}
else if (hit) {
if ((now - hit.issuedAt) < ((hit.lifetime || handlers.lifetime) * 0.65)) {
console.warn("tried to renew a certificate with over 1/3 of its lifetime left, ignoring");
cb(null, hit);
return;
}
}
return le._registerHelper(args, function (err, pems) {
return le._registerHelper(args, function (err/*, pems*/) {
if (err) {
cb(err);
return;
}
le._fetchHelper(args, function (err, cache) {
if (cache) {
cb(null, cache);
// Sanity Check
le._fetchHelper(args, function (err, pems) {
if (pems) {
cb(null, pems);
return;
}
// still couldn't read the certs after success... that's weird
cb(err, null);
});
}, function (err) {
console.error("[Error] Let's Encrypt failed:");
console.error(err.stack || new Error(err.message || err.toString()).stack);
// wasn't successful with lets encrypt, don't automatically try again for 12 hours
// TODO what's the better way to handle this?
// failure callback?
ipc[hostname] = {
context: null // TODO default context
, issuedAt: Date.now()
, lifetime: (12 * 60 * 60 * 1000)
// , expiresAt: generated in next step
};
cb(err, ipc[hostname]);
});
}, cb);
});
}
};

View File

@ -116,3 +116,4 @@ function getAccountByEmail(/*args*/) {
module.exports.getAccountByEmail = getAccountByEmail;
module.exports.getAccount = getAccount;
module.exports.createAccount = createAccount;

View File

@ -3,42 +3,71 @@
var fs = require('fs');
var PromiseA = require('bluebird');
module.exports.fetchFromDisk = function (args, defaults) {
var hostname = args.domains[0];
var certPath = (args.fullchainPath || defaults.fullchainPath)
|| (defaults.configDir
+ (args.fullchainTpl || defaults.fullchainTpl || ':hostname/fullchain.pem').replace(/:hostname/, hostname));
var privkeyPath = (args.privkeyPath || defaults.privkeyPath)
|| (defaults.configDir
+ (args.privkeyTpl || defaults.privkeyTpl || ':hostname/privkey.pem').replace(/:hostname/, hostname));
var chainPath = (args.chainPath || defaults.chainPath)
|| (defaults.configDir
+ (args.chainTpl || defaults.chainTpl || ':hostname/chain.pem').replace(/:hostname/, hostname));
/*
var fullchainPath = (args.fullchainPath || defaults.fullchainPath)
|| (defaults.configDir
+ (args.fullchainTpl || defaults.fullchainTpl || ':hostname/fullchain.pem').replace(/:hostname/, hostname));
*/
module.exports.tplConfigDir = function merge(configDir, defaults) {
Object.keys(defaults).forEach(function (key) {
if ('string' === typeof defaults[key]) {
defaults[key] = defaults[key].replace(':config', configDir).replace(':conf', configDir);
}
});
};
module.exports.merge = function merge(defaults, args) {
var copy = {};
Object.keys(defaults).forEach(function (key) {
copy[key] = defaults[key];
});
Object.keys(args).forEach(function (key) {
copy[key] = args[key];
});
return copy;
};
module.exports.tplHostname = function merge(hostname, copy) {
Object.keys(copy).forEach(function (key) {
if ('string' === typeof copy[key]) {
copy[key] = copy[key].replace(':hostname', hostname).replace(':host', hostname);
}
});
//return copy;
};
module.exports.fetchFromDisk = function (args) {
// TODO NO HARD-CODED DEFAULTS
if (!args.fullchainPath || !args.privkeyPath || !args.certPath || !args.chainPath) {
console.warn("missing one or more of args.privkeyPath, args.fullchainPath, args.certPath, args.chainPath");
console.warn("hard-coded conventional pathnames were for debugging and are not a stable part of the API");
}
return PromiseA.all([
fs.readFileAsync(privkeyPath, 'ascii')
, fs.readFileAsync(certPath, 'ascii')
, fs.readFileAsync(chainPath, 'ascii')
//, fs.readFileAsync(fullchainPath, 'ascii')
// note: if this ^^ gets added back in, the arrays below must change
return PromiseA.all([
fs.readFileAsync(args.privkeyPath, 'ascii') // 0
, fs.readFileAsync(args.certPath, 'ascii') // 1
, fs.readFileAsync(args.chainPath, 'ascii') // 2
// stat the file, not the link
, fs.statAsync(certPath)
, fs.statAsync(args.certPath) // 3
]).then(function (arr) {
// TODO parse certificate to determine lifetime and expiresAt
return {
key: arr[0] // privkey.pem
, cert: arr[1] // cert.pem
, chain: arr[2] // chain.pem
, fullchain: arr[1] + '\n' + arr[2] // fullchain.pem
, privkey: arr[0] // privkey.pem
, issuedAt: arr[4].mtime.valueOf() // ???
, fullchain: arr[1] + '\n' + arr[2] // fullchain.pem
, cert: arr[1] // cert.pem
, chain: arr[2] // chain.pem
, ca: arr[2] // chain.pem
, issuedAt: arr[3].mtime.valueOf() // ??? TODO parse to determine expiresAt and lifetime
};
}, function () {
}, function (err) {
if (true || args.debug) {
console.error(err.stack);
}
return null;
});
};

View File

@ -9,6 +9,8 @@ var LeCore = PromiseA.promisifyAll(require('letiny-core'));
var leCrypto = PromiseA.promisifyAll(LeCore.leCrypto);
var Accounts = require('./accounts');
var merge = require('./common').merge;
var tplHostname = require('./common').tplHostname;
var fetchFromConfigLiveDir = require('./common').fetchFromDisk;
var ipc = {}; // in-process cache
@ -32,8 +34,6 @@ function getAcmeUrls(args) {
function getCertificateAsync(account, args, defaults, handlers) {
//var pyconf = PromiseA.promisifyAll(require('pyconf'));
return leCrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (domain) {
return LeCore.getCertificateAsync({
newAuthzUrl: args._acmeUrls.newAuthz
@ -43,36 +43,45 @@ function getCertificateAsync(account, args, defaults, handlers) {
, domainPrivateKeyPem: domain.privateKeyPem
, domains: args.domains
//
// IMPORTANT
//
// setChallenge and removeChallenge are handed defaults
// instead of args because getChallenge does not have
// access to args
// (args is per-request, defaults is per instance)
//
, setChallenge: function (domain, key, value, done) {
var copy = merge(defaults, { domains: [domain] });
tplHostname(domain, copy);
args.domains = [domain];
args.webrootPath = args.webrootPath || defaults.webrootPath;
args.webrootPath = args.webrootPath;
if (4 === handlers.setChallenge.length) {
handlers.setChallenge(args, key, value, done);
handlers.setChallenge(copy, key, value, done);
}
else if (5 === handlers.setChallenge.length) {
// TODO merge templates with domain
handlers.setChallenge(defaults, domain, key, value, done);
handlers.setChallenge(copy, domain, key, value, done);
}
else {
done(new Error("handlers.setChallenge receives the wrong number of arguments"));
}
}
, removeChallenge: function (domain, key, done) {
args.domains = [domain];
args.webrootPath = args.webrootPath || defaults.webrootPath;
var copy = merge(defaults, { domains: [domain] });
tplHostname(domain, copy);
if (3 === handlers.removeChallenge.length) {
handlers.removeChallenge(args, key, done);
handlers.removeChallenge(copy, key, done);
}
else if (4 === handlers.removeChallenge.length) {
// TODO merge templates with domain
handlers.removeChallenge(defaults, domain, key, done);
handlers.removeChallenge(copy, domain, key, done);
}
else {
done(new Error("handlers.removeChallenge receives the wrong number of arguments"));
}
}
}).then(function (result) {
// TODO write pems={ca,cert,key} to disk
var liveDir = path.join(args.configDir, 'live', args.domains[0]);
var certPath = path.join(liveDir, 'cert.pem');
var fullchainPath = path.join(liveDir, 'fullchain.pem');
@ -82,12 +91,18 @@ function getCertificateAsync(account, args, defaults, handlers) {
result.fullchain = result.cert + '\n' + result.ca;
// TODO write to archive first, then write to live
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// TODO read renewal.conf.default, write renewal.conf
// var pyconf = PromiseA.promisifyAll(require('pyconf'));
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
return mkdirpAsync(liveDir).then(function () {
return PromiseA.all([
sfs.writeFileAsync(certPath, result.cert, 'ascii')
, sfs.writeFileAsync(chainPath, result.chain, 'ascii')
, sfs.writeFileAsync(chainPath, result.ca || result.chain, 'ascii')
, sfs.writeFileAsync(fullchainPath, result.fullchain, 'ascii')
, sfs.writeFileAsync(privkeyPath, result.key, 'ascii')
, sfs.writeFileAsync(privkeyPath, result.key || result.privkey, 'ascii')
]).then(function () {
// TODO format result licesy
//console.log(liveDir);
@ -97,6 +112,16 @@ function getCertificateAsync(account, args, defaults, handlers) {
, chainPath: chainPath
, fullchainPath: fullchainPath
, privkeyPath: privkeyPath
// some ambiguity here...
, privkey: result.key || result.privkey
, fullchain: result.fullchain || result.cert
, chain: result.ca || result.chain
// especially this one... might be cert only, might be fullchain
, cert: result.cert
, issuedAt: Date.now()
, lifetime: defaults.lifetime || handlers.lifetime
};
});
});
@ -106,9 +131,9 @@ function getCertificateAsync(account, args, defaults, handlers) {
function registerWithAcme(args, defaults, handlers) {
var pyconf = PromiseA.promisifyAll(require('pyconf'));
var server = args.server || defaults.server || LeCore.stagingServerUrl; // https://acme-v01.api.letsencrypt.org/directory
var server = args.server;
var acmeHostname = require('url').parse(server).hostname;
var configDir = args.configDir || defaults.configDir || LE.configDir;
var configDir = args.configDir;
args.server = server;
args.renewalDir = args.renewalDir || path.join(configDir, 'renewal', args.domains[0] + '.conf');
@ -145,7 +170,7 @@ function registerWithAcme(args, defaults, handlers) {
*/
//console.log(account);
return fetchFromConfigLiveDir(args, defaults).then(function (certs) {
return fetchFromConfigLiveDir(args).then(function (certs) {
// if nothing, register and save
// if something, check date (don't register unless 30+ days)
// if good, don't bother registering
@ -252,11 +277,22 @@ module.exports.create = function (defaults, handlers) {
var wrapped = {
registerAsync: function (args) {
//require('./common').registerWithAcme(args, defaults, handlers);
return registerWithAcme(args, defaults, handlers);
var copy = merge(args, defaults);
tplHostname(args.domains[0], copy);
if (args.debug) {
console.log('[LE DEBUG] reg domains', args.domains);
}
return registerWithAcme(copy, defaults, handlers);
}
, fetchAsync: function (args) {
return fetchFromConfigLiveDir(args, defaults);
var copy = merge(args, defaults);
tplHostname(args.domains[0], copy);
if (args.debug) {
console.log('[LE DEBUG] fetch domains', copy);
}
return fetchFromConfigLiveDir(copy, defaults);
}
};