@certd/acme-client 1.39.10 → 1.39.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Simple and unopinionated ACME client",
4
4
  "private": false,
5
5
  "author": "nmorsman",
6
- "version": "1.39.10",
6
+ "version": "1.39.12",
7
7
  "type": "module",
8
8
  "module": "scr/index.js",
9
9
  "main": "src/index.js",
@@ -18,7 +18,7 @@
18
18
  "types"
19
19
  ],
20
20
  "dependencies": {
21
- "@certd/basic": "^1.39.10",
21
+ "@certd/basic": "^1.39.12",
22
22
  "@peculiar/x509": "^1.11.0",
23
23
  "asn1js": "^3.0.5",
24
24
  "axios": "^1.9.0",
@@ -70,5 +70,5 @@
70
70
  "bugs": {
71
71
  "url": "https://github.com/publishlab/node-acme-client/issues"
72
72
  },
73
- "gitHead": "112a565bf74c50e16c27a2c0a716588f84f39789"
73
+ "gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
74
74
  }
package/src/auto.js CHANGED
@@ -21,7 +21,8 @@ const defaultOpts = {
21
21
  },
22
22
  challengeRemoveFn: async () => {
23
23
  throw new Error("Missing challengeRemoveFn()");
24
- }
24
+ },
25
+ waitDnsDiffuseTime: 30,
25
26
  };
26
27
 
27
28
  /**
package/src/client.js CHANGED
@@ -494,7 +494,7 @@ class AcmeClient {
494
494
  throw new Error('Unable to verify ACME challenge, URL not found');
495
495
  }
496
496
 
497
- const {challenges} = createChallengeFn({logger:this.logger});
497
+ const {challenges} = createChallengeFn({logger:this.logger,walkFromAuthoritative: this.opts.walkFromAuthoritative});
498
498
 
499
499
  const verify = challenges
500
500
  if (typeof verify[challenge.type] === 'undefined') {
@@ -577,7 +577,7 @@ class AcmeClient {
577
577
 
578
578
  const verifyFn = async (abort) => {
579
579
  if (this.opts.signal && this.opts.signal.aborted) {
580
- abort();
580
+ abort(true);
581
581
  throw new CancelError('用户取消');
582
582
  }
583
583
 
File without changes
package/src/util.js CHANGED
@@ -50,15 +50,18 @@ class Backoff {
50
50
 
51
51
  async function retryPromise(fn, attempts, backoff, logger = log) {
52
52
  let aborted = false;
53
+ let abortedFromUser = false;
53
54
 
54
55
  try {
55
- const setAbort = () => { aborted = true; }
56
+ const setAbort = (fromUser = false) => { aborted = true; abortedFromUser = fromUser; }
56
57
  const data = await fn(setAbort);
57
58
  return data;
58
59
  }
59
60
  catch (e) {
60
61
  if (aborted){
61
- logger(`用户取消重试`);
62
+ if (abortedFromUser){
63
+ logger(`用户取消重试`);
64
+ }
62
65
  throw e;
63
66
  }
64
67
  if ( ((backoff.attempts + 1) >= attempts)) {
@@ -249,7 +252,7 @@ async function resolveDomainBySoaRecord(recordName, logger = log) {
249
252
 
250
253
  async function getAuthoritativeDnsResolver(recordName, logger = log) {
251
254
  logger(`获取域名${recordName}的权威NS服务器: `);
252
- const resolver = new dns.Resolver();
255
+ const resolver = new dns.Resolver({timeout: 2000,tries: 2});
253
256
 
254
257
  try {
255
258
  /* Resolve root domain by SOA */
@@ -349,3 +352,5 @@ export {
349
352
  resolveDomainBySoaRecord
350
353
  };
351
354
 
355
+
356
+
package/src/verify.js CHANGED
@@ -4,19 +4,23 @@
4
4
 
5
5
  import dnsSdk from "dns"
6
6
  import https from 'https'
7
- import {log as defaultLog} from './logger.js'
7
+ import { log as defaultLog } from './logger.js'
8
8
  import axios from './axios.js'
9
9
  import * as util from './util.js'
10
- import {isAlpnCertificateAuthorizationValid} from './crypto/index.js'
11
- import {utils} from '@certd/basic'
10
+ import { isAlpnCertificateAuthorizationValid } from './crypto/index.js'
11
+ import { utils } from '@certd/basic'
12
12
 
13
13
  const dns = dnsSdk.promises
14
14
 
15
+ let walkFromAuthoritative = true
16
+ export function setWalkFromAuthoritative(value = true) {
17
+ walkFromAuthoritative = value
18
+ }
19
+
20
+ export function createChallengeFn(opts = {}) {
21
+ const logger = opts?.logger || { info: defaultLog, error: defaultLog, warn: defaultLog, debug: defaultLog }
15
22
 
16
- export function createChallengeFn(opts = {}){
17
- const logger = opts?.logger || {info:defaultLog,error:defaultLog,warn:defaultLog,debug:defaultLog}
18
-
19
- const log = function(...args){
23
+ const log = function (...args) {
20
24
  logger.info(...args)
21
25
  }
22
26
  /**
@@ -31,202 +35,212 @@ export function createChallengeFn(opts = {}){
31
35
  * @returns {Promise<boolean>}
32
36
  */
33
37
 
34
- async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix = `/.well-known/acme-challenge/${challenge.token}`) {
38
+ async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix = `/.well-known/acme-challenge/${challenge.token}`) {
35
39
 
36
- async function doQuery(challengeUrl){
37
- log(`正在测试请求 ${challengeUrl} `)
38
- // const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
39
- // const challengeUrl = `https://${authz.identifier.value}:${httpsPort}${suffix}`;
40
+ async function doQuery(challengeUrl) {
41
+ log(`正在测试请求 ${challengeUrl} `)
42
+ // const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
43
+ // const challengeUrl = `https://${authz.identifier.value}:${httpsPort}${suffix}`;
40
44
 
41
- /* May redirect to HTTPS with invalid/self-signed cert - https://letsencrypt.org/docs/challenge-types/#http-01-challenge */
42
- const httpsAgent = new https.Agent({ rejectUnauthorized: false });
45
+ /* May redirect to HTTPS with invalid/self-signed cert - https://letsencrypt.org/docs/challenge-types/#http-01-challenge */
46
+ const httpsAgent = new https.Agent({ rejectUnauthorized: false });
43
47
 
44
- log(`Sending HTTP query to ${authz.identifier.value}, suffix: ${suffix}, port: ${httpPort}`);
45
- let data = ""
46
- try{
47
- const resp = await axios.get(challengeUrl, { httpsAgent });
48
- data = (resp.data || '').replace(/\s+$/, '');
49
- }catch (e) {
50
- log(`[error] HTTP request error from ${authz.identifier.value}`,e.message);
51
- return false
52
- }
53
-
54
- if (!data || (data !== keyAuthorization)) {
55
- log(`[error] Authorization not found in HTTP response from ${authz.identifier.value}`);
56
- return false
57
- }
58
- return true
48
+ log(`Sending HTTP query to ${authz.identifier.value}, suffix: ${suffix}, port: ${httpPort}`);
49
+ let data = ""
50
+ try {
51
+ const resp = await axios.get(challengeUrl, { httpsAgent });
52
+ data = (resp.data || '').replace(/\s+$/, '');
53
+ } catch (e) {
54
+ log(`[error] HTTP request error from ${authz.identifier.value}`, e.message);
55
+ return false
56
+ }
59
57
 
60
- }
58
+ if (!data || (data !== keyAuthorization)) {
59
+ log(`[error] Authorization not found in HTTP response from ${authz.identifier.value}`);
60
+ return false
61
+ }
62
+ return true
61
63
 
62
- const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
63
- let host = authz.identifier.value;
64
- if(utils.domain.isIpv6(host)){
65
- host = `[${host}]`;
66
- }
67
- const challengeUrl = `http://${host}:${httpPort}${suffix}`;
68
-
69
- if (!await doQuery(challengeUrl)) {
70
- const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
71
- const httpsChallengeUrl = `https://${host}:${httpsPort}${suffix}`;
72
- const res = await doQuery(httpsChallengeUrl)
73
- if (!res) {
74
- throw new Error(`[error] 验证失败,请检查以上测试url是否可以正常访问`);
75
64
  }
76
- }
77
-
78
-
79
- log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
80
- return true;
81
- }
82
65
 
83
- /**
84
- * Walk DNS until TXT records are found
85
- */
86
-
87
- async function walkDnsChallengeRecord(recordName, resolver = dns,deep = 0) {
66
+ const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
67
+ let host = authz.identifier.value;
68
+ if (utils.domain.isIpv6(host)) {
69
+ host = `[${host}]`;
70
+ }
71
+ const challengeUrl = `http://${host}:${httpPort}${suffix}`;
72
+
73
+ if (!await doQuery(challengeUrl)) {
74
+ const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
75
+ const httpsChallengeUrl = `https://${host}:${httpsPort}${suffix}`;
76
+ const res = await doQuery(httpsChallengeUrl)
77
+ if (!res) {
78
+ throw new Error(`[error] 验证失败,请检查以上测试url是否可以正常访问`);
79
+ }
80
+ }
88
81
 
89
- let records = [];
90
82
 
91
- /* Resolve TXT records */
92
- try {
93
- log(`检查域名 ${recordName} 的TXT记录`);
94
- const txtRecords = await resolver.resolveTxt(recordName);
83
+ log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
84
+ return true;
85
+ }
95
86
 
96
- if (txtRecords && txtRecords.length) {
97
- log(`找到 ${txtRecords.length} TXT记录( ${recordName})`);
98
- log(`TXT records: ${JSON.stringify(txtRecords)}`);
99
- records = records.concat(...txtRecords);
87
+ /**
88
+ * Walk DNS until TXT records are found
89
+ */
90
+
91
+ async function walkDnsChallengeRecord(recordName, resolver = dns, deep = 0) {
92
+
93
+ let records = [];
94
+
95
+ const isAuthoritative = resolver === dns
96
+ /* Resolve TXT records */
97
+ try {
98
+ log(`检查域名 ${recordName} 的TXT记录(from ${isAuthoritative ? '本地DNS' : '权威DNS服务器'})`);
99
+ const txtRecords = await resolver.resolveTxt(recordName);
100
+ if (txtRecords && txtRecords.length) {
101
+ log(`找到 ${txtRecords.length} 条 TXT记录( ${recordName})`);
102
+ log(`TXT records: ${JSON.stringify(txtRecords)}`);
103
+ records = records.concat(...txtRecords);
104
+ }
105
+ } catch (e) {
106
+ log(`解析 TXT 记录出错, ${recordName} :${e.message}`);
100
107
  }
101
- } catch (e) {
102
- log(`解析 TXT 记录出错, ${recordName} :${e.message}`);
103
- }
104
108
 
105
- /* Resolve CNAME record first */
106
- try {
107
- log(`检查是否存在CNAME映射: ${recordName}`);
108
- const cnameRecords = await resolver.resolveCname(recordName);
109
-
110
- if (cnameRecords.length) {
111
- const cnameRecord = cnameRecords[0];
112
- log(`已找到${recordName}的CNAME记录,将检查: ${cnameRecord}`);
113
- let res= await walkTxtRecord(cnameRecord,deep+1);
114
- if (res && res.length) {
115
- log(`从CNAME中找到TXT记录: ${JSON.stringify(res)}`);
116
- records = records.concat(...res);
109
+ /* Resolve CNAME record first */
110
+ try {
111
+ log(`检查是否存在CNAME映射: ${recordName}`);
112
+ const cnameRecords = await resolver.resolveCname(recordName);
113
+
114
+ if (cnameRecords.length) {
115
+ const cnameRecord = cnameRecords[0];
116
+ log(`已找到${recordName}的CNAME记录,将检查: ${cnameRecord}`);
117
+ let res = await walkTxtRecord(cnameRecord, deep + 1);
118
+ if (res && res.length) {
119
+ log(`从CNAME中找到TXT记录: ${JSON.stringify(res)}`);
120
+ records = records.concat(...res);
121
+ }
122
+ } else {
123
+ log(`没有CNAME映射(${recordName})`);
117
124
  }
118
- }else{
119
- log(`没有CNAME映射(${recordName})`);
125
+ } catch (e) {
126
+ log(`检查CNAME出错(${recordName}) :${e.message}`);
120
127
  }
121
- } catch (e) {
122
- log(`检查CNAME出错(${recordName}) :${e.message}`);
128
+ return records
123
129
  }
124
- return records
125
- }
126
130
 
127
- async function walkTxtRecord(recordName,deep = 0) {
128
- if(deep >5){
129
- log(`walkTxtRecord too deep (#${deep}) , skip walk`)
130
- return []
131
- }
131
+ async function walkTxtRecord(recordName, deep = 0) {
132
+ if (deep > 5) {
133
+ log(`walkTxtRecord too deep (#${deep}) , skip walk`)
134
+ return []
135
+ }
132
136
 
133
- const txtRecords = []
134
- try {
135
- /* Default DNS resolver first */
136
- log('从本地DNS服务器获取TXT解析记录');
137
- const res = await walkDnsChallengeRecord(recordName,dns,deep);
138
- if (res && res.length > 0) {
139
- for (const item of res) {
140
- txtRecords.push(item)
137
+ const txtRecords = []
138
+ try {
139
+ /* Default DNS resolver first */
140
+ log('从本地DNS服务器获取TXT解析记录');
141
+ const res = await walkDnsChallengeRecord(recordName, dns, deep);
142
+ if (res && res.length > 0) {
143
+ for (const item of res) {
144
+ txtRecords.push(item)
145
+ }
141
146
  }
142
- }
143
147
 
144
- } catch (e) {
145
- log(`本地获取TXT解析记录失败:${e.message}`)
146
- }
148
+ } catch (e) {
149
+ log(`本地获取TXT解析记录失败:${e.message}`)
150
+ }
147
151
 
148
- try{
149
- /* Authoritative DNS resolver */
150
- log(`从域名权威服务器获取TXT解析记录`);
151
- const authoritativeResolver = await util.getAuthoritativeDnsResolver(recordName,log);
152
- const res = await walkDnsChallengeRecord(recordName, authoritativeResolver,deep);
153
- if (res && res.length > 0) {
154
- for (const item of res) {
155
- txtRecords.push(item)
152
+ if (walkFromAuthoritative !==false) {
153
+ try {
154
+ /* Authoritative DNS resolver */
155
+ log(`从域名权威服务器获取TXT解析记录`);
156
+ const authoritativeResolver = await util.getAuthoritativeDnsResolver(recordName, log);
157
+ const res = await walkDnsChallengeRecord(recordName, authoritativeResolver, deep);
158
+ if (res && res.length > 0) {
159
+ for (const item of res) {
160
+ txtRecords.push(item)
161
+ }
162
+ }
163
+ } catch (e) {
164
+ log(`权威服务器获取TXT解析记录失败:${e.message}`)
156
165
  }
166
+ }else{
167
+ log(`跳过从权威服务器获取TXT解析记录`);
157
168
  }
158
- }catch (e) {
159
- log(`权威服务器获取TXT解析记录失败:${e.message}`)
160
- }
161
169
 
162
- if (txtRecords.length === 0) {
163
- throw new Error(`没有找到TXT解析记录(${recordName})`);
164
- }
165
- return txtRecords;
166
- }
167
-
168
- /**
169
- * Verify ACME DNS challenge
170
- *
171
- * https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
172
- *
173
- * @param {object} authz Identifier authorization
174
- * @param {object} challenge Authorization challenge
175
- * @param {string} keyAuthorization Challenge key authorization
176
- * @param {string} [prefix] DNS prefix
177
- * @returns {Promise<boolean>}
178
- */
179
170
 
180
- async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '_acme-challenge.') {
181
- const recordName = `${prefix}${authz.identifier.value}`;
182
- log(`本地校验TXT记录): ${recordName}`);
183
- let recordValues = await walkTxtRecord(recordName);
184
- //去重
185
- recordValues = [...new Set(recordValues)];
186
- log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录:${recordValues}`);
187
- if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
188
- const err = `没有找到需要的DNS TXT记录: ${recordName},期望:${keyAuthorization},结果:${recordValues}`
189
- throw new Error(err);
171
+ if (txtRecords.length === 0) {
172
+ throw new Error(`没有找到TXT解析记录(${recordName})`);
173
+ }
174
+ return txtRecords;
190
175
  }
191
176
 
192
- log(`关键授权匹配成功(${challenge.type}/${recordName}):${keyAuthorization},校验成功, ACME challenge verified`);
193
- return true;
194
- }
195
-
196
- /**
197
- * Verify ACME TLS ALPN challenge
198
- *
199
- * https://datatracker.ietf.org/doc/html/rfc8737
200
- *
201
- * @param {object} authz Identifier authorization
202
- * @param {object} challenge Authorization challenge
203
- * @param {string} keyAuthorization Challenge key authorization
204
- * @returns {Promise<boolean>}
205
- */
177
+ /**
178
+ * Verify ACME DNS challenge
179
+ *
180
+ * https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
181
+ *
182
+ * @param {object} authz Identifier authorization
183
+ * @param {object} challenge Authorization challenge
184
+ * @param {string} keyAuthorization Challenge key authorization
185
+ * @param {string} [prefix] DNS prefix
186
+ * @returns {Promise<boolean>}
187
+ */
188
+
189
+ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '_acme-challenge.') {
190
+ const recordName = `${prefix}${authz.identifier.value}`;
191
+ log(`本地校验TXT记录): ${recordName}`);
192
+ let recordValues = await walkTxtRecord(recordName, 0, walkFromAuthoritative);
193
+ //去重
194
+ recordValues = [...new Set(recordValues)];
195
+ log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录:${recordValues}`);
196
+ if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
197
+ const err = `没有找到需要的DNS TXT记录: ${recordName},期望:${keyAuthorization},结果:${recordValues}`
198
+ throw new Error(err);
199
+ }
206
200
 
207
- async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
208
- const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
209
- const host = authz.identifier.value;
210
- log(`Establishing TLS connection with host: ${host}:${tlsAlpnPort}`);
201
+ log(`关键授权匹配成功(${challenge.type}/${recordName}):${keyAuthorization},校验成功, ACME challenge verified`);
202
+ return true;
203
+ }
211
204
 
212
- const certificate = await util.retrieveTlsAlpnCertificate(host, tlsAlpnPort);
213
- log('Certificate received from server successfully, matching key authorization in ALPN');
205
+ /**
206
+ * Verify ACME TLS ALPN challenge
207
+ *
208
+ * https://datatracker.ietf.org/doc/html/rfc8737
209
+ *
210
+ * @param {object} authz Identifier authorization
211
+ * @param {object} challenge Authorization challenge
212
+ * @param {string} keyAuthorization Challenge key authorization
213
+ * @returns {Promise<boolean>}
214
+ */
215
+
216
+ async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
217
+ const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
218
+ const host = authz.identifier.value;
219
+ log(`Establishing TLS connection with host: ${host}:${tlsAlpnPort}`);
220
+
221
+ const certificate = await util.retrieveTlsAlpnCertificate(host, tlsAlpnPort);
222
+ log('Certificate received from server successfully, matching key authorization in ALPN');
223
+
224
+ if (!isAlpnCertificateAuthorizationValid(certificate, keyAuthorization)) {
225
+ throw new Error(`Authorization not found in certificate from ${authz.identifier.value}`);
226
+ }
214
227
 
215
- if (!isAlpnCertificateAuthorizationValid(certificate, keyAuthorization)) {
216
- throw new Error(`Authorization not found in certificate from ${authz.identifier.value}`);
228
+ log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
229
+ return true;
217
230
  }
218
231
 
219
- log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
220
- return true;
221
- }
222
-
223
232
  return {
224
- challenges:{
233
+ challenges: {
225
234
  'http-01': verifyHttpChallenge,
226
235
  'dns-01': verifyDnsChallenge,
227
236
  'tls-alpn-01': verifyTlsAlpnChallenge,
228
237
  },
229
238
  walkTxtRecord,
239
+ walkDnsChallengeRecord,
230
240
  }
231
241
 
232
- }
242
+ }
243
+
244
+
245
+
246
+ // createChallengeFn({logger:{info:console.log}}).walkDnsChallengeRecord("handsfree.work")
package/types/index.d.ts CHANGED
@@ -68,6 +68,7 @@ export interface ClientAutoOptions {
68
68
  preferredChain?: string;
69
69
  signal?: AbortSignal;
70
70
  profile?:string;
71
+ waitDnsDiffuseTime?: number;
71
72
  }
72
73
 
73
74
  export class Client {
@@ -218,4 +219,6 @@ export function getAuthoritativeDnsResolver(record:string): Promise<any>;
218
219
 
219
220
  export const CancelError: typeof CancelError;
220
221
 
221
- export function resolveDomainBySoaRecord(domain: string): Promise<string>;
222
+ export function resolveDomainBySoaRecord(domain: string): Promise<string>;
223
+
224
+ export function setWalkFromAuthoritative(value = true): void;