/** * ACME auto helper */ const { readCsrDomains } = require('./crypto'); const { log } = require('./logger'); const defaultOpts = { csr: null, email: null, preferredChain: null, termsOfServiceAgreed: false, skipChallengeVerification: false, challengePriority: ['http-01', 'dns-01'], challengeCreateFn: async () => { throw new Error('Missing challengeCreateFn()'); }, challengeRemoveFn: async () => { throw new Error('Missing challengeRemoveFn()'); } }; /** * ACME client auto mode * * @param {AcmeClient} client ACME client * @param {object} userOpts Options * @returns {Promise} Certificate */ module.exports = async function(client, userOpts) { const opts = Object.assign({}, defaultOpts, userOpts); const accountPayload = { termsOfServiceAgreed: opts.termsOfServiceAgreed }; if (!Buffer.isBuffer(opts.csr)) { opts.csr = Buffer.from(opts.csr); } if (opts.email) { accountPayload.contact = [`mailto:${opts.email}`]; } /** * Register account */ log('[auto] Checking account'); try { client.getAccountUrl(); log('[auto] Account URL already exists, skipping account registration'); } catch (e) { log('[auto] Registering account'); await client.createAccount(accountPayload); } /** * Parse domains from CSR */ log('[auto] Parsing domains from Certificate Signing Request'); const csrDomains = readCsrDomains(opts.csr); const domains = [csrDomains.commonName].concat(csrDomains.altNames); const uniqueDomains = Array.from(new Set(domains)); log(`[auto] Resolved ${uniqueDomains.length} unique domains from parsing the Certificate Signing Request`); /** * Place order */ log('[auto] Placing new certificate order with ACME provider'); const orderPayload = { identifiers: uniqueDomains.map((d) => ({ type: 'dns', value: d })) }; const order = await client.createOrder(orderPayload); const authorizations = await client.getAuthorizations(order); log(`[auto] Placed certificate order successfully, received ${authorizations.length} identity authorizations`); /** * Resolve and satisfy challenges */ log('[auto] Resolving and satisfying authorization challenges'); const challengeFunc = async (authz) => { const d = authz.identifier.value; let challengeCompleted = false; /* Skip authz that already has valid status */ if (authz.status === 'valid') { log(`[auto] [${d}] Authorization already has valid status, no need to complete challenges`); return; } try { /* Select challenge based on priority */ const challenge = authz.challenges.sort((a, b) => { const aidx = opts.challengePriority.indexOf(a.type); const bidx = opts.challengePriority.indexOf(b.type); if (aidx === -1) return 1; if (bidx === -1) return -1; return aidx - bidx; }).slice(0, 1)[0]; if (!challenge) { throw new Error(`Unable to select challenge for ${d}, no challenge found`); } log(`[auto] [${d}] Found ${authz.challenges.length} challenges, selected type: ${challenge.type}`); /* Trigger challengeCreateFn() */ log(`[auto] [${d}] Trigger challengeCreateFn()`); const keyAuthorization = await client.getChallengeKeyAuthorization(challenge); let recordItem = null; try { recordItem = await opts.challengeCreateFn(authz, challenge, keyAuthorization); /* Challenge verification */ if (opts.skipChallengeVerification === true) { log(`[auto] [${d}] Skipping challenge verification since skipChallengeVerification=true`); } else { log(`[auto] [${d}] Running challenge verification`); await client.verifyChallenge(authz, challenge); } /* Complete challenge and wait for valid status */ log(`[auto] [${d}] Completing challenge with ACME provider and waiting for valid status`); await client.completeChallenge(challenge); challengeCompleted = true; await client.waitForValidStatus(challenge); } catch (e) { log(`[auto] [${d}] challengeCreateFn threw error: ${e.message}`); throw e; } finally { /* Trigger challengeRemoveFn(), suppress errors */ log(`[auto] [${d}] Trigger challengeRemoveFn()`); try { await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem); } catch (e) { log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`); } } } catch (e) { /* Deactivate pending authz when unable to complete challenge */ if (!challengeCompleted) { log(`[auto] [${d}] Unable to complete challenge: ${e.message}`); try { log(`[auto] [${d}] Deactivating failed authorization`); await client.deactivateAuthorization(authz); } catch (f) { /* Suppress deactivateAuthorization() errors */ log(`[auto] [${d}] Authorization deactivation threw error: ${f.message}`); } } throw e; } }; const challengePromises = authorizations.map((authz) => async () => { await challengeFunc(authz); }); log('开始challenge'); let promise = Promise.resolve(); function runPromisesSerially(tasks) { tasks.forEach((task) => { promise = promise.then(task); }); return promise; } await runPromisesSerially(challengePromises); log('challenge结束'); // log('[auto] Waiting for challenge valid status'); // await Promise.all(challengePromises); /** * Finalize order and download certificate */ log('[auto] Finalizing order and downloading certificate'); const finalized = await client.finalizeOrder(order, opts.csr); return client.getCertificate(finalized, opts.preferredChain); };