From 22e1d768afdb99a92c6597b033648a0dbe8fb884 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 17 Dec 2015 05:44:41 +0000 Subject: [PATCH] moving some parts to letsencrypt-express --- index.js | 108 +++++++-------------------------------- lib/account.js | 118 +++++++++++++++++++++++++++++++++++++++++++ lib/common.js | 32 ++++++++---- lib/letiny-core.js | 122 ++++----------------------------------------- 4 files changed, 168 insertions(+), 212 deletions(-) create mode 100644 lib/account.js diff --git a/index.js b/index.js index 2a11524..c3dac44 100644 --- a/index.js +++ b/index.js @@ -34,39 +34,6 @@ LE.merge = function merge(defaults, args) { return copy; }; -LE.cacheCertInfo = function (args, certInfo, ipc, handlers) { - // TODO IPC via process and worker to guarantee no races - // rather than just "really good odds" - - var hostname = args.domains[0]; - var now = Date.now(); - - // Stagger randomly by plus 0% to 25% to prevent all caches expiring at once - var rnd1 = (crypto.randomBytes(1)[0] / 255); - var memorizeFor = Math.floor(handlers.memorizeFor + ((handlers.memorizeFor / 4) * rnd1)); - // Stagger randomly to renew between n and 2n days before renewal is due - // this *greatly* reduces the risk of multiple cluster processes renewing the same domain at once - var rnd2 = (crypto.randomBytes(1)[0] / 255); - var bestIfUsedBy = certInfo.expiresAt - (handlers.renewWithin + Math.floor(handlers.renewWithin * rnd2)); - // Stagger randomly by plus 0 to 5 min to reduce risk of multiple cluster processes - // renewing at once on boot when the certs have expired - var rnd3 = (crypto.randomBytes(1)[0] / 255); - var renewTimeout = Math.floor((5 * 60 * 1000) * rnd3); - - certInfo.context = tls.createSecureContext({ - key: certInfo.key - , cert: certInfo.cert - //, ciphers // node's defaults are great - }); - certInfo.loadedAt = now; - certInfo.memorizeFor = memorizeFor; - certInfo.bestIfUsedBy = bestIfUsedBy; - certInfo.renewTimeout = renewTimeout; - - ipc[hostname] = certInfo; - return ipc[hostname]; -}; - // backend, defaults, handlers LE.create = function (defaults, handlers, backend) { var d, b, h; @@ -267,21 +234,22 @@ LE.create = function (defaults, handlers, backend) { } //console.log("[NLE]: begin registration"); - return backend.registerAsync(copy).then(function () { + return backend.registerAsync(copy).then(function (pems) { //console.log("[NLE]: end registration"); - // calls fetch because fetch calls cacheCertInfo - return le.fetch(args, cb); + cb(null, pems); + //return le.fetch(args, cb); }, cb); }); } , _fetchHelper: function (args, cb) { return backend.fetchAsync(args).then(function (certInfo) { - if (!certInfo) { - cb(null, null); - return; + if (args.debug) { + console.log('[LE] debug is on'); } - - var now = Date.now(); + if (true || args.debug) { + console.log('[LE] raw fetch certs', certInfo); + } + if (!certInfo) { cb(null, null); return; } // key, cert, issuedAt, lifetime, expiresAt if (!certInfo.expiresAt) { @@ -290,53 +258,19 @@ LE.create = function (defaults, handlers, backend) { if (!certInfo.lifetime) { certInfo.lifetime = (certInfo.lifetime || handlers.lifetime); } - // a pretty good hard buffer certInfo.expiresAt -= (1 * 24 * 60 * 60 * 100); - certInfo = LE.cacheCertInfo(args, certInfo, ipc, handlers); - if (now > certInfo.bestIfUsedBy && !certInfo.timeout) { - // EXPIRING - if (now > certInfo.expiresAt) { - // EXPIRED - certInfo.renewTimeout = Math.floor(certInfo.renewTimeout / 2); - } - certInfo.timeout = setTimeout(function () { - le.register(args, cb); - }, certInfo.renewTimeout); - } - cb(null, certInfo.context); + cb(null, certInfo); }, cb); } , fetch: function (args, cb) { - var hostname = args.domains[0]; - // TODO don't call now() every time because this is hot code - var now = Date.now(); - var certInfo = ipc[hostname]; - - // TODO once ECDSA is available, wait for cert renewal if its due - if (certInfo) { - if (now > certInfo.bestIfUsedBy && !certInfo.timeout) { - // EXPIRING - if (now > certInfo.expiresAt) { - // EXPIRED - certInfo.renewTimeout = Math.floor(certInfo.renewTimeout / 2); - } - - certInfo.timeout = setTimeout(function () { - le.register(args, cb); - }, certInfo.renewTimeout); - } - cb(null, certInfo.context); - - if ((now - certInfo.loadedAt) < (certInfo.memorizeFor)) { - // these aren't stale, so don't fall through - return; - } - } - le._fetchHelper(args, cb); } + , renew: function (args, cb) { + args.duplicate = false; + le.register(args, cb); + } , register: function (args, cb) { if (!Array.isArray(args.domains)) { cb(new Error('args.domains should be an array of domains')); @@ -349,16 +283,10 @@ LE.create = function (defaults, handlers, backend) { le._fetchHelper(args, function (err, hit) { var hostname = args.domains[0]; - if (err) { - cb(err); - return; - } - else if (hit) { - cb(null, hit); - return; - } + if (err) { cb(err); return; } + else if (hit) { cb(null, hit); return; } - return le._registerHelper(args, function (err) { + return le._registerHelper(args, function (err, pems) { if (err) { cb(err); return; @@ -366,7 +294,7 @@ LE.create = function (defaults, handlers, backend) { le._fetchHelper(args, function (err, cache) { if (cache) { - cb(null, cache.context); + cb(null, cache); return; } diff --git a/lib/account.js b/lib/account.js new file mode 100644 index 0000000..594a52e --- /dev/null +++ b/lib/account.js @@ -0,0 +1,118 @@ +'use strict'; + +var PromiseA = require('bluebird'); +var LeCore = require('letiny-core'); +var leCrypto = LeCore.leCrypto; +var path = require('path'); +var mkdirpAsync = PromiseA.promisify(require('mkdirp')); +var fs = PromiseA.promisifyAll(require('fs')); + +function createAccount(args, handlers) { + var os = require("os"); + var localname = os.hostname(); + + // TODO support ECDSA + // arg.rsaBitLength args.rsaExponent + return leCrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (pems) { + /* pems = { privateKeyPem, privateKeyJwk, publicKeyPem, publicKeyMd5 } */ + + return LeCore.registerNewAccountAsync({ + email: args.email + , newRegUrl: args._acmeUrls.newReg + , agreeToTerms: function (tosUrl, agree) { + // args.email = email; // already there + args.tosUrl = tosUrl; + handlers.agreeToTerms(args, agree); + } + , accountPrivateKeyPem: pems.privateKeyPem + + , debug: args.debug || handlers.debug + }).then(function (body) { + var accountDir = path.join(args.accountsDir, pems.publicKeyMd5); + + return mkdirpAsync(accountDir).then(function () { + + var isoDate = new Date().toISOString(); + var accountMeta = { + creation_host: localname + , creation_dt: isoDate + }; + + return PromiseA.all([ + // meta.json {"creation_host": "ns1.redirect-www.org", "creation_dt": "2015-12-11T04:14:38Z"} + fs.writeFileAsync(path.join(accountDir, 'meta.json'), JSON.stringify(accountMeta), 'utf8') + // private_key.json { "e", "d", "n", "q", "p", "kty", "qi", "dp", "dq" } + , fs.writeFileAsync(path.join(accountDir, 'private_key.json'), JSON.stringify(pems.privateKeyJwk), 'utf8') + // regr.json: + /* + { body: { contact: [ 'mailto:coolaj86@gmail.com' ], + agreement: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf', + key: { e: 'AQAB', kty: 'RSA', n: '...' } }, + uri: 'https://acme-v01.api.letsencrypt.org/acme/reg/71272', + new_authzr_uri: 'https://acme-v01.api.letsencrypt.org/acme/new-authz', + terms_of_service: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' } + */ + , fs.writeFileAsync(path.join(accountDir, 'regr.json'), JSON.stringify({ body: body }), 'utf8') + ]).then(function () { + return pems; + }); + }); + }); + }); +} + +function getAccount(accountId, args, handlers) { + var accountDir = path.join(args.accountsDir, accountId); + var files = {}; + var configs = ['meta.json', 'private_key.json', 'regr.json']; + + return PromiseA.all(configs.map(function (filename) { + var keyname = filename.slice(0, -5); + + return fs.readFileAsync(path.join(accountDir, filename), 'utf8').then(function (text) { + var data; + + try { + data = JSON.parse(text); + } catch(e) { + files[keyname] = { error: e }; + return; + } + + files[keyname] = data; + }, function (err) { + files[keyname] = { error: err }; + }); + })).then(function () { + + if (!Object.keys(files).every(function (key) { + return !files[key].error; + })) { + // TODO log renewal.conf + console.warn("Account '" + accountId + "' was currupt. No big deal (I think?). Creating a new one..."); + return createAccount(args, handlers); + } + + return leCrypto.parseAccountPrivateKeyAsync(files.private_key).then(function (keypair) { + files.accountId = accountId; // md5sum(publicKeyPem) + files.publicKeyMd5 = accountId; // md5sum(publicKeyPem) + files.publicKeyPem = keypair.publicKeyPem; // ascii PEM: ----BEGIN... + files.privateKeyPem = keypair.privateKeyPem; // ascii PEM: ----BEGIN... + files.privateKeyJson = keypair.private_key; // json { n: ..., e: ..., iq: ..., etc } + + return files; + }); + }); +} + +function getAccountByEmail(/*args*/) { + // If we read 10,000 account directories looking for + // just one email address, that could get crazy. + // We should have a folder per email and list + // each account as a file in the folder + // TODO + return PromiseA.resolve(null); +} + +module.exports.getAccountByEmail = getAccountByEmail; +module.exports.getAccount = getAccount; diff --git a/lib/common.js b/lib/common.js index f8ccde6..6c77e45 100644 --- a/lib/common.js +++ b/lib/common.js @@ -5,24 +5,38 @@ var PromiseA = require('bluebird'); module.exports.fetchFromDisk = function (args, defaults) { var hostname = args.domains[0]; - var crtpath = (args.fullchainPath || defaults.fullchainPath) + var certPath = (args.fullchainPath || defaults.fullchainPath) || (defaults.configDir + (args.fullchainTpl || defaults.fullchainTpl || ':hostname/fullchain.pem').replace(/:hostname/, hostname)); - var privpath = (args.privkeyPath || defaults.privkeyPath) + 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)); + */ + return PromiseA.all([ - fs.readFileAsync(privpath, 'ascii') - , fs.readFileAsync(crtpath, 'ascii') + fs.readFileAsync(privkeyPath, 'ascii') + , fs.readFileAsync(certPath, 'ascii') + , fs.readFileAsync(chainPath, 'ascii') + //, fs.readFileAsync(fullchainPath, 'ascii') // stat the file, not the link - , fs.statAsync(crtpath) + , fs.statAsync(certPath) ]).then(function (arr) { + // TODO parse certificate to determine lifetime and expiresAt return { - key: arr[0] // privkey.pem - , cert: arr[1] // fullchain.pem - // TODO parse centificate for lifetime / expiresAt - , issuedAt: arr[2].mtime.valueOf() + key: arr[0] // privkey.pem + , cert: arr[1] // cert.pem + , chain: arr[2] // chain.pem + , fullchain: arr[1] + '\n' + arr[2] // fullchain.pem + + , issuedAt: arr[4].mtime.valueOf() // ??? }; }, function () { return null; diff --git a/lib/letiny-core.js b/lib/letiny-core.js index 337109d..ca5272e 100644 --- a/lib/letiny-core.js +++ b/lib/letiny-core.js @@ -3,12 +3,11 @@ var PromiseA = require('bluebird'); var mkdirpAsync = PromiseA.promisify(require('mkdirp')); var path = require('path'); -var fs = PromiseA.promisifyAll(require('fs')); var sfs = require('safe-replace'); - var LE = require('../'); var LeCore = PromiseA.promisifyAll(require('letiny-core')); var leCrypto = PromiseA.promisifyAll(LeCore.leCrypto); +var Accounts = require('./accounts'); var fetchFromConfigLiveDir = require('./common').fetchFromDisk; @@ -30,115 +29,10 @@ function getAcmeUrls(args) { }); } -function createAccount(args, handlers) { - var os = require("os"); - var localname = os.hostname(); - // TODO support ECDSA - // arg.rsaBitLength args.rsaExponent - return leCrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (pems) { - /* pems = { privateKeyPem, privateKeyJwk, publicKeyPem, publicKeyMd5 } */ - - return LeCore.registerNewAccountAsync({ - email: args.email - , newRegUrl: args._acmeUrls.newReg - , agreeToTerms: function (tosUrl, agree) { - // args.email = email; // already there - args.tosUrl = tosUrl; - handlers.agreeToTerms(args, agree); - } - , accountPrivateKeyPem: pems.privateKeyPem - - , debug: args.debug || handlers.debug - }).then(function (body) { - var accountDir = path.join(args.accountsDir, pems.publicKeyMd5); - - return mkdirpAsync(accountDir).then(function () { - - var isoDate = new Date().toISOString(); - var accountMeta = { - creation_host: localname - , creation_dt: isoDate - }; - - return PromiseA.all([ - // meta.json {"creation_host": "ns1.redirect-www.org", "creation_dt": "2015-12-11T04:14:38Z"} - fs.writeFileAsync(path.join(accountDir, 'meta.json'), JSON.stringify(accountMeta), 'utf8') - // private_key.json { "e", "d", "n", "q", "p", "kty", "qi", "dp", "dq" } - , fs.writeFileAsync(path.join(accountDir, 'private_key.json'), JSON.stringify(pems.privateKeyJwk), 'utf8') - // regr.json: - /* - { body: { contact: [ 'mailto:coolaj86@gmail.com' ], - agreement: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf', - key: { e: 'AQAB', kty: 'RSA', n: '...' } }, - uri: 'https://acme-v01.api.letsencrypt.org/acme/reg/71272', - new_authzr_uri: 'https://acme-v01.api.letsencrypt.org/acme/new-authz', - terms_of_service: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' } - */ - , fs.writeFileAsync(path.join(accountDir, 'regr.json'), JSON.stringify({ body: body }), 'utf8') - ]).then(function () { - return pems; - }); - }); - }); - }); -} - -function getAccount(accountId, args, handlers) { - var accountDir = path.join(args.accountsDir, accountId); - var files = {}; - var configs = ['meta.json', 'private_key.json', 'regr.json']; - - return PromiseA.all(configs.map(function (filename) { - var keyname = filename.slice(0, -5); - - return fs.readFileAsync(path.join(accountDir, filename), 'utf8').then(function (text) { - var data; - - try { - data = JSON.parse(text); - } catch(e) { - files[keyname] = { error: e }; - return; - } - - files[keyname] = data; - }, function (err) { - files[keyname] = { error: err }; - }); - })).then(function () { - - if (!Object.keys(files).every(function (key) { - return !files[key].error; - })) { - // TODO log renewal.conf - console.warn("Account '" + accountId + "' was currupt. No big deal (I think?). Creating a new one..."); - return createAccount(args, handlers); - } - - return leCrypto.parseAccountPrivateKeyAsync(files.private_key).then(function (keypair) { - files.accountId = accountId; // md5sum(publicKeyPem) - files.publicKeyMd5 = accountId; // md5sum(publicKeyPem) - files.publicKeyPem = keypair.publicKeyPem; // ascii PEM: ----BEGIN... - files.privateKeyPem = keypair.privateKeyPem; // ascii PEM: ----BEGIN... - files.privateKeyJson = keypair.private_key; // json { n: ..., e: ..., iq: ..., etc } - - return files; - }); - }); -} - -function getAccountByEmail(args) { - // If we read 10,000 account directories looking for - // just one email address, that could get crazy. - // We should have a folder per email and list - // each account as a file in the folder - // TODO - return PromiseA.resolve(null); -} function getCertificateAsync(account, args, defaults, handlers) { - var pyconf = PromiseA.promisifyAll(require('pyconf')); + //var pyconf = PromiseA.promisifyAll(require('pyconf')); return leCrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (domain) { return LeCore.getCertificateAsync({ @@ -156,7 +50,8 @@ function getCertificateAsync(account, args, defaults, handlers) { handlers.setChallenge(args, key, value, done); } else if (5 === handlers.setChallenge.length) { - handlers.setChallenge(args, domain, key, value, done); + // TODO merge templates with domain + handlers.setChallenge(defaults, domain, key, value, done); } else { done(new Error("handlers.setChallenge receives the wrong number of arguments")); @@ -169,7 +64,8 @@ function getCertificateAsync(account, args, defaults, handlers) { handlers.removeChallenge(args, key, done); } else if (4 === handlers.removeChallenge.length) { - handlers.removeChallenge(args, domain, key, done); + // TODO merge templates with domain + handlers.removeChallenge(defaults, domain, key, done); } else { done(new Error("handlers.removeChallenge receives the wrong number of arguments")); @@ -225,7 +121,7 @@ function registerWithAcme(args, defaults, handlers) { return accountId; }, function (err) { if ("ENOENT" === err.code) { - return getAccountByEmail(args, handlers); + return Accounts.getAccountByEmail(args, handlers); } return PromiseA.reject(err); @@ -235,9 +131,9 @@ function registerWithAcme(args, defaults, handlers) { args._acmeUrls = urls; if (accountId) { - return getAccount(accountId, args, handlers); + return Accounts.getAccount(accountId, args, handlers); } else { - return createAccount(args, handlers); + return Accounts.createAccount(args, handlers); } }); }).then(function (account) {