2023-01-29 14:44:10 +08:00
/**
* ACME auto helper
*/
const { readCsrDomains } = require ( './crypto' ) ;
const { log } = require ( './logger' ) ;
2024-06-26 18:36:11 +08:00
const { wait } = require ( './wait' ) ;
2023-01-29 14:44:10 +08:00
const defaultOpts = {
csr : null ,
email : null ,
preferredChain : null ,
termsOfServiceAgreed : false ,
skipChallengeVerification : false ,
challengePriority : [ 'http-01' , 'dns-01' ] ,
2024-08-30 18:50:53 +08:00
challengeCreateFn : async ( ) => {
throw new Error ( 'Missing challengeCreateFn()' ) ;
} ,
challengeRemoveFn : async ( ) => {
throw new Error ( 'Missing challengeRemoveFn()' ) ;
} ,
2023-01-29 14:44:10 +08:00
} ;
/**
* ACME client auto mode
*
* @param {AcmeClient} client ACME client
* @param {object} userOpts Options
* @returns {Promise<buffer>} Certificate
*/
2024-05-22 19:24:07 +00:00
module . exports = async ( client , userOpts ) => {
const opts = { ... defaultOpts , ... userOpts } ;
2023-01-29 14:44:10 +08:00
const accountPayload = { termsOfServiceAgreed : opts . termsOfServiceAgreed } ;
if ( ! Buffer . isBuffer ( opts . csr ) ) {
opts . csr = Buffer . from ( opts . csr ) ;
}
if ( opts . email ) {
accountPayload . contact = [ ` mailto: ${ opts . email } ` ] ;
}
2024-07-04 01:14:09 +08:00
if ( opts . externalAccountBinding ) {
accountPayload . externalAccountBinding = opts . externalAccountBinding ;
}
2023-01-29 14:44:10 +08:00
/**
* 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' ) ;
2024-05-21 19:24:05 +00:00
const { commonName , altNames } = readCsrDomains ( opts . csr ) ;
const uniqueDomains = Array . from ( new Set ( [ commonName ] . concat ( altNames ) . filter ( ( d ) => d ) ) ) ;
2023-01-29 14:44:10 +08:00
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' ) ;
2024-03-06 18:36:10 +08:00
const clearTasks = [ ] ;
2023-01-29 14:44:10 +08:00
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 ) ;
2024-10-07 03:21:16 +08:00
2023-01-29 14:44:10 +08:00
try {
2024-10-07 03:21:16 +08:00
const { recordReq , recordRes , dnsProvider } = await opts . challengeCreateFn ( authz , challenge , keyAuthorization ) ;
2024-06-26 18:36:11 +08:00
log ( ` [auto] [ ${ d } ] challengeCreateFn success ` ) ;
log ( ` [auto] [ ${ d } ] add challengeRemoveFn() ` ) ;
clearTasks . push ( async ( ) => {
/* Trigger challengeRemoveFn(), suppress errors */
log ( ` [auto] [ ${ d } ] Trigger challengeRemoveFn() ` ) ;
try {
2024-10-07 03:21:16 +08:00
await opts . challengeRemoveFn ( authz , challenge , keyAuthorization , recordReq , recordRes , dnsProvider ) ;
2024-06-26 18:36:11 +08:00
}
catch ( e ) {
log ( ` [auto] [ ${ d } ] challengeRemoveFn threw error: ${ e . message } ` ) ;
}
} ) ;
2024-06-15 02:17:34 +08:00
// throw new Error('测试异常');
2023-01-29 14:44:10 +08:00
/* Challenge verification */
if ( opts . skipChallengeVerification === true ) {
2024-07-03 23:27:35 +08:00
log ( ` [auto] [ ${ d } ] Skipping challenge verification since skipChallengeVerification=true, wait 60s ` ) ;
await wait ( 60 * 1000 ) ;
2023-01-29 14:44:10 +08:00
}
else {
log ( ` [auto] [ ${ d } ] Running challenge verification ` ) ;
2024-08-23 13:15:06 +08:00
try {
await client . verifyChallenge ( authz , challenge ) ;
}
catch ( e ) {
log ( ` [auto] [ ${ d } ] challenge verification threw error: ${ e . message } ` ) ;
}
2023-01-29 14:44:10 +08:00
}
/* 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 ) ;
}
2023-05-24 17:00:04 +08:00
catch ( e ) {
log ( ` [auto] [ ${ d } ] challengeCreateFn threw error: ${ e . message } ` ) ;
throw e ;
}
2023-01-29 14:44:10 +08:00
}
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 ;
}
} ;
2024-08-23 13:15:06 +08:00
const domainSets = [ ] ;
2023-01-29 14:44:10 +08:00
2024-08-23 13:15:06 +08:00
authorizations . forEach ( ( authz ) => {
const d = authz . identifier . value ;
2024-10-22 16:21:35 +08:00
log ( ` authorization:domain = ${ d } , value = ${ JSON . stringify ( authz ) } ` ) ;
if ( authz . status === 'valid' ) {
log ( ` [auto] [ ${ d } ] Authorization already has valid status, no need to complete challenges ` ) ;
return ;
}
2024-08-23 13:15:06 +08:00
let setd = false ;
// eslint-disable-next-line no-restricted-syntax
for ( const group of domainSets ) {
if ( ! group [ d ] ) {
group [ d ] = authz ;
setd = true ;
2024-10-22 16:21:35 +08:00
break ;
2024-08-23 13:15:06 +08:00
}
}
if ( ! setd ) {
const group = { } ;
group [ d ] = authz ;
domainSets . push ( group ) ;
}
2023-01-29 14:44:10 +08:00
} ) ;
2024-10-22 16:21:35 +08:00
// log(`domainSets:${JSON.stringify(domainSets)}`);
2024-08-23 13:15:06 +08:00
const allChallengePromises = [ ] ;
// eslint-disable-next-line no-restricted-syntax
for ( const domainSet of domainSets ) {
const challengePromises = [ ] ;
// eslint-disable-next-line guard-for-in,no-restricted-syntax
for ( const domain in domainSet ) {
const authz = domainSet [ domain ] ;
challengePromises . push ( async ( ) => {
log ( ` [auto] [ ${ domain } ] Starting challenge ` ) ;
await challengeFunc ( authz ) ;
} ) ;
}
allChallengePromises . push ( challengePromises ) ;
}
log ( ` [auto] challengeGroups: ${ allChallengePromises . length } ` ) ;
2024-08-30 18:50:53 +08:00
2024-06-15 02:17:34 +08:00
function runAllPromise ( tasks ) {
let promise = Promise . resolve ( ) ;
tasks . forEach ( ( task ) => {
promise = promise . then ( task ) ;
} ) ;
return promise ;
2023-01-29 14:44:10 +08:00
}
2024-06-26 18:36:11 +08:00
async function runPromisePa ( tasks ) {
const results = [ ] ;
// eslint-disable-next-line no-await-in-loop,no-restricted-syntax
for ( const task of tasks ) {
results . push ( task ( ) ) ;
// eslint-disable-next-line no-await-in-loop
2024-06-26 19:07:37 +08:00
await wait ( 10000 ) ;
2024-06-26 18:36:11 +08:00
}
return Promise . all ( results ) ;
}
2024-06-15 02:17:34 +08:00
2024-10-22 11:23:59 +08:00
try {
log ( ` 开始challenge,共 ${ allChallengePromises . length } 组 ` ) ;
let i = 0 ;
// eslint-disable-next-line no-restricted-syntax
for ( const challengePromises of allChallengePromises ) {
i += 1 ;
log ( ` 开始第 ${ i } 组 ` ) ;
if ( opts . signal && opts . signal . aborted ) {
throw new Error ( '用户取消' ) ;
}
2024-06-15 02:17:34 +08:00
2024-08-30 18:50:53 +08:00
try {
// eslint-disable-next-line no-await-in-loop
2024-10-22 11:23:59 +08:00
await runPromisePa ( challengePromises ) ;
2024-08-30 18:50:53 +08:00
}
catch ( e ) {
2024-10-22 11:23:59 +08:00
log ( ` 证书申请失败 ${ e . message } ` ) ;
throw e ;
2024-08-30 18:50:53 +08:00
}
2024-10-22 16:21:35 +08:00
finally {
if ( client . opts . sslProvider !== 'google' ) {
// letsencrypt 如果同时检出两个TXT记录,会以第一个为准,就会校验失败,所以需要提前删除
2024-10-23 10:34:55 +08:00
// zerossl 此方式测试无问题
2024-10-22 16:21:35 +08:00
log ( ` 清理challenge痕迹,length: ${ clearTasks . length } ` ) ;
try {
// eslint-disable-next-line no-await-in-loop
await runAllPromise ( clearTasks ) ;
}
catch ( e ) {
log ( '清理challenge失败' ) ;
log ( e ) ;
}
}
}
2024-07-02 00:18:28 +08:00
}
2024-03-08 17:42:47 +08:00
}
2024-10-22 11:23:59 +08:00
finally {
2024-10-22 16:21:35 +08:00
if ( client . opts . sslProvider === 'google' ) {
2024-10-23 10:34:55 +08:00
// google 相同的域名txt记录是一样的,不能提前删除,否则校验失败,报错如下
// Error: The TXT record retrieved from _acme-challenge.bbc.handsfree.work.
// at the time the challenge was validated did not contain JshHVu7dt_DT6uYILWhokHefFVad2Q6Mw1L-fNZFcq8
// (the base64url-encoded SHA-256 digest of RlJZNBR0LWnxNK_xd2zqtYVvCiNJOKJ3J1NmCjU_9BjaUJgL3k-qSpIhQ-uF4FBS.NRyqT8fRiq6THzzrvkgzgR5Xai2LsA2SyGLAq_wT3qc).
// See https://tools.ietf.org/html/rfc8555#section-8.4 for more information.
2024-10-22 16:21:35 +08:00
log ( ` 清理challenge痕迹,length: ${ clearTasks . length } ` ) ;
try {
2024-10-22 11:23:59 +08:00
// eslint-disable-next-line no-await-in-loop
2024-10-22 16:21:35 +08:00
await runAllPromise ( clearTasks ) ;
}
catch ( e ) {
log ( '清理challenge失败' ) ;
log ( e ) ;
}
2024-10-22 11:23:59 +08:00
}
}
2024-08-30 18:50:53 +08:00
log ( 'challenge结束' ) ;
// log('[auto] Waiting for challenge valid status');
// await Promise.all(challengePromises);
/**
* Finalize order and download certificate
*/
2024-03-08 17:42:47 +08:00
2024-08-30 18:50:53 +08:00
log ( '[auto] Finalizing order and downloading certificate' ) ;
const finalized = await client . finalizeOrder ( order , opts . csr ) ;
const res = await client . getCertificate ( finalized , opts . preferredChain ) ;
return res ;
2024-03-08 17:42:47 +08:00
// try {
// await Promise.allSettled(challengePromises);
// }
// finally {
// log('清理challenge');
// await Promise.allSettled(clearTasks);
// }
2023-01-29 14:44:10 +08:00
} ;