From 25f8b591db2d2d7dbb1140a5c1483dcd7851ff10 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 8 Aug 2016 15:17:09 -0400 Subject: [PATCH] now passes tests for invalid account --- index.js | 15 +-- lib/core.js | 230 +++++++++++++++++++++++----------------- lib/utils.js | 29 ++++- tests/create-account.js | 117 ++++++++++++++++++++ 4 files changed, 285 insertions(+), 106 deletions(-) create mode 100644 tests/create-account.js diff --git a/index.js b/index.js index d1774fd..d5b5595 100644 --- a/index.js +++ b/index.js @@ -1,19 +1,20 @@ 'use strict'; -var leCore = require('letiny-core'); +var ACME = require('le-acme-core').ACME; var LE = module.exports; +LE.LE = LE; // in-process cache, shared between all instances var ipc = {}; LE.defaults = { - productionServerUrl: leCore.productionServerUrl -, stagingServerUrl: leCore.stagingServerUrl + productionServerUrl: ACME.productionServerUrl +, stagingServerUrl: ACME.stagingServerUrl -, rsaKeySize: leCore.rsaKeySize || 2048 -, challengeType: leCore.challengeType || 'http-01' +, rsaKeySize: ACME.rsaKeySize || 2048 +, challengeType: ACME.challengeType || 'http-01' -, acmeChallengePrefix: leCore.acmeChallengePrefix +, acmeChallengePrefix: ACME.acmeChallengePrefix }; // backwards compat @@ -50,7 +51,7 @@ LE._undefine = function (le) { LE.create = function (le) { var PromiseA = require('bluebird'); - le.acme = le.acme || leCore; + le.acme = le.acme || ACME.create({ debug: le.debug }); le.store = le.store || require('le-store-certbot').create({ debug: le.debug }); le.challenger = le.challenger || require('le-store-certbot').create({ debug: le.debug }); le.core = require('./lib/core'); diff --git a/lib/core.js b/lib/core.js index 15ff111..92fdb1e 100644 --- a/lib/core.js +++ b/lib/core.js @@ -36,77 +36,101 @@ module.exports.create = function (le) { // , accounts: { registerAsync: function (args) { - return RSA.generateKeypairAsync(args.rsaKeySize, 65537, { public: true, pem: true }).then(function (keypair) { + var err; - return le.acme.registerNewAccountAsync({ - email: args.email - , newRegUrl: args._acmeUrls.newReg - , agreeToTerms: function (tosUrl, agreeCb) { - if (true === args.agreeTos || tosUrl === args.agreeTos || tosUrl === le.agreeToTerms) { - agreeCb(null, tosUrl); - return; - } + if (!args.email || !args.agreeTos || (parseInt(args.rsaKeySize, 10) < 2048)) { + err = new Error( + "In order to register an account both 'email' and 'agreeTos' must be present" + + " and 'rsaKeySize' must be 2048 or greater." + ); + err.code = 'E_ARGS'; + return PromiseA.reject(err); + } - // args.email = email; // already there - // args.domains = domains // already there - args.tosUrl = tosUrl; - le.agreeToTerms(args, agreeCb); - } - , accountKeypair: keypair + return utils.testEmail(args.email).then(function () { - , debug: le.debug || args.debug - }).then(function (body) { - // TODO XXX use sha256 (the python client uses md5) - // TODO ssh fingerprint (noted on rsa-compat issues page, I believe) - keypair.publicKeyMd5 = crypto.createHash('md5').update(RSA.exportPublicPem(keypair)).digest('hex'); - keypair.publicKeySha256 = crypto.createHash('sha256').update(RSA.exportPublicPem(keypair)).digest('hex'); + return RSA.generateKeypairAsync(args.rsaKeySize, 65537, { public: true, pem: true }).then(function (keypair) { + // Note: the ACME urls are always fetched fresh on purpose + // TODO is this the right place for this? + return core.getAcmeUrlsAsync(args).then(function (urls) { + args._acmeUrls = urls; - var accountId = keypair.publicKeyMd5; - var regr = { body: body }; - var account = {}; + return le.acme.registerNewAccountAsync({ + email: args.email + , newRegUrl: args._acmeUrls.newReg + , agreeToTerms: function (tosUrl, agreeCb) { + if (true === args.agreeTos || tosUrl === args.agreeTos || tosUrl === le.agreeToTerms) { + agreeCb(null, tosUrl); + return; + } - args.accountId = accountId; + // args.email = email; // already there + // args.domains = domains // already there + args.tosUrl = tosUrl; + le.agreeToTerms(args, agreeCb); + } + , accountKeypair: keypair - account.keypair = keypair; - account.regr = regr; - account.accountId = accountId; - account.id = accountId; + , debug: le.debug || args.debug + }).then(function (body) { + // TODO XXX use sha256 (the python client uses md5) + // TODO ssh fingerprint (noted on rsa-compat issues page, I believe) + keypair.publicKeyMd5 = crypto.createHash('md5').update(RSA.exportPublicPem(keypair)).digest('hex'); + keypair.publicKeySha256 = crypto.createHash('sha256').update(RSA.exportPublicPem(keypair)).digest('hex'); - args.account = account; + var accountId = keypair.publicKeyMd5; + var regr = { body: body }; + var account = {}; - return le.store.accounts.setAsync(args, account).then(function () { - return account; + args.accountId = accountId; + + account.keypair = keypair; + account.regr = regr; + account.accountId = accountId; + account.id = accountId; + + args.account = account; + + return le.store.accounts.setAsync(args, account).then(function () { + return account; + }); + }); }); }); }); } - // getOrCreateAcmeAccount + , getAsync: function (args) { return core.accounts.checkAsync(args).then(function (account) { - if (account) { - return le.store.accounts.checkAccount(args); - } else { - return core.accounts.registerAsync(args); - } + if (account) { + return account; + } else { + return core.accounts.registerAsync(args); + } }); } - , checkAsync: function (args) { - return le.store.accounts.checkAccountId(args).then(function (accountId) { - if (!accountId) { + , checkAsync: function (args) { + var requiredArgs = ['accountId', 'email', 'domains', 'domain']; + if (!requiredArgs.some(function (key) { return -1 !== Object.keys(args).indexOf(key) })) { + return PromiseA.reject(new Error( + "In order to register or retrieve an account one of '" + requiredArgs.join("', '") + "' must be present" + )); + } + + var copy = utils.merge(args, le); + args = utils.tplCopy(copy); + + return le.store.accounts.checkAsync(args).then(function (account) { + + if (!account) { return null; } - args.accountId = accountId; + args.account = account; + args.accountId = account.id; - // Note: the ACME urls are always fetched fresh on purpose - return core.getAcmeUrlsAsync(args).then(function (urls) { - args._acmeUrls = urls; - - - // return le.store.accounts.checkAccountId(args).then(function (accountId) { - return le.store.accounts.checkAsync(args); - }); + return account; }); } } @@ -150,55 +174,61 @@ module.exports.create = function (le) { args.domainKeypair = domainKeypair; //args.registration = domainKey; - return le.acme.getCertificateAsync({ - debug: args.debug || le.debug + // Note: the ACME urls are always fetched fresh on purpose + // TODO is this the right place for this? + return core.getAcmeUrlsAsync(args).then(function (urls) { + args._acmeUrls = urls; - , newAuthzUrl: args._acmeUrls.newAuthz - , newCertUrl: args._acmeUrls.newCert + return le.acme.getCertificateAsync({ + debug: args.debug || le.debug - , accountKeypair: RSA.import(account.keypair) - , domainKeypair: domainKeypair - , domains: args.domains - , challengeType: args.challengeType + , newAuthzUrl: args._acmeUrls.newAuthz + , newCertUrl: args._acmeUrls.newCert - // - // 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 = utils.merge({ domains: [domain] }, le); - utils.tplCopy(copy); + , accountKeypair: RSA.import(account.keypair) + , domainKeypair: domainKeypair + , domains: args.domains + , challengeType: args.challengeType - //args.domains = [domain]; - args.domains = args.domains || [domain]; + // + // 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 = utils.merge({ domains: [domain] }, le); + utils.tplCopy(copy); - if (5 !== le.challenger.set.length) { - done(new Error("le.challenger.set receives the wrong number of arguments." - + " You must define setChallenge as function (opts, domain, key, val, cb) { }")); - return; + //args.domains = [domain]; + args.domains = args.domains || [domain]; + + if (5 !== le.challenger.set.length) { + done(new Error("le.challenger.set receives the wrong number of arguments." + + " You must define setChallenge as function (opts, domain, key, val, cb) { }")); + return; + } + + le.challenger.set(copy, domain, key, value, done); } + , removeChallenge: function (domain, key, done) { + var copy = utils.merge({ domains: [domain] }, le); + utils.tplCopy(copy); - le.challenger.set(copy, domain, key, value, done); - } - , removeChallenge: function (domain, key, done) { - var copy = utils.merge({ domains: [domain] }, le); - utils.tplCopy(copy); + if (4 !== le.challenger.remove.length) { + done(new Error("le.challenger.remove receives the wrong number of arguments." + + " You must define removeChallenge as function (opts, domain, key, cb) { }")); + return; + } - if (4 !== le.challenger.remove.length) { - done(new Error("le.challenger.remove receives the wrong number of arguments." - + " You must define removeChallenge as function (opts, domain, key, cb) { }")); - return; + le.challenger.remove(copy, domain, key, done); } - - le.challenger.remove(copy, domain, key, done); - } - }).then(utils.attachCertInfo); + }).then(utils.attachCertInfo); + }); }).then(function (results) { - // { cert, chain, fullchain, privkey } + // { cert, chain, privkey } args.pems = results; return le.store.certificates.setAsync(args).then(function () { @@ -207,6 +237,10 @@ module.exports.create = function (le) { }); }); } + , renewAsync: function (args) { + // TODO fetch email address if not present + return core.certificates.registerAsync(args); + } , checkAsync: function (args) { var copy = utils.merge(args, le); utils.tplCopy(copy); @@ -218,20 +252,20 @@ module.exports.create = function (le) { var copy = utils.merge(args, le); args = utils.tplCopy(copy); - if (args.duplicate) { - // we're forcing a refresh via 'dupliate: true' - return core.certificates.registerAsync(args); - } - return core.certificates.checkAsync(args).then(function (certs) { + if (!certs) { + // There is no cert available + return core.certificates.registerAsync(args); + } + var renewableAt = certs.expiresAt - le.renewWithin; //var halfLife = (certs.expiresAt - certs.issuedAt) / 2; //var renewable = (Date.now() - certs.issuedAt) > halfLife; - if (!certs || Date.now() >= renewableAt) { - // There is no cert available - // Or the cert is more than half-expired - return core.certificates.registerAsync(args); + if (args.duplicate || Date.now() >= renewableAt) { + // The cert is more than half-expired + // We're forcing a refresh via 'dupliate: true' + return core.certificates.renewAsync(args); } return PromiseA.reject(new Error( diff --git a/lib/utils.js b/lib/utils.js index 1755f15..eaaad17 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,6 +4,8 @@ var path = require('path'); var homeRe = new RegExp("^~(\\/|\\\|\\" + path.sep + ")"); var re = /^[a-zA-Z0-9\.\-]+$/; var punycode = require('punycode'); +var PromiseA = require('bluebird'); +var dns = PromiseA.promisifyAll(require('dns')); module.exports.attachCertInfo = function (results) { var getCertInfo = require('./cert-info').getCertInfo; @@ -33,7 +35,7 @@ module.exports.isValidDomain = function (domain) { module.exports.merge = function (/*defaults, args*/) { var allDefaults = Array.prototype.slice.apply(arguments); - var args = args.shift(); + var args = allDefaults.shift(); var copy = {}; allDefaults.forEach(function (defaults) { @@ -78,3 +80,28 @@ module.exports.tplCopy = function (copy) { return copy; }; + +module.exports.testEmail = function (email) { + var parts = (email||'').split('@'); + var err; + + if (2 !== parts.length || !parts[0] || !parts[1]) { + err = new Error("malformed email address '" + email + "'"); + err.code = 'E_EMAIL'; + return PromiseA.reject(err); + } + + return dns.resolveMxAsync(parts[1]).then(function (records) { + // records only returns when there is data + if (!records.length) { + throw new Error("sanity check fail: success, but no MX records returned"); + } + return email; + }, function (err) { + if ('ENODATA' === err.code) { + err = new Error("no MX records found for '" + parts[1] + "'"); + err.code = 'E_EMAIL'; + return PromiseA.reject(err); + } + }); +}; diff --git a/tests/create-account.js b/tests/create-account.js new file mode 100644 index 0000000..32a775f --- /dev/null +++ b/tests/create-account.js @@ -0,0 +1,117 @@ +'use strict'; + +var LE = require('../').LE; +var le = LE.create({ + server: 'staging' +, acme: require('le-acme-core').ACME.create() +, store: require('le-store-certbot').create({ + configDir: '~/letsencrypt.test/etc/' + }) +}); + +var testId = Math.round(Date.now() / 1000).toString(); +var fakeEmail = 'coolaj86+le.' + testId + '@example.com'; +var testEmail = 'coolaj86+le.' + testId + '@example.com'; +var testAccount; + +var tests = [ + function () { + return le.core.accounts.checkAsync({ + email: testEmail + }).then(function (account) { + if (account) { + console.error(account); + throw new Error("Test account should not exist."); + } + }); + } +, function () { + return le.core.accounts.registerAsync({ + email: testEmail + , agreeTos: false + , rsaKeySize: 2048 + }).then(function (/*account*/) { + throw new Error("Should not register if 'agreeTos' is not truthy."); + }, function (err) { + if (err.code !== 'E_ARGS') { + throw err; + } + }); + } +, function () { + return le.core.accounts.registerAsync({ + email: testEmail + , agreeTos: true + , rsaKeySize: 1024 + }).then(function (/*account*/) { + throw new Error("Should not register if 'rsaKeySize' is less than 2048."); + }, function (err) { + if (err.code !== 'E_ARGS') { + throw err; + } + }); + } +, function () { + return le.core.accounts.registerAsync({ + email: fakeEmail + , agreeTos: true + , rsaKeySize: 2048 + }).then(function (/*account*/) { + // TODO test mx record + throw new Error("Registration should NOT succeed with a bad email address."); + }, function (err) { + if (err.code !== 'E_EMAIL') { + throw err; + } + }); + } +, function () { + throw new Error('NOT IMPLEMENTED'); + return le.core.accounts.registerAsync({ + email: 'coolaj86+le.' + testId + '@example.com' + , agreeTos: true + , rsaKeySize: 2048 + }).then(function (account) { + testAccount = account; + if (!account) { + throw new Error("Registration should always return a new account."); + } + if (!account.email) { + throw new Error("Registration should return the email."); + } + if (!account.id) { + throw new Error("Registration should return the account id."); + } + }); + } +, function () { + return le.core.accounts.checkAsync({ + email: testAccount.email + }).then(function (account) { + if (!account) { + throw new Error("Test account should exist when searched by email."); + } + }); + } +, function () { + return le.core.accounts.checkAsync({ + accountId: testAccount.id + }).then(function (account) { + if (!account) { + throw new Error("Test account should exist when searched by account id."); + } + }); + } +]; + +function run() { + var test = tests.shift(); + if (!test) { + console.info('All tests passed'); + return; + } + + test().then(run); +} + +run();