diff --git a/src/acme2.js b/src/acme2.js index 48cf44dad..a197f8574 100644 --- a/src/acme2.js +++ b/src/acme2.js @@ -43,7 +43,7 @@ function Acme2(fqdn, domainObject, email) { const prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod' this.caDirectory = prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL; this.directory = {}; - this.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null; + this.forceHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null; this.wildcard = !!domainObject.tlsConfig.wildcard; this.domain = domainObject.domain; @@ -58,7 +58,7 @@ function Acme2(fqdn, domainObject, email) { this.certName = this.cn.replace('*.', '_.'); - debug(`Acme2: will get cert for fqdn: ${this.fqdn} cn: ${this.cn} certName: ${this.certName} wildcard: ${this.wildcard} http: ${this.performHttpAuthorization}`); + debug(`Acme2: will get cert for fqdn: ${this.fqdn} cn: ${this.cn} certName: ${this.certName} wildcard: ${this.wildcard} http: ${this.forceHttpAuthorization}`); } // urlsafe base64 encoding (jose) @@ -370,13 +370,8 @@ Acme2.prototype.downloadCertificate = async function (certUrl) { }); }; -Acme2.prototype.prepareHttpChallenge = async function (authorization) { - assert.strictEqual(typeof authorization, 'object'); - - debug(`prepareHttpChallenge: challenges: ${JSON.stringify(authorization)}`); - const httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; }); - if (httpChallenges.length === 0) throw new BoxError(BoxError.ACME_ERROR, 'no http challenges'); - const challenge = httpChallenges[0]; +Acme2.prototype.prepareHttpChallenge = async function (challenge) { + assert.strictEqual(typeof challenge, 'object'); debug(`prepareHttpChallenge: preparing for challenge ${JSON.stringify(challenge)}`); @@ -386,8 +381,6 @@ Acme2.prototype.prepareHttpChallenge = async function (authorization) { debug(`prepareHttpChallenge: writing ${keyAuthorization} to ${challengeFilePath}`); if (!safe.fs.writeFileSync(challengeFilePath, keyAuthorization)) throw new BoxError(BoxError.FS_ERROR, `Error writing challenge: ${safe.error.message}`); - - return challenge; }; Acme2.prototype.cleanupHttpChallenge = async function (challenge) { @@ -416,13 +409,10 @@ function getChallengeSubdomain(cn, domain) { return challengeSubdomain; } -Acme2.prototype.prepareDnsChallenge = async function (cn, authorization) { - assert.strictEqual(typeof authorization, 'object'); +Acme2.prototype.prepareDnsChallenge = async function (cn, challenge) { + assert.strictEqual(typeof challenge, 'object'); - debug(`prepareDnsChallenge: challenges: ${JSON.stringify(authorization)}`); - const dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; }); - if (dnsChallenges.length === 0) throw new BoxError(BoxError.ACME_ERROR, 'no dns challenges'); - const challenge = dnsChallenges[0]; + debug(`prepareDnsChallenge: preparing for challenge: ${JSON.stringify(challenge)}`); const keyAuthorization = this.getKeyAuthorization(challenge.token); const shasum = crypto.createHash('sha256'); @@ -436,8 +426,6 @@ Acme2.prototype.prepareDnsChallenge = async function (cn, authorization) { await dns.upsertDnsRecords(challengeSubdomain, this.domain, 'TXT', [ `"${txtValue}"` ]); await dns.waitForDnsRecord(challengeSubdomain, this.domain, 'TXT', txtValue, { times: 200 }); - - return challenge; }; Acme2.prototype.cleanupDnsChallenge = async function (cn, challenge) { @@ -460,22 +448,31 @@ Acme2.prototype.prepareChallenge = async function (cn, authorization) { assert.strictEqual(typeof cn, 'string'); assert.strictEqual(typeof authorization, 'object'); - debug(`prepareChallenge: http: ${this.performHttpAuthorization} cn: ${cn} authorization: ${JSON.stringify(authorization)}`); + debug(`prepareChallenge: http: ${this.forceHttpAuthorization} cn: ${cn} authorization: ${JSON.stringify(authorization)}`); - if (this.performHttpAuthorization) { - return await this.prepareHttpChallenge(authorization); - } else { - return await this.prepareDnsChallenge(cn, authorization); + // validation is cached by LE for 60 days or so. if a user switches from non-wildcard DNS (http challenge) to programmatic DNS (dns challenge), then + // LE remembers the challenge type and won't give us a dns challenge for 60 days! + // https://letsencrypt.org/docs/faq/#i-successfully-renewed-a-certificate-but-validation-didn-t-happen-this-time-how-is-that-possible + const dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; }); + const httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; }); + + if (this.forceHttpAuthorization || dnsChallenges.length === 0) { + if (httpChallenges.length === 0) throw new BoxError(BoxError.ACME_ERROR, 'no http challenges'); + await this.prepareHttpChallenge(httpChallenges[0]); + return httpChallenges[0]; } + + await this.prepareDnsChallenge(cn, dnsChallenges[0]); + return dnsChallenges[0]; }; Acme2.prototype.cleanupChallenge = async function (cn, challenge) { assert.strictEqual(typeof cn, 'string'); assert.strictEqual(typeof challenge, 'object'); - debug(`cleanupChallenge: http: ${this.performHttpAuthorization}`); + debug(`cleanupChallenge: http: ${this.forceHttpAuthorization}`); - if (this.performHttpAuthorization) { + if (this.forceHttpAuthorization) { await this.cleanupHttpChallenge(challenge); } else { await this.cleanupDnsChallenge(cn, challenge);