From 563f3ae3eb7c37676da5fbeb5ca432b1e257893e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 20 Dec 2015 05:13:41 +0000 Subject: [PATCH] manage renewals perfectly :-) --- index.js | 7 +- lib/accounts.js | 3 +- lib/core.js | 352 ++++++++++++++++++++++++++---------------- lib/renewal.conf.tpl | 2 +- tests/pyconf-write.js | 50 ++++++ 5 files changed, 275 insertions(+), 139 deletions(-) create mode 100644 tests/pyconf-write.js diff --git a/index.js b/index.js index be059fa..1656cd7 100644 --- a/index.js +++ b/index.js @@ -137,8 +137,11 @@ LE.create = function (defaults, handlers, backend) { // 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 // - 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("\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"); + // 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); } , _registerHelper: function (args, cb) { diff --git a/lib/accounts.js b/lib/accounts.js index d8db0b4..bff6e2e 100644 --- a/lib/accounts.js +++ b/lib/accounts.js @@ -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 files = {}; var configs = ['meta.json', 'private_key.json', 'regr.json']; diff --git a/lib/core.js b/lib/core.js index d27eabb..43780cf 100644 --- a/lib/core.js +++ b/lib/core.js @@ -31,131 +31,205 @@ function getAcmeUrls(args) { }); } -function writeCertificateAsync(result, args, defaults, handlers) { - if (args.debug) { - console.log("got certificate!"); - } - - result.fullchain = result.cert + '\n' + result.ca; - +function readRenewalConfig(args) { var pyconf = PromiseA.promisifyAll(require('pyconf')); - return pyconf.readFileAsync(args.renewalPath).then(function (obj) { - return obj; + return pyconf.readFileAsync(args.renewalPath).then(function (pyobj) { + return pyobj; }, 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 certPath = args.certPath || obj.cert || path.join(liveDir, 'cert.pem'); - var fullchainPath = args.fullchainPath || obj.fullchain || path.join(liveDir, 'fullchain.pem'); - var chainPath = args.chainPath || obj.chain || path.join(liveDir, 'chain.pem'); - var privkeyPath = args.privkeyPath || obj.privkey - //|| args.domainPrivateKeyPath || args.domainKeyPath || obj.keyPath - || path.join(liveDir, 'privkey.pem'); - - if (args.debug) { - console.log('################ privkeyPath ################'); - console.log(privkeyPath); - } - - var archiveDir = args.archiveDir || path.join(args.configDir, 'archive', args.domains[0]); - - var checkpoint = obj.checkpoint.toString(); - var certArchive = path.join(archiveDir, 'cert' + checkpoint + '.pem'); - var fullchainArchive = path.join(archiveDir, 'fullchain' + checkpoint + '.pem'); - var chainArchive = path.join(archiveDir, 'chain'+ checkpoint + '.pem'); - var privkeyArchive = path.join(archiveDir, 'privkey' + checkpoint + '.pem'); - - return mkdirpAsync(archiveDir).then(function () { - return PromiseA.all([ - sfs.writeFileAsync(certArchive, result.cert, 'ascii') - , sfs.writeFileAsync(chainArchive, result.ca || result.chain, 'ascii') - , sfs.writeFileAsync(fullchainArchive, result.fullchain, 'ascii') - , sfs.writeFileAsync(privkeyArchive, result.key || result.privkey || args.domainPrivateKeyPem, 'ascii') - ]); - }).then(function () { - return mkdirpAsync(liveDir); - }).then(function () { - return PromiseA.all([ - sfs.writeFileAsync(certPath, result.cert, 'ascii') - , sfs.writeFileAsync(chainPath, result.ca || result.chain, 'ascii') - , sfs.writeFileAsync(fullchainPath, result.fullchain, 'ascii') - , sfs.writeFileAsync(privkeyPath, result.key || result.privkey || args.domainPrivateKeyPem, 'ascii') - ]); - }).then(function () { - obj.checkpoint += 1; - - var updates = { - 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 () { - - return { - certPath: certPath - , chainPath: chainPath - , fullchainPath: fullchainPath - , privkeyPath: privkeyPath - - // some ambiguity here... - , privkey: result.key || result.privkey || args.domainPrivateKeyPem - , 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 - }; + return pyconf.readFileAsync(path.join(__dirname, 'renewal.conf.tpl')).then(function (pyobj) { + return pyobj; }); }); } -function getCertificateAsync(account, args, defaults, handlers) { +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) { + console.log("got certificate!"); + } + + var obj = args.pyobj; + var result = args.pems; + + result.fullchain = result.cert + '\n' + result.ca; + obj.checkpoints = parseInt(obj.checkpoints, 10) || 0; + + var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]); + + var certPath = args.certPath || obj.cert || path.join(liveDir, 'cert.pem'); + var fullchainPath = args.fullchainPath || obj.fullchain || path.join(liveDir, 'fullchain.pem'); + var chainPath = args.chainPath || obj.chain || path.join(liveDir, 'chain.pem'); + var privkeyPath = args.privkeyPath || obj.privkey + //|| args.domainPrivateKeyPath || args.domainKeyPath || obj.keyPath + || path.join(liveDir, 'privkey.pem'); + + if (args.debug) { + console.log('################ privkeyPath ################'); + console.log(privkeyPath); + } + + var archiveDir = args.archiveDir || path.join(args.configDir, 'archive', args.domains[0]); + + var checkpoints = obj.checkpoints.toString(); + var certArchive = path.join(archiveDir, 'cert' + checkpoints + '.pem'); + var fullchainArchive = path.join(archiveDir, 'fullchain' + checkpoints + '.pem'); + var chainArchive = path.join(archiveDir, 'chain'+ checkpoints + '.pem'); + var privkeyArchive = path.join(archiveDir, 'privkey' + checkpoints + '.pem'); + + return mkdirpAsync(archiveDir).then(function () { + return PromiseA.all([ + sfs.writeFileAsync(certArchive, result.cert, 'ascii') + , sfs.writeFileAsync(chainArchive, result.ca || result.chain, 'ascii') + , sfs.writeFileAsync(fullchainArchive, result.fullchain, 'ascii') + , sfs.writeFileAsync(privkeyArchive, result.key || result.privkey || args.domainPrivateKeyPem, 'ascii') + ]); + }).then(function () { + return mkdirpAsync(liveDir); + }).then(function () { + return PromiseA.all([ + sfs.writeFileAsync(certPath, result.cert, 'ascii') + , sfs.writeFileAsync(chainPath, result.ca || result.chain, 'ascii') + , sfs.writeFileAsync(fullchainPath, result.fullchain, 'ascii') + , sfs.writeFileAsync(privkeyPath, result.key || result.privkey || args.domainPrivateKeyPem, 'ascii') + ]); + }).then(function () { + obj.checkpoints += 1; + args.checkpoints += 1; + + return writeRenewalConfig(args); + }).then(function () { + + return { + certPath: certPath + , chainPath: chainPath + , fullchainPath: fullchainPath + , privkeyPath: privkeyPath + + // some ambiguity here... + , privkey: result.key || result.privkey || args.domainPrivateKeyPem + , 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 + }; + }); +} + +function getCertificateAsync(args, defaults, handlers) { + var account = args.account; + return leCrypto.generateRsaKeypairAsync(args.rsaKeySize, 65537).then(function (domainKey) { if (args.debug) { console.log("get certificate"); @@ -214,11 +288,12 @@ function getCertificateAsync(account, args, defaults, handlers) { } }); }).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) { // if nothing, register and save // if something, check date (don't register unless 30+ days) @@ -228,15 +303,15 @@ function getOrCreateDomainCertificate(account, args, defaults, handlers) { //console.log(certs); if (!certs) { // 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)) { // 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) { // YOLO! I be gettin' fresh certs 'erday! Yo! - return getCertificateAsync(account, args, defaults, handlers); + return getCertificateAsync(args, defaults, handlers); } else { 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; } }); -}; +} function getOrCreateAcmeAccount(args, defaults, handlers) { 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) { var accountId = renewal.account; @@ -279,7 +348,8 @@ function getOrCreateAcmeAccount(args, defaults, handlers) { if (args.debug) { console.log('[LE] use account'); } - return Accounts.getAccount(accountId, args, handlers); + args.accountId = accountId; + return Accounts.getAccount(args, handlers); } else { if (args.debug) { console.log('[LE] create account'); @@ -322,13 +392,25 @@ module.exports.create = function (defaults, handlers) { copy = merge(args, defaults); tplCopy(copy); - if (args.debug) { + if (copy.debug) { 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) { - console.log("account", account); - args.account = account; - return getOrCreateDomainCertificate(account, copy, defaults, handlers); + //console.log("account", account); + copy.account = account; + + return getOrCreateRenewal(copy).then(function (pyobj) { + + copy.pyobj = pyobj; + return getOrCreateDomainCertificate(copy, defaults, handlers); + }); }); } , fetchAsync: function (args) { diff --git a/lib/renewal.conf.tpl b/lib/renewal.conf.tpl index a67d82f..ad7ae0a 100644 --- a/lib/renewal.conf.tpl +++ b/lib/renewal.conf.tpl @@ -40,7 +40,7 @@ authenticator = webroot domains = :hostnames #comma,delimited,list rsa_key_size = :rsa_key_size # starts at 0 and increments at every renewal -checkpoints = :checkpoint_count +checkpoints = -1 manual_test_mode = False apache = False cert_path = :cert_path diff --git a/tests/pyconf-write.js b/tests/pyconf-write.js new file mode 100644 index 0000000..98bc3a6 --- /dev/null +++ b/tests/pyconf-write.js @@ -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); + }); +});