aigems commited on
Commit
158961a
·
1 Parent(s): d211a74
Files changed (9) hide show
  1. Dockerfile +6 -0
  2. app.js +71 -14
  3. package.json +18 -4
  4. public/index.html +33 -32
  5. public/js/main.js +97 -0
  6. routes/auth.js +27 -0
  7. routes/command.js +63 -0
  8. start.sh +0 -67
  9. utils/logger.js +21 -0
Dockerfile CHANGED
@@ -1,5 +1,8 @@
1
  FROM node:20
2
 
 
 
 
3
  WORKDIR /app
4
 
5
  COPY package*.json ./
@@ -10,4 +13,7 @@ COPY . .
10
 
11
  EXPOSE 7860
12
 
 
 
 
13
  CMD ["node", "app.js"]
 
1
  FROM node:20
2
 
3
+ ARG ADMIN_USERNAME=admin
4
+ ARG ADMIN_PASSWORD=password
5
+
6
  WORKDIR /app
7
 
8
  COPY package*.json ./
 
13
 
14
  EXPOSE 7860
15
 
16
+ ENV ADMIN_USERNAME=${ADMIN_USERNAME}
17
+ ENV ADMIN_PASSWORD=${ADMIN_PASSWORD}
18
+
19
  CMD ["node", "app.js"]
app.js CHANGED
@@ -1,25 +1,82 @@
1
  const express = require('express');
2
- const { exec } = require('child_process');
 
 
 
 
 
 
3
  const app = express();
4
- const port = 7860;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  app.use(express.json());
7
  app.use(express.static('public'));
8
 
9
- app.get('/', (req, res) => {
10
- res.sendFile(__dirname + '/public/index.html');
11
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- app.post('/execute', (req, res) => {
14
- exec(req.body.command, (error, stdout, stderr) => {
15
- if (error) {
16
- console.error(`执行错误: ${error}`);
17
- return res.status(500).json({ error: error.message });
18
- }
19
- res.json({ output: stdout, error: stderr });
20
- });
21
  });
22
 
23
  app.listen(port, () => {
24
- console.log(`Web 命令执行应用正在监听 http://localhost:${port}`);
25
  });
 
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 helmet = require('helmet');
7
+ const jwt = require('jsonwebtoken');
8
+
9
  const app = express();
10
+ const port = process.env.PORT || 7860;
11
+
12
+ // 配置日志记录器
13
+ const logger = winston.createLogger({
14
+ level: process.env.LOG_LEVEL || 'info',
15
+ format: winston.format.combine(
16
+ winston.format.timestamp(),
17
+ winston.format.json()
18
+ ),
19
+ transports: [
20
+ new winston.transports.File({ filename: 'error.log', level: 'error' }),
21
+ new winston.transports.File({ filename: 'combined.log' })
22
+ ]
23
+ });
24
+
25
+ if (process.env.NODE_ENV !== 'production') {
26
+ logger.add(new winston.transports.Console({
27
+ format: winston.format.simple()
28
+ }));
29
+ }
30
+
31
+ // 安全中间件
32
+ app.use(helmet());
33
+
34
+ // 速率限制
35
+ const limiter = rateLimit({
36
+ windowMs: 15 * 60 * 1000, // 15分钟
37
+ max: 100 // 每个IP限制100个请求
38
+ });
39
+ app.use('/api', limiter);
40
 
41
  app.use(express.json());
42
  app.use(express.static('public'));
43
 
44
+ // JWT 密钥
45
+ const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
46
+
47
+ // 中间件:验证 JWT
48
+ const authenticateJWT = (req, res, next) => {
49
+ const authHeader = req.headers.authorization;
50
+
51
+ if (authHeader) {
52
+ const token = authHeader.split(' ')[1];
53
+
54
+ jwt.verify(token, JWT_SECRET, (err, user) => {
55
+ if (err) {
56
+ return res.sendStatus(403);
57
+ }
58
+
59
+ req.user = user;
60
+ next();
61
+ });
62
+ } else {
63
+ res.sendStatus(401);
64
+ }
65
+ };
66
+
67
+ // 路由
68
+ const authRoutes = require('./routes/auth');
69
+ const commandRoutes = require('./routes/command');
70
+
71
+ app.use('/api', authRoutes);
72
+ app.use('/api', authenticateJWT, commandRoutes);
73
 
74
+ // 错误处理中间件
75
+ app.use((err, req, res, next) => {
76
+ logger.error(`未捕获的错误: ${err.message}`);
77
+ res.status(500).json({ error: '服务器内部错误' });
 
 
 
 
78
  });
79
 
80
  app.listen(port, () => {
81
+ logger.info(`Web 命令执行应用正在监听 http://localhost:${port}`);
82
  });
package.json CHANGED
@@ -1,12 +1,26 @@
1
  {
2
- "name": "web-ssh-app",
3
- "version": "1.0.0",
4
  "main": "app.js",
5
  "scripts": {
6
- "start": "node app.js"
 
 
 
7
  },
8
  "dependencies": {
9
  "express": "^4.17.1",
10
- "ssh2": "^1.11.0"
 
 
 
 
 
 
 
 
 
 
 
11
  }
12
  }
 
1
  {
2
+ "name": "web-command-executor",
3
+ "version": "1.2.0",
4
  "main": "app.js",
5
  "scripts": {
6
+ "start": "node app.js",
7
+ "dev": "nodemon app.js",
8
+ "lint": "eslint .",
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
@@ -1,41 +1,42 @@
1
  <!DOCTYPE html>
2
- <html lang="en">
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Web SSH</title>
7
- <style>
8
- body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
9
- #output { white-space: pre-wrap; background-color: #f0f0f0; padding: 10px; border-radius: 5px; max-height: 400px; overflow-y: auto; }
10
- #command { width: 70%; padding: 5px; }
11
- button { padding: 5px 10px; }
12
- </style>
13
  </head>
14
- <body>
15
- <h1>Web SSH</h1>
16
- <p>This application allows you to execute SSH commands on the server. Enter your command below and click "Execute".</p>
17
- <input type="text" id="command" placeholder="Enter SSH command">
18
- <button onclick="executeCommand()">Execute</button>
19
- <div id="output"></div>
20
 
21
- <script>
22
- async function executeCommand() {
23
- const command = document.getElementById('command').value;
24
- const output = document.getElementById('output');
25
- output.textContent = 'Executing command...';
 
 
 
 
26
 
27
- try {
28
- const response = await fetch('/execute', {
29
- method: 'POST',
30
- headers: { 'Content-Type': 'application/json' },
31
- body: JSON.stringify({ command })
32
- });
33
- const data = await response.json();
34
- output.textContent = data.output || data.error;
35
- } catch (error) {
36
- output.textContent = 'Error: ' + error.message;
37
- }
38
- }
39
- </script>
 
 
 
 
 
40
  </body>
 
41
  </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Web 命令执行</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/xss@1.0.14/dist/xss.min.js"></script>
 
 
 
 
10
  </head>
 
 
 
 
 
 
11
 
12
+ <body class="bg-gray-100 p-8">
13
+ <div class="max-w-2xl mx-auto bg-white p-6 rounded-lg shadow-md">
14
+ <h1 class="text-3xl font-bold mb-4">Web 命令执行</h1>
15
+
16
+ <div id="loginForm">
17
+ <input type="text" id="username" placeholder="用户名" class="p-2 border rounded mb-2 w-full">
18
+ <input type="password" id="password" placeholder="密码" class="p-2 border rounded mb-2 w-full">
19
+ <button id="loginButton" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">登录</button>
20
+ </div>
21
 
22
+ <div id="commandInterface" style="display: none;">
23
+ <p class="mb-4">此应用允许您在服务器上执行特定的命令。请在下方输入命令并点击"执行"或按回车键。</p>
24
+ <div class="flex mb-4">
25
+ <input type="text" id="command" placeholder="输入命令"
26
+ class="flex-grow p-2 border rounded-l focus:outline-none focus:ring-2 focus:ring-blue-500">
27
+ <button id="executeButton"
28
+ class="bg-blue-500 text-white px-4 py-2 rounded-r hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500">执行</button>
29
+ </div>
30
+ <div id="loadingIndicator" class="text-center mb-4" style="display: none;">
31
+ <div class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
32
+ </div>
33
+ <div id="output" class="bg-gray-200 p-4 rounded h-64 overflow-y-auto font-mono mb-4"></div>
34
+ <h2 class="text-2xl font-bold mb-2">命令历史</h2>
35
+ <ul id="history" class="list-disc pl-5"></ul>
36
+ </div>
37
+ </div>
38
+
39
+ <script src="/js/main.js"></script>
40
  </body>
41
+
42
  </html>
public/js/main.js ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const commandInput = document.getElementById('command');
2
+ const output = document.getElementById('output');
3
+ const history = document.getElementById('history');
4
+ const executeButton = document.getElementById('executeButton');
5
+ const loadingIndicator = document.getElementById('loadingIndicator');
6
+
7
+ commandInput.addEventListener('keypress', function (event) {
8
+ if (event.key === 'Enter') {
9
+ executeCommand();
10
+ }
11
+ });
12
+
13
+ async function executeCommand() {
14
+ const command = commandInput.value;
15
+ if (!command.trim()) return;
16
+
17
+ showLoading(true);
18
+ output.textContent = '正在执行命令...';
19
+
20
+ try {
21
+ const response = await fetch('/api/execute', {
22
+ method: 'POST',
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
26
+ },
27
+ body: JSON.stringify({ command })
28
+ });
29
+ const data = await response.json();
30
+ if (response.ok) {
31
+ output.textContent = data.output || data.error;
32
+ commandInput.value = '';
33
+ loadCommandHistory();
34
+ } else {
35
+ output.textContent = `错误: ${data.error}`;
36
+ }
37
+ } catch (error) {
38
+ output.textContent = '错误: ' + error.message;
39
+ } finally {
40
+ showLoading(false);
41
+ }
42
+ }
43
+
44
+ async function loadCommandHistory() {
45
+ try {
46
+ const response = await fetch('/api/command-history', {
47
+ headers: {
48
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
49
+ }
50
+ });
51
+ const data = await response.json();
52
+ history.innerHTML = '';
53
+ data.reverse().forEach(item => {
54
+ const li = document.createElement('li');
55
+ li.textContent = `${filterXSS(item.command)} (${new Date(item.timestamp).toLocaleString()})`;
56
+ li.className = 'cursor-pointer hover:text-blue-500';
57
+ li.onclick = () => {
58
+ commandInput.value = item.command;
59
+ };
60
+ history.appendChild(li);
61
+ });
62
+ } catch (error) {
63
+ console.error('加载命令历史失败:', error);
64
+ }
65
+ }
66
+
67
+ function showLoading(isLoading) {
68
+ loadingIndicator.style.display = isLoading ? 'block' : 'none';
69
+ executeButton.disabled = isLoading;
70
+ }
71
+
72
+ async function login() {
73
+ const username = document.getElementById('username').value;
74
+ const password = document.getElementById('password').value;
75
+ try {
76
+ const response = await fetch('/api/login', {
77
+ method: 'POST',
78
+ headers: { 'Content-Type': 'application/json' },
79
+ body: JSON.stringify({ username, password })
80
+ });
81
+ const data = await response.json();
82
+ if (response.ok) {
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
+ }
90
+ } catch (error) {
91
+ alert('登录错误: ' + error.message);
92
+ }
93
+ }
94
+
95
+ document.getElementById('loginButton').addEventListener('click', login);
96
+
97
+ loadCommandHistory();
routes/auth.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: '无效的用户名或密码' });
24
+ }
25
+ });
26
+
27
+ module.exports = router;
routes/command.js ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const { execFile } = require('child_process');
3
+ const fs = require('fs').promises;
4
+ const path = require('path');
5
+ const router = express.Router();
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, '..', 'command_history.json');
14
+
15
+ router.get('/command-history', async (req, res) => {
16
+ try {
17
+ const history = await fs.readFile(historyFilePath, 'utf-8');
18
+ res.json(JSON.parse(history));
19
+ } catch (error) {
20
+ logger.error('读取命令历史失败:', error);
21
+ res.status(500).json({ error: '无法读取命令历史' });
22
+ }
23
+ });
24
+
25
+ router.post('/execute', async (req, res) => {
26
+ const { command } = req.body;
27
+ const baseCommand = command.split(' ')[0];
28
+
29
+ if (!allowedCommands.includes(baseCommand)) {
30
+ logger.warn(`用户 ${req.user.username} 尝试执行未授权的命令: ${command}`);
31
+ return res.status(403).json({ error: '未授权的命令' });
32
+ }
33
+
34
+ execFile(baseCommand, command.split(' ').slice(1), { timeout: 5000 }, async (error, stdout, stderr) => {
35
+ if (error) {
36
+ logger.error(`命令执行错误: ${error.message}`);
37
+ return res.status(500).json({ error: error.message });
38
+ }
39
+
40
+ // 记录命令历史
41
+ try {
42
+ let history = [];
43
+ try {
44
+ const historyData = await fs.readFile(historyFilePath, 'utf-8');
45
+ history = JSON.parse(historyData);
46
+ } catch (readError) {
47
+ // 如果文件不存在或为空,使用空数组
48
+ }
49
+
50
+ history.push({ command, timestamp: new Date().toISOString(), user: req.user.username });
51
+ if (history.length > 100) history.shift(); // 保留最近100条命令
52
+
53
+ await fs.writeFile(historyFilePath, JSON.stringify(history, null, 2));
54
+ } catch (writeError) {
55
+ logger.error('写入命令历史失败:', writeError);
56
+ }
57
+
58
+ logger.info(`用户 ${req.user.username} 成功执行命令: ${command}`);
59
+ res.json({ output: stdout, error: stderr });
60
+ });
61
+ });
62
+
63
+ module.exports = router;
start.sh DELETED
@@ -1,67 +0,0 @@
1
- #!/bin/bash
2
-
3
- echo "===== Application Startup at $(date) ====="
4
- whoami
5
- cat /etc/passwd
6
-
7
- echo "===== System Information ====="
8
- uname -a
9
- echo "CPU: $(lscpu | grep 'Model name' | cut -f 2 -d ":")"
10
- echo "Memory: $(free -h | awk '/^Mem:/ {print $2}')"
11
- echo "Disk: $(df -h / | awk 'NR==2 {print $2}')"
12
- echo "==============================="
13
-
14
- # 检查并生成 SSH 主机密钥(如果不存在)
15
- if [ ! -f /etc/dropbear/dropbear_rsa_host_key ]; then
16
- echo "Generating RSA host key..."
17
- dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key
18
- fi
19
-
20
- # 启动 Dropbear,使用 2202 端口,允许密码认证,后台运行
21
- echo "Starting Dropbear..."
22
- dropbear -R -p 2202 -w -E -F &
23
-
24
- # 检查 Dropbear 是否成功启动
25
- for i in {1..10}; do
26
- if netstat -tuln | grep :2202 > /dev/null; then
27
- echo "Dropbear started successfully on port 2202"
28
- break
29
- fi
30
- if [ $i -eq 10 ]; then
31
- echo "Failed to start Dropbear after 10 attempts"
32
- exit 1
33
- fi
34
- echo "Waiting for Dropbear to start... (attempt $i)"
35
- sleep 1
36
- done
37
-
38
- echo "Dropbear version: $(dropbear -V 2>&1)"
39
-
40
- # 测试 SSH 连接
41
- echo "Testing SSH connection..."
42
- if ssh -p 2202 -o StrictHostKeyChecking=no user@localhost 'echo "SSH connection successful"'; then
43
- echo "SSH connection test passed"
44
- else
45
- echo "SSH connection test failed"
46
- exit 1
47
- fi
48
-
49
- # 显示 Dropbear 进程信息
50
- echo "Dropbear process:"
51
- ps aux | grep dropbear | grep -v grep
52
-
53
- # 显示监听端口
54
- echo "Listening ports:"
55
- netstat -tuln | grep LISTEN
56
-
57
- # 启动 Node.js 应用
58
- echo "Starting Node.js application..."
59
- npm start
60
-
61
- # 检查 Node.js 应用是否成功启动
62
- if ! pgrep -f "node app.js" > /dev/null; then
63
- echo "Failed to start Node.js application"
64
- exit 1
65
- fi
66
-
67
- echo "Node.js application started successfully"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
utils/logger.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const winston = require('winston');
2
+
3
+ const logger = winston.createLogger({
4
+ level: process.env.LOG_LEVEL || 'info',
5
+ format: winston.format.combine(
6
+ winston.format.timestamp(),
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
+
15
+ if (process.env.NODE_ENV !== 'production') {
16
+ logger.add(new winston.transports.Console({
17
+ format: winston.format.simple()
18
+ }));
19
+ }
20
+
21
+ module.exports = logger;