refactor
This commit is contained in:
parent
330e0e7832
commit
49d5346615
|
@ -1067,10 +1067,6 @@ Keypairs.signJws = function (opts) {
|
|||
protectedHeader = JSON.stringify(protect);
|
||||
}
|
||||
|
||||
// Not sure how to handle the empty case since ACME POST-as-GET must be empty
|
||||
//if (!payload) {
|
||||
// throw new Error("opts.payload should be JSON, string, or ArrayBuffer (it may be empty, but that must be explicit)");
|
||||
//}
|
||||
// Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc)
|
||||
if (payload && ('string' !== typeof payload)
|
||||
&& ('undefined' === typeof payload.byteLength)
|
||||
|
@ -1832,104 +1828,87 @@ ACME._setNonce = function (me, nonce) {
|
|||
}
|
||||
*/
|
||||
ACME._registerAccount = function (me, options) {
|
||||
if (me.debug) { console.debug('[acme-v2] accounts.create'); }
|
||||
//#console.debug('[acme-v2] accounts.create');
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
function agree(tosUrl) {
|
||||
var err;
|
||||
if (me._tos !== tosUrl) {
|
||||
err = new Error("You must agree to the ToS at '" + me._tos + "'");
|
||||
err.code = "E_AGREE_TOS";
|
||||
throw err;
|
||||
}
|
||||
|
||||
function agree(tosUrl) {
|
||||
var err;
|
||||
if (me._tos !== tosUrl) {
|
||||
err = new Error("You must agree to the ToS at '" + me._tos + "'");
|
||||
err.code = "E_AGREE_TOS";
|
||||
reject(err);
|
||||
return;
|
||||
return ACME._importKeypair(me, options.accountKey || options.accountKeypair).then(function (pair) {
|
||||
var contact;
|
||||
if (options.contact) {
|
||||
contact = options.contact.slice(0);
|
||||
} else if (options.email) {
|
||||
contact = [ 'mailto:' + options.email ];
|
||||
}
|
||||
|
||||
return ACME._importKeypair(me, options.accountKeypair).then(function (pair) {
|
||||
var contact;
|
||||
if (options.contact) {
|
||||
contact = options.contact.slice(0);
|
||||
} else if (options.email) {
|
||||
contact = [ 'mailto:' + options.email ];
|
||||
}
|
||||
var body = {
|
||||
termsOfServiceAgreed: tosUrl === me._tos
|
||||
, onlyReturnExisting: false
|
||||
, contact: contact
|
||||
};
|
||||
var pExt;
|
||||
if (options.externalAccount) {
|
||||
pExt = me.Keypairs.signJws({
|
||||
// TODO is HMAC the standard, or is this arbitrary?
|
||||
secret: options.externalAccount.secret
|
||||
, protected: {
|
||||
alg: options.externalAccount.alg || "HS256"
|
||||
, kid: options.externalAccount.id
|
||||
, url: me._directoryUrls.newAccount
|
||||
}
|
||||
, payload: Enc.binToBuf(JSON.stringify(pair.public))
|
||||
}).then(function (jws) {
|
||||
body.externalAccountBinding = jws;
|
||||
return body;
|
||||
});
|
||||
} else {
|
||||
pExt = Promise.resolve(body);
|
||||
}
|
||||
return pExt.then(function (body) {
|
||||
var payload = JSON.stringify(body);
|
||||
return ACME._jwsRequest(me, {
|
||||
options: options
|
||||
var body = {
|
||||
termsOfServiceAgreed: tosUrl === me._tos
|
||||
, onlyReturnExisting: false
|
||||
, contact: contact
|
||||
};
|
||||
var pExt;
|
||||
if (options.externalAccount) {
|
||||
pExt = me.Keypairs.signJws({
|
||||
// TODO is HMAC the standard, or is this arbitrary?
|
||||
secret: options.externalAccount.secret
|
||||
, protected: {
|
||||
alg: options.externalAccount.alg || "HS256"
|
||||
, kid: options.externalAccount.id
|
||||
, url: me._directoryUrls.newAccount
|
||||
, protected: { kid: false, jwk: pair.public }
|
||||
, payload: Enc.binToBuf(payload)
|
||||
}).then(function (resp) {
|
||||
var account = resp.body;
|
||||
}
|
||||
, payload: Enc.binToBuf(JSON.stringify(pair.public))
|
||||
}).then(function (jws) {
|
||||
body.externalAccountBinding = jws;
|
||||
return body;
|
||||
});
|
||||
} else {
|
||||
pExt = Promise.resolve(body);
|
||||
}
|
||||
return pExt.then(function (body) {
|
||||
var payload = JSON.stringify(body);
|
||||
return ACME._jwsRequest(me, {
|
||||
options: options
|
||||
, url: me._directoryUrls.newAccount
|
||||
, protected: { kid: false, jwk: pair.public }
|
||||
, payload: Enc.binToBuf(payload)
|
||||
}).then(function (resp) {
|
||||
var account = resp.body;
|
||||
|
||||
if (2 !== Math.floor(resp.statusCode / 100)) {
|
||||
throw new Error('account error: ' + JSON.stringify(resp.body));
|
||||
}
|
||||
if (2 !== Math.floor(resp.statusCode / 100)) {
|
||||
throw new Error('account error: ' + JSON.stringify(resp.body));
|
||||
}
|
||||
|
||||
var location = resp.headers.location;
|
||||
// the account id url
|
||||
options._kid = location;
|
||||
if (me.debug) { console.debug('[DEBUG] new account location:'); }
|
||||
if (me.debug) { console.debug(location); }
|
||||
if (me.debug) { console.debug(resp); }
|
||||
var location = resp.headers.location;
|
||||
// the account id url
|
||||
options._kid = location;
|
||||
//#console.debug('[DEBUG] new account location:');
|
||||
//#console.debug(location);
|
||||
//#console.debug(resp);
|
||||
|
||||
/*
|
||||
{
|
||||
contact: ["mailto:jon@example.com"],
|
||||
orders: "https://some-url",
|
||||
status: 'valid'
|
||||
}
|
||||
*/
|
||||
if (!account) { account = { _emptyResponse: true }; }
|
||||
// https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8
|
||||
if (!account.key) { account.key = {}; }
|
||||
account.key.kid = options._kid;
|
||||
return account;
|
||||
}).then(resolve, reject);
|
||||
/*
|
||||
{
|
||||
contact: ["mailto:jon@example.com"],
|
||||
orders: "https://some-url",
|
||||
status: 'valid'
|
||||
}
|
||||
*/
|
||||
if (!account) { account = { _emptyResponse: true }; }
|
||||
// https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8
|
||||
if (!account.key) { account.key = {}; }
|
||||
account.key.kid = options._kid;
|
||||
return account;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (me.debug) { console.debug('[acme-v2] agreeToTerms'); }
|
||||
if (1 === options.agreeToTerms.length) {
|
||||
// newer promise API
|
||||
return Promise.resolve(options.agreeToTerms(me._tos)).then(agree, reject);
|
||||
}
|
||||
else if (2 === options.agreeToTerms.length) {
|
||||
// backwards compat cb API
|
||||
return options.agreeToTerms(me._tos, function (err, tosUrl) {
|
||||
if (!err) { agree(tosUrl); return; }
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
else {
|
||||
reject(new Error('agreeToTerms has incorrect function signature.'
|
||||
+ ' Should be fn(tos) { return Promise<tos>; }'));
|
||||
}
|
||||
});
|
||||
return Promise.resolve().then(function () {
|
||||
return options.agreeToTerms(me._tos);
|
||||
}).then(agree);
|
||||
};
|
||||
/*
|
||||
POST /acme/new-order HTTP/1.1
|
||||
|
@ -1952,9 +1931,7 @@ ACME._registerAccount = function (me, options) {
|
|||
}
|
||||
*/
|
||||
ACME._getChallenges = function (me, options, authUrl) {
|
||||
if (me.debug) { console.debug('\n[DEBUG] getChallenges\n'); }
|
||||
// TODO POST-as-GET
|
||||
|
||||
//#console.debug('\n[DEBUG] getChallenges\n');
|
||||
return ACME._jwsRequest(me, {
|
||||
options: options
|
||||
, protected: { kid: options._kid }
|
||||
|
@ -1962,7 +1939,7 @@ ACME._getChallenges = function (me, options, authUrl) {
|
|||
, url: authUrl
|
||||
}).then(function (resp) {
|
||||
// Pre-emptive rather than lazy for interfaces that need to show the challenges to the user first
|
||||
return ACME._challengesToAuth(me, options, resp.body, false).then(function (auths) {
|
||||
return ACME._computeAuths(me, options, resp.body, false).then(function (auths) {
|
||||
resp.body._rawChallenges = resp.body.challenges;
|
||||
resp.body.challenges = auths;
|
||||
return resp.body;
|
||||
|
@ -1999,78 +1976,53 @@ ACME._testChallengeOptions = function () {
|
|||
}
|
||||
];
|
||||
};
|
||||
ACME._testChallenges = function (me, options) {
|
||||
var CHECK_DELAY = 0;
|
||||
return Promise.all(options.domains.map(function (identifierValue) {
|
||||
// TODO we really only need one to pass, not all to pass
|
||||
ACME._testChallenges = function (me, reals) {
|
||||
console.log('[DEBUG] testChallenges');
|
||||
if (me.skipDryRun || me.skipChallengeTest) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var nopts = {};
|
||||
Object.keys(reals).forEach(function (key) {
|
||||
nopts[key] = reals[key];
|
||||
});
|
||||
nopts.order = {};
|
||||
|
||||
return Promise.all(nopts.domains.map(function (name) {
|
||||
var challenges = ACME._testChallengeOptions();
|
||||
if (identifierValue.includes("*")) {
|
||||
var wild = '*.' === name.slice(0, 2);
|
||||
if (wild) {
|
||||
challenges = challenges.filter(function (ch) { return ch._wildcard; });
|
||||
}
|
||||
|
||||
var resp = {
|
||||
body: {
|
||||
identifier: { type: 'dns' , value: name.replace('*.', '') }
|
||||
, challenges: challenges
|
||||
, expires: new Date(Date.now() + (60 * 1000)).toISOString()
|
||||
, wildcard: name.includes('*.') || undefined
|
||||
}
|
||||
};
|
||||
// The dry-run comes first in the spirit of "fail fast"
|
||||
// (and protecting against challenge failure rate limits)
|
||||
var dryrun = true;
|
||||
var resp = {
|
||||
body: {
|
||||
identifier: {
|
||||
type: "dns"
|
||||
, value: identifierValue.replace(/^\*\./, '')
|
||||
}
|
||||
, challenges: challenges
|
||||
, expires: new Date(Date.now() + (60 * 1000)).toISOString()
|
||||
, wildcard: identifierValue.includes('*.') || undefined
|
||||
}
|
||||
};
|
||||
return ACME._challengesToAuth(me, options, resp.body, dryrun).then(function (auths) {
|
||||
resp.body._rawChallenges = resp.body.challenges;
|
||||
return ACME._computeAuths(me, nopts, resp.body, dryrun).then(function (auths) {
|
||||
resp.body.challenges = auths;
|
||||
|
||||
var auth = ACME._chooseAuth(options, resp.body.challenges);
|
||||
if (!auth) {
|
||||
// For example, wildcards require dns-01 and, if we don't have that, we have to bail
|
||||
var enabled = Object.keys(options.challenges).join(', ') || 'none';
|
||||
var suitable = resp.body.challenges.map(function (r) { return r.type; }).join(', ') || 'none';
|
||||
return Promise.reject(new Error(
|
||||
"None of the challenge types that you've enabled ( " + enabled + " )"
|
||||
+ " are suitable for validating the domain you've selected (" + identifierValue + ")."
|
||||
+ " You must enable one of ( " + suitable + " )."
|
||||
));
|
||||
}
|
||||
|
||||
// TODO remove skipChallengeTest
|
||||
if (me.skipDryRun || me.skipChallengeTest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('dns-01' === auth.type) {
|
||||
// Give the nameservers a moment to propagate
|
||||
CHECK_DELAY = 1.5 * 1000;
|
||||
}
|
||||
|
||||
if (!me._canUse[auth.type]) { return; }
|
||||
return ACME._setChallenge(me, options, auth).then(function () {
|
||||
return auth;
|
||||
});
|
||||
});
|
||||
})).then(function (auths) {
|
||||
auths = auths.filter(Boolean);
|
||||
if (!auths.length) { /*skip actual test*/ return; }
|
||||
return ACME._wait(CHECK_DELAY).then(function () {
|
||||
return Promise.all(auths.map(function (auth) {
|
||||
return ACME.challengeTests[auth.type](me, auth).then(function (result) {
|
||||
// not a blocker
|
||||
ACME._removeChallenge(me, options, auth);
|
||||
return result;
|
||||
});
|
||||
})).then(function (claims) {
|
||||
nopts.order.claims = claims;
|
||||
nopts.setChallengeWait = 0;
|
||||
|
||||
return ACME._setChallengesAll(me, nopts).then(function (valids) {
|
||||
return Promise.all(valids.map(function (auth) {
|
||||
ACME._removeChallenge(me, nopts, auth);
|
||||
}));
|
||||
});
|
||||
});
|
||||
};
|
||||
ACME._chooseAuth = function(options, auths) {
|
||||
ACME._chooseType = function(options, auths) {
|
||||
// For each of the challenge types that we support
|
||||
var auth;
|
||||
var challengeTypes = Object.keys(options.challenges);
|
||||
var challengeTypes = Object.keys(options.challenges || ACME._challengesMap);
|
||||
// ordered from most to least preferred
|
||||
challengeTypes = (options.challengePriority||[ 'tls-alpn-01', 'http-01', 'dns-01' ]).filter(function (chType) {
|
||||
return challengeTypes.includes(chType);
|
||||
|
@ -2089,15 +2041,17 @@ ACME._chooseAuth = function(options, auths) {
|
|||
|
||||
return auth;
|
||||
};
|
||||
ACME._challengesToAuth = function (me, options, request, dryrun) {
|
||||
ACME._challengesMap = {'http-01':0,'dns-01':0,'tls-alpn-01':0};
|
||||
ACME._computeAuths = function (me, options, request, dryrun) {
|
||||
console.log('[DEBUG] computeAuths');
|
||||
// we don't poison the dns cache with our dummy request
|
||||
var dnsPrefix = ACME.challengePrefixes['dns-01'];
|
||||
if (dryrun) {
|
||||
dnsPrefix = dnsPrefix.replace('acme-challenge', 'greenlock-dryrun-' + ACME._prnd(4));
|
||||
}
|
||||
var challengeTypes = Object.keys(options.challenges);
|
||||
var challengeTypes = Object.keys(options.challenges || ACME._challengesMap);
|
||||
|
||||
return ACME._importKeypair(me, options.accountKeypair).then(function (pair) {
|
||||
return ACME._importKeypair(me, options.accountKey || options.accountKeypair).then(function (pair) {
|
||||
return me.Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) {
|
||||
return Promise.all(request.challenges.map(function (challenge) {
|
||||
// Don't do extra work for challenges that we can't satisfy
|
||||
|
@ -2188,15 +2142,15 @@ ACME._postChallenge = function (me, options, auth) {
|
|||
}
|
||||
*/
|
||||
function deactivate() {
|
||||
if (me.debug) { console.debug('[acme-v2.js] deactivate:'); }
|
||||
//#console.debug('[acme-v2.js] deactivate:');
|
||||
return ACME._jwsRequest(me, {
|
||||
options: options
|
||||
, url: auth.url
|
||||
, protected: { kid: options._kid }
|
||||
, payload: Enc.binToBuf(JSON.stringify({ "status": "deactivated" }))
|
||||
}).then(function (resp) {
|
||||
if (me.debug) { console.debug('deactivate challenge: resp.body:'); }
|
||||
if (me.debug) { console.debug(resp.body); }
|
||||
}).then(function (/*#resp*/) {
|
||||
//#console.debug('deactivate challenge: resp.body:');
|
||||
//#console.debug(resp.body);
|
||||
return ACME._wait(DEAUTH_INTERVAL);
|
||||
});
|
||||
}
|
||||
|
@ -2210,11 +2164,10 @@ ACME._postChallenge = function (me, options, auth) {
|
|||
|
||||
count += 1;
|
||||
|
||||
if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); }
|
||||
// TODO POST-as-GET
|
||||
//#console.debug('\n[DEBUG] statusChallenge\n');
|
||||
return me.request({ method: 'GET', url: auth.url, json: true }).then(function (resp) {
|
||||
if ('processing' === resp.body.status) {
|
||||
if (me.debug) { console.debug('poll: again'); }
|
||||
//#console.debug('poll: again');
|
||||
return ACME._wait(RETRY_INTERVAL).then(pollStatus);
|
||||
}
|
||||
|
||||
|
@ -2223,12 +2176,12 @@ ACME._postChallenge = function (me, options, auth) {
|
|||
if (count >= MAX_PEND) {
|
||||
return ACME._wait(RETRY_INTERVAL).then(deactivate).then(respondToChallenge);
|
||||
}
|
||||
if (me.debug) { console.debug('poll: again'); }
|
||||
//#console.debug('poll: again');
|
||||
return ACME._wait(RETRY_INTERVAL).then(respondToChallenge);
|
||||
}
|
||||
|
||||
if ('valid' === resp.body.status) {
|
||||
if (me.debug) { console.debug('poll: valid'); }
|
||||
//#console.debug('poll: valid');
|
||||
|
||||
try {
|
||||
ACME._removeChallenge(me, options, auth);
|
||||
|
@ -2255,226 +2208,227 @@ ACME._postChallenge = function (me, options, auth) {
|
|||
}
|
||||
|
||||
function respondToChallenge() {
|
||||
if (me.debug) { console.debug('[acme-v2.js] responding to accept challenge:'); }
|
||||
//#console.debug('[acme-v2.js] responding to accept challenge:');
|
||||
return ACME._jwsRequest(me, {
|
||||
options: options
|
||||
, url: auth.url
|
||||
, protected: { kid: options._kid }
|
||||
, payload: Enc.binToBuf(JSON.stringify({}))
|
||||
}).then(function (resp) {
|
||||
if (me.debug) { console.debug('respond to challenge: resp.body:'); }
|
||||
if (me.debug) { console.debug(resp.body); }
|
||||
}).then(function (/*#resp*/) {
|
||||
//#console.debug('respond to challenge: resp.body:');
|
||||
//#console.debug(resp.body);
|
||||
return ACME._wait(RETRY_INTERVAL).then(pollStatus);
|
||||
});
|
||||
}
|
||||
|
||||
return respondToChallenge();
|
||||
};
|
||||
ACME._setChallenge = function (me, options, auth) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var challengers = options.challenges || {};
|
||||
var challenger = (challengers[auth.type] && challengers[auth.type].set) || options.setChallenge;
|
||||
try {
|
||||
if (1 === challenger.length) {
|
||||
challenger(auth).then(resolve).catch(reject);
|
||||
} else if (2 === challenger.length) {
|
||||
challenger(auth, function (err) {
|
||||
if(err) { reject(err); } else { resolve(); }
|
||||
});
|
||||
} else {
|
||||
// TODO remove this old backwards-compat
|
||||
var challengeCb = function(err) {
|
||||
if(err) { reject(err); } else { resolve(); }
|
||||
};
|
||||
// for backwards compat adding extra keys without changing params length
|
||||
Object.keys(auth).forEach(function (key) {
|
||||
challengeCb[key] = auth[key];
|
||||
});
|
||||
if (!ACME._setChallengeWarn) {
|
||||
console.warn("Please update to acme-v2 setChallenge(options) <Promise> or setChallenge(options, cb).");
|
||||
console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types.");
|
||||
ACME._setChallengeWarn = true;
|
||||
}
|
||||
challenger(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb);
|
||||
}
|
||||
} catch(e) {
|
||||
reject(e);
|
||||
}
|
||||
}).then(function () {
|
||||
// TODO: Do we still need this delay? Or shall we leave it to plugins to account for themselves?
|
||||
var DELAY = me.setChallengeWait || 500;
|
||||
if (me.debug) { console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY); }
|
||||
return ACME._wait(DELAY);
|
||||
});
|
||||
};
|
||||
|
||||
// options = { domains, claims, challenges, challengePriority }
|
||||
ACME._setChallengesAll = function (me, options) {
|
||||
var order = options.order;
|
||||
var setAuths = order.authorizations.slice(0);
|
||||
var claims = order.claims.slice(0);
|
||||
var validAuths = [];
|
||||
console.log("[DEBUG] setChallengesAll");
|
||||
var claims = options.order.claims.slice(0);
|
||||
var valids = [];
|
||||
var auths = [];
|
||||
// TODO: Do we still need this delay? Or shall we leave it to plugins to account for themselves?
|
||||
var DELAY = options.setChallengeWait || me.setChallengeWait || 500;
|
||||
|
||||
// Set any challenges, excpting ones that have already been validated
|
||||
function setNext() {
|
||||
var authUrl = setAuths.shift();
|
||||
var claim = claims.shift();
|
||||
if (!authUrl) { return Promise.resolve(); }
|
||||
if (!claim) { return Promise.resolve(); }
|
||||
|
||||
// var domain = options.domains[i]; // claim.identifier.value
|
||||
return Promise.resolve().then(function () {
|
||||
// For any challenges that are already valid,
|
||||
// add to the list and skip any checks.
|
||||
if (claim.challenges.some(function (ch) {
|
||||
if ('valid' === ch.status) {
|
||||
valids.push(ch);
|
||||
return true;
|
||||
}
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If it's already valid, we're golden it regardless
|
||||
if (claim.challenges.some(function (ch) { return 'valid' === ch.status; })) {
|
||||
return setNext();
|
||||
}
|
||||
// Get the list of challenge types we can validate.
|
||||
// Then order that list by preference
|
||||
// Select the first matching offered challenge type
|
||||
var usable = Object.keys(options.challenges || ACME._challengesMap);
|
||||
var selected = (options.challengePriority||[ 'tls-alpn-01', 'http-01', 'dns-01' ]).map(function (chType) {
|
||||
if (!usable.includes(chType)) { return; }
|
||||
return claim.challenges.filter(function (ch) {
|
||||
return ch.type === chType;
|
||||
})[0];
|
||||
}).filter(Boolean)[0];
|
||||
var ch;
|
||||
|
||||
var auth = ACME._chooseAuth(options, claim.challenges);
|
||||
if (!auth) {
|
||||
// For example, wildcards require dns-01 and, if we don't have that, we have to bail
|
||||
return Promise.reject(new Error(
|
||||
"Server didn't offer any challenge we can handle for '" + options.domains.join() + "'."
|
||||
));
|
||||
}
|
||||
// Bail with a descriptive message if no usable challenge could be selected
|
||||
if (!selected) {
|
||||
var enabled = usable.join(', ') || 'none';
|
||||
var suitable = claim.challenges.map(function (r) { return r.type; }).join(', ') || 'none';
|
||||
throw new Error(
|
||||
"None of the challenge types that you've enabled ( " + enabled + " )"
|
||||
+ " are suitable for validating the domain you've selected (" + claim.altname + ")."
|
||||
+ " You must enable one of ( " + suitable + " )."
|
||||
);
|
||||
}
|
||||
auths.push(selected);
|
||||
|
||||
auths.push(auth);
|
||||
return ACME._setChallenge(me, options, auth).then(setNext);
|
||||
// Give the nameservers a moment to propagate
|
||||
if ('dns-01' === selected.type) {
|
||||
DELAY = 1.5 * 1000;
|
||||
}
|
||||
|
||||
if (false === options.challenges) { return; }
|
||||
ch = options.challenges[selected.type] || {};
|
||||
if (!ch.set) {
|
||||
throw new Error("no handler for setting challenge");
|
||||
}
|
||||
return ch.set(selected);
|
||||
}).then(setNext);
|
||||
}
|
||||
|
||||
function checkNext() {
|
||||
var auth = auths.shift();
|
||||
if (!auth) { return; }
|
||||
if (!auth) { return Promise.resolve(valids); }
|
||||
|
||||
// These are not as much "valids" as they are "not invalids"
|
||||
if (!me._canUse[auth.type] || me.skipChallengeTest) {
|
||||
// not so much "valid" as "not invalid"
|
||||
// but in this case we can't confirm either way
|
||||
validAuths.push(auth);
|
||||
return Promise.resolve();
|
||||
valids.push(auth);
|
||||
return checkNext();
|
||||
}
|
||||
|
||||
return ACME.challengeTests[auth.type](me, auth).then(function () {
|
||||
validAuths.push(auth);
|
||||
valids.push(auth);
|
||||
}).then(checkNext);
|
||||
}
|
||||
|
||||
// Actually sets the challenge via ACME
|
||||
function challengeNext() {
|
||||
var auth = validAuths.shift();
|
||||
if (!auth) { return; }
|
||||
return ACME._postChallenge(me, options, auth).then(challengeNext);
|
||||
}
|
||||
|
||||
// First we set every challenge
|
||||
// Then we ask for each challenge to be checked
|
||||
// Doing otherwise would potentially cause us to poison our own DNS cache with misses
|
||||
return setNext().then(checkNext).then(challengeNext).then(function () {
|
||||
if (me.debug) { console.debug("[getCertificate] next.then"); }
|
||||
console.log('DEBUG 1 order:');
|
||||
console.log(order);
|
||||
return order.identifiers.map(function (ident) {
|
||||
return ident.value;
|
||||
});
|
||||
});
|
||||
// The reason we set every challenge in a batch first before checking any
|
||||
// is so that we don't poison our own DNS cache with misses.
|
||||
return setNext().then(function () {
|
||||
//#console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY);
|
||||
return ACME._wait(DELAY);
|
||||
}).then(checkNext);
|
||||
};
|
||||
ACME._finalizeOrder = function (me, options) {
|
||||
return ACME._getAccountKid(me, options).then(function () {
|
||||
return ACME._setChallengesAll(me, options).then(function () {
|
||||
if (!options.challenges && !options.challengePriority) {
|
||||
throw new Error("You must set either challenges or challengePrority");
|
||||
}
|
||||
return ACME._setChallengesAll(me, options).then(function (valids) {
|
||||
// options._kid added
|
||||
if (me.debug) { console.debug('finalizeOrder:'); }
|
||||
//#console.debug('finalizeOrder:');
|
||||
var order = options.order;
|
||||
var validatedDomains = options.order.identifiers.map(function (ident) {
|
||||
return ident.value;
|
||||
});
|
||||
return ACME._getCsrWeb64(me, options, validatedDomains).then(function (csr) {
|
||||
var body = { csr: csr };
|
||||
var payload = JSON.stringify(body);
|
||||
|
||||
function pollCert() {
|
||||
if (me.debug) { console.debug('[acme-v2.js] pollCert:'); }
|
||||
return ACME._jwsRequest(me, {
|
||||
options: options
|
||||
, url: options.order.finalizeUrl
|
||||
, protected: { kid: options._kid }
|
||||
, payload: Enc.binToBuf(payload)
|
||||
}).then(function (resp) {
|
||||
if (me.debug) { console.debug('order finalized: resp.body:'); }
|
||||
if (me.debug) { console.debug(resp.body); }
|
||||
// Actually sets the challenge via ACME
|
||||
function challengeNext() {
|
||||
var auth = valids.shift();
|
||||
if (!auth) { return Promise.resolve(); }
|
||||
return ACME._postChallenge(me, options, auth).then(challengeNext);
|
||||
}
|
||||
return challengeNext().then(function () {
|
||||
//#console.debug("[getCertificate] next.then");
|
||||
console.log('DEBUG 1 order:');
|
||||
console.log(options.order);
|
||||
return options.order.identifiers.map(function (ident) {
|
||||
return ident.value;
|
||||
});
|
||||
}).then(function () {
|
||||
return ACME._getCsrWeb64(me, options, validatedDomains).then(function (csr) {
|
||||
var body = { csr: csr };
|
||||
var payload = JSON.stringify(body);
|
||||
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3
|
||||
// Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid"
|
||||
if ('valid' === resp.body.status) {
|
||||
options._expires = resp.body.expires;
|
||||
options._certificate = resp.body.certificate;
|
||||
function pollCert() {
|
||||
//#console.debug('[acme-v2.js] pollCert:');
|
||||
return ACME._jwsRequest(me, {
|
||||
options: options
|
||||
, url: options.order.finalizeUrl
|
||||
, protected: { kid: options._kid }
|
||||
, payload: Enc.binToBuf(payload)
|
||||
}).then(function (resp) {
|
||||
//#console.debug('order finalized: resp.body:');
|
||||
//#console.debug(resp.body);
|
||||
|
||||
return resp.body; // return order
|
||||
}
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3
|
||||
// Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid"
|
||||
if ('valid' === resp.body.status) {
|
||||
options._expires = resp.body.expires;
|
||||
options._certificate = resp.body.certificate;
|
||||
|
||||
if ('processing' === resp.body.status) {
|
||||
return ACME._wait().then(pollCert);
|
||||
}
|
||||
return resp.body; // return order
|
||||
}
|
||||
|
||||
if (me.debug) { console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); }
|
||||
if ('processing' === resp.body.status) {
|
||||
return ACME._wait().then(pollCert);
|
||||
}
|
||||
|
||||
//#console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2));
|
||||
|
||||
if ('pending' === resp.body.status) {
|
||||
return Promise.reject(new Error(
|
||||
"Did not finalize order: status 'pending'."
|
||||
+ " Best guess: You have not accepted at least one challenge for each domain:\n"
|
||||
+ "Requested: '" + options.domains.join(', ') + "'\n"
|
||||
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
|
||||
+ JSON.stringify(resp.body, null, 2)
|
||||
));
|
||||
}
|
||||
|
||||
if ('invalid' === resp.body.status) {
|
||||
return Promise.reject(new Error(
|
||||
"Did not finalize order: status 'invalid'."
|
||||
+ " Best guess: One or more of the domain challenges could not be verified"
|
||||
+ " (or the order was canceled).\n"
|
||||
+ "Requested: '" + options.domains.join(', ') + "'\n"
|
||||
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
|
||||
+ JSON.stringify(resp.body, null, 2)
|
||||
));
|
||||
}
|
||||
|
||||
if ('ready' === resp.body.status) {
|
||||
return Promise.reject(new Error(
|
||||
"Did not finalize order: status 'ready'."
|
||||
+ " Hmmm... this state shouldn't be possible here. That was the last state."
|
||||
+ " This one should at least be 'processing'.\n"
|
||||
+ "Requested: '" + options.domains.join(', ') + "'\n"
|
||||
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
|
||||
+ JSON.stringify(resp.body, null, 2) + "\n\n"
|
||||
+ "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js"
|
||||
));
|
||||
}
|
||||
|
||||
if ('pending' === resp.body.status) {
|
||||
return Promise.reject(new Error(
|
||||
"Did not finalize order: status 'pending'."
|
||||
+ " Best guess: You have not accepted at least one challenge for each domain:\n"
|
||||
+ "Requested: '" + options.domains.join(', ') + "'\n"
|
||||
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
|
||||
+ JSON.stringify(resp.body, null, 2)
|
||||
));
|
||||
}
|
||||
|
||||
if ('invalid' === resp.body.status) {
|
||||
return Promise.reject(new Error(
|
||||
"Did not finalize order: status 'invalid'."
|
||||
+ " Best guess: One or more of the domain challenges could not be verified"
|
||||
+ " (or the order was canceled).\n"
|
||||
+ "Requested: '" + options.domains.join(', ') + "'\n"
|
||||
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
|
||||
+ JSON.stringify(resp.body, null, 2)
|
||||
));
|
||||
}
|
||||
|
||||
if ('ready' === resp.body.status) {
|
||||
return Promise.reject(new Error(
|
||||
"Did not finalize order: status 'ready'."
|
||||
+ " Hmmm... this state shouldn't be possible here. That was the last state."
|
||||
+ " This one should at least be 'processing'.\n"
|
||||
"Didn't finalize order: Unhandled status '" + resp.body.status + "'."
|
||||
+ " This is not one of the known statuses...\n"
|
||||
+ "Requested: '" + options.domains.join(', ') + "'\n"
|
||||
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
|
||||
+ JSON.stringify(resp.body, null, 2) + "\n\n"
|
||||
+ "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js"
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(
|
||||
"Didn't finalize order: Unhandled status '" + resp.body.status + "'."
|
||||
+ " This is not one of the known statuses...\n"
|
||||
+ "Requested: '" + options.domains.join(', ') + "'\n"
|
||||
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
|
||||
+ JSON.stringify(resp.body, null, 2) + "\n\n"
|
||||
+ "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js"
|
||||
));
|
||||
return pollCert();
|
||||
}).then(function () {
|
||||
//#console.debug('acme-v2: order was finalized');
|
||||
return me.request({ method: 'GET', url: options._certificate, json: true }).then(function (resp) {
|
||||
//#console.debug('acme-v2: csr submitted and cert received:');
|
||||
// https://github.com/certbot/certbot/issues/5721
|
||||
var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||'')));
|
||||
// cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */
|
||||
// TODO CSR.info
|
||||
var certs = {
|
||||
expires: order.expires
|
||||
, identifiers: order.identifiers
|
||||
, cert: certsarr.shift()
|
||||
, chain: certsarr.join('\n')
|
||||
};
|
||||
//#console.debug(certs);
|
||||
return certs;
|
||||
});
|
||||
}
|
||||
|
||||
return pollCert();
|
||||
}).then(function () {
|
||||
if (me.debug) { console.debug('acme-v2: order was finalized'); }
|
||||
// TODO POST-as-GET
|
||||
return me.request({ method: 'GET', url: options._certificate, json: true }).then(function (resp) {
|
||||
if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); }
|
||||
// https://github.com/certbot/certbot/issues/5721
|
||||
var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||'')));
|
||||
// cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */
|
||||
var certs = {
|
||||
expires: order.expires
|
||||
, identifiers: order.identifiers
|
||||
//, authorizations: order.authorizations
|
||||
, cert: certsarr.shift()
|
||||
//, privkey: privkeyPem
|
||||
, chain: certsarr.join('\n')
|
||||
};
|
||||
if (me.debug) { console.debug(certs); }
|
||||
return certs;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -2482,7 +2436,7 @@ ACME._finalizeOrder = function (me, options) {
|
|||
};
|
||||
ACME._createOrder = function (me, options) {
|
||||
return ACME._getAccountKid(me, options).then(function () {
|
||||
// options._kid added
|
||||
// options._kid added
|
||||
var body = {
|
||||
// raw wildcard syntax MUST be used here
|
||||
identifiers: options.domains.sort(function (a, b) {
|
||||
|
@ -2499,7 +2453,7 @@ ACME._createOrder = function (me, options) {
|
|||
};
|
||||
|
||||
var payload = JSON.stringify(body);
|
||||
if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); }
|
||||
//#console.debug('\n[DEBUG] newOrder\n');
|
||||
return ACME._jwsRequest(me, {
|
||||
options: options
|
||||
, url: me._directoryUrls.newOrder
|
||||
|
@ -2513,8 +2467,8 @@ ACME._createOrder = function (me, options) {
|
|||
, identifiers: body.identifiers
|
||||
, _response: resp.body
|
||||
};
|
||||
if (me.debug) { console.debug('[ordered]', location); } // the account id url
|
||||
if (me.debug) { console.debug(resp); }
|
||||
//#console.debug('[ordered]', location); // the account id url
|
||||
//#console.debug(resp);
|
||||
|
||||
if (!order.authorizations) {
|
||||
return Promise.reject(new Error(
|
||||
|
@ -2526,7 +2480,7 @@ ACME._createOrder = function (me, options) {
|
|||
return order;
|
||||
}).then(function (order) {
|
||||
var claims = [];
|
||||
if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); }
|
||||
//#console.debug("[acme-v2] POST newOrder has authorizations");
|
||||
var challengeAuths = order.authorizations.slice(0);
|
||||
|
||||
function getNext() {
|
||||
|
@ -2565,13 +2519,12 @@ ACME._getAccountKid = function (me, options) {
|
|||
return options._kid;
|
||||
});
|
||||
};
|
||||
// _kid
|
||||
// registerAccount
|
||||
// postChallenge
|
||||
// finalizeOrder
|
||||
// getCertificate
|
||||
|
||||
//
|
||||
// Helper Methods
|
||||
//
|
||||
ACME._getCertificate = function (me, options) {
|
||||
if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); }
|
||||
//#console.debug('[acme-v2] DEBUG get cert 1');
|
||||
|
||||
if (options.csr) {
|
||||
// TODO validate csr signature
|
||||
|
@ -2585,10 +2538,13 @@ ACME._getCertificate = function (me, options) {
|
|||
return Promise.reject(new Error("options.domains must be a list of string domain names,"
|
||||
+ " with the first being the subject of the certificate (or options.subject must specified)."));
|
||||
}
|
||||
if (!options.challenges) {
|
||||
return Promise.reject(new Error("You must specify challenge handlers."));
|
||||
}
|
||||
|
||||
// Do a little dry-run / self-test
|
||||
return ACME._testChallenges(me, options).then(function () {
|
||||
if (me.debug) { console.debug('[acme-v2] certificates.create'); }
|
||||
//#console.debug('[acme-v2] certificates.create');
|
||||
return ACME._createOrder(me, options).then(function (/*order*/) {
|
||||
// options.order = order;
|
||||
return ACME._finalizeOrder(me, options);
|
||||
|
@ -2607,7 +2563,7 @@ ACME._getCsrWeb64 = function (me, options, validatedDomains) {
|
|||
return Promise.resolve(csr);
|
||||
}
|
||||
|
||||
return ACME._importKeypair(me, options.serverKeypair || options.domainKeypair).then(function (pair) {
|
||||
return ACME._importKeypair(me, options.serverKey || options.serverKeypair || options.domainKeypair).then(function (pair) {
|
||||
return me.CSR({ jwk: pair.private, domains: validatedDomains, encoding: 'der' }).then(function (der) {
|
||||
return Enc.bufToUrlBase64(der);
|
||||
});
|
||||
|
@ -2708,13 +2664,13 @@ ACME._jwsRequest = function (me, bigopts) {
|
|||
if (!bigopts.protected.kid) { bigopts.protected.kid = bigopts.options._kid; }
|
||||
}
|
||||
return me.Keypairs.signJws(
|
||||
{ jwk: bigopts.options.accountKeypair.privateKeyJwk
|
||||
{ jwk: bigopts.accountKey || bigopts.options.accountKeypair.privateKeyJwk
|
||||
, protected: bigopts.protected
|
||||
, payload: bigopts.payload
|
||||
}
|
||||
).then(function (jws) {
|
||||
if (me.debug) { console.debug('[acme-v2] ' + bigopts.url + ':'); }
|
||||
if (me.debug) { console.debug(jws); }
|
||||
//#console.debug('[acme-v2] ' + bigopts.url + ':');
|
||||
//#console.debug(jws);
|
||||
return ACME._request(me, { url: bigopts.url, json: jws });
|
||||
});
|
||||
});
|
||||
|
@ -2769,7 +2725,7 @@ ACME._defaultRequest = function (opts) {
|
|||
};
|
||||
|
||||
ACME._importKeypair = function (me, kp) {
|
||||
var jwk = kp.privateKeyJwk;
|
||||
var jwk = kp.privateKeyJwk || kp.kty && kp;
|
||||
var p;
|
||||
if (jwk) {
|
||||
// nix the browser jwk extras
|
||||
|
@ -2791,18 +2747,6 @@ ACME._importKeypair = function (me, kp) {
|
|||
});
|
||||
};
|
||||
|
||||
/*
|
||||
TODO
|
||||
Per-Order State Params
|
||||
_kty
|
||||
_alg
|
||||
_finalize
|
||||
_expires
|
||||
_certificate
|
||||
_order
|
||||
_authorizations
|
||||
*/
|
||||
|
||||
ACME._toWebsafeBase64 = function (b64) {
|
||||
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,"");
|
||||
};
|
||||
|
@ -2850,20 +2794,14 @@ ACME._http01 = function (me, auth) {
|
|||
});
|
||||
};
|
||||
ACME._removeChallenge = function (me, options, auth) {
|
||||
var challengers = options.challenges || {};
|
||||
var removeChallenge = (challengers[auth.type] && challengers[auth.type].remove) || options.removeChallenge;
|
||||
if (1 === removeChallenge.length) {
|
||||
removeChallenge(auth).then(function () {}, function () {});
|
||||
} else if (2 === removeChallenge.length) {
|
||||
removeChallenge(auth, function (err) { return err; });
|
||||
} else {
|
||||
if (!ACME._removeChallengeWarn) {
|
||||
console.warn("Please update to acme-v2 removeChallenge(options) <Promise> or removeChallenge(options, cb).");
|
||||
console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types.");
|
||||
ACME._removeChallengeWarn = true;
|
||||
}
|
||||
removeChallenge(auth.request.identifier, auth.token, function () {});
|
||||
}
|
||||
return Promise.resolve().then(function () {
|
||||
if (!options.challenges) { return; }
|
||||
var ch = options.challenges[auth.type];
|
||||
ch.remove(auth).catch(function (e) {
|
||||
console.warn("challenge.remove error:");
|
||||
console.warn(e);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Enc.bufToUrlBase64 = function (u8) {
|
||||
|
|
Loading…
Reference in New Issue