vk-ssl-auto-deploy 0.8.3 → 0.8.5

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/README.md CHANGED
@@ -1,18 +1,48 @@
1
1
  # vk-ssl-auto-deploy
2
2
 
3
- SSL证书自动部署工具 - 提供HTTP API接口,支持证书文件自动上传和部署
3
+ 无忧SSL证书平台自动部署工具 - 提供HTTP API接口,支持证书文件自动上传和部署
4
+
5
+ ### 支持系统
6
+
7
+ | 系统 | 最低版本 |
8
+ |------|----------|
9
+ | Alibaba Cloud Linux | 3+ |
10
+ | CentOS | 7+ |
11
+ | Ubuntu | 20+ |
12
+ | Debian | 12+ |
13
+ | Fedora | 40+ |
14
+ | OpenSUSE | 15+ |
15
+ | Rocky Linux | 8+ |
16
+ | CentOS Stream | 9+ |
17
+ | AlmaLinux | 8+ |
18
+ | Anolis OS | 不支持 |
19
+ | Windows | 10 / 11 / Server 2016+ |
4
20
 
5
21
  ## 一键安装(推荐)
6
22
 
23
+ ### Linux 系统
24
+
7
25
  Linux 服务器执行以下命令,自动完成安装、部署和开机自启配置:
8
26
 
9
27
  ```bash
10
28
  curl -fsSL https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/install.sh | sudo bash
11
29
  ```
12
30
 
13
- 安装完成后,访问 `http://服务器IP:6001/admin`,默认密码:`admin@123456`
31
+ ### Windows 系统
32
+
33
+ **PowerShell (推荐):**
34
+
35
+ ```powershell
36
+ iwr -useb https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/install.ps1 | iex
37
+ ```
14
38
 
15
- > 支持 CentOS/RHEL 和 Debian/Ubuntu 系统,自动安装 Node.js 和 PM2
39
+ **CMD (命令提示符):**
40
+
41
+ ```cmd
42
+ powershell -c "iwr -useb https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/install.ps1 | iex"
43
+ ```
44
+
45
+ 安装完成后,访问 `http://服务器IP:6001/admin`,默认密码:`admin@123456`
16
46
 
17
47
  ---
18
48
 
@@ -30,7 +60,7 @@ curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash - && sudo yum insta
30
60
 
31
61
  #### Windows系统
32
62
 
33
- ```bash
63
+ ```powershell
34
64
  winget install --id OpenJS.NodeJS.LTS -e
35
65
  ```
36
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vk-ssl-auto-deploy",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "SSL证书自动部署工具 - 提供HTTP API接口,支持证书文件自动上传和部署",
5
5
  "main": "app.js",
6
6
  "scripts": {
@@ -1,8 +1,8 @@
1
- body {
2
- padding: 50px;
3
- font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4
- }
5
-
6
- a {
7
- color: #00B7FF;
8
- }
1
+ body {
2
+ padding: 50px;
3
+ font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4
+ }
5
+
6
+ a {
7
+ color: #00B7FF;
8
+ }
package/routes/admin.js CHANGED
@@ -3,6 +3,11 @@ const router = express.Router();
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const crypto = require('crypto');
6
+ const { exec } = require('child_process');
7
+ const { promisify } = require('util');
8
+ const AdmZip = require('adm-zip');
9
+
10
+ const execAsync = promisify(exec);
6
11
 
7
12
  const CONFIG_FILE = path.join(__dirname, '..', 'config.json');
8
13
 
@@ -266,4 +271,143 @@ router.get('/api/certs/local', verifyPassword, (req, res) => {
266
271
  }
267
272
  });
268
273
 
274
+ /**
275
+ * POST /api/test-nginx
276
+ * 测试 Nginx 路径是否正确
277
+ */
278
+ router.post('/api/test-nginx', verifyPassword, async (req, res) => {
279
+ try {
280
+ const { nginxPath } = req.body;
281
+
282
+ if (!nginxPath) {
283
+ return res.json({
284
+ code: 400,
285
+ msg: 'Nginx 路径不能为空',
286
+ data: null
287
+ });
288
+ }
289
+
290
+ // 检查文件是否存在
291
+ if (!fs.existsSync(nginxPath)) {
292
+ return res.json({
293
+ code: 404,
294
+ msg: 'Nginx 文件不存在',
295
+ data: null
296
+ });
297
+ }
298
+
299
+ // 执行 nginx -t 命令测试配置
300
+ try {
301
+ // 获取 nginx 所在目录作为工作目录
302
+ const nginxDir = path.dirname(nginxPath);
303
+
304
+ const { stdout, stderr } = await execAsync(`"${nginxPath}" -t`, {
305
+ timeout: 10000, // 10秒超时
306
+ cwd: nginxDir // 设置工作目录为 nginx 所在目录
307
+ });
308
+
309
+ // nginx -t 的输出通常在 stderr 中
310
+ const output = stderr || stdout;
311
+
312
+ // 检查是否包含 "syntax is ok" 和 "test is successful"
313
+ if (output.includes('syntax is ok') || output.includes('test is successful')) {
314
+ return res.json({
315
+ code: 0,
316
+ msg: 'Nginx 配置测试通过\n' + output,
317
+ data: { output }
318
+ });
319
+ } else {
320
+ return res.json({
321
+ code: 500,
322
+ msg: 'Nginx 配置测试失败\n' + output,
323
+ data: { output }
324
+ });
325
+ }
326
+ } catch (execError) {
327
+ // 执行命令失败
328
+ return res.json({
329
+ code: 500,
330
+ msg: 'Nginx 配置测试失败: ' + (execError.stderr || execError.message),
331
+ data: null
332
+ });
333
+ }
334
+ } catch (error) {
335
+ res.json({
336
+ code: 500,
337
+ msg: '测试失败: ' + error.message,
338
+ data: null
339
+ });
340
+ }
341
+ });
342
+
343
+ /**
344
+ * GET /api/cert/download/:fileName
345
+ * 下载证书(包含 cert.pem 和 cert.key 的 zip 压缩包)
346
+ */
347
+ router.get('/api/cert/download/:fileName', verifyPassword, (req, res) => {
348
+ try {
349
+ const { fileName } = req.params;
350
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
351
+ const certDir = path.isAbsolute(config.certSaveDir) ?
352
+ config.certSaveDir :
353
+ path.join(__dirname, '..', config.certSaveDir);
354
+
355
+ // 检查目录是否存在
356
+ if (!fs.existsSync(certDir)) {
357
+ return res.status(404).json({
358
+ code: 404,
359
+ msg: '证书目录不存在',
360
+ data: null
361
+ });
362
+ }
363
+
364
+ // 构建证书文件路径
365
+ const crtFile = path.join(certDir, `${fileName}.crt`);
366
+ const keyFile = path.join(certDir, `${fileName}.key`);
367
+
368
+ // 检查文件是否存在
369
+ if (!fs.existsSync(crtFile)) {
370
+ return res.status(404).json({
371
+ code: 404,
372
+ msg: '证书文件不存在',
373
+ data: null
374
+ });
375
+ }
376
+
377
+ if (!fs.existsSync(keyFile)) {
378
+ return res.status(404).json({
379
+ code: 404,
380
+ msg: '私钥文件不存在',
381
+ data: null
382
+ });
383
+ }
384
+
385
+ // 创建 ZIP 压缩包
386
+ const zip = new AdmZip();
387
+
388
+ // 添加证书文件(重命名为 cert.pem)
389
+ zip.addLocalFile(crtFile, '', 'cert.pem');
390
+
391
+ // 添加私钥文件(重命名为 cert.key)
392
+ zip.addLocalFile(keyFile, '', 'cert.key');
393
+
394
+ // 生成 ZIP 文件
395
+ const zipBuffer = zip.toBuffer();
396
+
397
+ // 设置响应头
398
+ res.setHeader('Content-Type', 'application/zip');
399
+ res.setHeader('Content-Disposition', `attachment; filename="${fileName}.zip"`);
400
+ res.setHeader('Content-Length', zipBuffer.length);
401
+
402
+ // 发送文件
403
+ res.send(zipBuffer);
404
+ } catch (error) {
405
+ res.status(500).json({
406
+ code: 500,
407
+ msg: '下载失败: ' + error.message,
408
+ data: null
409
+ });
410
+ }
411
+ });
412
+
269
413
  module.exports = router;
package/views/admin.ejs CHANGED
@@ -101,8 +101,8 @@
101
101
  border-radius: 8px;
102
102
  color: #e0e0e0;
103
103
  font-size: 0.95em;
104
- transition: all 0.3s;
105
104
  font-family: 'Consolas', 'Monaco', monospace;
105
+ will-change: border-color, box-shadow;
106
106
  }
107
107
 
108
108
  input[type="text"]:focus,
@@ -158,8 +158,9 @@
158
158
  font-size: 1em;
159
159
  font-weight: 600;
160
160
  cursor: pointer;
161
- transition: all 0.3s;
161
+ transition: transform 0.2s, box-shadow 0.2s;
162
162
  box-shadow: 0 4px 15px rgba(0, 255, 136, 0.3);
163
+ will-change: transform, box-shadow;
163
164
  }
164
165
 
165
166
  button:hover {
@@ -448,7 +449,8 @@
448
449
  align-items: center;
449
450
  justify-content: center;
450
451
  border-radius: 8px;
451
- transition: all 0.3s;
452
+ transition: background-color 0.2s, color 0.2s;
453
+ will-change: background-color, color;
452
454
  }
453
455
 
454
456
  .close-btn:hover {
@@ -470,6 +472,8 @@
470
472
  border-radius: 8px;
471
473
  color: #e0e0e0;
472
474
  font-size: 0.95em;
475
+ transition: border-color 0.2s, box-shadow 0.2s;
476
+ will-change: border-color, box-shadow;
473
477
  }
474
478
 
475
479
  .search-box input:focus {
@@ -660,7 +664,7 @@
660
664
 
661
665
  <!-- 本地证书列表 -->
662
666
  <div class="card full-width">
663
- <h2 class="card-title">本地证书列表</h2>
667
+ <h2 class="card-title">本地证书列表 <span id="certCount" style="color: #888; font-size: 0.8em; font-weight: normal;">(0)</span></h2>
664
668
  <div class="search-box">
665
669
  <input type="text" id="certSearch" placeholder="🔍 搜索证书域名...">
666
670
  </div>
@@ -742,7 +746,10 @@
742
746
  </div>
743
747
  <div class="form-group">
744
748
  <label for="nginxDir">Nginx路径</label>
745
- <input type="text" id="nginxDir" name="nginxDir" placeholder="请输入Nginx路径">
749
+ <div style="display: flex; gap: 10px; align-items: flex-start;">
750
+ <input type="text" id="nginxDir" name="nginxDir" placeholder="请输入Nginx路径" style="flex: 1;">
751
+ <button type="button" onclick="testNginxPath()" style="flex: none; padding: 12px 20px; min-width: auto;">测试</button>
752
+ </div>
746
753
  <div class="hint">Nginx可执行文件路径(留空则不重载)</div>
747
754
  </div>
748
755
  <div class="form-group">
@@ -943,6 +950,39 @@
943
950
  document.getElementById('configModal').classList.remove('show');
944
951
  }
945
952
 
953
+ // 测试Nginx路径
954
+ async function testNginxPath() {
955
+ const nginxPath = document.getElementById('nginxDir').value.trim();
956
+ if (!nginxPath) {
957
+ showAlert('提示', '请先输入Nginx路径', 'warning');
958
+ return;
959
+ }
960
+
961
+ addLog('→ 开始测试 Nginx 路径...', 'info');
962
+
963
+ try {
964
+ const response = await authenticatedFetch('/admin/api/test-nginx', {
965
+ method: 'POST',
966
+ headers: { 'Content-Type': 'application/json' },
967
+ body: JSON.stringify({ nginxPath })
968
+ });
969
+ const result = await response.json();
970
+
971
+ if (result.code === 0) {
972
+ addLog('✓ Nginx 路径测试成功', 'success');
973
+ showAlert('测试成功', result.msg || 'Nginx 配置测试通过', 'success');
974
+ } else {
975
+ addLog('✗ Nginx 路径测试失败: ' + result.msg, 'error');
976
+ showAlert('测试失败', result.msg, 'error');
977
+ }
978
+ } catch (error) {
979
+ if (error.message !== '未设置密码' && error.message !== '密码验证失败') {
980
+ addLog('✗ Nginx 路径测试失败: ' + error.message, 'error');
981
+ showAlert('测试失败', error.message, 'error');
982
+ }
983
+ }
984
+ }
985
+
946
986
  // 加载配置
947
987
  async function loadConfig() {
948
988
  try {
@@ -1028,6 +1068,9 @@
1028
1068
  function renderCerts(certs) {
1029
1069
  const container = document.getElementById('certListContainer');
1030
1070
 
1071
+ // 更新证书数量显示
1072
+ document.getElementById('certCount').textContent = `(${certs.length})`;
1073
+
1031
1074
  if (certs.length === 0) {
1032
1075
  container.innerHTML = '<div class="empty-state">暂无证书文件</div>';
1033
1076
  return;
@@ -1041,6 +1084,7 @@
1041
1084
  html += '<th>过期时间</th>';
1042
1085
  html += '<th>剩余天数</th>';
1043
1086
  html += '<th>状态</th>';
1087
+ html += '<th style="width: 100px;">操作</th>';
1044
1088
  html += '</tr></thead><tbody>';
1045
1089
 
1046
1090
  sortedCerts.forEach(cert => {
@@ -1056,6 +1100,7 @@
1056
1100
  html += `<td>${formatDate(cert.notAfter)}</td>`;
1057
1101
  html += `<td>${cert.remainingDays} 天</td>`;
1058
1102
  html += `<td><span class="status-badge ${statusClass}">${statusText}</span></td>`;
1103
+ html += `<td><button onclick="downloadCert('${escapeHtml(cert.fileName)}')" style="padding: 6px 12px; font-size: 0.85em;">下载</button></td>`;
1059
1104
  html += '</tr>';
1060
1105
  });
1061
1106
 
@@ -1063,6 +1108,59 @@
1063
1108
  container.innerHTML = html;
1064
1109
  }
1065
1110
 
1111
+ // 下载证书
1112
+ async function downloadCert(fileName) {
1113
+ try {
1114
+ addLog(`→ 开始下载证书: ${fileName}`, 'info');
1115
+
1116
+ const password = getStoredPassword();
1117
+ const response = await fetch(`/admin/api/cert/download/${encodeURIComponent(fileName)}`, {
1118
+ method: 'GET',
1119
+ headers: {
1120
+ 'X-Password': password
1121
+ }
1122
+ });
1123
+
1124
+ if (response.status === 401) {
1125
+ clearPassword();
1126
+ showPasswordModal();
1127
+ addLog('✗ 口令验证失败,请重新输入', 'error');
1128
+ return;
1129
+ }
1130
+
1131
+ if (!response.ok) {
1132
+ const result = await response.json();
1133
+ throw new Error(result.msg || '下载失败');
1134
+ }
1135
+
1136
+ // 获取文件名
1137
+ const contentDisposition = response.headers.get('Content-Disposition');
1138
+ let downloadFileName = `${fileName}.zip`;
1139
+ if (contentDisposition) {
1140
+ const match = contentDisposition.match(/filename[*]?=['"]?([^'";\n]+)['"]?/);
1141
+ if (match && match[1]) {
1142
+ downloadFileName = match[1];
1143
+ }
1144
+ }
1145
+
1146
+ // 下载文件
1147
+ const blob = await response.blob();
1148
+ const url = window.URL.createObjectURL(blob);
1149
+ const a = document.createElement('a');
1150
+ a.href = url;
1151
+ a.download = downloadFileName;
1152
+ document.body.appendChild(a);
1153
+ a.click();
1154
+ window.URL.revokeObjectURL(url);
1155
+ document.body.removeChild(a);
1156
+
1157
+ addLog(`✓ 证书下载成功: ${fileName}`, 'success');
1158
+ } catch (error) {
1159
+ addLog(`✗ 证书下载失败: ${error.message}`, 'error');
1160
+ showAlert('下载失败', error.message, 'error');
1161
+ }
1162
+ }
1163
+
1066
1164
  // 过滤证书
1067
1165
  function filterCerts() {
1068
1166
  const keyword = document.getElementById('certSearch').value.toLowerCase();
package/views/error.ejs CHANGED
@@ -1,3 +1,3 @@
1
- <h1><%= message %></h1>
2
- <h2><%= error.status %></h2>
3
- <pre><%= error.stack %></pre>
1
+ <h1><%= message %></h1>
2
+ <h2><%= error.status %></h2>
3
+ <pre><%= error.stack %></pre>
package/views/index.ejs CHANGED
@@ -1,10 +1,10 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title><%= title %></title>
5
- <link rel='stylesheet' href='/stylesheets/style.css' />
6
- </head>
7
- <body>
8
- <h1><%= title %></h1>
9
- </body>
10
- </html>
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= title %></title>
5
+ <link rel='stylesheet' href='/stylesheets/style.css' />
6
+ </head>
7
+ <body>
8
+ <h1><%= title %></h1>
9
+ </body>
10
+ </html>