manage renewals perfectly :-)

This commit is contained in:
AJ ONeal 2015-12-20 05:13:41 +00:00
parent a6b1b5cfa6
commit 563f3ae3eb
5 changed files with 275 additions and 139 deletions

View File

@ -137,8 +137,11 @@ LE.create = function (defaults, handlers, backend) {
// If you do not check these things, then someone could attack you // If you do not check these things, then someone could attack you
// and cause you, in return, to have your ip be rate-limit blocked // and cause you, in return, to have your ip be rate-limit blocked
// //
console.warn("\n[TODO]: node-letsencrypt: `validate(hostnames, cb)` needs to be implemented"); //console.warn("\n[TODO]: node-letsencrypt: `validate(hostnames, cb)` needs to be implemented");
console.warn("(it'll work fine without it, but for security - and convenience - it should be implemented\n"); //console.warn("(it'll work fine without it, but for security - and convenience - it should be implemented\n");
// UPDATE:
// it's actually probably better that we don't do this here and instead
// take care of it in the approveRegistrationCallback in letsencrypt-express
cb(null, true); cb(null, true);
} }
, _registerHelper: function (args, cb) { , _registerHelper: function (args, cb) {

View File

@ -72,7 +72,8 @@ function createAccount(args, handlers) {
}); });
} }
function getAccount(accountId, args, handlers) { function getAccount(args, handlers) {
var accountId = args.accountId;
var accountDir = path.join(args.accountsDir, accountId); var accountDir = path.join(args.accountsDir, accountId);
var files = {}; var files = {};
var configs = ['meta.json', 'private_key.json', 'regr.json']; var configs = ['meta.json', 'private_key.json', 'regr.json'];

View File

@ -31,23 +31,137 @@ function getAcmeUrls(args) {
}); });
} }
function writeCertificateAsync(result, args, defaults, handlers) { function readRenewalConfig(args) {
var pyconf = PromiseA.promisifyAll(require('pyconf'));
return pyconf.readFileAsync(args.renewalPath).then(function (pyobj) {
return pyobj;
}, function () {
return pyconf.readFileAsync(path.join(__dirname, 'renewal.conf.tpl')).then(function (pyobj) {
return pyobj;
});
});
}
function writeRenewalConfig(args) {
var pyobj = args.pyobj;
pyobj.checkpoints = parseInt(pyobj.checkpoints, 10) || 0;
var pyconf = PromiseA.promisifyAll(require('pyconf'));
var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]);
var certPath = args.certPath || pyobj.cert || path.join(liveDir, 'cert.pem');
var fullchainPath = args.fullchainPath || pyobj.fullchain || path.join(liveDir, 'fullchain.pem');
var chainPath = args.chainPath || pyobj.chain || path.join(liveDir, 'chain.pem');
var privkeyPath = args.privkeyPath || pyobj.privkey
//|| args.domainPrivateKeyPath || args.domainKeyPath || pyobj.keyPath
|| path.join(liveDir, 'privkey.pem');
if (args.debug) {
console.log('################ privkeyPath ################');
console.log(privkeyPath);
}
var updates = {
account: args.account.id
, configDir: args.configDir
, domains: args.domains
, email: args.email
, tos: args.agreeTos && true
// yes, it's an array. weird, right?
, webrootPath: args.webrootPath && [args.webrootPath] || []
, server: args.server || args.acmeDiscoveryUrl
, privkey: privkeyPath
, fullchain: fullchainPath
, cert: certPath
, chain: chainPath
, http01Port: args.http01Port
, keyPath: args.domainPrivateKeyPath || args.privkeyPath
, rsaKeySize: args.rsaKeySize
, checkpoints: pyobj.checkpoints
/* // TODO XXX what's the deal with these? they don't make sense
// are they just old junk? or do they have a meaning that I don't know about?
, fullchainPath: path.join(args.configDir, 'chain.pem')
, certPath: path.join(args.configDir, 'cert.pem')
, chainPath: path.join(args.configDir, 'chain.pem')
*/ // TODO XXX end
, workDir: args.workDir
, logsDir: args.logsDir
};
// final section is completely dynamic
// :hostname = :webroot_path
args.domains.forEach(function (hostname) {
updates[hostname] = args.webrootPath;
});
// must write back to the original pyobject or
// annotations will be lost
Object.keys(updates).forEach(function (key) {
pyobj[key] = updates[key];
});
return mkdirpAsync(path.dirname(args.renewalPath)).then(function () {
return pyconf.writeFileAsync(args.renewalPath, pyobj);
}).then(function () {
return pyobj;
});
}
function getOrCreateRenewal(args) {
return readRenewalConfig(args).then(function (pyobj) {
var minver = pyobj.checkpoints >= 0;
args.pyobj = pyobj;
if (!minver) {
args.checkpoints = 0;
pyobj.checkpoints = 0;
return writeRenewalConfig(args);
}
// args.account.id = pyobj.account
// args.configDir = args.configDir || pyobj.configDir;
args.checkpoints = pyobj.checkpoints;
args.agreeTos = (args.agreeTos || pyobj.tos) && true;
args.email = args.email || pyobj.email;
args.domains = args.domains || pyobj.domains;
// yes, it's an array. weird, right?
args.webrootPath = args.webrootPath || pyobj.webrootPath[0];
args.server = args.server || args.acmeDiscoveryUrl || pyobj.server;
args.certPath = args.certPath || pyobj.cert;
args.privkeyPath = args.privkeyPath || pyobj.privkey;
args.chainPath = args.chainPath || pyobj.chain;
args.fullchainPath = args.fullchainPath || pyobj.fullchain;
//, workDir: args.workDir
//, logsDir: args.logsDir
args.rsaKeySize = args.rsaKeySize || pyobj.rsaKeySize;
args.http01Port = args.http01Port || pyobj.http01Port;
args.domainKeyPath = args.domainPrivateKeyPath || args.domainKeyPath || args.keyPath || pyobj.keyPath;
return writeRenewalConfig(args);
});
}
function writeCertificateAsync(args, defaults, handlers) {
if (args.debug) { if (args.debug) {
console.log("got certificate!"); console.log("got certificate!");
} }
var obj = args.pyobj;
var result = args.pems;
result.fullchain = result.cert + '\n' + result.ca; result.fullchain = result.cert + '\n' + result.ca;
obj.checkpoints = parseInt(obj.checkpoints, 10) || 0;
var pyconf = PromiseA.promisifyAll(require('pyconf'));
return pyconf.readFileAsync(args.renewalPath).then(function (obj) {
return obj;
}, function () {
return pyconf.readFileAsync(path.join(__dirname, 'renewal.conf.tpl')).then(function (obj) {
return obj;
});
}).then(function (obj) {
obj.checkpoint = parseInt(obj.checkpoint, 10) || 0;
var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]); var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]);
@ -65,11 +179,11 @@ function writeCertificateAsync(result, args, defaults, handlers) {
var archiveDir = args.archiveDir || path.join(args.configDir, 'archive', args.domains[0]); var archiveDir = args.archiveDir || path.join(args.configDir, 'archive', args.domains[0]);
var checkpoint = obj.checkpoint.toString(); var checkpoints = obj.checkpoints.toString();
var certArchive = path.join(archiveDir, 'cert' + checkpoint + '.pem'); var certArchive = path.join(archiveDir, 'cert' + checkpoints + '.pem');
var fullchainArchive = path.join(archiveDir, 'fullchain' + checkpoint + '.pem'); var fullchainArchive = path.join(archiveDir, 'fullchain' + checkpoints + '.pem');
var chainArchive = path.join(archiveDir, 'chain'+ checkpoint + '.pem'); var chainArchive = path.join(archiveDir, 'chain'+ checkpoints + '.pem');
var privkeyArchive = path.join(archiveDir, 'privkey' + checkpoint + '.pem'); var privkeyArchive = path.join(archiveDir, 'privkey' + checkpoints + '.pem');
return mkdirpAsync(archiveDir).then(function () { return mkdirpAsync(archiveDir).then(function () {
return PromiseA.all([ return PromiseA.all([
@ -88,51 +202,10 @@ function writeCertificateAsync(result, args, defaults, handlers) {
, sfs.writeFileAsync(privkeyPath, result.key || result.privkey || args.domainPrivateKeyPem, 'ascii') , sfs.writeFileAsync(privkeyPath, result.key || result.privkey || args.domainPrivateKeyPem, 'ascii')
]); ]);
}).then(function () { }).then(function () {
obj.checkpoint += 1; obj.checkpoints += 1;
args.checkpoints += 1;
var updates = { return writeRenewalConfig(args);
account: args.accountId || args.account.id
, cert: certPath
, privkey: privkeyPath
, chain: chainPath
, fullchain: fullchainPath
, configDir: args.configDir
, workDir: args.workDir
, tos: args.agreeTos && true
, http01Port: args.http01Port
, keyPath: args.domainPrivateKeyPath || args.privkeyPath
, email: args.email
, domains: args.domains
, rsaKeySize: args.rsaKeySize
, checkpoints: obj.checkpoint
// TODO XXX what's the deal with these? they don't make sense
// are they just old junk? or do they have a meaning that I don't know about?
, fullchainPath: path.join(args.configDir, 'chain.pem')
, certPath: path.join(args.configDir, 'cert.pem')
, chainPath: path.join(args.configDir, 'chain.pem')
// TODO XXX end
// yes, it's an array. weird, right?
, webrootPath: args.webrootPath && [args.webrootPath] || []
, server: args.server || args.acmeDiscoveryUrl
, logsDir: args.logsDir
};
// final section is completely dynamic
// :hostname = :webroot_path
args.domains.forEach(function (hostname) {
updates[hostname] = args.webrootPath;
});
// must write back to the original object or
// annotations will be lost
Object.keys(updates).forEach(function (key) {
obj[key] = updates[key];
});
return mkdirpAsync(path.dirname(args.renewalPath)).then(function () {
return pyconf.writeFileAsync(args.renewalPath, obj);
});
}).then(function () { }).then(function () {
return { return {
@ -152,10 +225,11 @@ function writeCertificateAsync(result, args, defaults, handlers) {
, lifetime: defaults.lifetime || handlers.lifetime , lifetime: defaults.lifetime || handlers.lifetime
}; };
}); });
});
} }
function getCertificateAsync(account, args, defaults, handlers) { function getCertificateAsync(args, defaults, handlers) {
var account = args.account;
return leCrypto.generateRsaKeypairAsync(args.rsaKeySize, 65537).then(function (domainKey) { return leCrypto.generateRsaKeypairAsync(args.rsaKeySize, 65537).then(function (domainKey) {
if (args.debug) { if (args.debug) {
console.log("get certificate"); console.log("get certificate");
@ -214,11 +288,12 @@ function getCertificateAsync(account, args, defaults, handlers) {
} }
}); });
}).then(function (results) { }).then(function (results) {
return writeCertificateAsync(results, args, defaults, handlers); args.pems = results;
return writeCertificateAsync(args, defaults, handlers);
}); });
} }
function getOrCreateDomainCertificate(account, args, defaults, handlers) { function getOrCreateDomainCertificate(args, defaults, handlers) {
return fetchFromConfigLiveDir(args).then(function (certs) { return fetchFromConfigLiveDir(args).then(function (certs) {
// if nothing, register and save // if nothing, register and save
// if something, check date (don't register unless 30+ days) // if something, check date (don't register unless 30+ days)
@ -228,15 +303,15 @@ function getOrCreateDomainCertificate(account, args, defaults, handlers) {
//console.log(certs); //console.log(certs);
if (!certs) { if (!certs) {
// no certs, seems like a good time to get some // no certs, seems like a good time to get some
return getCertificateAsync(account, args, defaults, handlers); return getCertificateAsync(args, defaults, handlers);
} }
else if (certs.issuedAt > (27 * 24 * 60 * 60 * 1000)) { else if (certs.issuedAt > (27 * 24 * 60 * 60 * 1000)) {
// cert is at least 27 days old we can renew that // cert is at least 27 days old we can renew that
return getCertificateAsync(account, args, defaults, handlers); return getCertificateAsync(args, defaults, handlers);
} }
else if (args.duplicate) { else if (args.duplicate) {
// YOLO! I be gettin' fresh certs 'erday! Yo! // YOLO! I be gettin' fresh certs 'erday! Yo!
return getCertificateAsync(account, args, defaults, handlers); return getCertificateAsync(args, defaults, handlers);
} }
else { else {
console.warn('[WARN] Ignoring renewal attempt for certificate less than 27 days old. Use args.duplicate to force.'); console.warn('[WARN] Ignoring renewal attempt for certificate less than 27 days old. Use args.duplicate to force.');
@ -244,16 +319,10 @@ function getOrCreateDomainCertificate(account, args, defaults, handlers) {
return certs; return certs;
} }
}); });
}; }
function getOrCreateAcmeAccount(args, defaults, handlers) { function getOrCreateAcmeAccount(args, defaults, handlers) {
var pyconf = PromiseA.promisifyAll(require('pyconf')); var pyconf = PromiseA.promisifyAll(require('pyconf'));
var server = args.server;
var acmeHostname = require('url').parse(server).hostname;
var configDir = args.configDir;
args.renewalPath = args.renewalPath || path.join(configDir, 'renewal', args.domains[0] + '.conf');
args.accountsDir = args.accountsDir || path.join(configDir, 'accounts', acmeHostname, 'directory');
return pyconf.readFileAsync(args.renewalPath).then(function (renewal) { return pyconf.readFileAsync(args.renewalPath).then(function (renewal) {
var accountId = renewal.account; var accountId = renewal.account;
@ -279,7 +348,8 @@ function getOrCreateAcmeAccount(args, defaults, handlers) {
if (args.debug) { if (args.debug) {
console.log('[LE] use account'); console.log('[LE] use account');
} }
return Accounts.getAccount(accountId, args, handlers); args.accountId = accountId;
return Accounts.getAccount(args, handlers);
} else { } else {
if (args.debug) { if (args.debug) {
console.log('[LE] create account'); console.log('[LE] create account');
@ -322,13 +392,25 @@ module.exports.create = function (defaults, handlers) {
copy = merge(args, defaults); copy = merge(args, defaults);
tplCopy(copy); tplCopy(copy);
if (args.debug) { if (copy.debug) {
console.log('[LE DEBUG] reg domains', args.domains); console.log('[LE DEBUG] reg domains', args.domains);
} }
var url = require('url');
var acmeLocation = url.parse(copy.server);
var acmeHostpath = path.join(acmeLocation.hostname, acmeLocation.pathname);
copy.renewalPath = copy.renewalPath || path.join(copy.configDir, 'renewal', copy.domains[0] + '.conf');
copy.accountsDir = copy.accountsDir || path.join(copy.configDir, 'accounts', acmeHostpath);
return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) { return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) {
console.log("account", account); //console.log("account", account);
args.account = account; copy.account = account;
return getOrCreateDomainCertificate(account, copy, defaults, handlers);
return getOrCreateRenewal(copy).then(function (pyobj) {
copy.pyobj = pyobj;
return getOrCreateDomainCertificate(copy, defaults, handlers);
});
}); });
} }
, fetchAsync: function (args) { , fetchAsync: function (args) {

View File

@ -40,7 +40,7 @@ authenticator = webroot
domains = :hostnames #comma,delimited,list domains = :hostnames #comma,delimited,list
rsa_key_size = :rsa_key_size rsa_key_size = :rsa_key_size
# starts at 0 and increments at every renewal # starts at 0 and increments at every renewal
checkpoints = :checkpoint_count checkpoints = -1
manual_test_mode = False manual_test_mode = False
apache = False apache = False
cert_path = :cert_path cert_path = :cert_path

50
tests/pyconf-write.js Normal file
View File

@ -0,0 +1,50 @@
'use strict';
var PromiseA = require('bluebird');
var pyconf = PromiseA.promisifyAll(require('pyconf'));
var mkdirpAsync = PromiseA.promisify(require('mkdirp'));
var path = require('path');
pyconf.readFileAsync(path.join(__dirname, 'lib', 'renewal.conf.tpl')).then(function (obj) {
var domains = ['example.com', 'www.example.com'];
var webrootPath = '/tmp/www/example.com';
console.log(obj);
var keys = obj.__keys;
var lines = obj.__lines;
obj.__keys = null;
obj.__lines = null;
var updates = {
account: 'ACCOUNT_ID'
, cert: 'CERT_PATH'
, privkey: 'PRIVATEKEY_PATH'
, configDir: 'CONFIG_DIR'
, tos: true
, http01Port: 80
, domains: domains
};
// final section is completely dynamic
// :hostname = :webroot_path
domains.forEach(function (hostname) {
updates[hostname] = webrootPath;
});
// must write back to the original object or
// annotations will be lost
Object.keys(updates).forEach(function (key) {
obj[key] = updates[key];
});
var renewalPath = '/tmp/letsencrypt/renewal/example.com.conf';
return mkdirpAsync(path.dirname(renewalPath)).then(function () {
console.log(obj);
obj.__keys = keys;
obj.__lines = lines;
return pyconf.writeFileAsync(renewalPath, obj);
});
});