diff --git a/lib/core.js b/lib/core.js index a6ec745..bcfbfd2 100644 --- a/lib/core.js +++ b/lib/core.js @@ -7,210 +7,234 @@ module.exports.create = function (le) { var LeCore = PromiseA.promisifyAll(require('letiny-core')); var crypto = require('crypto'); - function attachCertInfo(results) { - var getCertInfo = require('./cert-info').getCertInfo; - // XXX Note: Parsing the certificate info comes at a great cost (~500kb) - var certInfo = getCertInfo(results.cert); + var core = { + // + // Helpers + // + getAcmeUrlsAsync: function (args) { + var now = Date.now(); - //results.issuedAt = arr[3].mtime.valueOf() - results.issuedAt = Date(certInfo.notBefore.value).valueOf(); // Date.now() - results.expiresAt = Date(certInfo.notAfter.value).valueOf(); + // TODO check response header on request for cache time + if ((now - le._ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) { + return PromiseA.resolve(le._ipc.acmeUrls); + } - return results; - } + return LeCore.getAcmeUrlsAsync(args.server).then(function (data) { + le._ipc.acmeUrlsUpdatedAt = Date.now(); + le._ipc.acmeUrls = data; - function createAccount(args, handlers) { - return RSA.generateKeypairAsync(args.rsaKeySize, 65537, { public: true, pem: true }).then(function (keypair) { + return le._ipc.acmeUrls; + }); + } - 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); - } - , accountKeypair: keypair - , debug: defaults.debug || args.debug || handlers.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'); + // + // The Main Enchilada + // - var accountId = keypair.publicKeyMd5; - var regr = { body: body }; - var account = {}; + // + // Accounts + // + , accounts: { + registerAsync: function (args) { + return RSA.generateKeypairAsync(args.rsaKeySize, 65537, { public: true, pem: true }).then(function (keypair) { - args.accountId = accountId; + return LeCore.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; + } - account.keypair = keypair; - account.regr = regr; - account.accountId = accountId; - account.id = accountId; + // args.email = email; // already there + // args.domains = domains // already there + args.tosUrl = tosUrl; + le.agreeToTerms(args, agreeCb); + } + , accountKeypair: keypair - args.account = account; + , 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 backend.setAccountAsync(args, account).then(function () { - return account; + var accountId = keypair.publicKeyMd5; + var regr = { body: body }; + var 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; + }); + }); }); - }); - }); - } - - function getAcmeUrls(args) { - var now = Date.now(); - - // TODO check response header on request for cache time - if ((now - le._ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) { - return PromiseA.resolve(le._ipc.acmeUrls); - } - - return LeCore.getAcmeUrlsAsync(args.server).then(function (data) { - le._ipc.acmeUrlsUpdatedAt = Date.now(); - le._ipc.acmeUrls = data; - - return le._ipc.acmeUrls; - }); - } - - function getCertificateAsync(args, defaults, handlers) { - - function log() { - if (args.debug || defaults.debug) { - console.log.apply(console, arguments); } - } + // 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); + } + }); + } + , checkAsync: function (args) { + return le.store.accounts.checkAccountId(args).then(function (accountId) { - var account = args.account; - var promise; - var keypairOpts = { public: true, pem: true }; - - promise = backend.getPrivatePem(args).then(function (pem) { - return RSA.import({ privateKeyPem: pem }); - }, function (/*err*/) { - return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) { - keypair.privateKeyPem = RSA.exportPrivatePem(keypair); - keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair); - return backend.setPrivatePem(args, keypair); - }); - }); - - return promise.then(function (domainKeypair) { - log("[le/core.js] get certificate"); - - args.domainKeypair = domainKeypair; - //args.registration = domainKey; - - return LeCore.getCertificateAsync({ - debug: args.debug - - , newAuthzUrl: args._acmeUrls.newAuthz - , newCertUrl: args._acmeUrls.newCert - - , accountKeypair: RSA.import(account.keypair) - , domainKeypair: domainKeypair - , domains: args.domains - , challengeType: args.challengeType - - // - // 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 = handlers.merge({ domains: [domain] }, defaults, backendDefaults); - handlers.tplCopy(copy); - - //args.domains = [domain]; - args.domains = args.domains || [domain]; - - if (5 !== handlers.setChallenge.length) { - done(new Error("handlers.setChallenge receives the wrong number of arguments." - + " You must define setChallenge as function (opts, domain, key, val, cb) { }")); - return; + if (!accountId) { + return null; } - handlers.setChallenge(copy, domain, key, value, done); - } - , removeChallenge: function (domain, key, done) { - var copy = handlers.merge({ domains: [domain] }, defaults, backendDefaults); - handlers.tplCopy(copy); - - if (4 !== handlers.removeChallenge.length) { - done(new Error("handlers.removeChallenge receives the wrong number of arguments." - + " You must define removeChallenge as function (opts, domain, key, cb) { }")); - return; - } - - handlers.removeChallenge(copy, domain, key, done); - } - }).then(attachCertInfo); - }).then(function (results) { - // { cert, chain, fullchain, privkey } - - args.pems = results; - return backend.setRegistration(args, defaults, handlers); - }); - } - - function getOrCreateDomainCertificate(args, defaults, handlers) { - if (args.duplicate) { - // we're forcing a refresh via 'dupliate: true' - return getCertificateAsync(args, defaults, handlers); - } - - return wrapped.fetchAsync(args).then(function (certs) { - var halfLife = (certs.expiresAt - certs.issuedAt) / 2; - - if (!certs || (Date.now() - certs.issuedAt) > halfLife) { - // There is no cert available - // Or the cert is more than half-expired - return getCertificateAsync(args, defaults, handlers); - } - - return PromiseA.reject(new Error( - "[ERROR] Certificate issued at '" - + new Date(certs.issuedAt).toISOString() + "' and expires at '" - + new Date(certs.expiresAt).toISOString() + "'. Ignoring renewal attempt until half-life at '" - + new Date(certs.issuedA + halfLife).toISOString() + "'. Set { duplicate: true } to force." - )); - }); - } - - // returns 'account' from lib/accounts { meta, regr, keypair, accountId (id) } - function getOrCreateAcmeAccount(args, defaults, handlers) { - function log() { - if (args.debug) { - console.log.apply(console, arguments); - } - } - - return backend.getAccountId(args).then(function (accountId) { - - // Note: the ACME urls are always fetched fresh on purpose - return getAcmeUrls(args).then(function (urls) { - args._acmeUrls = urls; - - if (accountId) { - log('[le/core.js] use account'); - args.accountId = accountId; - return backend.getAccount(args, handlers); - } else { - log('[le/core.js] create account'); - return createAccount(args, handlers); - } - }); - }); - } - var wrapped = { - registerAsync: function (args) { + // 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); + }); + }); + } + } + + , certificates: { + // getCertificateAsync: + registerAsync: function (args) { + + function log() { + if (args.debug || le.debug) { + console.log.apply(console, arguments); + } + } + + var account = args.account; + var keypairOpts = { public: true, pem: true }; + + var promise = le.store.certificates.checkKeypairAsync(args).then(function (keypair) { + return RSA.import(keypair); + }, function (/*err*/) { + return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) { + keypair.privateKeyPem = RSA.exportPrivatePem(keypair); + keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair); + return le.store.certificates.setKeypairAsync(args, keypair); + }); + }); + + return promise.then(function (domainKeypair) { + log("[le/core.js] get certificate"); + + args.domainKeypair = domainKeypair; + //args.registration = domainKey; + + return LeCore.getCertificateAsync({ + debug: args.debug || le.debug + + , newAuthzUrl: args._acmeUrls.newAuthz + , newCertUrl: args._acmeUrls.newCert + + , accountKeypair: RSA.import(account.keypair) + , domainKeypair: domainKeypair + , domains: args.domains + , challengeType: args.challengeType + + // + // 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); + + //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); + + 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); + } + }).then(utils.attachCertInfo); + }).then(function (results) { + // { cert, chain, fullchain, privkey } + + args.pems = results; + return le.store.certificates.setAsync(args); + }); + } + // checkAsync + , checkAsync: function (args) { + var copy = utils.merge(args, le); + utils.tplCopy(copy); + + return le.store.certificates.checkAsync(copy).then(utils.attachCertInfo); + } + // getOrCreateDomainCertificate + , getAsync: function (args) { + var copy = utils.merge(args, le); + 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) { + 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); + } + + return PromiseA.reject(new Error( + "[ERROR] Certificate issued at '" + + new Date(certs.issuedAt).toISOString() + "' and expires at '" + + new Date(certs.expiresAt).toISOString() + "'. Ignoring renewal attempt until half-life at '" + + new Date(renewableAt).toISOString() + "'. Set { duplicate: true } to force." + )); + }); + } + } + + // returns 'account' from lib/accounts { meta, regr, keypair, accountId (id) } + , registerAsync: function (args) { var err; if (!Array.isArray(args.domains)) { @@ -226,16 +250,16 @@ module.exports.create = function (le) { return PromiseA.reject(err); } - var copy = handlers.merge(args, defaults, backendDefaults); - handlers.tplCopy(copy); + var copy = utils.merge(args, le); + utils.tplCopy(copy); - return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) { + return core.accounts.getAsync(copy).then(function (account) { copy.account = account; return backend.getOrCreateRenewal(copy).then(function (pyobj) { copy.pyobj = pyobj; - return getOrCreateDomainCertificate(copy, defaults, handlers); + return core.certificates.getAsync(copy); }); }).then(function (result) { return result; @@ -243,25 +267,7 @@ module.exports.create = function (le) { return PromiseA.reject(err); }); } - , getOrCreateAccount: function (args) { - return createAccount(args, handlers); - } - , configureAsync: function (hargs) { - var copy = handlers.merge(hargs, defaults, backendDefaults); - handlers.tplCopy(copy); - - return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) { - copy.account = account; - return backend.getOrCreateRenewal(copy); - }); - } - , fetchAsync: function (args) { - var copy = handlers.merge(args, defaults); - handlers.tplCopy(copy); - - return backend.fetchAsync(copy).then(attachCertInfo); - } }; - return wrapped; + return core; };