@certd/acme-client 1.37.5 → 1.37.7
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 +18 -3
- package/src/client.js +10 -2
- package/src/index.js +3 -0
- package/src/util.js +18 -18
- package/src/verify.js +23 -13
- package/types/index.d.ts +5 -1
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.37.
|
|
6
|
+
"version": "1.37.7",
|
|
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.37.
|
|
21
|
+
"@certd/basic": "^1.37.7",
|
|
22
22
|
"@peculiar/x509": "^1.11.0",
|
|
23
23
|
"asn1js": "^3.0.5",
|
|
24
24
|
"axios": "^1.7.2",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"bugs": {
|
|
71
71
|
"url": "https://github.com/publishlab/node-acme-client/issues"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "55d2a1f09b617bc73bd81a65796446c4602ed1b2"
|
|
74
74
|
}
|
package/src/auto.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
import { readCsrDomains } from "./crypto/index.js";
|
|
5
5
|
import { wait } from "./wait.js";
|
|
6
6
|
import { CancelError } from "./error.js";
|
|
7
|
+
import { domainUtils } from '@certd/basic';
|
|
8
|
+
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
const defaultOpts = {
|
|
@@ -65,7 +68,7 @@ export default async (client, userOpts) => {
|
|
|
65
68
|
* Parse domains from CSR
|
|
66
69
|
*/
|
|
67
70
|
|
|
68
|
-
log("[auto] Parsing domains from Certificate Signing Request
|
|
71
|
+
log("[auto] Parsing domains from Certificate Signing Request");
|
|
69
72
|
const { commonName, altNames } = readCsrDomains(opts.csr);
|
|
70
73
|
const uniqueDomains = Array.from(new Set([commonName].concat(altNames).filter((d) => d)));
|
|
71
74
|
|
|
@@ -76,9 +79,21 @@ export default async (client, userOpts) => {
|
|
|
76
79
|
*/
|
|
77
80
|
|
|
78
81
|
log("[auto] Placing new certificate order with ACME provider");
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
|
|
83
|
+
let hasIp = false
|
|
84
|
+
const orderPayload = { identifiers: uniqueDomains.map((d) =>{
|
|
85
|
+
// 判断是否为IP(v4或v6),否则按域名处理
|
|
86
|
+
const type = domainUtils.isIp(d) ? 'ip' : 'dns';
|
|
87
|
+
if(type === 'ip'){
|
|
88
|
+
hasIp = true
|
|
89
|
+
}
|
|
90
|
+
return { type, value: d }
|
|
91
|
+
}) };
|
|
92
|
+
if (opts.profile && client.sslProvider.startsWith("letsencrypt") ){
|
|
81
93
|
orderPayload.profile = opts.profile;
|
|
94
|
+
if(hasIp){
|
|
95
|
+
orderPayload.profile = "shortlived"
|
|
96
|
+
}
|
|
82
97
|
}
|
|
83
98
|
const order = await client.createOrder(orderPayload);
|
|
84
99
|
const authorizations = await client.getAuthorizations(order);
|
package/src/client.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createHash } from 'crypto';
|
|
|
7
7
|
import { getPemBodyAsB64u } from './crypto/index.js';
|
|
8
8
|
import HttpClient from './http.js';
|
|
9
9
|
import AcmeApi from './api.js';
|
|
10
|
-
import
|
|
10
|
+
import {createChallengeFn} from './verify.js';
|
|
11
11
|
import * as util from './util.js';
|
|
12
12
|
import auto from './auto.js';
|
|
13
13
|
import { CancelError } from './error.js';
|
|
@@ -492,6 +492,9 @@ class AcmeClient {
|
|
|
492
492
|
throw new Error('Unable to verify ACME challenge, URL not found');
|
|
493
493
|
}
|
|
494
494
|
|
|
495
|
+
const {challenges} = createChallengeFn({logger:this.logger});
|
|
496
|
+
|
|
497
|
+
const verify = challenges
|
|
495
498
|
if (typeof verify[challenge.type] === 'undefined') {
|
|
496
499
|
throw new Error(`Unable to verify ACME challenge, unknown type: ${challenge.type}`);
|
|
497
500
|
}
|
|
@@ -507,7 +510,12 @@ class AcmeClient {
|
|
|
507
510
|
};
|
|
508
511
|
|
|
509
512
|
this.log('Waiting for ACME challenge verification(等待ACME检查验证)');
|
|
510
|
-
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
const log = (...args)=>{
|
|
516
|
+
this.logger.info(...args)
|
|
517
|
+
}
|
|
518
|
+
return util.retry(verifyFn, this.backoffOpts,log);
|
|
511
519
|
}
|
|
512
520
|
|
|
513
521
|
/**
|
package/src/index.js
CHANGED
|
@@ -21,6 +21,9 @@ export const directory = {
|
|
|
21
21
|
staging: 'https://acme-staging-v02.api.letsencrypt.org/directory',
|
|
22
22
|
production: 'https://acme-v02.api.letsencrypt.org/directory',
|
|
23
23
|
},
|
|
24
|
+
letsencrypt_staging: {
|
|
25
|
+
production: 'https://acme-staging-v02.api.letsencrypt.org/directory',
|
|
26
|
+
},
|
|
24
27
|
zerossl: {
|
|
25
28
|
staging: 'https://acme.zerossl.com/v2/DV90',
|
|
26
29
|
production: 'https://acme.zerossl.com/v2/DV90',
|
package/src/util.js
CHANGED
|
@@ -48,7 +48,7 @@ class Backoff {
|
|
|
48
48
|
* @returns {Promise}
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
|
-
async function retryPromise(fn, attempts, backoff) {
|
|
51
|
+
async function retryPromise(fn, attempts, backoff, logger = log) {
|
|
52
52
|
let aborted = false;
|
|
53
53
|
|
|
54
54
|
try {
|
|
@@ -60,12 +60,12 @@ async function retryPromise(fn, attempts, backoff) {
|
|
|
60
60
|
throw e;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
logger(`Promise rejected: ${e.message}`);
|
|
64
64
|
const duration = backoff.duration();
|
|
65
|
-
|
|
65
|
+
logger(`Promise rejected attempt #${backoff.attempts}, ${duration}ms 后重试: ${e.message}`);
|
|
66
66
|
|
|
67
67
|
await new Promise((resolve) => { setTimeout(resolve, duration); });
|
|
68
|
-
return retryPromise(fn, attempts, backoff);
|
|
68
|
+
return retryPromise(fn, attempts, backoff, logger);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -80,9 +80,9 @@ async function retryPromise(fn, attempts, backoff) {
|
|
|
80
80
|
* @returns {Promise}
|
|
81
81
|
*/
|
|
82
82
|
|
|
83
|
-
function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) {
|
|
83
|
+
function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}, logger = log) {
|
|
84
84
|
const backoff = new Backoff({ min, max });
|
|
85
|
-
return retryPromise(fn, attempts, backoff);
|
|
85
|
+
return retryPromise(fn, attempts, backoff, logger);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
@@ -216,21 +216,21 @@ function formatResponseError(resp) {
|
|
|
216
216
|
* @returns {Promise<string>} Root domain name
|
|
217
217
|
*/
|
|
218
218
|
|
|
219
|
-
async function resolveDomainBySoaRecord(recordName) {
|
|
219
|
+
async function resolveDomainBySoaRecord(recordName, logger = log) {
|
|
220
220
|
try {
|
|
221
221
|
await dns.resolveSoa(recordName);
|
|
222
|
-
|
|
222
|
+
logger(`找到${recordName}的SOA记录`);
|
|
223
223
|
return recordName;
|
|
224
224
|
}
|
|
225
225
|
catch (e) {
|
|
226
|
-
|
|
226
|
+
logger(`找不到${recordName}的SOA记录,继续往主域名查找`);
|
|
227
227
|
const parentRecordName = recordName.split('.').slice(1).join('.');
|
|
228
228
|
|
|
229
229
|
if (!parentRecordName.includes('.')) {
|
|
230
230
|
throw new Error('SOA record查找失败');
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
return resolveDomainBySoaRecord(parentRecordName);
|
|
233
|
+
return resolveDomainBySoaRecord(parentRecordName,logger);
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
|
|
@@ -241,18 +241,18 @@ async function resolveDomainBySoaRecord(recordName) {
|
|
|
241
241
|
* @returns {Promise<dns.Resolver>} DNS resolver
|
|
242
242
|
*/
|
|
243
243
|
|
|
244
|
-
async function getAuthoritativeDnsResolver(recordName) {
|
|
245
|
-
|
|
244
|
+
async function getAuthoritativeDnsResolver(recordName, logger = log) {
|
|
245
|
+
logger(`获取域名${recordName}的权威NS服务器: `);
|
|
246
246
|
const resolver = new dns.Resolver();
|
|
247
247
|
|
|
248
248
|
try {
|
|
249
249
|
/* Resolve root domain by SOA */
|
|
250
|
-
const domain = await resolveDomainBySoaRecord(
|
|
250
|
+
const domain = await resolveDomainBySoaRecord(recordNam,logger);
|
|
251
251
|
|
|
252
252
|
/* Resolve authoritative NS addresses */
|
|
253
|
-
|
|
253
|
+
logger(`获取到权威NS服务器name: ${domain}`);
|
|
254
254
|
const nsRecords = await dns.resolveNs(domain);
|
|
255
|
-
|
|
255
|
+
logger(`域名权威NS服务器:${nsRecords}`);
|
|
256
256
|
const nsAddrArray = await Promise.all(nsRecords.map(async (r) => dns.resolve4(r)));
|
|
257
257
|
const nsAddresses = [].concat(...nsAddrArray).filter((a) => a);
|
|
258
258
|
|
|
@@ -261,16 +261,16 @@ async function getAuthoritativeDnsResolver(recordName) {
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
/* Authoritative NS success */
|
|
264
|
-
|
|
264
|
+
logger(`Found ${nsAddresses.length} authoritative NS addresses for domain: ${domain}`);
|
|
265
265
|
resolver.setServers(nsAddresses);
|
|
266
266
|
}
|
|
267
267
|
catch (e) {
|
|
268
|
-
|
|
268
|
+
logger(`Authoritative NS lookup error(获取权威NS服务器地址失败): ${e.message}`);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
/* Return resolver */
|
|
272
272
|
const addresses = resolver.getServers();
|
|
273
|
-
|
|
273
|
+
logger(`DNS resolver addresses(域名的权威NS服务器地址): ${addresses.join(', ')}`);
|
|
274
274
|
|
|
275
275
|
return resolver;
|
|
276
276
|
}
|
package/src/verify.js
CHANGED
|
@@ -4,14 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
import dnsSdk from "dns"
|
|
6
6
|
import https from 'https'
|
|
7
|
-
import {log} 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
10
|
import {isAlpnCertificateAuthorizationValid} from './crypto/index.js'
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
const dns = dnsSdk.promises
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export function createChallengeFn(opts = {}){
|
|
17
|
+
const logger = opts?.logger || {info:defaultLog,error:defaultLog,warn:defaultLog,debug:defaultLog}
|
|
18
|
+
|
|
19
|
+
const log = function(...args){
|
|
20
|
+
logger.info(...args)
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
15
23
|
* Verify ACME HTTP challenge
|
|
16
24
|
*
|
|
17
25
|
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.3
|
|
@@ -112,7 +120,7 @@ async function walkDnsChallengeRecord(recordName, resolver = dns,deep = 0) {
|
|
|
112
120
|
return records
|
|
113
121
|
}
|
|
114
122
|
|
|
115
|
-
|
|
123
|
+
async function walkTxtRecord(recordName,deep = 0) {
|
|
116
124
|
if(deep >5){
|
|
117
125
|
log(`walkTxtRecord too deep (#${deep}) , skip walk`)
|
|
118
126
|
return []
|
|
@@ -136,7 +144,7 @@ export async function walkTxtRecord(recordName,deep = 0) {
|
|
|
136
144
|
try{
|
|
137
145
|
/* Authoritative DNS resolver */
|
|
138
146
|
log(`从域名权威服务器获取TXT解析记录`);
|
|
139
|
-
const authoritativeResolver = await util.getAuthoritativeDnsResolver(recordName);
|
|
147
|
+
const authoritativeResolver = await util.getAuthoritativeDnsResolver(recordName,log);
|
|
140
148
|
const res = await walkDnsChallengeRecord(recordName, authoritativeResolver,deep);
|
|
141
149
|
if (res && res.length > 0) {
|
|
142
150
|
for (const item of res) {
|
|
@@ -173,7 +181,8 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
|
|
|
173
181
|
recordValues = [...new Set(recordValues)];
|
|
174
182
|
log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录:${recordValues}`);
|
|
175
183
|
if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
|
|
176
|
-
|
|
184
|
+
const err = `没有找到需要的DNS TXT记录: ${recordName},期望:${keyAuthorization},结果:${recordValues}`
|
|
185
|
+
throw new Error(err);
|
|
177
186
|
}
|
|
178
187
|
|
|
179
188
|
log(`关键授权匹配成功(${challenge.type}/${recordName}):${keyAuthorization},校验成功, ACME challenge verified`);
|
|
@@ -207,12 +216,13 @@ async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
|
|
|
207
216
|
return true;
|
|
208
217
|
}
|
|
209
218
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
219
|
+
return {
|
|
220
|
+
challenges:{
|
|
221
|
+
'http-01': verifyHttpChallenge,
|
|
222
|
+
'dns-01': verifyDnsChallenge,
|
|
223
|
+
'tls-alpn-01': verifyTlsAlpnChallenge,
|
|
224
|
+
},
|
|
225
|
+
walkTxtRecord,
|
|
226
|
+
}
|
|
213
227
|
|
|
214
|
-
|
|
215
|
-
'http-01': verifyHttpChallenge,
|
|
216
|
-
'dns-01': verifyDnsChallenge,
|
|
217
|
-
'tls-alpn-01': verifyTlsAlpnChallenge,
|
|
218
|
-
};
|
|
228
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -108,6 +108,9 @@ export const directory: {
|
|
|
108
108
|
staging: string,
|
|
109
109
|
production: string
|
|
110
110
|
},
|
|
111
|
+
letsencrypt_staging: {
|
|
112
|
+
production: string
|
|
113
|
+
},
|
|
111
114
|
zerossl: {
|
|
112
115
|
staging: string,
|
|
113
116
|
production: string
|
|
@@ -204,7 +207,8 @@ export const agents: any;
|
|
|
204
207
|
|
|
205
208
|
export function setLogger(fn: (message: any, ...args: any[]) => void): void;
|
|
206
209
|
|
|
207
|
-
export function
|
|
210
|
+
export function createChallengeFn(opts?: {logger?:any}): any;
|
|
211
|
+
// export function walkTxtRecord(record: any): Promise<string[]>;
|
|
208
212
|
export function getAuthoritativeDnsResolver(record:string): Promise<any>;
|
|
209
213
|
|
|
210
214
|
export const CancelError: typeof CancelError;
|