vk-ssl-auto-deploy 0.0.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/README.md +200 -0
- package/app.js +43 -0
- package/bin/www.js +92 -0
- package/config.json +8 -0
- package/package.json +62 -0
- package/public/stylesheets/style.css +8 -0
- package/routes/cert.js +236 -0
- package/routes/index.js +9 -0
- package/routes/test.js +13 -0
- package/views/error.ejs +3 -0
- package/views/index.ejs +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
### 1. NPM 全局安装(推荐)
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
npm install -g vk-ssl-auto-deploy
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
安装后可以直接使用命令启动:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 直接启动
|
|
11
|
+
vk-ssl
|
|
12
|
+
|
|
13
|
+
# 或使用 PM2 管理(需要先安装 PM2)
|
|
14
|
+
npm install -g pm2
|
|
15
|
+
cd <安装目录>
|
|
16
|
+
npm run start
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 2. 本地开发安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# 克隆项目
|
|
23
|
+
git clone <repository-url>
|
|
24
|
+
cd vk-ssl-auto-deploy
|
|
25
|
+
|
|
26
|
+
# 安装依赖
|
|
27
|
+
npm install
|
|
28
|
+
|
|
29
|
+
# 启动运行
|
|
30
|
+
npm dev
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 3. PM2 进程管理(生产环境推荐)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# 启动服务
|
|
37
|
+
npm run start
|
|
38
|
+
|
|
39
|
+
# 停止服务
|
|
40
|
+
npm run stop
|
|
41
|
+
|
|
42
|
+
# 重启服务
|
|
43
|
+
npm run restart
|
|
44
|
+
|
|
45
|
+
# 查看日志
|
|
46
|
+
npm run logs
|
|
47
|
+
|
|
48
|
+
# 删除进程
|
|
49
|
+
npm run delete
|
|
50
|
+
|
|
51
|
+
# 查看进程
|
|
52
|
+
npm run list
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 4. 证书自动部署API
|
|
56
|
+
|
|
57
|
+
#### 接口说明
|
|
58
|
+
|
|
59
|
+
证书续期后台可以通过HTTP POST请求将证书文件自动部署到本项目。
|
|
60
|
+
|
|
61
|
+
#### 请求地址
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
POST http://your-domain:3000/api/deploy-cert
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
#### 请求参数
|
|
68
|
+
|
|
69
|
+
- Content-Type: `multipart/form-data`
|
|
70
|
+
- `name`: 证书名称(必填),用于创建子目录,例如 `aaa` 会创建 `cert/aaa` 目录
|
|
71
|
+
- `file`: zip格式的证书文件(必填)
|
|
72
|
+
|
|
73
|
+
#### 返回格式
|
|
74
|
+
|
|
75
|
+
成功响应:
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"code": 0,
|
|
79
|
+
"msg": "证书部署成功",
|
|
80
|
+
"path": "cert/aaa",
|
|
81
|
+
"fileCount": 11
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
失败响应:
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"code": 错误码,
|
|
89
|
+
"msg": "错误信息"
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### 错误码说明
|
|
94
|
+
|
|
95
|
+
- `1001`: 缺少参数 name
|
|
96
|
+
- `1002`: 缺少文件
|
|
97
|
+
- `1003`: name参数包含非法字符
|
|
98
|
+
- `2001`: 解压zip文件失败
|
|
99
|
+
- `5000`: 服务器内部错误
|
|
100
|
+
|
|
101
|
+
#### 证书文件说明
|
|
102
|
+
|
|
103
|
+
上传的zip文件解压后应包含以下内容:
|
|
104
|
+
|
|
105
|
+
- 私钥.pem(私钥文件)
|
|
106
|
+
- 证书.pem(证书文件,包含证书链)
|
|
107
|
+
- 中间证书.pem(中间证书文件,pem格式)
|
|
108
|
+
- cert.crt(证书文件,包含证书链,与证书.pem内容一致)
|
|
109
|
+
- cert.key(私钥文件,pem格式,与私钥.pem内容一致)
|
|
110
|
+
- intermediate.crt(中间证书文件,pem格式,中间证书.pem内容一致)
|
|
111
|
+
- cert.der(der格式证书文件)
|
|
112
|
+
- cert.jks(jks格式证书文件,java服务器使用)
|
|
113
|
+
- cert.p7b(p7b格式证书文件)
|
|
114
|
+
- cert.pfx(pfx格式证书文件,iis服务器使用)
|
|
115
|
+
- one.pem(证书和私钥简单合并成一个文件,pem格式,crt正文+key正文)
|
|
116
|
+
|
|
117
|
+
#### 使用示例
|
|
118
|
+
|
|
119
|
+
使用curl命令:
|
|
120
|
+
```bash
|
|
121
|
+
curl -X POST http://localhost:3000/api/deploy-cert \
|
|
122
|
+
-F "name=example" \
|
|
123
|
+
-F "file=@/path/to/cert.zip"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
使用JavaScript(fetch):
|
|
127
|
+
```javascript
|
|
128
|
+
const formData = new FormData();
|
|
129
|
+
formData.append('name', 'example');
|
|
130
|
+
formData.append('file', fileBlob);
|
|
131
|
+
|
|
132
|
+
fetch('http://localhost:3000/api/deploy-cert', {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
body: formData
|
|
135
|
+
})
|
|
136
|
+
.then(res => res.json())
|
|
137
|
+
.then(data => console.log(data));
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 5. 配置说明
|
|
141
|
+
|
|
142
|
+
编辑 `config.json` 文件可以修改服务端口和路由前缀:
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"port": 6001,
|
|
147
|
+
"routerName": "api",
|
|
148
|
+
"ipWhitelist": []
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- `port`: 服务监听端口(默认 6001)
|
|
153
|
+
- `routerName`: API 路由前缀(默认 "api",即访问路径为 `/api/...`)
|
|
154
|
+
- `ipWhitelist`: IP白名单,默认为空,表示不限制IP,如果需要限制IP,可以填写IP地址,多个IP地址用逗号分隔
|
|
155
|
+
|
|
156
|
+
### 6. License
|
|
157
|
+
|
|
158
|
+
MIT
|
|
159
|
+
|
|
160
|
+
### 7. 发布到 NPM(维护者使用)
|
|
161
|
+
|
|
162
|
+
#### 发布前准备
|
|
163
|
+
|
|
164
|
+
1. 确保已登录 npm 账号:
|
|
165
|
+
```bash
|
|
166
|
+
npm login
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
2. 更新版本号(遵循语义化版本):
|
|
170
|
+
```bash
|
|
171
|
+
# 修复bug
|
|
172
|
+
npm version patch
|
|
173
|
+
|
|
174
|
+
# 新功能
|
|
175
|
+
npm version minor
|
|
176
|
+
|
|
177
|
+
# 破坏性更新
|
|
178
|
+
npm version major
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### 发布命令
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# 发布到 npm
|
|
185
|
+
npm publish
|
|
186
|
+
|
|
187
|
+
# 如果是第一次发布公开包
|
|
188
|
+
npm publish --access public
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### 测试包
|
|
192
|
+
|
|
193
|
+
发布前可以本地测试:
|
|
194
|
+
```bash
|
|
195
|
+
# 打包测试
|
|
196
|
+
npm pack
|
|
197
|
+
|
|
198
|
+
# 本地安装测试
|
|
199
|
+
npm install -g ./vk-ssl-auto-deploy-1.0.0.tgz
|
|
200
|
+
```
|
package/app.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const createError = require('http-errors');
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const cookieParser = require('cookie-parser');
|
|
5
|
+
const logger = require('morgan');
|
|
6
|
+
|
|
7
|
+
const indexRouter = require('./routes/index');
|
|
8
|
+
const testRouter = require('./routes/test');
|
|
9
|
+
const certRouter = require('./routes/cert');
|
|
10
|
+
|
|
11
|
+
const config = require('./config');
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const app = express();
|
|
15
|
+
|
|
16
|
+
// view engine setup
|
|
17
|
+
app.set('views', path.join(__dirname, 'views'));
|
|
18
|
+
app.set('view engine', 'ejs');
|
|
19
|
+
|
|
20
|
+
app.use(logger('dev'));
|
|
21
|
+
app.use(express.json());
|
|
22
|
+
app.use(express.urlencoded({ extended: false }));
|
|
23
|
+
app.use(cookieParser());
|
|
24
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
|
25
|
+
|
|
26
|
+
app.use('/', indexRouter);
|
|
27
|
+
app.use('/test', testRouter);
|
|
28
|
+
app.use(`/${config.routerName}`, certRouter);
|
|
29
|
+
|
|
30
|
+
// catch 404 and forward to error handler
|
|
31
|
+
app.use((req, res, next) => {
|
|
32
|
+
next(createError(404));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// error handler
|
|
36
|
+
app.use((err, req, res, next) => {
|
|
37
|
+
res.locals.message = err.message;
|
|
38
|
+
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
|
39
|
+
res.status(err.status || 500);
|
|
40
|
+
res.render('error');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
module.exports = app;
|
package/bin/www.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Module dependencies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const app = require('../app');
|
|
8
|
+
const debug = require('debug')('express-template:server');
|
|
9
|
+
const http = require('http');
|
|
10
|
+
const config = require('../config');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get port from environment and store in Express.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const port = normalizePort(process.env.PORT || config.port);
|
|
17
|
+
app.set('port', port);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create HTTP server.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const server = http.createServer(app);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Listen on provided port, on all network interfaces.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
server.listen(port);
|
|
30
|
+
console.log('listenPort:', port)
|
|
31
|
+
server.on('error', onError);
|
|
32
|
+
server.on('listening', onListening);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Normalize a port into a number, string, or false.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
function normalizePort(val) {
|
|
39
|
+
const port = parseInt(val, 10);
|
|
40
|
+
|
|
41
|
+
if (isNaN(port)) {
|
|
42
|
+
// named pipe
|
|
43
|
+
return val;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (port >= 0) {
|
|
47
|
+
// port number
|
|
48
|
+
return port;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Event listener for HTTP server "error" event.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
function onError(error) {
|
|
59
|
+
if (error.syscall !== 'listen') {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const bind = typeof port === 'string' ?
|
|
64
|
+
'Pipe ' + port :
|
|
65
|
+
'Port ' + port;
|
|
66
|
+
|
|
67
|
+
// handle specific listen errors with friendly messages
|
|
68
|
+
switch (error.code) {
|
|
69
|
+
case 'EACCES':
|
|
70
|
+
console.error(bind + ' requires elevated privileges');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
break;
|
|
73
|
+
case 'EADDRINUSE':
|
|
74
|
+
console.error(bind + ' is already in use');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
break;
|
|
77
|
+
default:
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Event listener for HTTP server "listening" event.
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
function onListening() {
|
|
87
|
+
const addr = server.address();
|
|
88
|
+
const bind = typeof addr === 'string' ?
|
|
89
|
+
'pipe ' + addr :
|
|
90
|
+
'port ' + addr.port;
|
|
91
|
+
debug('Listening on ' + bind);
|
|
92
|
+
}
|
package/config.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vk-ssl-auto-deploy",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "SSL证书自动部署工具 - 提供HTTP API接口,支持证书文件自动上传和部署",
|
|
5
|
+
"main": "app.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vk-ssl": "./bin/www.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "node ./bin/www",
|
|
11
|
+
"start": "pm2 start ./bin/www.js --name vk-ssl-auto-deploy",
|
|
12
|
+
"stop": "pm2 stop vk-ssl-auto-deploy",
|
|
13
|
+
"restart": "pm2 restart vk-ssl-auto-deploy",
|
|
14
|
+
"delete": "pm2 delete vk-ssl-auto-deploy",
|
|
15
|
+
"logs": "pm2 logs vk-ssl-auto-deploy",
|
|
16
|
+
"list": "pm2 list"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"ssl",
|
|
20
|
+
"certificate",
|
|
21
|
+
"auto-deploy",
|
|
22
|
+
"express",
|
|
23
|
+
"证书部署",
|
|
24
|
+
"自动化",
|
|
25
|
+
"无忧SSL证书"
|
|
26
|
+
],
|
|
27
|
+
"author": "VK",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/yourusername/vk-ssl-auto-deploy.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/yourusername/vk-ssl-auto-deploy/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/yourusername/vk-ssl-auto-deploy#readme",
|
|
37
|
+
"files": [
|
|
38
|
+
"bin/",
|
|
39
|
+
"routes/",
|
|
40
|
+
"views/",
|
|
41
|
+
"public/",
|
|
42
|
+
"app.js",
|
|
43
|
+
"config.json",
|
|
44
|
+
"README.md"
|
|
45
|
+
],
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=12.0.0"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"adm-zip": "^0.5.16",
|
|
51
|
+
"cookie-parser": "~1.4.4",
|
|
52
|
+
"debug": "~2.6.9",
|
|
53
|
+
"ejs": "~2.6.1",
|
|
54
|
+
"express": "~4.16.1",
|
|
55
|
+
"http-errors": "~1.6.3",
|
|
56
|
+
"morgan": "~1.9.1",
|
|
57
|
+
"multer": "^2.0.2"
|
|
58
|
+
},
|
|
59
|
+
"optionalDependencies": {
|
|
60
|
+
"pm2": "^5.3.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/routes/cert.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const multer = require('multer');
|
|
4
|
+
const AdmZip = require('adm-zip');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { exec } = require('child_process');
|
|
8
|
+
const { promisify } = require('util');
|
|
9
|
+
const config = require('../config.json');
|
|
10
|
+
const { validateCertFile } = require('../utils/certValidator');
|
|
11
|
+
const { getClientIP, isIPInWhitelist } = require('../utils/ipValidator');
|
|
12
|
+
|
|
13
|
+
const execAsync = promisify(exec);
|
|
14
|
+
|
|
15
|
+
// 使用内存存储
|
|
16
|
+
const upload = multer({
|
|
17
|
+
storage: multer.memoryStorage(),
|
|
18
|
+
limits: {
|
|
19
|
+
fileSize: 10 * 1024 * 1024 // 限制文件大小为10MB
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* POST /api/deploy-cert
|
|
25
|
+
* 接收证书zip文件并自动部署
|
|
26
|
+
*
|
|
27
|
+
* 参数:
|
|
28
|
+
* - name: 证书名称(用于创建子目录)
|
|
29
|
+
* - file: zip文件
|
|
30
|
+
*
|
|
31
|
+
* 返回:
|
|
32
|
+
* - 成功: { code: 0, msg: '证书部署成功', path: 'cert/xxx' }
|
|
33
|
+
* - 失败: { code: 错误码, msg: '错误信息' }
|
|
34
|
+
*/
|
|
35
|
+
router.post('/deploy-cert', upload.single('file'), async (req, res, next) => {
|
|
36
|
+
try {
|
|
37
|
+
// 0. IP白名单验证
|
|
38
|
+
const clientIP = getClientIP(req);
|
|
39
|
+
if (!isIPInWhitelist(clientIP, config.ipWhitelist)) {
|
|
40
|
+
console.warn(`[IP白名单拦截] IP: ${clientIP} 尝试部署证书但不在白名单中`);
|
|
41
|
+
return res.json({
|
|
42
|
+
code: 4030,
|
|
43
|
+
msg: 'IP地址不在白名单中,访问被拒绝',
|
|
44
|
+
ip: clientIP
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`[证书部署请求] 来源IP: ${clientIP}`);
|
|
49
|
+
|
|
50
|
+
// 1. 验证参数
|
|
51
|
+
if (!req.body.name) {
|
|
52
|
+
return res.json({ code: 1001, msg: '缺少参数: name' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!req.file) {
|
|
56
|
+
return res.json({ code: 1002, msg: '缺少文件: file' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const certName = req.body.name;
|
|
60
|
+
|
|
61
|
+
// 验证name参数,防止路径注入
|
|
62
|
+
if (certName.includes('..') || certName.includes('/') || certName.includes('\\')) {
|
|
63
|
+
return res.json({ code: 1003, msg: 'name参数包含非法字符' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. 创建cert目录和子目录
|
|
67
|
+
const projectRoot = path.join(__dirname, '..');
|
|
68
|
+
const certBaseDir = path.join(projectRoot, 'cert');
|
|
69
|
+
const certDir = path.join(certBaseDir, certName);
|
|
70
|
+
|
|
71
|
+
// 确保cert根目录存在
|
|
72
|
+
if (!fs.existsSync(certBaseDir)) {
|
|
73
|
+
fs.mkdirSync(certBaseDir, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 如果目标目录已存在,先删除(实现覆盖更新)
|
|
77
|
+
if (fs.existsSync(certDir)) {
|
|
78
|
+
fs.rmSync(certDir, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 创建目标目录
|
|
82
|
+
fs.mkdirSync(certDir, { recursive: true });
|
|
83
|
+
|
|
84
|
+
// 3. 解压zip文件并进行安全验证
|
|
85
|
+
try {
|
|
86
|
+
const zip = new AdmZip(req.file.buffer);
|
|
87
|
+
const zipEntries = zip.getEntries();
|
|
88
|
+
|
|
89
|
+
// 统计信息
|
|
90
|
+
const stats = {
|
|
91
|
+
total: 0, // 总文件数
|
|
92
|
+
saved: 0, // 成功保存的文件数
|
|
93
|
+
filtered: 0, // 被过滤的文件数
|
|
94
|
+
savedFiles: [], // 保存的文件列表
|
|
95
|
+
filteredFiles: [] // 被过滤的文件列表(含原因)
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// 遍历并验证每个文件
|
|
99
|
+
zipEntries.forEach((entry) => {
|
|
100
|
+
if (!entry.isDirectory) {
|
|
101
|
+
stats.total++;
|
|
102
|
+
|
|
103
|
+
const filename = path.basename(entry.entryName);
|
|
104
|
+
const entryData = entry.getData();
|
|
105
|
+
|
|
106
|
+
// 安全验证
|
|
107
|
+
const validation = validateCertFile(filename, entryData, entry.entryName);
|
|
108
|
+
|
|
109
|
+
if (validation.valid) {
|
|
110
|
+
// 验证通过,保存文件
|
|
111
|
+
try {
|
|
112
|
+
const entryPath = path.join(certDir, entry.entryName);
|
|
113
|
+
|
|
114
|
+
// 确保父目录存在
|
|
115
|
+
const entryDir = path.dirname(entryPath);
|
|
116
|
+
if (!fs.existsSync(entryDir)) {
|
|
117
|
+
fs.mkdirSync(entryDir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 写入文件
|
|
121
|
+
fs.writeFileSync(entryPath, entryData);
|
|
122
|
+
|
|
123
|
+
stats.saved++;
|
|
124
|
+
stats.savedFiles.push(entry.entryName);
|
|
125
|
+
|
|
126
|
+
console.log(`[文件已保存] ${entry.entryName} (${entryData.length} bytes)`);
|
|
127
|
+
} catch (writeError) {
|
|
128
|
+
console.error(`[文件写入失败] ${entry.entryName}:`, writeError.message);
|
|
129
|
+
stats.filtered++;
|
|
130
|
+
stats.filteredFiles.push({
|
|
131
|
+
filename: entry.entryName,
|
|
132
|
+
reason: '文件写入失败: ' + writeError.message
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// 验证失败,过滤该文件
|
|
137
|
+
stats.filtered++;
|
|
138
|
+
stats.filteredFiles.push({
|
|
139
|
+
filename: entry.entryName,
|
|
140
|
+
reason: validation.reason
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
console.warn(`[文件已过滤] ${entry.entryName}: ${validation.reason}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// 检查是否至少保存了一个文件
|
|
149
|
+
if (stats.saved === 0) {
|
|
150
|
+
// 如果没有任何有效文件,删除创建的目录
|
|
151
|
+
if (fs.existsSync(certDir)) {
|
|
152
|
+
fs.rmSync(certDir, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return res.json({
|
|
156
|
+
code: 2002,
|
|
157
|
+
msg: 'zip文件中没有有效的证书文件',
|
|
158
|
+
stats: stats
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(`[证书部署成功] 名称: ${certName}, 路径: ${certDir}, 保存: ${stats.saved}个, 过滤: ${stats.filtered}个`);
|
|
163
|
+
|
|
164
|
+
// 4. 执行回调命令
|
|
165
|
+
const commandResults = [];
|
|
166
|
+
if (config.callbackCommand && Array.isArray(config.callbackCommand) && config.callbackCommand.length > 0) {
|
|
167
|
+
console.log(`[开始执行回调命令] 共${config.callbackCommand.length}个命令`);
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < config.callbackCommand.length; i++) {
|
|
170
|
+
const command = config.callbackCommand[i];
|
|
171
|
+
const commandResult = {
|
|
172
|
+
command: command,
|
|
173
|
+
index: i + 1,
|
|
174
|
+
success: false,
|
|
175
|
+
output: '',
|
|
176
|
+
error: ''
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
console.log(`[执行命令 ${i + 1}/${config.callbackCommand.length}] ${command}`);
|
|
181
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
182
|
+
timeout: 30000, // 30秒超时
|
|
183
|
+
maxBuffer: 1024 * 1024 // 1MB buffer
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
commandResult.success = true;
|
|
187
|
+
commandResult.output = stdout ? stdout.trim() : '';
|
|
188
|
+
if (stderr) {
|
|
189
|
+
commandResult.error = stderr.trim();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log(`[命令执行成功 ${i + 1}/${config.callbackCommand.length}] ${command}`);
|
|
193
|
+
if (stdout) console.log(`[命令输出] ${stdout.trim()}`);
|
|
194
|
+
if (stderr) console.warn(`[命令stderr] ${stderr.trim()}`);
|
|
195
|
+
|
|
196
|
+
} catch (cmdError) {
|
|
197
|
+
commandResult.success = false;
|
|
198
|
+
commandResult.error = cmdError.message;
|
|
199
|
+
if (cmdError.stdout) commandResult.output = cmdError.stdout.trim();
|
|
200
|
+
if (cmdError.stderr) commandResult.error += '\n' + cmdError.stderr.trim();
|
|
201
|
+
|
|
202
|
+
console.error(`[命令执行失败 ${i + 1}/${config.callbackCommand.length}] ${command}:`, cmdError.message);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
commandResults.push(commandResult);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(`[回调命令执行完成] 成功: ${commandResults.filter(r => r.success).length}个, 失败: ${commandResults.filter(r => !r.success).length}个`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return res.json({
|
|
212
|
+
code: 0,
|
|
213
|
+
msg: '证书部署成功',
|
|
214
|
+
path: 'cert/' + certName,
|
|
215
|
+
stats: {
|
|
216
|
+
total: stats.total,
|
|
217
|
+
saved: stats.saved,
|
|
218
|
+
filtered: stats.filtered,
|
|
219
|
+
savedFiles: stats.savedFiles,
|
|
220
|
+
filteredFiles: stats.filteredFiles
|
|
221
|
+
},
|
|
222
|
+
commandResults: commandResults.length > 0 ? commandResults : undefined
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
} catch (zipError) {
|
|
226
|
+
console.error('[解压失败]', zipError);
|
|
227
|
+
return res.json({ code: 2001, msg: '解压zip文件失败: ' + zipError.message });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error('[证书部署错误]', error);
|
|
232
|
+
return res.json({ code: 5000, msg: '服务器内部错误: ' + error.message });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
module.exports = router;
|
package/routes/index.js
ADDED
package/routes/test.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
|
|
4
|
+
/* GET users listing. */
|
|
5
|
+
router.get('/', (req, res, next) => {
|
|
6
|
+
res.send('respond with a resource');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
router.get('/test', (req, res, next) => {
|
|
10
|
+
res.send('hello word');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
module.exports = router;
|
package/views/error.ejs
ADDED