vk-ssl-auto-deploy 0.6.8 → 0.7.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/app.js CHANGED
@@ -3,13 +3,13 @@ const express = require('express');
3
3
  const path = require('path');
4
4
  const cookieParser = require('cookie-parser');
5
5
  const logger = require('morgan');
6
+ const http = require('http');
6
7
 
7
8
  const indexRouter = require('./routes/index');
8
9
  const testRouter = require('./routes/test');
9
10
  const certRouter = require('./routes/cert');
10
-
11
- const config = require('./config.json');
12
-
11
+ const adminRouter = require('./routes/admin');
12
+ const { setupWebSocket, broadcastLog } = require('./utils/websocket');
13
13
 
14
14
  const app = express();
15
15
 
@@ -26,6 +26,7 @@ app.use(express.static(path.join(__dirname, 'public')));
26
26
  app.use('/', indexRouter);
27
27
  app.use('/test', testRouter);
28
28
  app.use(`/api/cert`, certRouter);
29
+ app.use('/admin', adminRouter);
29
30
 
30
31
  // catch 404 and forward to error handler
31
32
  app.use((req, res, next) => {
@@ -42,11 +43,21 @@ app.use((err, req, res, next) => {
42
43
 
43
44
 
44
45
  // 启动服务器
45
- const port = process.env.PORT || config.port;
46
+ const port = process.env.PORT || 6001;
46
47
  const host = process.env.HOST || '0.0.0.0';
47
48
 
48
- app.listen(port, host, () => {
49
- console.log('listenPort:', port)
49
+ // 创建 HTTP 服务器
50
+ const server = http.createServer(app);
51
+
52
+ // 设置 WebSocket 服务器
53
+ setupWebSocket(server);
54
+
55
+ // 启动服务器
56
+ server.listen(port, host, () => {
57
+ console.log('listenPort:', port);
58
+ console.log('管理后台地址: http://' + (host === '0.0.0.0' ? 'localhost' : host) + ':' + port + '/admin');
50
59
  });
51
60
 
52
- module.exports = app;
61
+ // 导出应用和广播函数
62
+ module.exports = app;
63
+ module.exports.broadcastLog = broadcastLog;
package/config.json CHANGED
@@ -1,8 +1,8 @@
1
- {
2
- "key": "",
3
- "certSaveDir": "/vk-cert",
4
- "nginxDir": "/www/server/nginx/sbin/nginx",
5
- "domains": [],
6
- "callbackCommand": [],
7
- "port": 6001
1
+ {
2
+ "key": "",
3
+ "certSaveDir": "/vk-cert",
4
+ "nginxDir": "/www/server/nginx/sbin/nginx",
5
+ "domains": [],
6
+ "callbackCommand": [],
7
+ "password": "admin@123456"
8
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vk-ssl-auto-deploy",
3
- "version": "0.6.8",
3
+ "version": "0.7.1",
4
4
  "description": "SSL证书自动部署工具 - 提供HTTP API接口,支持证书文件自动上传和部署",
5
5
  "main": "app.js",
6
6
  "bin": {
@@ -35,7 +35,6 @@
35
35
  },
36
36
  "homepage": "https://gitee.com/vk1688/vk-ssl-auto-deploy",
37
37
  "files": [
38
- "bin/",
39
38
  "routes/",
40
39
  "utils/",
41
40
  "views/",
@@ -58,9 +57,10 @@
58
57
  "http-errors": "~1.6.3",
59
58
  "morgan": "~1.9.1",
60
59
  "multer": "^2.0.2",
61
- "node-forge": "^1.3.1"
60
+ "node-forge": "^1.3.1",
61
+ "ws": "^8.18.3"
62
62
  },
63
63
  "optionalDependencies": {
64
64
  "pm2": "^5.3.0"
65
65
  }
66
- }
66
+ }
@@ -0,0 +1,266 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+
7
+ const CONFIG_FILE = path.join(__dirname, '..', 'config.json');
8
+
9
+ // 密码验证中间件
10
+ function verifyPassword(req, res, next) {
11
+ try {
12
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
13
+ const configPassword = config.password;
14
+ const requestPassword = req.headers['x-password'];
15
+
16
+ // 如果配置中没有设置密码,直接通过
17
+ if (!configPassword) {
18
+ return next();
19
+ }
20
+
21
+ // 验证密码
22
+ if (requestPassword === configPassword) {
23
+ return next();
24
+ }
25
+
26
+ // 密码错误
27
+ return res.status(401).json({
28
+ code: 401,
29
+ msg: '口令错误',
30
+ data: null
31
+ });
32
+ } catch (error) {
33
+ return res.status(500).json({
34
+ code: 500,
35
+ msg: '验证失败: ' + error.message,
36
+ data: null
37
+ });
38
+ }
39
+ }
40
+
41
+ // 读取证书信息的工具函数(从 cert.js 复制)
42
+ function parseCertificate(certContent) {
43
+ try {
44
+ // 优先使用 Node.js 内置的 crypto 模块
45
+ if (crypto.X509Certificate) {
46
+ const cert = new crypto.X509Certificate(certContent);
47
+ const parseCN = (dn) => {
48
+ const cnMatch = dn.match(/CN=([^,]+)/);
49
+ return cnMatch ? cnMatch[1] : '';
50
+ };
51
+ return {
52
+ notBefore: new Date(cert.validFrom),
53
+ notAfter: new Date(cert.validTo),
54
+ subject: parseCN(cert.subject),
55
+ issuer: parseCN(cert.issuer)
56
+ };
57
+ }
58
+ // 降级方案:使用 node-forge
59
+ const forge = require('node-forge');
60
+ const cert = forge.pki.certificateFromPem(certContent);
61
+ return {
62
+ notBefore: cert.validity.notBefore,
63
+ notAfter: cert.validity.notAfter,
64
+ subject: cert.subject.attributes.find(attr => attr.name === 'commonName')?.value || '',
65
+ issuer: cert.issuer.attributes.find(attr => attr.name === 'commonName')?.value || ''
66
+ };
67
+ } catch (error) {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * GET /admin
74
+ * 管理页面(不需要密码验证)
75
+ */
76
+ router.get('/', (req, res) => {
77
+ res.render('admin', { title: 'SSL证书管理后台' });
78
+ });
79
+
80
+ /**
81
+ * POST /api/verify-password
82
+ * 验证密码(用于前端验证)
83
+ */
84
+ router.post('/api/verify-password', (req, res) => {
85
+ try {
86
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
87
+ const configPassword = config.password;
88
+ const requestPassword = req.headers['x-password'] || req.body.password;
89
+
90
+ // 如果配置中没有设置密码,直接通过
91
+ if (!configPassword) {
92
+ return res.json({
93
+ code: 0,
94
+ msg: 'success',
95
+ data: { valid: true }
96
+ });
97
+ }
98
+
99
+ // 验证密码
100
+ if (requestPassword === configPassword) {
101
+ return res.json({
102
+ code: 0,
103
+ msg: 'success',
104
+ data: { valid: true }
105
+ });
106
+ }
107
+
108
+ // 密码错误
109
+ return res.status(401).json({
110
+ code: 401,
111
+ msg: '口令错误',
112
+ data: { valid: false }
113
+ });
114
+ } catch (error) {
115
+ return res.status(500).json({
116
+ code: 500,
117
+ msg: '验证失败: ' + error.message,
118
+ data: null
119
+ });
120
+ }
121
+ });
122
+
123
+ /**
124
+ * GET /api/config
125
+ * 获取配置信息
126
+ */
127
+ router.get('/api/config', verifyPassword, (req, res) => {
128
+ try {
129
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
130
+ res.json({
131
+ code: 0,
132
+ msg: 'success',
133
+ data: config
134
+ });
135
+ } catch (error) {
136
+ res.json({
137
+ code: 500,
138
+ msg: '读取配置失败: ' + error.message,
139
+ data: null
140
+ });
141
+ }
142
+ });
143
+
144
+ /**
145
+ * POST /api/config
146
+ * 保存配置信息
147
+ */
148
+ router.post('/api/config', verifyPassword, (req, res) => {
149
+ try {
150
+ const newConfig = req.body;
151
+
152
+ // 验证必填字段
153
+ if (!newConfig.key) {
154
+ return res.json({
155
+ code: 400,
156
+ msg: 'API Key 不能为空',
157
+ data: null
158
+ });
159
+ }
160
+
161
+ if (!newConfig.certSaveDir) {
162
+ return res.json({
163
+ code: 400,
164
+ msg: '证书保存目录不能为空',
165
+ data: null
166
+ });
167
+ }
168
+
169
+ // 确保 domains 和 callbackCommand 是数组
170
+ if (!Array.isArray(newConfig.domains)) {
171
+ newConfig.domains = [];
172
+ }
173
+ if (!Array.isArray(newConfig.callbackCommand)) {
174
+ newConfig.callbackCommand = [];
175
+ }
176
+
177
+ // 保存配置文件
178
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, '\t'), 'utf8');
179
+
180
+ // 重新加载配置(清除 require 缓存)
181
+ delete require.cache[require.resolve('../config.json')];
182
+
183
+ res.json({
184
+ code: 0,
185
+ msg: '配置保存成功',
186
+ data: newConfig
187
+ });
188
+ } catch (error) {
189
+ res.json({
190
+ code: 500,
191
+ msg: '保存配置失败: ' + error.message,
192
+ data: null
193
+ });
194
+ }
195
+ });
196
+
197
+ /**
198
+ * GET /api/certs/local
199
+ * 获取本地证书列表及有效期信息
200
+ */
201
+ router.get('/api/certs/local', verifyPassword, (req, res) => {
202
+ try {
203
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
204
+ const certDir = path.isAbsolute(config.certSaveDir) ?
205
+ config.certSaveDir :
206
+ path.join(__dirname, '..', config.certSaveDir);
207
+
208
+ // 检查目录是否存在
209
+ if (!fs.existsSync(certDir)) {
210
+ return res.json({
211
+ code: 0,
212
+ msg: 'success',
213
+ data: []
214
+ });
215
+ }
216
+
217
+ // 读取目录中的所有 .crt 文件
218
+ const files = fs.readdirSync(certDir);
219
+ const crtFiles = files.filter(f => f.endsWith('.crt'));
220
+
221
+ const certList = [];
222
+
223
+ for (const fileName of crtFiles) {
224
+ const filePath = path.join(certDir, fileName);
225
+ const certContent = fs.readFileSync(filePath, 'utf8');
226
+ const certInfo = parseCertificate(certContent);
227
+
228
+ if (certInfo) {
229
+ const now = new Date();
230
+ const expiryDate = new Date(certInfo.notAfter);
231
+ const remainingMs = expiryDate - now;
232
+ const remainingDays = Math.floor(remainingMs / (1000 * 60 * 60 * 24));
233
+
234
+ // 提取域名(从文件名)
235
+ const baseName = fileName.replace('.crt', '');
236
+
237
+ certList.push({
238
+ fileName: baseName,
239
+ domain: certInfo.subject || baseName,
240
+ issuer: certInfo.issuer,
241
+ notBefore: certInfo.notBefore.toISOString(),
242
+ notAfter: certInfo.notAfter.toISOString(),
243
+ remainingDays: remainingDays,
244
+ status: remainingDays < 0 ? 'expired' : (remainingDays < 14 ? 'warning' : 'valid')
245
+ });
246
+ }
247
+ }
248
+
249
+ // 按剩余天数排序(过期的在前,然后是快过期的)
250
+ certList.sort((a, b) => a.remainingDays - b.remainingDays);
251
+
252
+ res.json({
253
+ code: 0,
254
+ msg: 'success',
255
+ data: certList
256
+ });
257
+ } catch (error) {
258
+ res.json({
259
+ code: 500,
260
+ msg: '获取证书列表失败: ' + error.message,
261
+ data: null
262
+ });
263
+ }
264
+ });
265
+
266
+ module.exports = router;
package/routes/cert.js CHANGED
@@ -8,10 +8,75 @@ const crypto = require('crypto');
8
8
  const axios = require('axios');
9
9
  const forge = require('node-forge');
10
10
  const chalk = require('chalk');
11
- const config = require('../config.json');
12
11
 
13
12
  const execAsync = promisify(exec);
14
13
 
14
+ // 获取广播函数
15
+ let broadcastLog = null;
16
+ try {
17
+ const app = require('../app');
18
+ broadcastLog = app.broadcastLog;
19
+ } catch (error) {
20
+ // 如果获取失败,使用空函数
21
+ broadcastLog = () => {};
22
+ }
23
+
24
+ // 配置文件路径
25
+ const CONFIG_FILE = path.join(__dirname, '..', 'config.json');
26
+
27
+ // 密码验证中间件
28
+ function verifyPassword(req, res, next) {
29
+ try {
30
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
31
+ const configPassword = config.password;
32
+ const requestPassword = req.headers['x-password'];
33
+
34
+ // 如果配置中没有设置密码,直接通过
35
+ if (!configPassword) {
36
+ return next();
37
+ }
38
+
39
+ // 验证密码
40
+ if (requestPassword === configPassword) {
41
+ return next();
42
+ }
43
+
44
+ // 密码错误
45
+ return res.status(401).json({
46
+ code: 401,
47
+ msg: '口令错误',
48
+ data: null
49
+ });
50
+ } catch (error) {
51
+ return res.status(500).json({
52
+ code: 500,
53
+ msg: '验证失败: ' + error.message,
54
+ data: null
55
+ });
56
+ }
57
+ }
58
+
59
+ /**
60
+ * 动态获取配置(每次调用都重新读取文件)
61
+ * @returns {Object} 配置对象
62
+ */
63
+ function getConfig() {
64
+ try {
65
+ const configContent = fs.readFileSync(CONFIG_FILE, 'utf8');
66
+ return JSON.parse(configContent);
67
+ } catch (error) {
68
+ logger.error('[配置读取错误]', error.message);
69
+ // 返回默认配置
70
+ return {
71
+ key: '',
72
+ certSaveDir: '/vk-cert',
73
+ nginxDir: '',
74
+ domains: [],
75
+ callbackCommand: []
76
+ };
77
+ }
78
+ }
79
+
15
80
  // 日志工具 - 带颜色输出(仅使用5种颜色:白色、灰色、黄色、绿色、红色)
16
81
  const logger = {
17
82
  // 错误 - 红色
@@ -34,7 +99,6 @@ const logger = {
34
99
 
35
100
  // 无忧SSL API配置
36
101
  const WUYOU_API_BASE = 'https://openapi-ssl.vk168.top/http'; // 请勿修改
37
- const API_KEY = config.key;
38
102
 
39
103
  // QPS限制:每次API调用后等待时间(毫秒)
40
104
  const API_CALL_DELAY = 1000; // 确保QPS不超过1
@@ -118,13 +182,14 @@ function releaseLock() {
118
182
  */
119
183
  async function getCertList(pageIndex = 1, pageSize = 10000) {
120
184
  try {
185
+ const config = getConfig(); // 动态获取配置
121
186
  logger.info(`[API调用] 获取证书列表 - 页码: ${pageIndex}, 每页: ${pageSize}`);
122
187
 
123
188
  const params = {
124
189
  pageIndex,
125
190
  pageSize,
126
191
  status: 3,
127
- key: API_KEY
192
+ key: config.key
128
193
  };
129
194
 
130
195
  // 添加domains过滤条件
@@ -137,12 +202,13 @@ async function getCertList(pageIndex = 1, pageSize = 10000) {
137
202
  timeout: 60000
138
203
  }
139
204
  );
140
-
141
- logger.success(`[API响应] 获取证书列表成功 - 共 ${response.data?.data?.total || 0} 条记录`);
142
-
205
+ if (response.data?.code === 0) {
206
+ logger.success(`[API响应] 获取证书列表成功 - 共 ${response.data?.data?.total || 0} 条记录`);
207
+ } else {
208
+ logger.error('[API错误] 获取证书列表失败:', response.data?.msg);
209
+ }
143
210
  // QPS限制:调用后等待
144
211
  await sleep(API_CALL_DELAY);
145
-
146
212
  return response.data;
147
213
  } catch (error) {
148
214
  logger.error('[API错误] 获取证书列表失败:', error.message);
@@ -157,11 +223,12 @@ async function getCertList(pageIndex = 1, pageSize = 10000) {
157
223
  */
158
224
  async function downloadCerts(certIds) {
159
225
  try {
226
+ const config = getConfig(); // 动态获取配置
160
227
  logger.info(`[API调用] 批量下载证书 - 数量: ${certIds.length}`);
161
228
  const params = {
162
229
  certIds,
163
230
  type: "nginx",
164
- key: API_KEY
231
+ key: config.key
165
232
  };
166
233
 
167
234
  const response = await axios.post(
@@ -295,6 +362,7 @@ function generateFileName(domains) {
295
362
  * @param {string} keyContent - 私钥内容
296
363
  */
297
364
  function saveCertFiles(fileName, certContent, keyContent) {
365
+ const config = getConfig(); // 动态获取配置
298
366
  const certDir = path.isAbsolute(config.certSaveDir) ?
299
367
  config.certSaveDir :
300
368
  path.join(__dirname, '..', config.certSaveDir);
@@ -321,6 +389,7 @@ function saveCertFiles(fileName, certContent, keyContent) {
321
389
  * 重载Nginx配置
322
390
  */
323
391
  async function reloadNginx() {
392
+ const config = getConfig(); // 动态获取配置
324
393
  if (!config.nginxDir || !config.nginxDir.trim()) {
325
394
  logger.dim('[Nginx重载] 未配置nginxDir,跳过重载');
326
395
  return { success: true, message: '未配置nginxDir' };
@@ -349,6 +418,8 @@ async function reloadNginx() {
349
418
  * 执行证书自动部署任务
350
419
  */
351
420
  async function executeDeployTask() {
421
+ const config = getConfig(); // 动态获取配置
422
+
352
423
  if (isTaskRunning) {
353
424
  logger.warn('[任务执行] 已有任务正在执行,跳过本次执行');
354
425
  return { success: false, message: '任务正在执行中' };
@@ -387,7 +458,7 @@ async function executeDeployTask() {
387
458
  const certListResponse = await getCertList(1, 10000);
388
459
 
389
460
  if (!certListResponse || certListResponse.code !== 0) {
390
- throw new Error(`获取证书列表失败: ${certListResponse?.message || '未知错误'}`);
461
+ throw new Error(`获取证书列表失败: ${certListResponse?.msg || certListResponse?.message || '未知错误'}`);
391
462
  }
392
463
 
393
464
  const certList = certListResponse.data?.rows || [];
@@ -490,7 +561,7 @@ async function executeDeployTask() {
490
561
  const downloadResponse = await downloadCerts(certIds);
491
562
 
492
563
  if (!downloadResponse || downloadResponse.code !== 0) {
493
- throw new Error(`下载失败: ${downloadResponse?.message || '未知错误'}`);
564
+ throw new Error(`下载失败: ${downloadResponse?.msg || downloadResponse?.message || '未知错误'}`);
494
565
  }
495
566
 
496
567
  const downloadedCerts = downloadResponse.data?.certs || [];
@@ -577,6 +648,13 @@ async function executeDeployTask() {
577
648
  logger.dim(`[证书保存目录] ${config.certSaveDir}`);
578
649
  logger.divider();
579
650
  console.log('');
651
+
652
+ // 任务完成后,通过WebSocket通知前端刷新证书列表
653
+ if (broadcastLog && result.stats.downloadedCerts > 0) {
654
+ setTimeout(() => {
655
+ broadcastLog('CERT_UPDATE_COMPLETE', 'cert_update');
656
+ }, 1000);
657
+ }
580
658
  }
581
659
 
582
660
  return result;
@@ -604,8 +682,8 @@ function initScheduledTask() {
604
682
  };
605
683
 
606
684
  // scheduledExecutionTime = {
607
- // hour: 14,
608
- // minute: 13
685
+ // hour: 17,
686
+ // minute: 9
609
687
  // }
610
688
 
611
689
  logger.success(
@@ -674,9 +752,7 @@ function initScheduledTask() {
674
752
  if (lastExecutionDate !== currentDate) {
675
753
  // 换行后输出触发信息
676
754
  console.log('');
677
- logger.success(
678
- `[定时任务] 触发执行 - 当前时间: ${currentHour}:${currentMinute.toString().padStart(2, '0')}`
679
- );
755
+ logger.success(`[定时任务] 触发执行`);
680
756
  lastExecutionDate = currentDate;
681
757
 
682
758
  // 异步执行任务,使用try-catch确保异常不会影响定时器
@@ -685,6 +761,7 @@ function initScheduledTask() {
685
761
  }).finally(() => {
686
762
  needWrite = true;
687
763
  lastCountdownText = "";
764
+ logger.log(`[定时任务] 运行结束`);
688
765
  })
689
766
  }
690
767
  }
@@ -705,7 +782,8 @@ function initScheduledTask() {
705
782
  * GET /api/cert/status
706
783
  * 获取任务状态
707
784
  */
708
- router.get('/status', (req, res) => {
785
+ router.get('/status', verifyPassword, (req, res) => {
786
+ const config = getConfig(); // 动态获取配置
709
787
  res.json({
710
788
  code: 0,
711
789
  msg: 'success',
@@ -724,11 +802,50 @@ router.get('/status', (req, res) => {
724
802
  });
725
803
  });
726
804
 
805
+ /**
806
+ * GET /api/cert/schedule-info
807
+ * 获取定时任务信息和服务器时间
808
+ */
809
+ router.get('/schedule-info', verifyPassword, (req, res) => {
810
+ const now = new Date();
811
+ const serverTime = now.toISOString();
812
+
813
+ // 计算下次执行时间
814
+ let nextExecutionTime = null;
815
+ if (scheduledExecutionTime) {
816
+ const targetTime = new Date(now);
817
+ targetTime.setHours(scheduledExecutionTime.hour, scheduledExecutionTime.minute, 0, 0);
818
+
819
+ // 如果今天的执行时间已过,则计算明天的执行时间
820
+ if (now >= targetTime) {
821
+ targetTime.setDate(targetTime.getDate() + 1);
822
+ }
823
+
824
+ nextExecutionTime = targetTime.toISOString();
825
+ }
826
+
827
+ res.json({
828
+ code: 0,
829
+ msg: 'success',
830
+ data: {
831
+ serverTime,
832
+ scheduledExecutionTime: scheduledExecutionTime ? {
833
+ hour: scheduledExecutionTime.hour,
834
+ minute: scheduledExecutionTime.minute,
835
+ formatted: `${scheduledExecutionTime.hour}:${scheduledExecutionTime.minute.toString().padStart(2, '0')}`
836
+ } : null,
837
+ nextExecutionTime,
838
+ isTaskRunning,
839
+ lastExecutionDate
840
+ }
841
+ });
842
+ });
843
+
727
844
  /**
728
845
  * GET /api/cert/execute
729
846
  * 手动触发任务执行
730
847
  */
731
- router.get('/execute', async (req, res) => {
848
+ router.get('/execute', verifyPassword, async (req, res) => {
732
849
  logger.info('[手动执行] 收到手动执行请求');
733
850
 
734
851
  if (isTaskRunning) {