@certd/acme-client 1.22.6 → 1.24.1
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 +7 -7
- package/src/api.js +17 -1
- package/src/auto.js +80 -32
- package/src/axios.js +87 -1
- package/src/client.js +12 -1
- package/src/crypto/forge.js +12 -5
- package/src/crypto/index.js +0 -1
- package/src/http.js +6 -3
- package/src/util.js +45 -8
- package/types/index.d.ts +2 -0
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.24.1",
|
|
7
7
|
"main": "src/index.js",
|
|
8
8
|
"types": "types/index.d.ts",
|
|
9
9
|
"license": "MIT",
|
|
@@ -16,24 +16,24 @@
|
|
|
16
16
|
"types"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@peculiar/x509": "^1.
|
|
19
|
+
"@peculiar/x509": "^1.11.0",
|
|
20
20
|
"asn1js": "^3.0.5",
|
|
21
21
|
"axios": "^1.7.2",
|
|
22
|
-
"debug": "^4.
|
|
22
|
+
"debug": "^4.3.5",
|
|
23
23
|
"https-proxy-agent": "^7.0.4",
|
|
24
24
|
"node-forge": "^1.3.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@types/node": "^20.
|
|
27
|
+
"@types/node": "^20.14.10",
|
|
28
28
|
"chai": "^4.4.1",
|
|
29
29
|
"chai-as-promised": "^7.1.2",
|
|
30
30
|
"eslint": "^8.57.0",
|
|
31
31
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
32
32
|
"eslint-plugin-import": "^2.29.1",
|
|
33
33
|
"jsdoc-to-markdown": "^8.0.1",
|
|
34
|
-
"mocha": "^10.
|
|
34
|
+
"mocha": "^10.6.0",
|
|
35
35
|
"nock": "^13.5.4",
|
|
36
|
-
"tsd": "^0.31.
|
|
36
|
+
"tsd": "^0.31.1",
|
|
37
37
|
"typescript": "^4.8.4",
|
|
38
38
|
"uuid": "^8.3.2"
|
|
39
39
|
},
|
|
@@ -59,5 +59,5 @@
|
|
|
59
59
|
"bugs": {
|
|
60
60
|
"url": "https://github.com/publishlab/node-acme-client/issues"
|
|
61
61
|
},
|
|
62
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "bef6b981e26a010a797734e508de6822de8564f5"
|
|
63
63
|
}
|
package/src/api.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const util = require('./util');
|
|
6
|
+
const { log } = require('./logger');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* AcmeApi
|
|
@@ -17,6 +18,21 @@ class AcmeApi {
|
|
|
17
18
|
this.accountUrl = accountUrl;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
getLocationFromHeader(resp) {
|
|
22
|
+
let locationUrl = resp.headers.location;
|
|
23
|
+
const mapping = this.http.urlMapping;
|
|
24
|
+
if (mapping.mappings) {
|
|
25
|
+
// eslint-disable-next-line guard-for-in,no-restricted-syntax
|
|
26
|
+
for (const key in mapping.mappings) {
|
|
27
|
+
const url = mapping.mappings[key];
|
|
28
|
+
if (locationUrl.indexOf(url) > -1) {
|
|
29
|
+
locationUrl = locationUrl.replace(url, key);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return locationUrl;
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
/**
|
|
21
37
|
* Get account URL
|
|
22
38
|
*
|
|
@@ -103,7 +119,7 @@ class AcmeApi {
|
|
|
103
119
|
|
|
104
120
|
/* Set account URL */
|
|
105
121
|
if (resp.headers.location) {
|
|
106
|
-
this.accountUrl = resp
|
|
122
|
+
this.accountUrl = this.getLocationFromHeader(resp);
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
return resp;
|
package/src/auto.js
CHANGED
|
@@ -13,8 +13,12 @@ const defaultOpts = {
|
|
|
13
13
|
termsOfServiceAgreed: false,
|
|
14
14
|
skipChallengeVerification: false,
|
|
15
15
|
challengePriority: ['http-01', 'dns-01'],
|
|
16
|
-
challengeCreateFn: async () => {
|
|
17
|
-
|
|
16
|
+
challengeCreateFn: async () => {
|
|
17
|
+
throw new Error('Missing challengeCreateFn()');
|
|
18
|
+
},
|
|
19
|
+
challengeRemoveFn: async () => {
|
|
20
|
+
throw new Error('Missing challengeRemoveFn()');
|
|
21
|
+
},
|
|
18
22
|
};
|
|
19
23
|
|
|
20
24
|
/**
|
|
@@ -137,9 +141,13 @@ module.exports = async (client, userOpts) => {
|
|
|
137
141
|
}
|
|
138
142
|
else {
|
|
139
143
|
log(`[auto] [${d}] Running challenge verification`);
|
|
140
|
-
|
|
144
|
+
try {
|
|
145
|
+
await client.verifyChallenge(authz, challenge);
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
log(`[auto] [${d}] challenge verification threw error: ${e.message}`);
|
|
149
|
+
}
|
|
141
150
|
}
|
|
142
|
-
|
|
143
151
|
/* Complete challenge and wait for valid status */
|
|
144
152
|
log(`[auto] [${d}] Completing challenge with ACME provider and waiting for valid status`);
|
|
145
153
|
await client.completeChallenge(challenge);
|
|
@@ -170,11 +178,42 @@ module.exports = async (client, userOpts) => {
|
|
|
170
178
|
throw e;
|
|
171
179
|
}
|
|
172
180
|
};
|
|
181
|
+
const domainSets = [];
|
|
173
182
|
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
authorizations.forEach((authz) => {
|
|
184
|
+
const d = authz.identifier.value;
|
|
185
|
+
let setd = false;
|
|
186
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
187
|
+
for (const group of domainSets) {
|
|
188
|
+
if (!group[d]) {
|
|
189
|
+
group[d] = authz;
|
|
190
|
+
setd = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (!setd) {
|
|
194
|
+
const group = {};
|
|
195
|
+
group[d] = authz;
|
|
196
|
+
domainSets.push(group);
|
|
197
|
+
}
|
|
176
198
|
});
|
|
177
199
|
|
|
200
|
+
const allChallengePromises = [];
|
|
201
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
202
|
+
for (const domainSet of domainSets) {
|
|
203
|
+
const challengePromises = [];
|
|
204
|
+
// eslint-disable-next-line guard-for-in,no-restricted-syntax
|
|
205
|
+
for (const domain in domainSet) {
|
|
206
|
+
const authz = domainSet[domain];
|
|
207
|
+
challengePromises.push(async () => {
|
|
208
|
+
log(`[auto] [${domain}] Starting challenge`);
|
|
209
|
+
await challengeFunc(authz);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
allChallengePromises.push(challengePromises);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
log(`[auto] challengeGroups:${allChallengePromises.length}`);
|
|
216
|
+
|
|
178
217
|
function runAllPromise(tasks) {
|
|
179
218
|
let promise = Promise.resolve();
|
|
180
219
|
tasks.forEach((task) => {
|
|
@@ -194,39 +233,48 @@ module.exports = async (client, userOpts) => {
|
|
|
194
233
|
return Promise.all(results);
|
|
195
234
|
}
|
|
196
235
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Finalize order and download certificate
|
|
208
|
-
*/
|
|
236
|
+
log(`开始challenge,共${allChallengePromises.length}组`);
|
|
237
|
+
let i = 0;
|
|
238
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
239
|
+
for (const challengePromises of allChallengePromises) {
|
|
240
|
+
i += 1;
|
|
241
|
+
log(`开始第${i}组`);
|
|
242
|
+
if (opts.signal && opts.signal.aborted) {
|
|
243
|
+
throw new Error('用户取消');
|
|
244
|
+
}
|
|
209
245
|
|
|
210
|
-
log('[auto] Finalizing order and downloading certificate');
|
|
211
|
-
const finalized = await client.finalizeOrder(order, opts.csr);
|
|
212
|
-
return await client.getCertificate(finalized, opts.preferredChain);
|
|
213
|
-
}
|
|
214
|
-
catch (e) {
|
|
215
|
-
log('证书申请失败');
|
|
216
|
-
log(e);
|
|
217
|
-
throw new Error(`证书申请失败:${e.message}`);
|
|
218
|
-
}
|
|
219
|
-
finally {
|
|
220
|
-
log(`清理challenge痕迹,length:${clearTasks.length}`);
|
|
221
246
|
try {
|
|
222
|
-
await
|
|
247
|
+
// eslint-disable-next-line no-await-in-loop
|
|
248
|
+
await runPromisePa(challengePromises);
|
|
223
249
|
}
|
|
224
250
|
catch (e) {
|
|
225
|
-
log(
|
|
226
|
-
|
|
251
|
+
log(`证书申请失败${e.message}`);
|
|
252
|
+
throw e;
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
log(`清理challenge痕迹,length:${clearTasks.length}`);
|
|
256
|
+
try {
|
|
257
|
+
// eslint-disable-next-line no-await-in-loop
|
|
258
|
+
await runAllPromise(clearTasks);
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
log('清理challenge失败');
|
|
262
|
+
log(e);
|
|
263
|
+
}
|
|
227
264
|
}
|
|
228
265
|
}
|
|
266
|
+
log('challenge结束');
|
|
267
|
+
|
|
268
|
+
// log('[auto] Waiting for challenge valid status');
|
|
269
|
+
// await Promise.all(challengePromises);
|
|
270
|
+
/**
|
|
271
|
+
* Finalize order and download certificate
|
|
272
|
+
*/
|
|
229
273
|
|
|
274
|
+
log('[auto] Finalizing order and downloading certificate');
|
|
275
|
+
const finalized = await client.finalizeOrder(order, opts.csr);
|
|
276
|
+
const res = await client.getCertificate(finalized, opts.preferredChain);
|
|
277
|
+
return res;
|
|
230
278
|
// try {
|
|
231
279
|
// await Promise.allSettled(challengePromises);
|
|
232
280
|
// }
|
package/src/axios.js
CHANGED
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const axios = require('axios');
|
|
6
|
+
const { parseRetryAfterHeader } = require('./util');
|
|
7
|
+
const { log } = require('./logger');
|
|
6
8
|
const pkg = require('./../package.json');
|
|
7
9
|
|
|
10
|
+
const { AxiosError } = axios;
|
|
11
|
+
|
|
8
12
|
/**
|
|
9
|
-
*
|
|
13
|
+
* Defaults
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
16
|
const instance = axios.create();
|
|
@@ -19,6 +23,9 @@ instance.defaults.acmeSettings = {
|
|
|
19
23
|
httpChallengePort: 80,
|
|
20
24
|
httpsChallengePort: 443,
|
|
21
25
|
tlsAlpnChallengePort: 443,
|
|
26
|
+
|
|
27
|
+
retryMaxAttempts: 5,
|
|
28
|
+
retryDefaultDelay: 5,
|
|
22
29
|
};
|
|
23
30
|
// instance.defaults.proxy = {
|
|
24
31
|
// host: '192.168.34.139',
|
|
@@ -33,6 +40,85 @@ instance.defaults.acmeSettings = {
|
|
|
33
40
|
|
|
34
41
|
instance.defaults.adapter = 'http';
|
|
35
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Retry requests on server errors or when rate limited
|
|
45
|
+
*
|
|
46
|
+
* https://datatracker.ietf.org/doc/html/rfc8555#section-6.6
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
function isRetryableError(error) {
|
|
50
|
+
return (error.code !== 'ECONNABORTED')
|
|
51
|
+
&& (error.code !== 'ERR_NOCK_NO_MATCH')
|
|
52
|
+
&& (!error.response
|
|
53
|
+
|| (error.response.status === 429)
|
|
54
|
+
|| ((error.response.status >= 500) && (error.response.status <= 599)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* https://github.com/axios/axios/blob/main/lib/core/settle.js */
|
|
58
|
+
function validateStatus(response) {
|
|
59
|
+
const validator = response.config.retryValidateStatus;
|
|
60
|
+
|
|
61
|
+
if (!response.status || !validator || validator(response.status)) {
|
|
62
|
+
return response;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
throw new AxiosError(
|
|
66
|
+
`Request failed with status code ${response.status}`,
|
|
67
|
+
(Math.floor(response.status / 100) === 4) ? AxiosError.ERR_BAD_REQUEST : AxiosError.ERR_BAD_RESPONSE,
|
|
68
|
+
response.config,
|
|
69
|
+
response.request,
|
|
70
|
+
response,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Pass all responses through the error interceptor */
|
|
75
|
+
instance.interceptors.request.use((config) => {
|
|
76
|
+
if (!('retryValidateStatus' in config)) {
|
|
77
|
+
config.retryValidateStatus = config.validateStatus;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
config.validateStatus = () => false;
|
|
81
|
+
return config;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/* Handle request retries if applicable */
|
|
85
|
+
instance.interceptors.response.use(null, async (error) => {
|
|
86
|
+
const { config, response } = error;
|
|
87
|
+
|
|
88
|
+
if (!config) {
|
|
89
|
+
return Promise.reject(error);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Pick up errors we want to retry */
|
|
93
|
+
if (isRetryableError(error)) {
|
|
94
|
+
const { retryMaxAttempts, retryDefaultDelay } = instance.defaults.acmeSettings;
|
|
95
|
+
config.retryAttempt = ('retryAttempt' in config) ? (config.retryAttempt + 1) : 1;
|
|
96
|
+
|
|
97
|
+
if (config.retryAttempt <= retryMaxAttempts) {
|
|
98
|
+
const code = response ? `HTTP ${response.status}` : error.code;
|
|
99
|
+
log(`Caught ${code}, retry attempt ${config.retryAttempt}/${retryMaxAttempts} to URL ${config.url}`);
|
|
100
|
+
|
|
101
|
+
/* Attempt to parse Retry-After header, fallback to default delay */
|
|
102
|
+
let retryAfter = response ? parseRetryAfterHeader(response.headers['retry-after']) : 0;
|
|
103
|
+
|
|
104
|
+
if (retryAfter > 0) {
|
|
105
|
+
log(`Found retry-after response header with value: ${response.headers['retry-after']}, waiting ${retryAfter} seconds`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
retryAfter = (retryDefaultDelay * config.retryAttempt);
|
|
109
|
+
log(`Unable to locate or parse retry-after response header, waiting ${retryAfter} seconds`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Wait and retry the request */
|
|
113
|
+
await new Promise((resolve) => { setTimeout(resolve, (retryAfter * 1000)); });
|
|
114
|
+
return instance(config);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Validate and return response */
|
|
119
|
+
return validateStatus(response);
|
|
120
|
+
});
|
|
121
|
+
|
|
36
122
|
/**
|
|
37
123
|
* Export instance
|
|
38
124
|
*/
|
package/src/client.js
CHANGED
|
@@ -300,7 +300,8 @@ class AcmeClient {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
/* Add URL to response */
|
|
303
|
-
resp.data.url =
|
|
303
|
+
resp.data.url = this.api.getLocationFromHeader(resp);
|
|
304
|
+
|
|
304
305
|
return resp.data;
|
|
305
306
|
}
|
|
306
307
|
|
|
@@ -490,6 +491,9 @@ class AcmeClient {
|
|
|
490
491
|
const keyAuthorization = await this.getChallengeKeyAuthorization(challenge);
|
|
491
492
|
|
|
492
493
|
const verifyFn = async () => {
|
|
494
|
+
if (this.opts.signal && this.opts.signal.aborted) {
|
|
495
|
+
throw new Error('用户取消');
|
|
496
|
+
}
|
|
493
497
|
await verify[challenge.type](authz, challenge, keyAuthorization);
|
|
494
498
|
};
|
|
495
499
|
|
|
@@ -513,6 +517,9 @@ class AcmeClient {
|
|
|
513
517
|
*/
|
|
514
518
|
|
|
515
519
|
async completeChallenge(challenge) {
|
|
520
|
+
if (this.opts.signal && this.opts.signal.aborted) {
|
|
521
|
+
throw new Error('用户取消');
|
|
522
|
+
}
|
|
516
523
|
const resp = await this.api.completeChallenge(challenge.url, {});
|
|
517
524
|
return resp.data;
|
|
518
525
|
}
|
|
@@ -550,6 +557,10 @@ class AcmeClient {
|
|
|
550
557
|
}
|
|
551
558
|
|
|
552
559
|
const verifyFn = async (abort) => {
|
|
560
|
+
if (this.opts.signal && this.opts.signal.aborted) {
|
|
561
|
+
throw new Error('用户取消');
|
|
562
|
+
}
|
|
563
|
+
|
|
553
564
|
const resp = await this.api.apiRequest(item.url, null, [200]);
|
|
554
565
|
|
|
555
566
|
/* Verify status */
|
package/src/crypto/forge.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
const net = require('net');
|
|
11
11
|
const { promisify } = require('util');
|
|
12
12
|
const forge = require('node-forge');
|
|
13
|
+
const { createPrivateEcdsaKey, getPublicKey } = require('./index');
|
|
13
14
|
|
|
14
15
|
const generateKeyPair = promisify(forge.pki.rsa.generateKeyPair);
|
|
15
16
|
|
|
@@ -378,13 +379,17 @@ function formatCsrAltNames(altNames) {
|
|
|
378
379
|
* }, certificateKey);
|
|
379
380
|
*/
|
|
380
381
|
|
|
381
|
-
exports.createCsr = async (data,
|
|
382
|
-
|
|
383
|
-
|
|
382
|
+
exports.createCsr = async (data, keyType = null) => {
|
|
383
|
+
let key = null;
|
|
384
|
+
if (keyType === 'ec') {
|
|
385
|
+
key = await createPrivateEcdsaKey();
|
|
384
386
|
}
|
|
385
|
-
else
|
|
386
|
-
key =
|
|
387
|
+
else {
|
|
388
|
+
key = await createPrivateKey(data.keySize);
|
|
387
389
|
}
|
|
390
|
+
// else if (!Buffer.isBuffer(key)) {
|
|
391
|
+
// key = Buffer.from(key);
|
|
392
|
+
// }
|
|
388
393
|
|
|
389
394
|
if (typeof data.altNames === 'undefined') {
|
|
390
395
|
data.altNames = [];
|
|
@@ -396,6 +401,8 @@ exports.createCsr = async (data, key = null) => {
|
|
|
396
401
|
const privateKey = forge.pki.privateKeyFromPem(key);
|
|
397
402
|
const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e);
|
|
398
403
|
csr.publicKey = publicKey;
|
|
404
|
+
// const privateKey = key;
|
|
405
|
+
// csr.publicKey = getPublicKey(key);
|
|
399
406
|
|
|
400
407
|
/* Ensure subject common name is present in SAN - https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf */
|
|
401
408
|
if (data.commonName && !data.altNames.includes(data.commonName)) {
|
package/src/crypto/index.js
CHANGED
|
@@ -290,7 +290,6 @@ exports.readCsrDomains = (csrPem) => {
|
|
|
290
290
|
if (Buffer.isBuffer(csrPem)) {
|
|
291
291
|
csrPem = csrPem.toString();
|
|
292
292
|
}
|
|
293
|
-
|
|
294
293
|
const dec = x509.PemConverter.decodeFirst(csrPem);
|
|
295
294
|
const csr = new x509.Pkcs10CertificateRequest(dec);
|
|
296
295
|
return parseDomains(csr);
|
package/src/http.js
CHANGED
|
@@ -55,7 +55,7 @@ class HttpClient {
|
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
57
|
async request(url, method, opts = {}) {
|
|
58
|
-
if (this.urlMapping && this.urlMapping.enabled
|
|
58
|
+
if (this.urlMapping && this.urlMapping.enabled && this.urlMapping.mappings) {
|
|
59
59
|
// eslint-disable-next-line no-restricted-syntax
|
|
60
60
|
for (const key in this.urlMapping.mappings) {
|
|
61
61
|
if (url.includes(key)) {
|
|
@@ -93,9 +93,11 @@ class HttpClient {
|
|
|
93
93
|
*/
|
|
94
94
|
|
|
95
95
|
async getDirectory() {
|
|
96
|
-
const
|
|
96
|
+
const now = Math.floor(Date.now() / 1000);
|
|
97
|
+
const age = (now - this.directoryTimestamp);
|
|
97
98
|
|
|
98
99
|
if (!this.directoryCache || (age > this.directoryMaxAge)) {
|
|
100
|
+
log(`Refreshing ACME directory, age: ${age}`);
|
|
99
101
|
const resp = await this.request(this.directoryUrl, 'get');
|
|
100
102
|
|
|
101
103
|
if (resp.status >= 400) {
|
|
@@ -107,6 +109,7 @@ class HttpClient {
|
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
this.directoryCache = resp.data;
|
|
112
|
+
this.directoryTimestamp = now;
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
return this.directoryCache;
|
|
@@ -131,7 +134,7 @@ class HttpClient {
|
|
|
131
134
|
*
|
|
132
135
|
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
|
|
133
136
|
*
|
|
134
|
-
* @returns {Promise<string>}
|
|
137
|
+
* @returns {Promise<string>} Nonce
|
|
135
138
|
*/
|
|
136
139
|
|
|
137
140
|
async getNonce() {
|
package/src/util.js
CHANGED
|
@@ -84,9 +84,12 @@ function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* Parse URLs from
|
|
87
|
+
* Parse URLs from Link header
|
|
88
88
|
*
|
|
89
|
-
*
|
|
89
|
+
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2
|
|
90
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
|
|
91
|
+
*
|
|
92
|
+
* @param {string} header Header contents
|
|
90
93
|
* @param {string} rel Link relation, default: `alternate`
|
|
91
94
|
* @returns {string[]} Array of URLs
|
|
92
95
|
*/
|
|
@@ -102,6 +105,37 @@ function parseLinkHeader(header, rel = 'alternate') {
|
|
|
102
105
|
return results.filter((r) => r);
|
|
103
106
|
}
|
|
104
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Parse date or duration from Retry-After header
|
|
110
|
+
*
|
|
111
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
|
112
|
+
*
|
|
113
|
+
* @param {string} header Header contents
|
|
114
|
+
* @returns {number} Retry duration in seconds
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
function parseRetryAfterHeader(header) {
|
|
118
|
+
const sec = parseInt(header, 10);
|
|
119
|
+
const date = new Date(header);
|
|
120
|
+
|
|
121
|
+
/* Seconds into the future */
|
|
122
|
+
if (Number.isSafeInteger(sec) && (sec > 0)) {
|
|
123
|
+
return sec;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Future date string */
|
|
127
|
+
if (date instanceof Date && !Number.isNaN(date)) {
|
|
128
|
+
const now = new Date();
|
|
129
|
+
const diff = Math.ceil((date.getTime() - now.getTime()) / 1000);
|
|
130
|
+
|
|
131
|
+
if (diff > 0) {
|
|
132
|
+
return diff;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
105
139
|
/**
|
|
106
140
|
* Find certificate chain with preferred issuer common name
|
|
107
141
|
* - If issuer is found in multiple chains, the closest to root wins
|
|
@@ -161,14 +195,16 @@ function findCertificateChainForIssuer(chains, issuer) {
|
|
|
161
195
|
function formatResponseError(resp) {
|
|
162
196
|
let result;
|
|
163
197
|
|
|
164
|
-
if (resp.data
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
198
|
+
if (resp.data) {
|
|
199
|
+
if (resp.data.error) {
|
|
200
|
+
result = resp.data.error.detail || resp.data.error;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
result = resp.data.detail || JSON.stringify(resp.data);
|
|
204
|
+
}
|
|
169
205
|
}
|
|
170
206
|
|
|
171
|
-
return result.replace(/\n/g, '');
|
|
207
|
+
return (result || '').replace(/\n/g, '');
|
|
172
208
|
}
|
|
173
209
|
|
|
174
210
|
/**
|
|
@@ -296,6 +332,7 @@ async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) {
|
|
|
296
332
|
module.exports = {
|
|
297
333
|
retry,
|
|
298
334
|
parseLinkHeader,
|
|
335
|
+
parseRetryAfterHeader,
|
|
299
336
|
findCertificateChainForIssuer,
|
|
300
337
|
formatResponseError,
|
|
301
338
|
getAuthoritativeDnsResolver,
|
package/types/index.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export interface ClientOptions {
|
|
|
45
45
|
backoffMin?: number;
|
|
46
46
|
backoffMax?: number;
|
|
47
47
|
urlMapping?: UrlMapping;
|
|
48
|
+
signal?: AbortSignal;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export interface ClientExternalAccountBindingOptions {
|
|
@@ -61,6 +62,7 @@ export interface ClientAutoOptions {
|
|
|
61
62
|
skipChallengeVerification?: boolean;
|
|
62
63
|
challengePriority?: string[];
|
|
63
64
|
preferredChain?: string;
|
|
65
|
+
signal?: AbortSignal;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
export class Client {
|