ok
Browse files- Dockerfile +1 -5
- app.js +12 -16
- package.json +9 -11
- public/index.html +2 -1
- public/js/main.js +8 -4
- routes/auth.js +5 -7
- routes/command.js +22 -5
- utils/logger.js +2 -5
Dockerfile
CHANGED
@@ -11,14 +11,10 @@ RUN npm install
|
|
11 |
|
12 |
COPY . .
|
13 |
|
14 |
-
RUN mkdir -p /app/data /app/
|
15 |
-
chown -R node:node /app
|
16 |
-
|
17 |
-
USER node
|
18 |
|
19 |
EXPOSE 7860
|
20 |
|
21 |
-
ENV NODE_ENV=production
|
22 |
ENV ADMIN_USERNAME=${ADMIN_USERNAME}
|
23 |
ENV ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
24 |
|
|
|
11 |
|
12 |
COPY . .
|
13 |
|
14 |
+
RUN mkdir -p /app/data && chown -R node:node /app /app/data
|
|
|
|
|
|
|
15 |
|
16 |
EXPOSE 7860
|
17 |
|
|
|
18 |
ENV ADMIN_USERNAME=${ADMIN_USERNAME}
|
19 |
ENV ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
20 |
|
app.js
CHANGED
@@ -1,11 +1,11 @@
|
|
1 |
const express = require('express');
|
2 |
const winston = require('winston');
|
|
|
3 |
const path = require('path');
|
4 |
const rateLimit = require('express-rate-limit');
|
|
|
5 |
const helmet = require('helmet');
|
6 |
const jwt = require('jsonwebtoken');
|
7 |
-
const cors = require('cors');
|
8 |
-
const fs = require('fs');
|
9 |
|
10 |
const app = express();
|
11 |
const port = process.env.PORT || 7860;
|
@@ -31,12 +31,11 @@ if (process.env.NODE_ENV !== 'production') {
|
|
31 |
|
32 |
// 安全中间件
|
33 |
app.use(helmet());
|
34 |
-
app.use(cors());
|
35 |
|
36 |
// 速率限制
|
37 |
const limiter = rateLimit({
|
38 |
-
windowMs: 15 * 60 * 1000,
|
39 |
-
max: 100
|
40 |
});
|
41 |
app.use('/api', limiter);
|
42 |
|
@@ -79,19 +78,16 @@ app.use((err, req, res, next) => {
|
|
79 |
res.status(500).json({ error: '服务器内部错误' });
|
80 |
});
|
81 |
|
82 |
-
//
|
83 |
-
const
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
app.listen(port, () => {
|
89 |
logger.info(`Web 命令执行应用正在监听 http://localhost:${port}`);
|
90 |
});
|
91 |
-
}
|
|
|
92 |
logger.error('无法创建命令历史文件:', err);
|
93 |
process.exit(1);
|
94 |
-
}
|
95 |
-
};
|
96 |
-
|
97 |
-
startServer();
|
|
|
1 |
const express = require('express');
|
2 |
const winston = require('winston');
|
3 |
+
const fs = require('fs').promises;
|
4 |
const path = require('path');
|
5 |
const rateLimit = require('express-rate-limit');
|
6 |
+
const { promises: fsPromises } = require('fs');
|
7 |
const helmet = require('helmet');
|
8 |
const jwt = require('jsonwebtoken');
|
|
|
|
|
9 |
|
10 |
const app = express();
|
11 |
const port = process.env.PORT || 7860;
|
|
|
31 |
|
32 |
// 安全中间件
|
33 |
app.use(helmet());
|
|
|
34 |
|
35 |
// 速率限制
|
36 |
const limiter = rateLimit({
|
37 |
+
windowMs: 15 * 60 * 1000, // 15分钟
|
38 |
+
max: 100 // 每个IP限制100个请求
|
39 |
});
|
40 |
app.use('/api', limiter);
|
41 |
|
|
|
78 |
res.status(500).json({ error: '服务器内部错误' });
|
79 |
});
|
80 |
|
81 |
+
// 确保命令历史文件存在
|
82 |
+
const historyFilePath = path.join(__dirname, 'data', 'command_history.json');
|
83 |
+
fsPromises.access(historyFilePath)
|
84 |
+
.catch(() => fsPromises.writeFile(historyFilePath, '[]'))
|
85 |
+
.then(() => {
|
|
|
86 |
app.listen(port, () => {
|
87 |
logger.info(`Web 命令执行应用正在监听 http://localhost:${port}`);
|
88 |
});
|
89 |
+
})
|
90 |
+
.catch(err => {
|
91 |
logger.error('无法创建命令历史文件:', err);
|
92 |
process.exit(1);
|
93 |
+
});
|
|
|
|
|
|
package.json
CHANGED
@@ -9,20 +9,18 @@
|
|
9 |
"test": "jest"
|
10 |
},
|
11 |
"dependencies": {
|
12 |
-
"express": "^4.
|
13 |
-
"winston": "^3.
|
14 |
-
"helmet": "^
|
15 |
-
"express-rate-limit": "^
|
16 |
-
"jsonwebtoken": "^
|
17 |
-
"cors": "^2.8.5",
|
18 |
-
"bcrypt": "^5.1.0"
|
19 |
},
|
20 |
"devDependencies": {
|
21 |
-
"nodemon": "^
|
22 |
-
"eslint": "^
|
23 |
-
"jest": "^
|
24 |
},
|
25 |
"engines": {
|
26 |
-
"node": ">=
|
27 |
}
|
28 |
}
|
|
|
9 |
"test": "jest"
|
10 |
},
|
11 |
"dependencies": {
|
12 |
+
"express": "^4.17.1",
|
13 |
+
"winston": "^3.3.3",
|
14 |
+
"helmet": "^4.6.0",
|
15 |
+
"express-rate-limit": "^5.3.0",
|
16 |
+
"jsonwebtoken": "^8.5.1"
|
|
|
|
|
17 |
},
|
18 |
"devDependencies": {
|
19 |
+
"nodemon": "^2.0.7",
|
20 |
+
"eslint": "^7.32.0",
|
21 |
+
"jest": "^27.0.6"
|
22 |
},
|
23 |
"engines": {
|
24 |
+
"node": ">=14.0.0"
|
25 |
}
|
26 |
}
|
public/index.html
CHANGED
@@ -7,8 +7,9 @@
|
|
7 |
<title>Web 命令执行</title>
|
8 |
<script src="https://cdn.tailwindcss.com"></script>
|
9 |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/xss.min.js"></script>
|
10 |
-
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700&display=swap" rel="stylesheet">
|
11 |
<style>
|
|
|
|
|
12 |
body {
|
13 |
font-family: 'Noto Sans SC', sans-serif;
|
14 |
}
|
|
|
7 |
<title>Web 命令执行</title>
|
8 |
<script src="https://cdn.tailwindcss.com"></script>
|
9 |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/xss.min.js"></script>
|
|
|
10 |
<style>
|
11 |
+
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700&display=swap');
|
12 |
+
|
13 |
body {
|
14 |
font-family: 'Noto Sans SC', sans-serif;
|
15 |
}
|
public/js/main.js
CHANGED
@@ -11,8 +11,8 @@ commandInput.addEventListener('keypress', function (event) {
|
|
11 |
});
|
12 |
|
13 |
async function executeCommand() {
|
14 |
-
const command = commandInput.value
|
15 |
-
if (!command) return;
|
16 |
|
17 |
showLoading(true);
|
18 |
output.textContent = '正在执行命令...';
|
@@ -30,7 +30,7 @@ async function executeCommand() {
|
|
30 |
if (response.ok) {
|
31 |
output.textContent = data.output || data.error;
|
32 |
commandInput.value = '';
|
33 |
-
|
34 |
} else {
|
35 |
output.textContent = `错误: ${data.error}`;
|
36 |
}
|
@@ -83,7 +83,7 @@ async function login() {
|
|
83 |
localStorage.setItem('token', data.token);
|
84 |
document.getElementById('loginForm').style.display = 'none';
|
85 |
document.getElementById('commandInterface').style.display = 'block';
|
86 |
-
|
87 |
} else {
|
88 |
alert('登录失败: ' + data.error);
|
89 |
}
|
@@ -94,6 +94,8 @@ async function login() {
|
|
94 |
|
95 |
document.getElementById('loginButton').addEventListener('click', login);
|
96 |
|
|
|
|
|
97 |
function checkLoginStatus() {
|
98 |
const token = localStorage.getItem('token');
|
99 |
if (token) {
|
@@ -103,5 +105,7 @@ function checkLoginStatus() {
|
|
103 |
}
|
104 |
}
|
105 |
|
|
|
106 |
window.addEventListener('load', checkLoginStatus);
|
|
|
107 |
document.getElementById('executeButton').addEventListener('click', executeCommand);
|
|
|
11 |
});
|
12 |
|
13 |
async function executeCommand() {
|
14 |
+
const command = commandInput.value;
|
15 |
+
if (!command.trim()) return;
|
16 |
|
17 |
showLoading(true);
|
18 |
output.textContent = '正在执行命令...';
|
|
|
30 |
if (response.ok) {
|
31 |
output.textContent = data.output || data.error;
|
32 |
commandInput.value = '';
|
33 |
+
loadCommandHistory();
|
34 |
} else {
|
35 |
output.textContent = `错误: ${data.error}`;
|
36 |
}
|
|
|
83 |
localStorage.setItem('token', data.token);
|
84 |
document.getElementById('loginForm').style.display = 'none';
|
85 |
document.getElementById('commandInterface').style.display = 'block';
|
86 |
+
loadCommandHistory();
|
87 |
} else {
|
88 |
alert('登录失败: ' + data.error);
|
89 |
}
|
|
|
94 |
|
95 |
document.getElementById('loginButton').addEventListener('click', login);
|
96 |
|
97 |
+
loadCommandHistory();
|
98 |
+
|
99 |
function checkLoginStatus() {
|
100 |
const token = localStorage.getItem('token');
|
101 |
if (token) {
|
|
|
105 |
}
|
106 |
}
|
107 |
|
108 |
+
// 在页面加载时调用此函数
|
109 |
window.addEventListener('load', checkLoginStatus);
|
110 |
+
|
111 |
document.getElementById('executeButton').addEventListener('click', executeCommand);
|
routes/auth.js
CHANGED
@@ -1,25 +1,23 @@
|
|
1 |
const express = require('express');
|
2 |
const jwt = require('jsonwebtoken');
|
3 |
const router = express.Router();
|
4 |
-
const bcrypt = require('bcrypt');
|
5 |
|
6 |
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
7 |
-
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1h';
|
8 |
|
9 |
const users = [
|
10 |
{
|
11 |
id: 1,
|
12 |
username: process.env.ADMIN_USERNAME,
|
13 |
-
password:
|
14 |
}
|
15 |
];
|
16 |
|
17 |
-
router.post('/login',
|
18 |
const { username, password } = req.body;
|
19 |
-
const user = users.find(u => u.username === username);
|
20 |
|
21 |
-
if (user
|
22 |
-
const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn:
|
23 |
res.json({ token });
|
24 |
} else {
|
25 |
res.status(401).json({ error: '无效的用户名或密码' });
|
|
|
1 |
const express = require('express');
|
2 |
const jwt = require('jsonwebtoken');
|
3 |
const router = express.Router();
|
|
|
4 |
|
5 |
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
|
|
6 |
|
7 |
const users = [
|
8 |
{
|
9 |
id: 1,
|
10 |
username: process.env.ADMIN_USERNAME,
|
11 |
+
password: process.env.ADMIN_PASSWORD
|
12 |
}
|
13 |
];
|
14 |
|
15 |
+
router.post('/login', (req, res) => {
|
16 |
const { username, password } = req.body;
|
17 |
+
const user = users.find(u => u.username === username && u.password === password);
|
18 |
|
19 |
+
if (user) {
|
20 |
+
const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '1h' });
|
21 |
res.json({ token });
|
22 |
} else {
|
23 |
res.status(401).json({ error: '无效的用户名或密码' });
|
routes/command.js
CHANGED
@@ -6,13 +6,21 @@ const router = express.Router();
|
|
6 |
|
7 |
const logger = require('../utils/logger');
|
8 |
|
|
|
9 |
const allowedCommands = ['ls', 'pwd', 'whoami', 'date', 'echo', 'cat'];
|
|
|
|
|
10 |
const historyFilePath = path.join(__dirname, '..', 'data', 'command_history.json');
|
11 |
|
12 |
router.get('/command-history', async (req, res) => {
|
13 |
try {
|
14 |
-
|
15 |
-
|
|
|
|
|
|
|
|
|
|
|
16 |
res.json(history);
|
17 |
} catch (error) {
|
18 |
logger.error('处理命令历史请求失败:', error);
|
@@ -24,6 +32,7 @@ router.post('/execute', async (req, res) => {
|
|
24 |
const { command } = req.body;
|
25 |
const baseCommand = command.split(' ')[0];
|
26 |
|
|
|
27 |
// if (!allowedCommands.includes(baseCommand)) {
|
28 |
// logger.warn(`用户 ${req.user.username} 尝试执行未授权的命令: ${command}`);
|
29 |
// return res.status(403).json({ error: '未授权的命令' });
|
@@ -35,11 +44,19 @@ router.post('/execute', async (req, res) => {
|
|
35 |
return res.status(500).json({ error: error.message });
|
36 |
}
|
37 |
|
|
|
38 |
try {
|
39 |
-
|
40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
history.push({ command, timestamp: new Date().toISOString(), user: req.user.username });
|
42 |
-
history
|
|
|
43 |
await fs.writeFile(historyFilePath, JSON.stringify(history, null, 2));
|
44 |
} catch (writeError) {
|
45 |
logger.error('写入命令历史失败:', writeError);
|
|
|
6 |
|
7 |
const logger = require('../utils/logger');
|
8 |
|
9 |
+
// 命令白名单
|
10 |
const allowedCommands = ['ls', 'pwd', 'whoami', 'date', 'echo', 'cat'];
|
11 |
+
|
12 |
+
// 历史命令文件路径
|
13 |
const historyFilePath = path.join(__dirname, '..', 'data', 'command_history.json');
|
14 |
|
15 |
router.get('/command-history', async (req, res) => {
|
16 |
try {
|
17 |
+
let history = [];
|
18 |
+
try {
|
19 |
+
const historyData = await fs.readFile(historyFilePath, 'utf-8');
|
20 |
+
history = JSON.parse(historyData);
|
21 |
+
} catch (readError) {
|
22 |
+
logger.warn('读取命令历史失败,使用空数组:', readError);
|
23 |
+
}
|
24 |
res.json(history);
|
25 |
} catch (error) {
|
26 |
logger.error('处理命令历史请求失败:', error);
|
|
|
32 |
const { command } = req.body;
|
33 |
const baseCommand = command.split(' ')[0];
|
34 |
|
35 |
+
// 白名单检查的部分
|
36 |
// if (!allowedCommands.includes(baseCommand)) {
|
37 |
// logger.warn(`用户 ${req.user.username} 尝试执行未授权的命令: ${command}`);
|
38 |
// return res.status(403).json({ error: '未授权的命令' });
|
|
|
44 |
return res.status(500).json({ error: error.message });
|
45 |
}
|
46 |
|
47 |
+
// 记录命令历史
|
48 |
try {
|
49 |
+
let history = [];
|
50 |
+
try {
|
51 |
+
const historyData = await fs.readFile(historyFilePath, 'utf-8');
|
52 |
+
history = JSON.parse(historyData);
|
53 |
+
} catch (readError) {
|
54 |
+
// 如果文件不存在或为空,使用空数组
|
55 |
+
}
|
56 |
+
|
57 |
history.push({ command, timestamp: new Date().toISOString(), user: req.user.username });
|
58 |
+
if (history.length > 100) history.shift(); // 保留最近100条命令
|
59 |
+
|
60 |
await fs.writeFile(historyFilePath, JSON.stringify(history, null, 2));
|
61 |
} catch (writeError) {
|
62 |
logger.error('写入命令历史失败:', writeError);
|
utils/logger.js
CHANGED
@@ -1,7 +1,4 @@
|
|
1 |
const winston = require('winston');
|
2 |
-
const path = require('path');
|
3 |
-
|
4 |
-
const logDir = path.join(__dirname, '..', 'logs');
|
5 |
|
6 |
const logger = winston.createLogger({
|
7 |
level: process.env.LOG_LEVEL || 'info',
|
@@ -10,8 +7,8 @@ const logger = winston.createLogger({
|
|
10 |
winston.format.json()
|
11 |
),
|
12 |
transports: [
|
13 |
-
new winston.transports.File({ filename:
|
14 |
-
new winston.transports.File({ filename:
|
15 |
]
|
16 |
});
|
17 |
|
|
|
1 |
const winston = require('winston');
|
|
|
|
|
|
|
2 |
|
3 |
const logger = winston.createLogger({
|
4 |
level: process.env.LOG_LEVEL || 'info',
|
|
|
7 |
winston.format.json()
|
8 |
),
|
9 |
transports: [
|
10 |
+
new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
11 |
+
new winston.transports.File({ filename: 'combined.log' })
|
12 |
]
|
13 |
});
|
14 |
|