1+ import * as crypto from 'node:crypto' ;
12import * as ACME from 'acme-client' ;
23import { PersistentCertCache } from './cert-cache.js' ;
34
@@ -43,33 +44,48 @@ export class AcmeCA {
4344 } )
4445 ) ;
4546
46- getChallengeResponse ( token : string ) {
47- return this . pendingAcmeChallenges [ token ] ;
47+ async getChallengeResponse ( token : string ) {
48+ return ( await this . acmeClient ) . getChallengeKeyAuthorization ( {
49+ token,
50+ type : 'http-01' ,
51+ url : '' ,
52+ status : 'pending'
53+ } ) ;
4854 }
4955
5056 tryGetCertificateSync ( domain : string ) {
5157 const cachedCert = this . certCache . getCert ( domain ) ;
5258
59+ if ( cachedCert ) {
60+ console . log ( `Found cached cert for ${ domain } (hash:${ crypto . hash ( 'sha256' , cachedCert . cert ) } , expiry: ${ new Date ( cachedCert . expiry ) . toISOString ( ) } )` ) ;
61+ }
62+
5363 if ( ! cachedCert || cachedCert . expiry - Date . now ( ) < ONE_WEEK ) {
54- this . getCertificate ( domain ) ; // Trigger a cert refresh
64+ const attemptId = Math . random ( ) . toString ( 16 ) . slice ( 2 ) ;
65+ console . log ( `Starting async cert update (${ attemptId } ) for domain ${ domain } ` ) ;
66+ this . getCertificate ( domain , { attemptId } ) ; // Trigger a cert refresh
5567 }
5668
5769 return cachedCert ;
5870 }
5971
6072 private async getCertificate (
6173 domain : string ,
62- options : { forceRegenerate ?: boolean } = { }
74+ options : { forceRegenerate ?: boolean , attemptId : string }
6375 ) : Promise < AcmeGeneratedCertificate > {
76+ if ( ! options . attemptId ) {
77+ options . attemptId = Math . random ( ) . toString ( 16 ) . slice ( 2 ) ;
78+ }
79+
6480 const cachedCert = this . certCache . getCert ( domain ) ;
6581 if ( cachedCert && ! options . forceRegenerate ) {
6682 // If we have this cert in the cache, we generally want to use that.
6783
6884 if ( cachedCert . expiry <= Date . now ( ) - ONE_MINUTE ) {
6985 // Expired - clear this data and get a new certificate somehow
70- console . log ( `Renewing totally expired certificate for ${ domain } ` ) ;
86+ console . log ( `Renewing totally expired certificate for ${ domain } ( ${ options . attemptId } ) ` ) ;
7187 this . certCache . clearCache ( domain ) ;
72- return this . getCertificate ( domain ) ;
88+ return this . getCertificate ( domain , { attemptId : options . attemptId } ) ;
7389 }
7490
7591 if (
@@ -78,50 +94,52 @@ export class AcmeCA {
7894 ) {
7995 // Not yet expired, but expiring soon - we want to refresh this certificate, but
8096 // we're OK to do it async and keep using the current one for now.
81- console . log ( `Renewing near-expiry certificate for ${ domain } ` ) ;
97+ console . log ( `Renewing near-expiry certificate for ${ domain } ( ${ options . attemptId } ) ` ) ;
8298
8399 this . pendingCertRenewals [ domain ] = this . getCertificate ( domain , {
84- forceRegenerate : true
100+ forceRegenerate : true ,
101+ attemptId : options . attemptId
85102 } ) ;
86103 }
87104
88105 return cachedCert ;
89106 }
90107
91- if ( ! cachedCert ) console . log ( `No cached cert for ${ domain } ` ) ;
92- else if ( options . forceRegenerate ) console . log ( `Force regenerating cert for ${ domain } ` ) ;
108+ if ( ! cachedCert ) console . log ( `No cached cert for ${ domain } ( ${ options . attemptId } ) ` ) ;
109+ else if ( options . forceRegenerate ) console . log ( `Force regenerating cert for ${ domain } ( ${ options . attemptId } ) ` ) ;
93110
94111 if ( this . pendingCertRenewals [ domain ] && ! options . forceRegenerate ) {
95112 // Coalesce updates for pending certs into one
96113 return this . pendingCertRenewals [ domain ] ! ;
97114 }
98115
99- const refreshPromise : Promise < AcmeGeneratedCertificate > = this . requestNewCertificate ( domain )
100- . then ( ( certData ) => {
101- if (
102- this . pendingCertRenewals [ domain ] &&
103- this . pendingCertRenewals [ domain ] !== refreshPromise
104- ) {
105- // Don't think this should happen, but if we're somehow ever not the current cert
106- // update, delegate to the 'real' cert update instead.
107- return this . pendingCertRenewals [ domain ] ! ;
108- }
116+ const refreshPromise : Promise < AcmeGeneratedCertificate > = this . requestNewCertificate ( domain , {
117+ attemptId : options . attemptId
118+ } ) . then ( ( certData ) => {
119+ if (
120+ this . pendingCertRenewals [ domain ] &&
121+ this . pendingCertRenewals [ domain ] !== refreshPromise
122+ ) {
123+ // Don't think this should happen, but if we're somehow ever not the current cert
124+ // update, delegate to the 'real' cert update instead.
125+ return this . pendingCertRenewals [ domain ] ! ;
126+ }
109127
110- delete this . pendingCertRenewals [ domain ] ;
111- this . certCache . cacheCert ( { ...certData , domain } ) ;
112- return certData ;
113- } )
114- . catch ( ( e ) => {
115- console . log ( ' Cert request failed' , e ) ;
116- return this . getCertificate ( domain , { forceRegenerate : true } ) ;
117- } )
128+ delete this . pendingCertRenewals [ domain ] ;
129+ this . certCache . cacheCert ( { ...certData , domain } ) ;
130+ console . log ( `Cert refresh completed for domain ${ domain } ( ${ options . attemptId } ), hash: ${ crypto . hash ( 'sha256' , certData . cert ) } ` ) ;
131+ return certData ;
132+ } ) . catch ( ( e ) => {
133+ console . log ( ` Cert refresh failed ( ${ options . attemptId } )` , e ) ;
134+ return this . getCertificate ( domain , { forceRegenerate : true , attemptId : options . attemptId } ) ;
135+ } ) ;
118136
119137 this . pendingCertRenewals [ domain ] = refreshPromise ;
120138 return refreshPromise ;
121139 }
122140
123- private async requestNewCertificate ( domain : string ) : Promise < AcmeGeneratedCertificate > {
124- console . log ( `Requesting new certificate for ${ domain } ` ) ;
141+ private async requestNewCertificate ( domain : string , options : { attemptId : string } ) : Promise < AcmeGeneratedCertificate > {
142+ console . log ( `Requesting new certificate for ${ domain } ( ${ options . attemptId } ) ` ) ;
125143
126144 const [ key , csr ] = await ACME . crypto . createCsr ( {
127145 commonName : domain
@@ -134,33 +152,31 @@ export class AcmeCA {
134152 skipChallengeVerification : true ,
135153 challengeCreateFn : async ( _authz , challenge , keyAuth ) => {
136154 if ( challenge . type !== 'http-01' ) {
137- throw new Error ( `Unexpected ${ challenge . type } challenge` ) ;
155+ throw new Error ( `Unexpected ${ challenge . type } challenge ( ${ options . attemptId } ) ` ) ;
138156 }
139157
140- console . log ( `Preparing for ${ challenge . type } ACME challenge` ) ;
158+ console . log ( `Preparing for ${ challenge . type } ACME challenge ${ challenge . token } ( ${ options . attemptId } ) ` ) ;
141159
142160 this . pendingAcmeChallenges [ challenge . token ] = keyAuth ;
143161 } ,
144162 challengeRemoveFn : async ( _authz , challenge ) => {
145163 if ( challenge . type !== 'http-01' ) {
146- throw new Error ( `Unexpected ${ challenge . type } challenge` ) ;
164+ throw new Error ( `Unexpected ${ challenge . type } challenge ( ${ options . attemptId } ) ` ) ;
147165 }
148166
149167 console . log ( `Removing ACME ${
150168 challenge . status
151169 } ${
152170 challenge . type
153- } challenge (validated: ${
154- challenge . validated
155- } , error: ${
156- JSON . stringify ( challenge . error )
157- } )`)
171+ } challenge ${
172+ JSON . stringify ( challenge )
173+ } ) (${ options . attemptId } )`) ;
158174
159175 delete this . pendingAcmeChallenges [ challenge . token ] ;
160176 }
161177 } ) ;
162178
163- console . log ( `Successfully ACMEd new certificate for ${ domain } ` ) ;
179+ console . log ( `Successfully ACMEd new certificate for ${ domain } ( ${ options . attemptId } ) ` ) ;
164180
165181 return {
166182 key : key . toString ( ) ,
0 commit comments