diff --git a/index.js b/index.js index fa8bfb4..9a67191 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,7 @@ var util = require('util'); function promisifyAllSelf(obj) { if (obj.__promisified) { return obj; } Object.keys(obj).forEach(function (key) { - if ('function' === typeof obj[key]) { + if ('function' === typeof obj[key] && !/Async$/.test(key)) { obj[key + 'Async'] = util.promisify(obj[key]); } }); @@ -271,13 +271,19 @@ Greenlock.create = function (gl) { } }); - if (gl.store.create) { - gl.store = gl.store.create(gl); + try { + if (gl.store.create) { gl.store = gl.store.create(gl); } + gl.store = promisifyAllSelf(gl.store); + gl.store.accounts = promisifyAllSelf(gl.store.accounts); + gl.store.certificates = promisifyAllSelf(gl.store.certificates); + gl._storeOpts = gl.store.options || gl.store.getOptions(); + } catch(e) { + console.error(e); + console.error("\nPROBABLE CAUSE:\n" + + "\tYour le-store module should have a create function and return { options, accounts, certificates }\n"); + process.exit(18); + return; } - gl.store = promisifyAllSelf(gl.store); - gl.store.accounts = promisifyAllSelf(gl.store.accounts); - gl.store.certificates = promisifyAllSelf(gl.store.certificates); - gl._storeOpts = gl.store.getOptions(); Object.keys(gl._storeOpts).forEach(function (key) { if (!(key in gl)) { gl[key] = gl._storeOpts[key]; diff --git a/lib/core.js b/lib/core.js index 962cb1b..aaf1555 100644 --- a/lib/core.js +++ b/lib/core.js @@ -45,6 +45,7 @@ module.exports.create = function (gl) { return PromiseA.resolve(gl._ipc.acmeUrls); } + // TODO acme-v2/nocompat return gl.acme.getAcmeUrlsAsync(args.server).then(function (data) { gl._ipc.acmeUrlsUpdatedAt = Date.now(); gl._ipc.acmeUrls = data; @@ -68,6 +69,8 @@ module.exports.create = function (gl) { var copy = utils.merge(args, gl); var disagreeTos; args = utils.tplCopy(copy); + if (!args.account) { args.account = {}; } + if ('object' === typeof args.account && !args.account.id) { args.account.id = args.accountId || args.email || ''; } disagreeTos = (!args.agreeTos && 'undefined' !== typeof args.agreeTos); if (!args.email || disagreeTos || (parseInt(args.rsaKeySize, 10) < 2048)) { @@ -80,30 +83,45 @@ module.exports.create = function (gl) { } return utils.testEmail(args.email).then(function () { + if (args.account && args.account.privkey && (args.account.privkey.jwk || args.account.privkey.pem)) { + // TODO import jwk or pem and return it here + console.warn("TODO: implement accounts.checkKeypairAsync skipping"); + } + var newKeypair = true; + var accountKeypair; var promise = gl.store.accounts.checkKeypairAsync(args).then(function (keypair) { if (keypair) { - return RSA.import(keypair); + // TODO keypairs + newKeypair = false; + accountKeypair = RSA.import(keypair); + return; } if (args.accountKeypair) { - return gl.store.accounts.setKeypairAsync(args, RSA.import(args.accountKeypair)); + // TODO keypairs + accountKeypair = RSA.import(args.accountKeypair); + return; } var keypairOpts = { bitlen: args.rsaKeySize, exp: 65537, public: true, pem: true }; - return RSA.generateKeypairAsync(keypairOpts).then(function (keypair) { + // TODO keypairs + return (args.generateKeypair||RSA.generateKeypairAsync)(keypairOpts).then(function (keypair) { keypair.privateKeyPem = RSA.exportPrivatePem(keypair); keypair.publicKeyPem = RSA.exportPublicPem(keypair); keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair); - return gl.store.accounts.setKeypairAsync(args, keypair); + accountKeypair = keypair; }); - }); + }).then(function () { + return accountKeypair; + }); return promise.then(function (keypair) { // Note: the ACME urls are always fetched fresh on purpose - // TODO is this the right place for this? + // TODO acme-v2/nocompat return core.getAcmeUrlsAsync(args).then(function (urls) { args._acmeUrls = urls; + // TODO acme-v2/nocompat return gl.acme.registerNewAccountAsync({ email: args.email , newRegUrl: args._acmeUrls.newReg @@ -131,13 +149,18 @@ module.exports.create = function (gl) { , newAuthzUrl: args._acmeUrls.newAuthz }; + return gl.store.accounts.setKeypairAsync(args, keypair).then(function () { // TODO move templating of arguments to right here? - return gl.store.accounts.setAsync(args, reg).then(function (account) { - // should now have account.id and account.accountId - args.account = account; - args.accountId = account.id; - return account; - }); + if (!gl.store.accounts.setAsync) { return PromiseA.resolve({ keypair: keypair }); } + return gl.store.accounts.setAsync(args, reg).then(function (account) { + if (account && 'object' !== typeof account) { + throw new Error("store.accounts.setAsync should either return 'null' or an object with at least a string 'id'"); + } + if (!account) { account = {}; } + account.keypair = keypair; + return account; + }); + }); }); }); }); @@ -145,13 +168,20 @@ module.exports.create = function (gl) { } // Accounts + // (only used for keypair) , getAsync: function (args) { return core.accounts.checkAsync(args).then(function (account) { - if (account) { - return account; - } else { + if (!account) { return core.accounts.registerAsync(args); } + if (account.keypair) { return account; } + + if (!args.account) { args.account = {}; } + if ('object' === typeof args.account && !args.account.id) { args.account.id = args.accountId || args.email || ''; } + var copy = utils.merge(args, gl); + args = utils.tplCopy(copy); + return gl.store.accounts.checkKeypairAsync(args).then(function (keypair) { + if (keypair) { return { keypair: keypair }; } return core.accounts.registerAsync(args); - } + }); }); } @@ -166,7 +196,12 @@ module.exports.create = function (gl) { var copy = utils.merge(args, gl); args = utils.tplCopy(copy); + if (!args.account) { args.account = {}; } + if ('object' === typeof args.account && !args.account.id) { args.account.id = args.accountId || args.email || ''; } + // we can re-register the same account until we're blue in the face and it's all the same + // of course, we can also skip the lookup if we do store the account, but whatever + if (!gl.store.accounts.checkAsync) { return null; } return gl.store.accounts.checkAsync(args).then(function (account) { if (!account) { @@ -243,22 +278,35 @@ module.exports.create = function (gl) { return core.accounts.getAsync(args).then(function (account) { args.account = account; + + if (args.certificate && args.certificate.privkey && (args.certificate.privkey.jwk || args.certificate.privkey.pem)) { + // TODO import jwk or pem and return it here + console.warn("TODO: implement certificates.checkKeypairAsync skipping"); + } + var domainKeypair; + // This has been done in the getAsync already, so we skip it here + // if approveDomains doesn't set subject, we set it here + //args.subject = args.subject || args.domains[0]; var promise = gl.store.certificates.checkKeypairAsync(args).then(function (keypair) { if (keypair) { - return RSA.import(keypair); + domainKeypair = RSA.import(keypair); + return; } if (args.domainKeypair) { - return gl.store.certificates.setKeypairAsync(args, RSA.import(args.domainKeypair)); + domainKeypair = RSA.import(args.domainKeypair); + return; } var keypairOpts = { bitlen: args.rsaKeySize, exp: 65537, public: true, pem: true }; - return RSA.generateKeypairAsync(keypairOpts).then(function (keypair) { + return (args.generateKeypair||RSA.generateKeypairAsync)(keypairOpts).then(function (keypair) { keypair.privateKeyPem = RSA.exportPrivatePem(keypair); keypair.publicKeyPem = RSA.exportPublicPem(keypair); keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair); - return gl.store.certificates.setKeypairAsync(args, keypair); + domainKeypair = keypair; }); + }).then(function () { + return domainKeypair; }); return promise.then(function (domainKeypair) { @@ -312,17 +360,25 @@ module.exports.create = function (gl) { log(args.debug, 'calling greenlock.acme.getCertificateAsync', certReq.domains); + // TODO acme-v2/nocompat return gl.acme.getCertificateAsync(certReq).then(utils.attachCertInfo); }); }).then(function (results) { + //var requested = {}; + //var issued = {}; // { cert, chain, privkey /*TODO, subject, altnames, issuedAt, expiresAt */ } // args.certs.privkey = RSA.exportPrivatePem(options.domainKeypair); args.certs = results; // args.pems is deprecated args.pems = results; - return gl.store.certificates.setAsync(args).then(function () { - return results; + // This has been done in the getAsync already, so we skip it here + // if approveDomains doesn't set subject, we set it here + //args.subject = args.subject || args.domains[0]; + return gl.store.certificates.setKeypairAsync(args, domainKeypair).then(function () { + return gl.store.certificates.setAsync(args).then(function () { + return results; + }); }); }); }); @@ -377,13 +433,19 @@ module.exports.create = function (gl) { } , checkAsync: function (args) { var copy = utils.merge(args, gl); - utils.tplCopy(copy); + // if approveDomains doesn't set subject, we set it here + copy.subject = copy.subject || copy.domains[0]; + args = utils.tplCopy(copy); // returns pems - return gl.store.certificates.checkAsync(copy).then(function (cert) { + return gl.store.certificates.checkAsync(args).then(function (cert) { if (cert) { - log(args.debug, 'checkAsync found existing certificates'); - return utils.attachCertInfo(cert); + cert = utils.attachCertInfo(cert); + if (utils.certHasDomain(cert, args.domain)) { + log(args.debug, 'checkAsync found existing certificates'); + return cert; + } + log(args.debug, 'checkAsync found mismatched / incomplete certificates'); } log(args.debug, 'checkAsync failed to find certificates'); @@ -393,10 +455,17 @@ module.exports.create = function (gl) { // Certificates , getAsync: function (args) { var copy = utils.merge(args, gl); + // if approveDomains doesn't set subject, we set it here + copy.subject = copy.subject || copy.domains[0]; args = utils.tplCopy(copy); + if (args.certificate && args.certificate.privkey && args.certificate.cert && args.certificate.chain) { + // TODO skip fetching a certificate if it's fetched during approveDomains + console.warn("TODO: implement certificates.checkAsync skipping"); + } return core.certificates.checkAsync(args).then(function (certs) { - if (!certs) { + if (certs) { certs = utils.attachCertInfo(certs); } + if (!certs || !utils.certHasDomain(certs, args.domain)) { // There is no cert available if (false !== args.securityUpdates && !args._communityMemberAdded) { try { diff --git a/lib/utils-test.js b/lib/utils-test.js new file mode 100644 index 0000000..964d727 --- /dev/null +++ b/lib/utils-test.js @@ -0,0 +1,24 @@ +'use strict'; + +var utils = require('./utils.js') +var cert = { subject: 'example.com', altnames: ['*.bar.com','foo.net'] }; +if (utils.certHasDomain(cert, 'bad.com')) { + throw new Error("allowed bad domain"); +} +if (!utils.certHasDomain(cert, 'example.com')) { + throw new Error("missed subject"); +} +if (utils.certHasDomain(cert, 'bar.com')) { + throw new Error("allowed bad (missing) sub"); +} +if (!utils.certHasDomain(cert, 'foo.bar.com')) { + throw new Error("didn't allow valid wildcarded-domain"); +} +if (utils.certHasDomain(cert, 'dub.foo.bar.com')) { + throw new Error("allowed sub-sub domain"); +} +if (!utils.certHasDomain(cert, 'foo.net')) { + throw new Error("missed altname"); +} + +console.info("PASSED"); diff --git a/lib/utils.js b/lib/utils.js index 5dd7e04..36acb02 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -9,9 +9,7 @@ var promisify = (require('util').promisify || require('bluebird').promisify); var dnsResolveMxAsync = promisify(require('dns').resolveMx); module.exports.attachCertInfo = function (results) { - // XXX Note: Parsing the certificate info comes at a great cost (~500kb) - var getCertInfo = require('cert-info').info; - var certInfo = getCertInfo(results.cert); + var certInfo = require('cert-info').info(results.cert); // subject, altnames, issuedAt, expiresAt Object.keys(certInfo).forEach(function (key) { @@ -21,6 +19,20 @@ module.exports.attachCertInfo = function (results) { return results; }; +module.exports.certHasDomain = function (certInfo, _domain) { + var names = (certInfo.altnames || []).slice(0); + names.push(certInfo.subject); + return names.some(function (name) { + var domain = _domain.toLowerCase(); + name = name.toLowerCase(); + if ('*.' === name.substr(0, 2)) { + name = name.substr(2); + domain = domain.split('.').slice(1).join('.'); + } + return name === domain; + }); +}; + module.exports.isValidDomain = function (domain) { if (re.test(domain)) { return domain; @@ -58,7 +70,7 @@ module.exports.tplCopy = function (copy) { var tplKeys; copy.hostnameGet = function (copy) { - return (copy.domains || [])[0] || copy.domain; + return copy.subject || (copy.domains || [])[0] || copy.domain; }; Object.keys(copy).forEach(function (key) { diff --git a/package.json b/package.json index 121152f..f8a30e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "greenlock", - "version": "2.6.9", + "version": "2.6.10", "description": "Let's Encrypt for node.js on npm", "main": "index.js", "files": [