certctl-cli 1.0.5 → 1.0.6
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/API.md +600 -0
- package/README.md +59 -4
- package/bin/certctl-darwin-amd64 +0 -0
- package/bin/certctl-darwin-arm64 +0 -0
- package/bin/certctl-linux-amd64 +0 -0
- package/bin/certctl-linux-arm64 +0 -0
- package/bin/certctl-windows-amd64.exe +0 -0
- package/examples/README.md +84 -0
- package/examples/basic-usage.js +231 -0
- package/examples/express-server.js +398 -0
- package/package.json +4 -2
package/API.md
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
# Certctl Programmatic API 使用指南
|
|
2
|
+
|
|
3
|
+
如果你在 Node.js 项目中需要通过代码调用证书申请功能,有以下两种方式:
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 方式一:通过子进程调用 CLI(推荐,当前可用)
|
|
8
|
+
|
|
9
|
+
这是目前最稳定的方式,直接调用 `certctl` 命令。
|
|
10
|
+
|
|
11
|
+
### 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install certctl-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 基础用法
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
const { spawn } = require('child_process');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
// 获取 certctl 二进制路径
|
|
24
|
+
function getCertctlPath() {
|
|
25
|
+
return path.join(__dirname, 'node_modules', '.bin', 'certctl');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 申请证书
|
|
30
|
+
* @param {Object} options - 配置选项
|
|
31
|
+
* @param {string} options.domain - 域名 (必填)
|
|
32
|
+
* @param {string} options.email - 邮箱 (必填)
|
|
33
|
+
* @param {string} options.output - 输出目录 (可选,默认 ./certs)
|
|
34
|
+
* @param {string} options.dns - DNS 提供商 (可选: aliyun/tencentcloud)
|
|
35
|
+
* @param {Object} options.credentials - DNS 凭证 (可选)
|
|
36
|
+
*/
|
|
37
|
+
function applyCertificate(options) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const args = [
|
|
40
|
+
'apply',
|
|
41
|
+
'-d', options.domain,
|
|
42
|
+
'-e', options.email,
|
|
43
|
+
'-o', options.output || './certs'
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// 自动 DNS 验证
|
|
47
|
+
if (options.dns) {
|
|
48
|
+
args.push('--dns', options.dns);
|
|
49
|
+
|
|
50
|
+
if (options.dns === 'aliyun' && options.credentials) {
|
|
51
|
+
args.push(
|
|
52
|
+
'--ali-key', options.credentials.accessKeyId,
|
|
53
|
+
'--ali-secret', options.credentials.accessKeySecret
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (options.dns === 'tencentcloud' && options.credentials) {
|
|
58
|
+
args.push(
|
|
59
|
+
'--tencent-id', options.credentials.secretId,
|
|
60
|
+
'--tencent-secret', options.credentials.secretKey
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const certctl = spawn(getCertctlPath(), args, {
|
|
66
|
+
stdio: ['inherit', 'pipe', 'pipe']
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
let stdout = '';
|
|
70
|
+
let stderr = '';
|
|
71
|
+
|
|
72
|
+
certctl.stdout.on('data', (data) => {
|
|
73
|
+
stdout += data.toString();
|
|
74
|
+
console.log(data.toString()); // 实时输出日志
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
certctl.stderr.on('data', (data) => {
|
|
78
|
+
stderr += data.toString();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
certctl.on('close', (code) => {
|
|
82
|
+
if (code === 0) {
|
|
83
|
+
resolve({
|
|
84
|
+
success: true,
|
|
85
|
+
domain: options.domain,
|
|
86
|
+
output: options.output || './certs',
|
|
87
|
+
stdout
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
reject(new Error(`证书申请失败: ${stderr || stdout}`));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 使用示例
|
|
97
|
+
async function main() {
|
|
98
|
+
try {
|
|
99
|
+
// 示例 1: 阿里云自动验证
|
|
100
|
+
const result = await applyCertificate({
|
|
101
|
+
domain: 'example.com',
|
|
102
|
+
email: 'admin@example.com',
|
|
103
|
+
output: './my-certs',
|
|
104
|
+
dns: 'aliyun',
|
|
105
|
+
credentials: {
|
|
106
|
+
accessKeyId: process.env.ALICLOUD_ACCESS_KEY,
|
|
107
|
+
accessKeySecret: process.env.ALICLOUD_SECRET_KEY
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
console.log('证书申请成功:', result);
|
|
112
|
+
|
|
113
|
+
// 证书文件位置
|
|
114
|
+
const certPath = `./my-certs/${result.domain}/${result.domain}.pem`;
|
|
115
|
+
const keyPath = `./my-certs/${result.domain}/${result.domain}.key`;
|
|
116
|
+
|
|
117
|
+
console.log('证书路径:', certPath);
|
|
118
|
+
console.log('私钥路径:', keyPath);
|
|
119
|
+
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error('申请失败:', error.message);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
main();
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 续期证书
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
const { spawn } = require('child_process');
|
|
132
|
+
|
|
133
|
+
function renewCertificate(domain, outputDir = './certs') {
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const certctl = spawn('certctl', [
|
|
136
|
+
'renew',
|
|
137
|
+
'-d', domain,
|
|
138
|
+
'-o', outputDir
|
|
139
|
+
], {
|
|
140
|
+
stdio: 'pipe'
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
let output = '';
|
|
144
|
+
certctl.stdout.on('data', (data) => {
|
|
145
|
+
output += data.toString();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
certctl.on('close', (code) => {
|
|
149
|
+
if (code === 0) {
|
|
150
|
+
resolve({ success: true, output });
|
|
151
|
+
} else {
|
|
152
|
+
reject(new Error(`续期失败: ${output}`));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 使用
|
|
159
|
+
renewCertificate('example.com', './my-certs')
|
|
160
|
+
.then(() => console.log('续期成功'))
|
|
161
|
+
.catch(err => console.error(err));
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 验证证书
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
const { spawn } = require('child_process');
|
|
168
|
+
|
|
169
|
+
function verifyCertificate(domain, certPath) {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const args = ['verify', '-d', domain];
|
|
172
|
+
if (certPath) {
|
|
173
|
+
args.push('-p', certPath);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const certctl = spawn('certctl', args, {
|
|
177
|
+
stdio: 'pipe'
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
let output = '';
|
|
181
|
+
certctl.stdout.on('data', (data) => {
|
|
182
|
+
output += data.toString();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
certctl.on('close', (code) => {
|
|
186
|
+
resolve({
|
|
187
|
+
valid: code === 0,
|
|
188
|
+
output
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 使用
|
|
195
|
+
verifyCertificate('example.com')
|
|
196
|
+
.then(result => {
|
|
197
|
+
console.log('验证结果:', result.valid ? '通过' : '失败');
|
|
198
|
+
console.log(result.output);
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### 修复证书链
|
|
203
|
+
|
|
204
|
+
```javascript
|
|
205
|
+
const { spawn } = require('child_process');
|
|
206
|
+
|
|
207
|
+
function fixCertificateChain(domain, certPath) {
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const args = ['fix-chain'];
|
|
210
|
+
|
|
211
|
+
if (certPath) {
|
|
212
|
+
args.push('-p', certPath);
|
|
213
|
+
} else if (domain) {
|
|
214
|
+
args.push('-d', domain);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const certctl = spawn('certctl', args, {
|
|
218
|
+
stdio: 'pipe'
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
let output = '';
|
|
222
|
+
certctl.stdout.on('data', (data) => {
|
|
223
|
+
output += data.toString();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
certctl.on('close', (code) => {
|
|
227
|
+
if (code === 0) {
|
|
228
|
+
resolve({ success: true, output });
|
|
229
|
+
} else {
|
|
230
|
+
reject(new Error(`修复失败: ${output}`));
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## 方式二:完整的 Node.js SDK(需要额外开发)
|
|
240
|
+
|
|
241
|
+
如果你需要更优雅的 API,可以开发一个包装器。以下是一个设计提案:
|
|
242
|
+
|
|
243
|
+
### 安装方式
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
npm install certctl-sdk
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 期望的 API 设计
|
|
250
|
+
|
|
251
|
+
```javascript
|
|
252
|
+
const { CertctlClient } = require('certctl-sdk');
|
|
253
|
+
|
|
254
|
+
// 创建客户端
|
|
255
|
+
const client = new CertctlClient({
|
|
256
|
+
email: 'admin@example.com',
|
|
257
|
+
outputDir: './certs',
|
|
258
|
+
staging: false // 是否为测试环境
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// 申请证书
|
|
262
|
+
const cert = await client.apply({
|
|
263
|
+
domain: 'example.com',
|
|
264
|
+
dns: {
|
|
265
|
+
provider: 'aliyun',
|
|
266
|
+
accessKeyId: 'your-key',
|
|
267
|
+
accessKeySecret: 'your-secret'
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
console.log(cert.paths);
|
|
272
|
+
// {
|
|
273
|
+
// cert: './certs/example.com/example.com.pem',
|
|
274
|
+
// key: './certs/example.com/example.com.key'
|
|
275
|
+
// }
|
|
276
|
+
|
|
277
|
+
// 续期
|
|
278
|
+
await client.renew('example.com');
|
|
279
|
+
|
|
280
|
+
// 验证
|
|
281
|
+
const isValid = await client.verify('example.com');
|
|
282
|
+
|
|
283
|
+
// 修复证书链
|
|
284
|
+
await client.fixChain('example.com');
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### 是否需要开发 SDK?
|
|
288
|
+
|
|
289
|
+
| 场景 | 建议 |
|
|
290
|
+
|------|------|
|
|
291
|
+
| 只是简单调用 | 使用方式一(子进程)即可 |
|
|
292
|
+
| 需要深度集成 | 可以开发 `certctl-sdk` |
|
|
293
|
+
| 需要事件通知 | 子进程方式可以实时获取输出 |
|
|
294
|
+
| 需要类型支持 | 可以添加 TypeScript 定义 |
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## TypeScript 类型定义
|
|
299
|
+
|
|
300
|
+
如果你使用 TypeScript,以下是类型定义:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// certctl.d.ts
|
|
304
|
+
|
|
305
|
+
export interface ApplyOptions {
|
|
306
|
+
domain: string;
|
|
307
|
+
email: string;
|
|
308
|
+
output?: string;
|
|
309
|
+
dns?: 'aliyun' | 'tencentcloud';
|
|
310
|
+
credentials?: {
|
|
311
|
+
accessKeyId?: string;
|
|
312
|
+
accessKeySecret?: string;
|
|
313
|
+
secretId?: string;
|
|
314
|
+
secretKey?: string;
|
|
315
|
+
};
|
|
316
|
+
staging?: boolean;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface ApplyResult {
|
|
320
|
+
success: boolean;
|
|
321
|
+
domain: string;
|
|
322
|
+
output: string;
|
|
323
|
+
stdout: string;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export interface VerifyResult {
|
|
327
|
+
valid: boolean;
|
|
328
|
+
output: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export declare function applyCertificate(options: ApplyOptions): Promise<ApplyResult>;
|
|
332
|
+
export declare function renewCertificate(domain: string, outputDir?: string): Promise<{ success: boolean; output: string }>;
|
|
333
|
+
export declare function verifyCertificate(domain: string, certPath?: string): Promise<VerifyResult>;
|
|
334
|
+
export declare function fixCertificateChain(domain?: string, certPath?: string): Promise<{ success: boolean; output: string }>;
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## 完整项目示例
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
my-project/
|
|
343
|
+
├── src/
|
|
344
|
+
│ ├── certctl-wrapper.js # 封装 certctl 调用
|
|
345
|
+
│ ├── server.js # 你的应用
|
|
346
|
+
│ └── config.js
|
|
347
|
+
├── certs/ # 证书输出目录
|
|
348
|
+
├── package.json
|
|
349
|
+
└── .env
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### certctl-wrapper.js
|
|
353
|
+
|
|
354
|
+
```javascript
|
|
355
|
+
const { spawn } = require('child_process');
|
|
356
|
+
const path = require('path');
|
|
357
|
+
const fs = require('fs');
|
|
358
|
+
|
|
359
|
+
class CertctlWrapper {
|
|
360
|
+
constructor(options = {}) {
|
|
361
|
+
this.email = options.email;
|
|
362
|
+
this.outputDir = options.outputDir || './certs';
|
|
363
|
+
this.binaryPath = options.binaryPath || 'certctl';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
_spawn(args) {
|
|
367
|
+
return new Promise((resolve, reject) => {
|
|
368
|
+
const proc = spawn(this.binaryPath, args, {
|
|
369
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
let stdout = '';
|
|
373
|
+
let stderr = '';
|
|
374
|
+
|
|
375
|
+
proc.stdout.on('data', (data) => {
|
|
376
|
+
stdout += data.toString();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
proc.stderr.on('data', (data) => {
|
|
380
|
+
stderr += data.toString();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
proc.on('close', (code) => {
|
|
384
|
+
if (code === 0) {
|
|
385
|
+
resolve({ stdout, stderr, code });
|
|
386
|
+
} else {
|
|
387
|
+
reject(new Error(`Process exited with code ${code}: ${stderr || stdout}`));
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async apply(domain, dnsOptions = null) {
|
|
394
|
+
const args = [
|
|
395
|
+
'apply',
|
|
396
|
+
'-d', domain,
|
|
397
|
+
'-e', this.email,
|
|
398
|
+
'-o', this.outputDir
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
if (dnsOptions) {
|
|
402
|
+
args.push('--dns', dnsOptions.provider);
|
|
403
|
+
|
|
404
|
+
if (dnsOptions.provider === 'aliyun') {
|
|
405
|
+
args.push(
|
|
406
|
+
'--ali-key', dnsOptions.accessKeyId,
|
|
407
|
+
'--ali-secret', dnsOptions.accessKeySecret
|
|
408
|
+
);
|
|
409
|
+
} else if (dnsOptions.provider === 'tencentcloud') {
|
|
410
|
+
args.push(
|
|
411
|
+
'--tencent-id', dnsOptions.secretId,
|
|
412
|
+
'--tencent-secret', dnsOptions.secretKey
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const result = await this._spawn(args);
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
success: true,
|
|
421
|
+
domain,
|
|
422
|
+
paths: {
|
|
423
|
+
cert: path.join(this.outputDir, domain, `${domain}.pem`),
|
|
424
|
+
key: path.join(this.outputDir, domain, `${domain}.key`)
|
|
425
|
+
},
|
|
426
|
+
output: result.stdout
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async renew(domain) {
|
|
431
|
+
const args = ['renew', '-d', domain, '-o', this.outputDir];
|
|
432
|
+
const result = await this._spawn(args);
|
|
433
|
+
return { success: true, output: result.stdout };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async verify(domain) {
|
|
437
|
+
const args = ['verify', '-d', domain];
|
|
438
|
+
try {
|
|
439
|
+
const result = await this._spawn(args);
|
|
440
|
+
return { valid: true, output: result.stdout };
|
|
441
|
+
} catch (err) {
|
|
442
|
+
return { valid: false, output: err.message };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async fixChain(domain) {
|
|
447
|
+
const args = ['fix-chain', '-d', domain];
|
|
448
|
+
const result = await this._spawn(args);
|
|
449
|
+
return { success: true, output: result.stdout };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
getCertificatePaths(domain) {
|
|
453
|
+
return {
|
|
454
|
+
cert: path.join(this.outputDir, domain, `${domain}.pem`),
|
|
455
|
+
key: path.join(this.outputDir, domain, `${domain}.key`)
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
exists(domain) {
|
|
460
|
+
const paths = this.getCertificatePaths(domain);
|
|
461
|
+
return fs.existsSync(paths.cert) && fs.existsSync(paths.key);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
module.exports = { CertctlWrapper };
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### server.js
|
|
469
|
+
|
|
470
|
+
```javascript
|
|
471
|
+
const { CertctlWrapper } = require('./certctl-wrapper');
|
|
472
|
+
const express = require('express');
|
|
473
|
+
|
|
474
|
+
const app = express();
|
|
475
|
+
const certClient = new CertctlWrapper({
|
|
476
|
+
email: process.env.ACME_EMAIL,
|
|
477
|
+
outputDir: './certs'
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// API: 申请证书
|
|
481
|
+
app.post('/api/certs/:domain', async (req, res) => {
|
|
482
|
+
try {
|
|
483
|
+
const { domain } = req.params;
|
|
484
|
+
|
|
485
|
+
// 检查是否已存在
|
|
486
|
+
if (certClient.exists(domain)) {
|
|
487
|
+
return res.json({
|
|
488
|
+
success: true,
|
|
489
|
+
message: '证书已存在',
|
|
490
|
+
paths: certClient.getCertificatePaths(domain)
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// 申请证书
|
|
495
|
+
const result = await certClient.apply(domain, {
|
|
496
|
+
provider: 'aliyun',
|
|
497
|
+
accessKeyId: process.env.ALI_KEY,
|
|
498
|
+
accessKeySecret: process.env.ALI_SECRET
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
res.json(result);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
res.status(500).json({ success: false, error: error.message });
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// API: 获取证书信息
|
|
508
|
+
app.get('/api/certs/:domain', async (req, res) => {
|
|
509
|
+
try {
|
|
510
|
+
const { domain } = req.params;
|
|
511
|
+
const verifyResult = await certClient.verify(domain);
|
|
512
|
+
|
|
513
|
+
res.json({
|
|
514
|
+
domain,
|
|
515
|
+
exists: certClient.exists(domain),
|
|
516
|
+
valid: verifyResult.valid,
|
|
517
|
+
paths: certClient.getCertificatePaths(domain)
|
|
518
|
+
});
|
|
519
|
+
} catch (error) {
|
|
520
|
+
res.status(500).json({ error: error.message });
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
app.listen(3000, () => {
|
|
525
|
+
console.log('Server running on http://localhost:3000');
|
|
526
|
+
});
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
## 常见问题
|
|
532
|
+
|
|
533
|
+
### Q: 如何在 Docker 中使用?
|
|
534
|
+
|
|
535
|
+
```dockerfile
|
|
536
|
+
FROM node:18-alpine
|
|
537
|
+
|
|
538
|
+
# 安装 certctl
|
|
539
|
+
RUN npm install -g certctl-cli
|
|
540
|
+
|
|
541
|
+
WORKDIR /app
|
|
542
|
+
COPY package*.json ./
|
|
543
|
+
RUN npm install
|
|
544
|
+
|
|
545
|
+
COPY . .
|
|
546
|
+
|
|
547
|
+
CMD ["node", "server.js"]
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### Q: 如何监控证书到期?
|
|
551
|
+
|
|
552
|
+
```javascript
|
|
553
|
+
const fs = require('fs');
|
|
554
|
+
const { spawn } = require('child_process');
|
|
555
|
+
|
|
556
|
+
// 定期检查证书有效期
|
|
557
|
+
function checkCertExpiry(domain, certPath) {
|
|
558
|
+
return new Promise((resolve) => {
|
|
559
|
+
const certctl = spawn('certctl', ['verify', '-d', domain, '-p', certPath]);
|
|
560
|
+
let output = '';
|
|
561
|
+
|
|
562
|
+
certctl.stdout.on('data', (data) => {
|
|
563
|
+
output += data.toString();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
certctl.on('close', () => {
|
|
567
|
+
// 解析输出获取剩余天数
|
|
568
|
+
const match = output.match(/剩余 (\d+) 天/);
|
|
569
|
+
if (match) {
|
|
570
|
+
const days = parseInt(match[1]);
|
|
571
|
+
resolve({ days, needsRenew: days <= 30 });
|
|
572
|
+
} else {
|
|
573
|
+
resolve({ days: 0, needsRenew: true });
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// 定时任务
|
|
580
|
+
setInterval(async () => {
|
|
581
|
+
const domains = ['example.com', 'api.example.com'];
|
|
582
|
+
|
|
583
|
+
for (const domain of domains) {
|
|
584
|
+
const status = await checkCertExpiry(domain, `./certs/${domain}/${domain}.pem`);
|
|
585
|
+
|
|
586
|
+
if (status.needsRenew) {
|
|
587
|
+
console.log(`证书 ${domain} 即将过期 (${status.days} 天),开始续期...`);
|
|
588
|
+
await renewCertificate(domain);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}, 24 * 60 * 60 * 1000); // 每天检查一次
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## 下一步
|
|
597
|
+
|
|
598
|
+
1. **当前可用**:使用方式一(子进程调用)
|
|
599
|
+
2. **未来规划**:如果需要真正的 Node.js SDK,可以开发 `certctl-sdk` 包
|
|
600
|
+
3. **需求反馈**:如果有特定需求,请提交 Issue
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# certctl-cli
|
|
2
2
|
|
|
3
|
-
轻量级 SSL 证书申请工具 / Lightweight SSL Certificate
|
|
3
|
+
轻量级 SSL 证书申请工具 / Lightweight SSL Certificate Tool
|
|
4
4
|
|
|
5
|
-
支持通配符证书申请 &
|
|
5
|
+
支持通配符证书申请 & 阿里云/腾讯云 DNS 自动验证。
|
|
6
6
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
npm install -g certctl-cli
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
## 使用
|
|
13
|
+
## CLI 使用
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
# 交互式菜单
|
|
@@ -22,16 +22,71 @@ certctl apply -d example.com -e admin@example.com
|
|
|
22
22
|
# 阿里云 DNS 自动验证
|
|
23
23
|
certctl apply -d example.com --dns aliyun --ali-key YOUR_KEY --ali-secret YOUR_SECRET
|
|
24
24
|
|
|
25
|
+
# 腾讯云 DNS 自动验证
|
|
26
|
+
certctl apply -d example.com --dns tencentcloud --tencent-id YOUR_ID --tencent-secret YOUR_KEY
|
|
27
|
+
|
|
25
28
|
# 查看证书
|
|
26
29
|
certctl list
|
|
27
30
|
|
|
28
31
|
# 续期证书
|
|
29
32
|
certctl renew
|
|
33
|
+
|
|
34
|
+
# 验证证书
|
|
35
|
+
certctl verify -d example.com
|
|
36
|
+
|
|
37
|
+
# 修复证书链(自动补全中间证书)
|
|
38
|
+
certctl fix-chain -d example.com
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Programmatic API(在代码中调用)
|
|
42
|
+
|
|
43
|
+
如果你需要在 Node.js 项目中通过代码调用证书申请功能,请参考:
|
|
44
|
+
|
|
45
|
+
📚 **[完整 API 文档](./API.md)**
|
|
46
|
+
|
|
47
|
+
快速示例:
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
const { spawn } = require('child_process');
|
|
51
|
+
|
|
52
|
+
// 申请证书
|
|
53
|
+
const certctl = spawn('certctl', [
|
|
54
|
+
'apply',
|
|
55
|
+
'-d', 'example.com',
|
|
56
|
+
'-e', 'admin@example.com',
|
|
57
|
+
'--dns', 'aliyun',
|
|
58
|
+
'--ali-key', process.env.ALI_KEY,
|
|
59
|
+
'--ali-secret', process.env.ALI_SECRET
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
certctl.on('close', (code) => {
|
|
63
|
+
if (code === 0) {
|
|
64
|
+
console.log('证书申请成功!');
|
|
65
|
+
console.log('证书位置: ./certs/example.com/example.com.pem');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
30
68
|
```
|
|
31
69
|
|
|
70
|
+
完整封装示例见 [API.md](./API.md)。
|
|
71
|
+
|
|
72
|
+
## 特性
|
|
73
|
+
|
|
74
|
+
- 🔐 **通配符证书**:支持 `*.example.com`
|
|
75
|
+
- 🤖 **自动 DNS 验证**:阿里云/腾讯云 DNS 自动验证
|
|
76
|
+
- 🛠️ **证书链修复**:自动补全缺失的中间证书
|
|
77
|
+
- 📱 **微信小程序兼容**:确保证书链完整
|
|
78
|
+
- 🌍 **多语言**:中英文界面
|
|
79
|
+
- 🚀 **轻量快速**:Go 实现,单二进制文件
|
|
80
|
+
|
|
81
|
+
## 环境要求
|
|
82
|
+
|
|
83
|
+
- Node.js >= 14
|
|
84
|
+
- 支持 macOS、Linux、Windows
|
|
85
|
+
|
|
32
86
|
## 文档
|
|
33
87
|
|
|
34
|
-
|
|
88
|
+
- CLI 文档:https://github.com/Heartbeatc/certctl#readme
|
|
89
|
+
- API 文档:[API.md](./API.md)
|
|
35
90
|
|
|
36
91
|
## License
|
|
37
92
|
|
package/bin/certctl-darwin-amd64
CHANGED
|
Binary file
|
package/bin/certctl-darwin-arm64
CHANGED
|
Binary file
|
package/bin/certctl-linux-amd64
CHANGED
|
Binary file
|
package/bin/certctl-linux-arm64
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Certctl Node.js 示例
|
|
2
|
+
|
|
3
|
+
这里提供了在 Node.js 项目中使用 certctl 的各种示例。
|
|
4
|
+
|
|
5
|
+
## 示例列表
|
|
6
|
+
|
|
7
|
+
### 1. basic-usage.js - 基础使用示例
|
|
8
|
+
|
|
9
|
+
展示如何在 Node.js 代码中调用 certctl 命令。
|
|
10
|
+
|
|
11
|
+
**功能:**
|
|
12
|
+
- 申请证书(支持阿里云自动验证)
|
|
13
|
+
- 验证证书
|
|
14
|
+
- 修复证书链
|
|
15
|
+
- 续期证书
|
|
16
|
+
|
|
17
|
+
**运行:**
|
|
18
|
+
```bash
|
|
19
|
+
# 使用手动验证
|
|
20
|
+
node basic-usage.js example.com
|
|
21
|
+
|
|
22
|
+
# 使用阿里云自动验证(需设置环境变量)
|
|
23
|
+
export ALI_KEY=your-access-key
|
|
24
|
+
export ALI_SECRET=your-access-secret
|
|
25
|
+
export ACME_EMAIL=admin@example.com
|
|
26
|
+
node basic-usage.js example.com
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. express-server.js - Express HTTP API
|
|
30
|
+
|
|
31
|
+
提供一个完整的 HTTP API 服务,通过 REST API 管理证书。
|
|
32
|
+
|
|
33
|
+
**功能:**
|
|
34
|
+
- 列出所有证书
|
|
35
|
+
- 申请新证书
|
|
36
|
+
- 续期证书
|
|
37
|
+
- 验证证书
|
|
38
|
+
- 修复证书链
|
|
39
|
+
- 下载证书文件
|
|
40
|
+
|
|
41
|
+
**安装:**
|
|
42
|
+
```bash
|
|
43
|
+
npm install express certctl-cli
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**运行:**
|
|
47
|
+
```bash
|
|
48
|
+
node express-server.js
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**API 调用示例:**
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# 列出所有证书
|
|
55
|
+
curl http://localhost:3000/api/certs
|
|
56
|
+
|
|
57
|
+
# 申请证书(阿里云自动验证)
|
|
58
|
+
curl -X POST http://localhost:3000/api/certs/example.com \
|
|
59
|
+
-H "Content-Type: application/json" \
|
|
60
|
+
-d '{
|
|
61
|
+
"dns": {
|
|
62
|
+
"provider": "aliyun",
|
|
63
|
+
"accessKeyId": "your-key",
|
|
64
|
+
"accessKeySecret": "your-secret"
|
|
65
|
+
}
|
|
66
|
+
}'
|
|
67
|
+
|
|
68
|
+
# 验证证书
|
|
69
|
+
curl -X POST http://localhost:3000/api/certs/example.com/verify
|
|
70
|
+
|
|
71
|
+
# 续期证书
|
|
72
|
+
curl -X POST http://localhost:3000/api/certs/example.com/renew
|
|
73
|
+
|
|
74
|
+
# 修复证书链
|
|
75
|
+
curl -X POST http://localhost:3000/api/certs/example.com/fix-chain
|
|
76
|
+
|
|
77
|
+
# 下载证书
|
|
78
|
+
curl http://localhost:3000/api/certs/example.com/download?type=cert -o cert.pem
|
|
79
|
+
curl http://localhost:3000/api/certs/example.com/download?type=key -o key.pem
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 完整 API 文档
|
|
83
|
+
|
|
84
|
+
更多详细信息请参考:[API.md](../API.md)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Certctl Node.js 基础使用示例
|
|
3
|
+
*
|
|
4
|
+
* 运行前请确保:
|
|
5
|
+
* 1. npm install certctl-cli
|
|
6
|
+
* 2. 设置环境变量:ALI_KEY 和 ALI_SECRET(如果使用阿里云)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { spawn } = require('child_process');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
// ==================== 配置 ====================
|
|
14
|
+
const CONFIG = {
|
|
15
|
+
email: process.env.ACME_EMAIL || 'admin@example.com',
|
|
16
|
+
outputDir: './my-certificates',
|
|
17
|
+
// 阿里云配置
|
|
18
|
+
aliyun: {
|
|
19
|
+
accessKeyId: process.env.ALI_KEY,
|
|
20
|
+
accessKeySecret: process.env.ALI_SECRET
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ==================== 工具函数 ====================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 执行 certctl 命令
|
|
28
|
+
*/
|
|
29
|
+
function runCertctl(args) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
console.log(`执行: certctl ${args.join(' ')}`);
|
|
32
|
+
|
|
33
|
+
const certctl = spawn('certctl', args, {
|
|
34
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let stdout = '';
|
|
38
|
+
let stderr = '';
|
|
39
|
+
|
|
40
|
+
certctl.stdout.on('data', (data) => {
|
|
41
|
+
stdout += data.toString();
|
|
42
|
+
console.log(data.toString());
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
certctl.stderr.on('data', (data) => {
|
|
46
|
+
stderr += data.toString();
|
|
47
|
+
console.error(data.toString());
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
certctl.on('close', (code) => {
|
|
51
|
+
if (code === 0) {
|
|
52
|
+
resolve({ stdout, stderr, code });
|
|
53
|
+
} else {
|
|
54
|
+
reject(new Error(`命令失败 (code ${code}): ${stderr || stdout}`));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 申请证书
|
|
62
|
+
*/
|
|
63
|
+
async function applyCertificate(domain) {
|
|
64
|
+
console.log(`\n📝 正在为 ${domain} 申请证书...\n`);
|
|
65
|
+
|
|
66
|
+
const args = [
|
|
67
|
+
'apply',
|
|
68
|
+
'-d', domain,
|
|
69
|
+
'-e', CONFIG.email,
|
|
70
|
+
'-o', CONFIG.outputDir
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// 如果配置了阿里云,使用自动验证
|
|
74
|
+
if (CONFIG.aliyun.accessKeyId && CONFIG.aliyun.accessKeySecret) {
|
|
75
|
+
args.push(
|
|
76
|
+
'--dns', 'aliyun',
|
|
77
|
+
'--ali-key', CONFIG.aliyun.accessKeyId,
|
|
78
|
+
'--ali-secret', CONFIG.aliyun.accessKeySecret
|
|
79
|
+
);
|
|
80
|
+
console.log('使用阿里云 DNS 自动验证');
|
|
81
|
+
} else {
|
|
82
|
+
console.log('使用手动 DNS 验证,请按提示添加 TXT 记录');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await runCertctl(args);
|
|
87
|
+
|
|
88
|
+
const certPath = path.join(CONFIG.outputDir, domain, `${domain}.pem`);
|
|
89
|
+
const keyPath = path.join(CONFIG.outputDir, domain, `${domain}.key`);
|
|
90
|
+
|
|
91
|
+
console.log('\n✅ 证书申请成功!');
|
|
92
|
+
console.log(`📄 证书路径: ${certPath}`);
|
|
93
|
+
console.log(`🔑 私钥路径: ${keyPath}`);
|
|
94
|
+
|
|
95
|
+
return { success: true, certPath, keyPath };
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('\n❌ 证书申请失败:', error.message);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 验证证书
|
|
104
|
+
*/
|
|
105
|
+
async function verifyCertificate(domain) {
|
|
106
|
+
console.log(`\n🔍 正在验证 ${domain} 的证书...\n`);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const result = await runCertctl(['verify', '-d', domain, '-o', CONFIG.outputDir]);
|
|
110
|
+
console.log('\n✅ 证书验证通过');
|
|
111
|
+
return { valid: true, output: result.stdout };
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.log('\n⚠️ 证书验证未通过');
|
|
114
|
+
return { valid: false, error: error.message };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 修复证书链
|
|
120
|
+
*/
|
|
121
|
+
async function fixCertificateChain(domain) {
|
|
122
|
+
console.log(`\n🔧 正在修复 ${domain} 的证书链...\n`);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await runCertctl(['fix-chain', '-d', domain, '-o', CONFIG.outputDir]);
|
|
126
|
+
console.log('\n✅ 证书链修复完成');
|
|
127
|
+
return { success: true };
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('\n❌ 证书链修复失败:', error.message);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 续期证书
|
|
136
|
+
*/
|
|
137
|
+
async function renewCertificate(domain) {
|
|
138
|
+
console.log(`\n🔄 正在续期 ${domain} 的证书...\n`);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await runCertctl(['renew', '-d', domain, '-o', CONFIG.outputDir]);
|
|
142
|
+
console.log('\n✅ 证书续期成功');
|
|
143
|
+
return { success: true };
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('\n❌ 证书续期失败:', error.message);
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 检查证书是否存在
|
|
152
|
+
*/
|
|
153
|
+
function certificateExists(domain) {
|
|
154
|
+
const certPath = path.join(CONFIG.outputDir, domain, `${domain}.pem`);
|
|
155
|
+
const keyPath = path.join(CONFIG.outputDir, domain, `${domain}.key`);
|
|
156
|
+
return fs.existsSync(certPath) && fs.existsSync(keyPath);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 获取证书路径
|
|
161
|
+
*/
|
|
162
|
+
function getCertificatePaths(domain) {
|
|
163
|
+
return {
|
|
164
|
+
cert: path.join(CONFIG.outputDir, domain, `${domain}.pem`),
|
|
165
|
+
key: path.join(CONFIG.outputDir, domain, `${domain}.key`)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ==================== 主程序 ====================
|
|
170
|
+
|
|
171
|
+
async function main() {
|
|
172
|
+
const domain = process.argv[2] || 'example.com';
|
|
173
|
+
|
|
174
|
+
console.log('========================================');
|
|
175
|
+
console.log('Certctl Node.js 示例');
|
|
176
|
+
console.log('========================================');
|
|
177
|
+
console.log(`域名: ${domain}`);
|
|
178
|
+
console.log(`邮箱: ${CONFIG.email}`);
|
|
179
|
+
console.log(`输出目录: ${CONFIG.outputDir}`);
|
|
180
|
+
console.log('========================================\n');
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
// 步骤 1: 检查证书是否已存在
|
|
184
|
+
if (certificateExists(domain)) {
|
|
185
|
+
console.log(`证书已存在,跳过申请步骤`);
|
|
186
|
+
|
|
187
|
+
// 步骤 2: 验证现有证书
|
|
188
|
+
const verifyResult = await verifyCertificate(domain);
|
|
189
|
+
|
|
190
|
+
if (!verifyResult.valid) {
|
|
191
|
+
console.log('证书验证失败,尝试修复...');
|
|
192
|
+
await fixCertificateChain(domain);
|
|
193
|
+
|
|
194
|
+
// 重新验证
|
|
195
|
+
const recheck = await verifyCertificate(domain);
|
|
196
|
+
if (!recheck.valid) {
|
|
197
|
+
console.error('修复后仍无法通过验证');
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
// 步骤 1: 申请证书
|
|
203
|
+
await applyCertificate(domain);
|
|
204
|
+
|
|
205
|
+
// 步骤 2: 验证证书
|
|
206
|
+
await verifyCertificate(domain);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 输出证书信息
|
|
210
|
+
const paths = getCertificatePaths(domain);
|
|
211
|
+
console.log('\n📋 证书信息:');
|
|
212
|
+
console.log(` 域名: ${domain}`);
|
|
213
|
+
console.log(` 证书: ${paths.cert}`);
|
|
214
|
+
console.log(` 私钥: ${paths.key}`);
|
|
215
|
+
|
|
216
|
+
// 读取证书内容(可选)
|
|
217
|
+
if (fs.existsSync(paths.cert)) {
|
|
218
|
+
const stats = fs.statSync(paths.cert);
|
|
219
|
+
console.log(` 大小: ${(stats.size / 1024).toFixed(2)} KB`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log('\n✨ 完成!');
|
|
223
|
+
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error('\n💥 发生错误:', error.message);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 运行
|
|
231
|
+
main();
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Certctl + Express 集成示例
|
|
3
|
+
*
|
|
4
|
+
* 提供 HTTP API 来管理证书
|
|
5
|
+
*
|
|
6
|
+
* 安装依赖:
|
|
7
|
+
* npm install express certctl-cli
|
|
8
|
+
*
|
|
9
|
+
* 运行:
|
|
10
|
+
* node express-server.js
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const express = require('express');
|
|
14
|
+
const { spawn } = require('child_process');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
|
|
18
|
+
const app = express();
|
|
19
|
+
app.use(express.json());
|
|
20
|
+
|
|
21
|
+
// 配置
|
|
22
|
+
const CONFIG = {
|
|
23
|
+
email: process.env.ACME_EMAIL || 'admin@example.com',
|
|
24
|
+
certsDir: process.env.CERTS_DIR || './certs',
|
|
25
|
+
port: process.env.PORT || 3000
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ==================== Certctl 封装类 ====================
|
|
29
|
+
|
|
30
|
+
class CertctlService {
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.email = options.email;
|
|
33
|
+
this.certsDir = options.certsDir;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 执行 certctl 命令
|
|
38
|
+
*/
|
|
39
|
+
exec(args) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const proc = spawn('certctl', args, {
|
|
42
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let stdout = '';
|
|
46
|
+
let stderr = '';
|
|
47
|
+
|
|
48
|
+
proc.stdout.on('data', (data) => stdout += data);
|
|
49
|
+
proc.stderr.on('data', (data) => stderr += data);
|
|
50
|
+
|
|
51
|
+
proc.on('close', (code) => {
|
|
52
|
+
if (code === 0) {
|
|
53
|
+
resolve({ stdout, stderr, code });
|
|
54
|
+
} else {
|
|
55
|
+
reject(new Error(stderr || stdout));
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 申请证书
|
|
63
|
+
*/
|
|
64
|
+
async apply(domain, dnsOptions = null) {
|
|
65
|
+
const args = [
|
|
66
|
+
'apply',
|
|
67
|
+
'-d', domain,
|
|
68
|
+
'-e', this.email,
|
|
69
|
+
'-o', this.certsDir
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
if (dnsOptions?.provider === 'aliyun') {
|
|
73
|
+
args.push(
|
|
74
|
+
'--dns', 'aliyun',
|
|
75
|
+
'--ali-key', dnsOptions.accessKeyId,
|
|
76
|
+
'--ali-secret', dnsOptions.accessKeySecret
|
|
77
|
+
);
|
|
78
|
+
} else if (dnsOptions?.provider === 'tencentcloud') {
|
|
79
|
+
args.push(
|
|
80
|
+
'--dns', 'tencentcloud',
|
|
81
|
+
'--tencent-id', dnsOptions.secretId,
|
|
82
|
+
'--tencent-secret', dnsOptions.secretKey
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await this.exec(args);
|
|
87
|
+
return this.getCertificateInfo(domain);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 验证证书
|
|
92
|
+
*/
|
|
93
|
+
async verify(domain) {
|
|
94
|
+
try {
|
|
95
|
+
await this.exec(['verify', '-d', domain, '-o', this.certsDir]);
|
|
96
|
+
return { valid: true };
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return { valid: false, error: error.message };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 修复证书链
|
|
104
|
+
*/
|
|
105
|
+
async fixChain(domain) {
|
|
106
|
+
await this.exec(['fix-chain', '-d', domain, '-o', this.certsDir]);
|
|
107
|
+
return { success: true };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 续期证书
|
|
112
|
+
*/
|
|
113
|
+
async renew(domain) {
|
|
114
|
+
await this.exec(['renew', '-d', domain, '-o', this.certsDir]);
|
|
115
|
+
return this.getCertificateInfo(domain);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 获取证书信息
|
|
120
|
+
*/
|
|
121
|
+
getCertificateInfo(domain) {
|
|
122
|
+
const certPath = path.join(this.certsDir, domain, `${domain}.pem`);
|
|
123
|
+
const keyPath = path.join(this.certsDir, domain, `${domain}.key`);
|
|
124
|
+
|
|
125
|
+
const exists = fs.existsSync(certPath) && fs.existsSync(keyPath);
|
|
126
|
+
|
|
127
|
+
let stats = null;
|
|
128
|
+
if (exists) {
|
|
129
|
+
stats = fs.statSync(certPath);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
domain,
|
|
134
|
+
exists,
|
|
135
|
+
paths: {
|
|
136
|
+
cert: certPath,
|
|
137
|
+
key: keyPath
|
|
138
|
+
},
|
|
139
|
+
createdAt: stats ? stats.birthtime : null
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 列出所有证书
|
|
145
|
+
*/
|
|
146
|
+
listCertificates() {
|
|
147
|
+
const certs = [];
|
|
148
|
+
|
|
149
|
+
if (!fs.existsSync(this.certsDir)) {
|
|
150
|
+
return certs;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const domains = fs.readdirSync(this.certsDir);
|
|
154
|
+
|
|
155
|
+
for (const domain of domains) {
|
|
156
|
+
const domainPath = path.join(this.certsDir, domain);
|
|
157
|
+
const stat = fs.statSync(domainPath);
|
|
158
|
+
|
|
159
|
+
if (stat.isDirectory()) {
|
|
160
|
+
certs.push(this.getCertificateInfo(domain));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return certs;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ==================== 初始化服务 ====================
|
|
169
|
+
|
|
170
|
+
const certService = new CertctlService({
|
|
171
|
+
email: CONFIG.email,
|
|
172
|
+
certsDir: CONFIG.certsDir
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ==================== API 路由 ====================
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* GET /api/certs
|
|
179
|
+
* 列出所有证书
|
|
180
|
+
*/
|
|
181
|
+
app.get('/api/certs', (req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
const certs = certService.listCertificates();
|
|
184
|
+
res.json({ success: true, data: certs });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
res.status(500).json({ success: false, error: error.message });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* GET /api/certs/:domain
|
|
192
|
+
* 获取证书信息
|
|
193
|
+
*/
|
|
194
|
+
app.get('/api/certs/:domain', async (req, res) => {
|
|
195
|
+
try {
|
|
196
|
+
const { domain } = req.params;
|
|
197
|
+
const info = certService.getCertificateInfo(domain);
|
|
198
|
+
|
|
199
|
+
if (!info.exists) {
|
|
200
|
+
return res.status(404).json({
|
|
201
|
+
success: false,
|
|
202
|
+
error: '证书不存在'
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 同时验证证书有效性
|
|
207
|
+
const verifyResult = await certService.verify(domain);
|
|
208
|
+
|
|
209
|
+
res.json({
|
|
210
|
+
success: true,
|
|
211
|
+
data: {
|
|
212
|
+
...info,
|
|
213
|
+
valid: verifyResult.valid
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
} catch (error) {
|
|
217
|
+
res.status(500).json({ success: false, error: error.message });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* POST /api/certs/:domain
|
|
223
|
+
* 申请证书
|
|
224
|
+
*
|
|
225
|
+
* Body:
|
|
226
|
+
* {
|
|
227
|
+
* "dns": {
|
|
228
|
+
* "provider": "aliyun",
|
|
229
|
+
* "accessKeyId": "xxx",
|
|
230
|
+
* "accessKeySecret": "xxx"
|
|
231
|
+
* }
|
|
232
|
+
* }
|
|
233
|
+
*/
|
|
234
|
+
app.post('/api/certs/:domain', async (req, res) => {
|
|
235
|
+
try {
|
|
236
|
+
const { domain } = req.params;
|
|
237
|
+
const { dns } = req.body;
|
|
238
|
+
|
|
239
|
+
// 检查是否已存在
|
|
240
|
+
if (certService.getCertificateInfo(domain).exists) {
|
|
241
|
+
return res.status(409).json({
|
|
242
|
+
success: false,
|
|
243
|
+
error: '证书已存在,请先删除或使用续期接口'
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 申请证书
|
|
248
|
+
const result = await certService.apply(domain, dns);
|
|
249
|
+
|
|
250
|
+
res.json({
|
|
251
|
+
success: true,
|
|
252
|
+
message: '证书申请成功',
|
|
253
|
+
data: result
|
|
254
|
+
});
|
|
255
|
+
} catch (error) {
|
|
256
|
+
res.status(500).json({ success: false, error: error.message });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* POST /api/certs/:domain/renew
|
|
262
|
+
* 续期证书
|
|
263
|
+
*/
|
|
264
|
+
app.post('/api/certs/:domain/renew', async (req, res) => {
|
|
265
|
+
try {
|
|
266
|
+
const { domain } = req.params;
|
|
267
|
+
|
|
268
|
+
if (!certService.getCertificateInfo(domain).exists) {
|
|
269
|
+
return res.status(404).json({
|
|
270
|
+
success: false,
|
|
271
|
+
error: '证书不存在'
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const result = await certService.renew(domain);
|
|
276
|
+
|
|
277
|
+
res.json({
|
|
278
|
+
success: true,
|
|
279
|
+
message: '证书续期成功',
|
|
280
|
+
data: result
|
|
281
|
+
});
|
|
282
|
+
} catch (error) {
|
|
283
|
+
res.status(500).json({ success: false, error: error.message });
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* POST /api/certs/:domain/verify
|
|
289
|
+
* 验证证书
|
|
290
|
+
*/
|
|
291
|
+
app.post('/api/certs/:domain/verify', async (req, res) => {
|
|
292
|
+
try {
|
|
293
|
+
const { domain } = req.params;
|
|
294
|
+
const result = await certService.verify(domain);
|
|
295
|
+
|
|
296
|
+
res.json({
|
|
297
|
+
success: true,
|
|
298
|
+
data: result
|
|
299
|
+
});
|
|
300
|
+
} catch (error) {
|
|
301
|
+
res.status(500).json({ success: false, error: error.message });
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* POST /api/certs/:domain/fix-chain
|
|
307
|
+
* 修复证书链
|
|
308
|
+
*/
|
|
309
|
+
app.post('/api/certs/:domain/fix-chain', async (req, res) => {
|
|
310
|
+
try {
|
|
311
|
+
const { domain } = req.params;
|
|
312
|
+
|
|
313
|
+
if (!certService.getCertificateInfo(domain).exists) {
|
|
314
|
+
return res.status(404).json({
|
|
315
|
+
success: false,
|
|
316
|
+
error: '证书不存在'
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
await certService.fixChain(domain);
|
|
321
|
+
|
|
322
|
+
res.json({
|
|
323
|
+
success: true,
|
|
324
|
+
message: '证书链修复成功'
|
|
325
|
+
});
|
|
326
|
+
} catch (error) {
|
|
327
|
+
res.status(500).json({ success: false, error: error.message });
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* GET /api/certs/:domain/download
|
|
333
|
+
* 下载证书文件
|
|
334
|
+
*/
|
|
335
|
+
app.get('/api/certs/:domain/download', (req, res) => {
|
|
336
|
+
try {
|
|
337
|
+
const { domain } = req.params;
|
|
338
|
+
const { type } = req.query; // 'cert' 或 'key'
|
|
339
|
+
|
|
340
|
+
const info = certService.getCertificateInfo(domain);
|
|
341
|
+
|
|
342
|
+
if (!info.exists) {
|
|
343
|
+
return res.status(404).json({
|
|
344
|
+
success: false,
|
|
345
|
+
error: '证书不存在'
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const filePath = type === 'key' ? info.paths.key : info.paths.cert;
|
|
350
|
+
|
|
351
|
+
if (!fs.existsSync(filePath)) {
|
|
352
|
+
return res.status(404).json({
|
|
353
|
+
success: false,
|
|
354
|
+
error: '文件不存在'
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
res.download(filePath);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
res.status(500).json({ success: false, error: error.message });
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ==================== 启动服务 ====================
|
|
365
|
+
|
|
366
|
+
app.listen(CONFIG.port, () => {
|
|
367
|
+
console.log(`
|
|
368
|
+
╔════════════════════════════════════════╗
|
|
369
|
+
║ Certctl HTTP API Server ║
|
|
370
|
+
╠════════════════════════════════════════╣
|
|
371
|
+
║ 端口: http://localhost:${CONFIG.port} ║
|
|
372
|
+
║ 邮箱: ${CONFIG.email} ║
|
|
373
|
+
║ 目录: ${CONFIG.certsDir} ║
|
|
374
|
+
╚════════════════════════════════════════╝
|
|
375
|
+
|
|
376
|
+
API 列表:
|
|
377
|
+
GET /api/certs - 列出所有证书
|
|
378
|
+
GET /api/certs/:domain - 获取证书信息
|
|
379
|
+
POST /api/certs/:domain - 申请证书
|
|
380
|
+
POST /api/certs/:domain/renew - 续期证书
|
|
381
|
+
POST /api/certs/:domain/verify - 验证证书
|
|
382
|
+
POST /api/certs/:domain/fix-chain - 修复证书链
|
|
383
|
+
GET /api/certs/:domain/download?type=cert|key - 下载证书
|
|
384
|
+
|
|
385
|
+
示例:
|
|
386
|
+
curl http://localhost:${CONFIG.port}/api/certs
|
|
387
|
+
|
|
388
|
+
curl -X POST http://localhost:${CONFIG.port}/api/certs/example.com \\
|
|
389
|
+
-H "Content-Type: application/json" \\
|
|
390
|
+
-d '{
|
|
391
|
+
"dns": {
|
|
392
|
+
"provider": "aliyun",
|
|
393
|
+
"accessKeyId": "your-key",
|
|
394
|
+
"accessKeySecret": "your-secret"
|
|
395
|
+
}
|
|
396
|
+
}'
|
|
397
|
+
`);
|
|
398
|
+
});
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "certctl-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Lightweight SSL Certificate CLI Tool",
|
|
5
5
|
"bin": {
|
|
6
6
|
"certctl": "bin/run.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
|
-
"bin/"
|
|
9
|
+
"bin/",
|
|
10
|
+
"examples/",
|
|
11
|
+
"API.md"
|
|
10
12
|
],
|
|
11
13
|
"keywords": [
|
|
12
14
|
"ssl",
|