@certd/acme-client 1.31.11 → 1.32.0
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 +3 -3
- package/src/auto.js +106 -121
- package/src/client.js +6 -6
- package/src/verify.js +3 -3
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.
|
|
6
|
+
"version": "1.32.0",
|
|
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.
|
|
21
|
+
"@certd/basic": "^1.32.0",
|
|
22
22
|
"@peculiar/x509": "^1.11.0",
|
|
23
23
|
"asn1js": "^3.0.5",
|
|
24
24
|
"axios": "^1.7.2",
|
|
@@ -67,5 +67,5 @@
|
|
|
67
67
|
"bugs": {
|
|
68
68
|
"url": "https://github.com/publishlab/node-acme-client/issues"
|
|
69
69
|
},
|
|
70
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "7c4756da815758b79eb7b7dae0be02d7a8435e0a"
|
|
71
71
|
}
|
package/src/auto.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ACME auto helper
|
|
3
3
|
*/
|
|
4
|
-
import { readCsrDomains } from
|
|
5
|
-
import { log } from
|
|
6
|
-
import { wait } from
|
|
7
|
-
import { CancelError } from
|
|
4
|
+
import { readCsrDomains } from "./crypto/index.js";
|
|
5
|
+
import { log } from "./logger.js";
|
|
6
|
+
import { wait } from "./wait.js";
|
|
7
|
+
import { CancelError } from "./error.js";
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
const defaultOpts = {
|
|
@@ -13,13 +13,13 @@ const defaultOpts = {
|
|
|
13
13
|
preferredChain: null,
|
|
14
14
|
termsOfServiceAgreed: false,
|
|
15
15
|
skipChallengeVerification: false,
|
|
16
|
-
challengePriority: [
|
|
16
|
+
challengePriority: ["http-01", "dns-01"],
|
|
17
17
|
challengeCreateFn: async () => {
|
|
18
|
-
throw new Error(
|
|
18
|
+
throw new Error("Missing challengeCreateFn()");
|
|
19
19
|
},
|
|
20
20
|
challengeRemoveFn: async () => {
|
|
21
|
-
throw new Error(
|
|
22
|
-
}
|
|
21
|
+
throw new Error("Missing challengeRemoveFn()");
|
|
22
|
+
}
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -30,7 +30,7 @@ const defaultOpts = {
|
|
|
30
30
|
* @returns {Promise<buffer>} Certificate
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
export default
|
|
33
|
+
export default async (client, userOpts) => {
|
|
34
34
|
const opts = { ...defaultOpts, ...userOpts };
|
|
35
35
|
const accountPayload = { termsOfServiceAgreed: opts.termsOfServiceAgreed };
|
|
36
36
|
|
|
@@ -49,14 +49,13 @@ export default async (client, userOpts) => {
|
|
|
49
49
|
* Register account
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
-
log(
|
|
52
|
+
log("[auto] Checking account");
|
|
53
53
|
|
|
54
54
|
try {
|
|
55
55
|
client.getAccountUrl();
|
|
56
|
-
log(
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
log('[auto] Registering account (注册证书申请账户)');
|
|
56
|
+
log("[auto] Account URL already exists, skipping account registration( 证书申请账户已存在,跳过注册 )");
|
|
57
|
+
} catch (e) {
|
|
58
|
+
log("[auto] Registering account (注册证书申请账户)");
|
|
60
59
|
await client.createAccount(accountPayload);
|
|
61
60
|
}
|
|
62
61
|
|
|
@@ -64,7 +63,7 @@ export default async (client, userOpts) => {
|
|
|
64
63
|
* Parse domains from CSR
|
|
65
64
|
*/
|
|
66
65
|
|
|
67
|
-
log(
|
|
66
|
+
log("[auto] Parsing domains from Certificate Signing Request ");
|
|
68
67
|
const { commonName, altNames } = readCsrDomains(opts.csr);
|
|
69
68
|
const uniqueDomains = Array.from(new Set([commonName].concat(altNames).filter((d) => d)));
|
|
70
69
|
|
|
@@ -74,8 +73,8 @@ export default async (client, userOpts) => {
|
|
|
74
73
|
* Place order
|
|
75
74
|
*/
|
|
76
75
|
|
|
77
|
-
log(
|
|
78
|
-
const orderPayload = { identifiers: uniqueDomains.map((d) => ({ type:
|
|
76
|
+
log("[auto] Placing new certificate order with ACME provider");
|
|
77
|
+
const orderPayload = { identifiers: uniqueDomains.map((d) => ({ type: "dns", value: d })) };
|
|
79
78
|
const order = await client.createOrder(orderPayload);
|
|
80
79
|
const authorizations = await client.getAuthorizations(order);
|
|
81
80
|
|
|
@@ -85,82 +84,81 @@ export default async (client, userOpts) => {
|
|
|
85
84
|
* Resolve and satisfy challenges
|
|
86
85
|
*/
|
|
87
86
|
|
|
88
|
-
log(
|
|
87
|
+
log("[auto] Resolving and satisfying authorization challenges");
|
|
89
88
|
|
|
90
89
|
const clearTasks = [];
|
|
90
|
+
const localVerifyTasks = [];
|
|
91
|
+
const completeChallengeTasks = [];
|
|
91
92
|
|
|
92
93
|
const challengeFunc = async (authz) => {
|
|
93
94
|
const d = authz.identifier.value;
|
|
94
95
|
let challengeCompleted = false;
|
|
95
96
|
|
|
96
97
|
/* Skip authz that already has valid status */
|
|
97
|
-
if (authz.status ===
|
|
98
|
+
if (authz.status === "valid") {
|
|
98
99
|
log(`[auto] [${d}] Authorization already has valid status, no need to complete challenges`);
|
|
99
100
|
return;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
const keyAuthorizationGetter = async (challenge) => {
|
|
103
104
|
return await client.getChallengeKeyAuthorization(challenge);
|
|
104
|
-
}
|
|
105
|
+
};
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
log(`[auto] [${d}]
|
|
107
|
+
async function deactivateAuth(e) {
|
|
108
|
+
log(`[auto] [${d}] Unable to complete challenge: ${e.message}`);
|
|
108
109
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider);
|
|
115
|
-
}
|
|
116
|
-
catch (e) {
|
|
117
|
-
log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
// throw new Error('测试异常');
|
|
121
|
-
/* Challenge verification */
|
|
122
|
-
if (opts.skipChallengeVerification === true) {
|
|
123
|
-
log(`[auto] [${d}] 跳过本地验证(skipChallengeVerification=true),等待 60s`);
|
|
124
|
-
await wait(60 * 1000);
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
log(`[auto] [${d}] 开始本地验证, type = ${challenge.type}`);
|
|
128
|
-
try {
|
|
129
|
-
await client.verifyChallenge(authz, challenge);
|
|
130
|
-
}
|
|
131
|
-
catch (e) {
|
|
132
|
-
log(`[auto] [${d}] 本地验证失败,尝试请求ACME提供商获取状态: ${e.message}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
/* Complete challenge and wait for valid status */
|
|
136
|
-
log(`[auto] [${d}] 请求ACME提供商完成验证,等待返回valid状态`);
|
|
137
|
-
await client.completeChallenge(challenge);
|
|
138
|
-
challengeCompleted = true;
|
|
139
|
-
|
|
140
|
-
await client.waitForValidStatus(challenge);
|
|
141
|
-
}
|
|
142
|
-
catch (e) {
|
|
143
|
-
log(`[auto] [${d}] challengeCreateFn threw error: ${e.message}`);
|
|
144
|
-
throw e;
|
|
110
|
+
log(`[auto] [${d}] Deactivating failed authorization`);
|
|
111
|
+
await client.deactivateAuthorization(authz);
|
|
112
|
+
} catch (f) {
|
|
113
|
+
/* Suppress deactivateAuthorization() errors */
|
|
114
|
+
log(`[auto] [${d}] Authorization deactivation threw error: ${f.message}`);
|
|
145
115
|
}
|
|
146
116
|
}
|
|
147
|
-
catch (e) {
|
|
148
|
-
/* Deactivate pending authz when unable to complete challenge */
|
|
149
|
-
if (!challengeCompleted) {
|
|
150
|
-
log(`[auto] [${d}] Unable to complete challenge: ${e.message}`);
|
|
151
117
|
|
|
118
|
+
log(`[auto] [${d}] Trigger challengeCreateFn()`);
|
|
119
|
+
try {
|
|
120
|
+
const { recordReq, recordRes, dnsProvider, challenge, keyAuthorization } = await opts.challengeCreateFn(authz, keyAuthorizationGetter);
|
|
121
|
+
clearTasks.push(async () => {
|
|
122
|
+
/* Trigger challengeRemoveFn(), suppress errors */
|
|
123
|
+
log(`[auto] [${d}] Trigger challengeRemoveFn()`);
|
|
152
124
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
125
|
+
await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`);
|
|
155
128
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
localVerifyTasks.push(async () => {
|
|
132
|
+
/* Challenge verification */
|
|
133
|
+
log(`[auto] [${d}] 开始本地验证, type = ${challenge.type}`);
|
|
134
|
+
try {
|
|
135
|
+
await client.verifyChallenge(authz, challenge);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
log(`[auto] [${d}] 本地验证失败,尝试请求ACME提供商获取状态: ${e.message}`);
|
|
159
138
|
}
|
|
160
|
-
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
completeChallengeTasks.push(async () => {
|
|
142
|
+
/* Complete challenge and wait for valid status */
|
|
143
|
+
log(`[auto] [${d}] 请求ACME提供商完成验证`);
|
|
144
|
+
try{
|
|
145
|
+
await client.completeChallenge(challenge);
|
|
146
|
+
}catch (e) {
|
|
147
|
+
await deactivateAuth(e);
|
|
148
|
+
throw e;
|
|
149
|
+
}
|
|
150
|
+
challengeCompleted = true;
|
|
151
|
+
log(`[auto] [${d}] 等待返回valid状态`);
|
|
152
|
+
await client.waitForValidStatus(challenge,d);
|
|
153
|
+
});
|
|
154
|
+
|
|
161
155
|
|
|
156
|
+
} catch (e) {
|
|
157
|
+
log(`[auto] [${d}] challengeCreateFn threw error: ${e.message}`);
|
|
158
|
+
await deactivateAuth(e);
|
|
162
159
|
throw e;
|
|
163
160
|
}
|
|
161
|
+
|
|
164
162
|
};
|
|
165
163
|
const domainSets = [];
|
|
166
164
|
|
|
@@ -168,7 +166,7 @@ export default async (client, userOpts) => {
|
|
|
168
166
|
const d = authz.identifier.value;
|
|
169
167
|
log(`authorization:domain = ${d}, value = ${JSON.stringify(authz)}`);
|
|
170
168
|
|
|
171
|
-
if (authz.status ===
|
|
169
|
+
if (authz.status === "valid") {
|
|
172
170
|
log(`[auto] [${d}] Authorization already has valid status, no need to complete challenges`);
|
|
173
171
|
return;
|
|
174
172
|
}
|
|
@@ -192,8 +190,9 @@ export default async (client, userOpts) => {
|
|
|
192
190
|
|
|
193
191
|
const allChallengePromises = [];
|
|
194
192
|
// eslint-disable-next-line no-restricted-syntax
|
|
193
|
+
const challengePromises = [];
|
|
194
|
+
allChallengePromises.push(challengePromises);
|
|
195
195
|
for (const domainSet of domainSets) {
|
|
196
|
-
const challengePromises = [];
|
|
197
196
|
// eslint-disable-next-line guard-for-in,no-restricted-syntax
|
|
198
197
|
for (const domain in domainSet) {
|
|
199
198
|
const authz = domainSet[domain];
|
|
@@ -202,12 +201,11 @@ export default async (client, userOpts) => {
|
|
|
202
201
|
await challengeFunc(authz);
|
|
203
202
|
});
|
|
204
203
|
}
|
|
205
|
-
allChallengePromises.push(challengePromises);
|
|
206
204
|
}
|
|
207
205
|
|
|
208
206
|
log(`[auto] challengeGroups:${allChallengePromises.length}`);
|
|
209
207
|
|
|
210
|
-
function runAllPromise(tasks) {
|
|
208
|
+
async function runAllPromise(tasks) {
|
|
211
209
|
let promise = Promise.resolve();
|
|
212
210
|
tasks.forEach((task) => {
|
|
213
211
|
promise = promise.then(task);
|
|
@@ -215,73 +213,60 @@ export default async (client, userOpts) => {
|
|
|
215
213
|
return promise;
|
|
216
214
|
}
|
|
217
215
|
|
|
218
|
-
async function runPromisePa(tasks) {
|
|
216
|
+
async function runPromisePa(tasks, waitTime = 5000) {
|
|
219
217
|
const results = [];
|
|
220
218
|
// eslint-disable-next-line no-await-in-loop,no-restricted-syntax
|
|
221
219
|
for (const task of tasks) {
|
|
222
220
|
results.push(task());
|
|
223
221
|
// eslint-disable-next-line no-await-in-loop
|
|
224
|
-
await wait(
|
|
222
|
+
await wait(waitTime);
|
|
225
223
|
}
|
|
226
224
|
return Promise.all(results);
|
|
227
225
|
}
|
|
228
226
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
227
|
+
log(`开始challenge,共${allChallengePromises.length}组`);
|
|
228
|
+
let i = 0;
|
|
229
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
230
|
+
for (const challengePromises of allChallengePromises) {
|
|
231
|
+
i += 1;
|
|
232
|
+
log(`开始第${i}组`);
|
|
233
|
+
if (opts.signal && opts.signal.aborted) {
|
|
234
|
+
throw new CancelError("用户取消");
|
|
235
|
+
}
|
|
239
236
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// letsencrypt 如果同时检出两个TXT记录,会以第一个为准,就会校验失败,所以需要提前删除
|
|
251
|
-
// zerossl 此方式测试无问题
|
|
252
|
-
log(`清理challenge痕迹,length:${clearTasks.length}`);
|
|
253
|
-
try {
|
|
254
|
-
// eslint-disable-next-line no-await-in-loop
|
|
255
|
-
await runAllPromise(clearTasks);
|
|
256
|
-
}
|
|
257
|
-
catch (e) {
|
|
258
|
-
log('清理challenge失败');
|
|
259
|
-
log(e);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
237
|
+
try {
|
|
238
|
+
// eslint-disable-next-line no-await-in-loop
|
|
239
|
+
await runPromisePa(challengePromises);
|
|
240
|
+
if (opts.skipChallengeVerification === true) {
|
|
241
|
+
log(`跳过本地验证(skipChallengeVerification=true),等待 60s`);
|
|
242
|
+
await wait(60 * 1000);
|
|
243
|
+
} else {
|
|
244
|
+
await runPromisePa(localVerifyTasks, 1000);
|
|
245
|
+
log("本地校验完成,等待30s")
|
|
246
|
+
await wait(30 * 1000)
|
|
262
247
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
//
|
|
271
|
-
//
|
|
248
|
+
|
|
249
|
+
log("开始向提供商请求挑战验证");
|
|
250
|
+
await runPromisePa(completeChallengeTasks, 1000);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
log(`证书申请失败${e.message}`);
|
|
253
|
+
throw e;
|
|
254
|
+
} finally {
|
|
255
|
+
// letsencrypt 如果同时检出两个TXT记录,会以第一个为准,就会校验失败,所以需要提前删除
|
|
256
|
+
// zerossl 此方式测试无问题
|
|
272
257
|
log(`清理challenge痕迹,length:${clearTasks.length}`);
|
|
273
258
|
try {
|
|
274
|
-
|
|
259
|
+
// eslint-disable-next-line no-await-in-loop
|
|
275
260
|
await runAllPromise(clearTasks);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
log('清理challenge失败');
|
|
261
|
+
} catch (e) {
|
|
262
|
+
log("清理challenge失败");
|
|
279
263
|
log(e);
|
|
280
264
|
}
|
|
281
265
|
}
|
|
282
266
|
}
|
|
283
267
|
|
|
284
|
-
|
|
268
|
+
|
|
269
|
+
log("challenge结束");
|
|
285
270
|
|
|
286
271
|
// log('[auto] Waiting for challenge valid status');
|
|
287
272
|
// await Promise.all(challengePromises);
|
|
@@ -289,7 +274,7 @@ export default async (client, userOpts) => {
|
|
|
289
274
|
* Finalize order and download certificate
|
|
290
275
|
*/
|
|
291
276
|
|
|
292
|
-
log(
|
|
277
|
+
log("[auto] Finalizing order and downloading certificate");
|
|
293
278
|
const finalized = await client.finalizeOrder(order, opts.csr);
|
|
294
279
|
const res = await client.getCertificate(finalized, opts.preferredChain);
|
|
295
280
|
return res;
|
package/src/client.js
CHANGED
|
@@ -554,9 +554,9 @@ class AcmeClient {
|
|
|
554
554
|
* ```
|
|
555
555
|
*/
|
|
556
556
|
|
|
557
|
-
async waitForValidStatus(item) {
|
|
557
|
+
async waitForValidStatus(item,d) {
|
|
558
558
|
if (!item.url) {
|
|
559
|
-
throw new Error(
|
|
559
|
+
throw new Error(`[${d}] Unable to verify status of item, URL not found`);
|
|
560
560
|
}
|
|
561
561
|
|
|
562
562
|
const verifyFn = async (abort) => {
|
|
@@ -568,23 +568,23 @@ class AcmeClient {
|
|
|
568
568
|
const resp = await this.api.apiRequest(item.url, null, [200]);
|
|
569
569
|
|
|
570
570
|
/* Verify status */
|
|
571
|
-
log(`Item has status(挑战状态): ${resp.data.status}`);
|
|
571
|
+
log(`[${d}] Item has status(挑战状态): ${resp.data.status}`);
|
|
572
572
|
|
|
573
573
|
if (invalidStates.includes(resp.data.status)) {
|
|
574
574
|
abort();
|
|
575
575
|
throw new Error(util.formatResponseError(resp));
|
|
576
576
|
}
|
|
577
577
|
else if (pendingStates.includes(resp.data.status)) {
|
|
578
|
-
throw new Error(
|
|
578
|
+
throw new Error(`[${d}] Operation is pending or processing(当前仍然在等待状态)`);
|
|
579
579
|
}
|
|
580
580
|
else if (validStates.includes(resp.data.status)) {
|
|
581
581
|
return resp.data;
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
-
throw new Error(`Unexpected item status: ${resp.data.status}`);
|
|
584
|
+
throw new Error(`[${d}] Unexpected item status: ${resp.data.status}`);
|
|
585
585
|
};
|
|
586
586
|
|
|
587
|
-
log(`Waiting for valid status (等待valid状态): ${item.url}`, this.backoffOpts);
|
|
587
|
+
log(`[${d}] Waiting for valid status (等待valid状态): ${item.url}`, this.backoffOpts);
|
|
588
588
|
return util.retry(verifyFn, this.backoffOpts);
|
|
589
589
|
}
|
|
590
590
|
|
package/src/verify.js
CHANGED
|
@@ -98,7 +98,7 @@ export async function walkTxtRecord(recordName,deep = 0) {
|
|
|
98
98
|
try {
|
|
99
99
|
/* Default DNS resolver first */
|
|
100
100
|
log('从本地DNS服务器获取TXT解析记录');
|
|
101
|
-
const res = await walkDnsChallengeRecord(recordName,
|
|
101
|
+
const res = await walkDnsChallengeRecord(recordName,dns,deep);
|
|
102
102
|
if (res && res.length > 0) {
|
|
103
103
|
for (const item of res) {
|
|
104
104
|
txtRecords.push(item)
|
|
@@ -147,12 +147,12 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
|
|
|
147
147
|
let recordValues = await walkTxtRecord(recordName);
|
|
148
148
|
//去重
|
|
149
149
|
recordValues = [...new Set(recordValues)];
|
|
150
|
-
log(`DNS查询成功, 找到 ${recordValues.length} 条TXT
|
|
150
|
+
log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录:${recordValues}`);
|
|
151
151
|
if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
|
|
152
152
|
throw new Error(`没有找到需要的DNS TXT记录: ${recordName},期望:${keyAuthorization},结果:${recordValues}`);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
log(`关键授权匹配成功(${challenge.type}/${recordName}
|
|
155
|
+
log(`关键授权匹配成功(${challenge.type}/${recordName}):${keyAuthorization},校验成功, ACME challenge verified`);
|
|
156
156
|
return true;
|
|
157
157
|
}
|
|
158
158
|
|