updates for letsencrypt-express
This commit is contained in:
parent
60f9968177
commit
3b71f14c9e
111
index.js
111
index.js
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -116,3 +116,4 @@ function getAccountByEmail(/*args*/) {
|
|||
|
||||
module.exports.getAccountByEmail = getAccountByEmail;
|
||||
module.exports.getAccount = getAccount;
|
||||
module.exports.createAccount = createAccount;
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue