├── .gitignore ├── server ├── data │ ├── servers.json │ ├── users.json │ └── rules.json ├── .DS_Store ├── public │ ├── fonts │ │ ├── element-icons.f1a45d74.ttf │ │ └── element-icons.ff18efd1.woff │ ├── index.html │ └── css │ │ └── app.6e665435.css ├── nodemon.json ├── .env.example ├── config.json ├── routes │ ├── authRoutes.js │ ├── serverRoutes.js │ └── rulesRoutes.js ├── package.json ├── start.sh ├── middlewares │ └── authMiddleware.js ├── scripts │ └── createAdmin.js ├── models │ ├── User.js │ ├── Server.js │ └── Rule.js ├── controllers │ ├── authController.js │ ├── rulesController.js │ └── serverController.js ├── services │ └── cacheService.js └── app.js ├── client ├── .npmrc ├── .DS_Store ├── babel.config.js ├── src │ ├── views │ │ ├── extensions │ │ │ └── RulesForward.js │ │ ├── Home.vue │ │ ├── Profile.vue │ │ ├── Login.vue │ │ ├── Register.vue │ │ └── Rules.vue │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── auth.js │ │ │ ├── servers.js │ │ │ └── rules.js │ ├── main.js │ ├── router │ │ └── index.js │ ├── App.vue │ └── components │ │ ├── ChangePasswordForm.vue │ │ └── ServerForm.vue ├── public │ └── index.html ├── vue.config.js └── package.json ├── readme ├── index.png ├── login.png ├── rules.png ├── addssh.png ├── servers.png └── firsttime.png ├── deployimg ├── 001.png ├── 002.png ├── 003.png ├── 004.png └── 005.png ├── .env.example ├── USE.md ├── .env ├── start-all.sh ├── package.json ├── Clawcloud.md ├── Dockerfile ├── Localdo.md ├── intcentos.sh ├── Dockerdo.md ├── projectStructure.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | data -------------------------------------------------------------------------------- /server/data/servers.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": [] 3 | } -------------------------------------------------------------------------------- /client/.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | fund=false 3 | audit=false -------------------------------------------------------------------------------- /client/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fiftonb/Gnftato/HEAD/client/.DS_Store -------------------------------------------------------------------------------- /readme/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fiftonb/Gnftato/HEAD/readme/index.png -------------------------------------------------------------------------------- /readme/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fiftonb/Gnftato/HEAD/readme/login.png -------------------------------------------------------------------------------- /readme/rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fiftonb/Gnftato/HEAD/readme/rules.png -------------------------------------------------------------------------------- /server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fiftonb/Gnftato/HEAD/server/.DS_Store -------------------------------------------------------------------------------- /deployimg/001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fiftonb/Gnftato/HEAD/deployimg/001.png -------------------------------------------------------------------------------- /deployimg/002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fiftonb/Gnftato/HEAD/deployimg/002.png -------------------------------------------------------------------------------- /deployimg/003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fiftonb/Gnftato/HEAD/deployimg/003.png -------------------------------------------------------------------------------- /deployimg/004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fiftonb/Gnftato/HEAD/deployimg/004.png -------------------------------------------------------------------------------- /deployimg/005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fiftonb/Gnftato/HEAD/deployimg/005.png -------------------------------------------------------------------------------- /readme/addssh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fiftonb/Gnftato/HEAD/readme/addssh.png -------------------------------------------------------------------------------- /readme/servers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fiftonb/Gnftato/HEAD/readme/servers.png -------------------------------------------------------------------------------- /readme/firsttime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fiftonb/Gnftato/HEAD/readme/firsttime.png -------------------------------------------------------------------------------- /client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ], 5 | plugins: [] 6 | }; -------------------------------------------------------------------------------- /server/public/fonts/element-icons.f1a45d74.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fiftonb/Gnftato/HEAD/server/public/fonts/element-icons.f1a45d74.ttf -------------------------------------------------------------------------------- /server/public/fonts/element-icons.ff18efd1.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fiftonb/Gnftato/HEAD/server/public/fonts/element-icons.ff18efd1.woff -------------------------------------------------------------------------------- /client/src/views/extensions/RulesForward.js: -------------------------------------------------------------------------------- 1 | // RulesForward.js - 端口转发功能相关扩展 2 | 3 | export default { 4 | methods: { 5 | // 这里放置所有与转发功能相关的方法 6 | } 7 | }; -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import servers from './modules/servers'; 4 | import rules from './modules/rules'; 5 | import auth from './modules/auth'; 6 | 7 | Vue.use(Vuex); 8 | 9 | export default new Vuex.Store({ 10 | modules: { 11 | servers, 12 | rules, 13 | auth 14 | } 15 | }); -------------------------------------------------------------------------------- /server/data/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "id": "1745080953892", 5 | "username": "admin", 6 | "password": "$2b$10$ImfljVlZ2RGq5lmHC9a0KuZaeXv1AFgTTGKNCYhS0sZJUUvPMHaSG", 7 | "isAdmin": true, 8 | "createdAt": "2025-04-19T16:42:33.892Z", 9 | "updatedAt": "2025-04-19T17:42:36.935Z" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["**/*.js", "**/*.json"], 3 | "ignore": [ 4 | "node_modules", 5 | "logs/*", 6 | "*.log", 7 | "tmp/*", 8 | ".git", 9 | "public/*", 10 | "sessions/*", 11 | ".ssh/*", 12 | "**/.DS_Store", 13 | "**/.tmp.*" 14 | ], 15 | "verbose": true, 16 | "ext": "js,json", 17 | "delay": "1000" 18 | } -------------------------------------------------------------------------------- /server/.env.example: -------------------------------------------------------------------------------- 1 | # 服务器配置 2 | PORT=3001 3 | HOST=0.0.0.0 4 | CORS_ORIGIN=* 5 | 6 | # 数据目录配置 7 | DATA_DIR=./server/data 8 | 9 | # 数据库配置 10 | DB_PATH=./data/database.json 11 | 12 | # 日志配置 13 | LOG_LEVEL=info 14 | LOG_DIR=./logs 15 | 16 | # 临时文件目录 17 | TMP_DIR=./tmp 18 | 19 | # 应用模式配置 20 | NODE_ENV=development 21 | # 设置为 true 可避免 nodemon 频繁重启 22 | STABLE_MODE=true -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 服务器配置 2 | PORT=3001 3 | HOST=0.0.0.0 4 | CORS_ORIGIN=* 5 | 6 | # 数据目录配置 7 | DATA_DIR=./server/data 8 | 9 | # JWT配置 10 | JWT_SECRET=your-secret-key-change-this 11 | JWT_EXPIRES_IN=7d 12 | 13 | # 日志配置 14 | LOG_LEVEL=info 15 | LOG_DIR=./logs 16 | 17 | # 临时文件目录 18 | TMP_DIR=./tmp 19 | 20 | # 应用模式配置 21 | NODE_ENV=development 22 | # 设置为 true 可避免 nodemon 频繁重启 23 | STABLE_MODE=true -------------------------------------------------------------------------------- /server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "PORT": 3001, 3 | "HOST": "0.0.0.0", 4 | "CORS_ORIGIN": "*", 5 | "DATA_DIR": "./server/data", 6 | "DB_PATH": "./data/database.json", 7 | "JWT_SECRET": "iptato-secure-jwt-secret-key", 8 | "JWT_EXPIRES_IN": "7d", 9 | "LOG_LEVEL": "info", 10 | "LOG_DIR": "./logs", 11 | "TMP_DIR": "./tmp", 12 | "NODE_ENV": "development", 13 | "STABLE_MODE": "true" 14 | } -------------------------------------------------------------------------------- /USE.md: -------------------------------------------------------------------------------- 1 | ## 使用指南 2 | 3 | 1. 访问前端界面,使用管理员账户登录系统 4 | ![login](readme/login.png) 5 | 2. 登录后进入首页 6 | ![index](readme/index.png) 7 | 3. 点击开始管理服务器进入服务器管理界面 8 | ![servers](readme/servers.png) 9 | 4. 添加服务器:点击"添加服务器",填写服务器信息并测试连接 10 | ![addssh](readme/addssh.png) 11 | 5. 连接服务器:在服务器列表中点击"连接"按钮 12 | 6. 管理规则:点击"管理规则"进入相应服务器的规则管理页面 13 | 7. 若首次部署,需点击开始部署 14 | ![firsttime](readme/firsttime.png) 15 | 8. 根据需要配置出入网规则 16 | ![rules](readme/rules.png) -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # 服务器配置 2 | PORT=3001 3 | HOST=0.0.0.0 4 | CORS_ORIGIN=* 5 | 6 | # 数据目录配置 7 | DATA_DIR=./server/data 8 | 9 | # 数据库配置 10 | DB_PATH=./data/database.json 11 | 12 | # JWT配置 13 | JWT_SECRET=iptato-secure-jwt-secret-key 14 | JWT_EXPIRES_IN=7d 15 | 16 | # 日志配置 17 | LOG_LEVEL=info 18 | LOG_DIR=./logs 19 | 20 | # 临时文件目录 21 | TMP_DIR=./tmp 22 | 23 | # 应用模式配置 24 | NODE_ENV=development 25 | # 设置为 true 可避免 nodemon 频繁重启 26 | STABLE_MODE=true -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Nftato防火墙管理面板 9 | 10 | 11 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /server/routes/authRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const authController = require('../controllers/authController'); 4 | const { protect } = require('../middlewares/authMiddleware'); 5 | 6 | // 注册新用户 7 | router.post('/register', authController.register); 8 | 9 | // 用户登录 10 | router.post('/login', authController.login); 11 | 12 | // 获取当前登录用户信息 (需要身份验证) 13 | router.get('/me', protect, authController.getCurrentUser); 14 | 15 | // 更新用户密码 (需要身份验证) 16 | router.put('/update-password', protect, authController.updatePassword); 17 | 18 | module.exports = router; -------------------------------------------------------------------------------- /server/public/index.html: -------------------------------------------------------------------------------- 1 | Nftato防火墙管理面板
-------------------------------------------------------------------------------- /start-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 显示启动信息 4 | echo "正在启动 Gnftato 应用..." 5 | 6 | # 启动后端(稳定模式) 7 | echo "启动后端服务器..." 8 | cd server 9 | ./start.sh & 10 | BACKEND_PID=$! 11 | cd .. 12 | 13 | # 等待后端启动 14 | sleep 2 15 | 16 | # 检查前端目录 17 | if [ -d "client" ]; then 18 | echo "启动前端应用..." 19 | cd client 20 | npm run serve & 21 | FRONTEND_PID=$! 22 | cd .. 23 | else 24 | echo "错误: 找不到前端目录!" 25 | kill $BACKEND_PID 26 | exit 1 27 | fi 28 | 29 | echo "应用启动完成!" 30 | echo "前端运行在: http://localhost:8080" 31 | echo "后端运行在: http://localhost:3001" 32 | echo "按 Ctrl+C 停止所有服务" 33 | 34 | # 等待用户中断 35 | wait 36 | 37 | # 确保子进程被终止 38 | trap "kill $BACKEND_PID $FRONTEND_PID 2>/dev/null" EXIT -------------------------------------------------------------------------------- /client/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devServer: { 3 | proxy: { 4 | '/api': { 5 | target: 'http://localhost:3001', 6 | changeOrigin: true, 7 | logLevel: 'debug', 8 | pathRewrite: { 9 | '^/api': '/api' 10 | }, 11 | onProxyReq(proxyReq, req, res) { 12 | console.log('代理请求:', req.method, req.url, '->', 13 | proxyReq.protocol + '//' + proxyReq.host + proxyReq.path); 14 | }, 15 | onError(err, req, res) { 16 | console.error('代理错误:', err); 17 | } 18 | } 19 | }, 20 | host: '0.0.0.0' 21 | }, 22 | outputDir: '../server/public', 23 | publicPath: '/', 24 | lintOnSave: false, 25 | transpileDependencies: true 26 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nftato-panel", 3 | "version": "1.0.0", 4 | "description": "Nftato防火墙规则多服务器管理面板", 5 | "main": "index.js", 6 | "scripts": { 7 | "install:all": "npm install && cd server && npm install && cd ../client && npm install", 8 | "start": "cd server && npm start", 9 | "dev:server": "cd server && npm run dev", 10 | "dev:client": "cd client && npm run serve", 11 | "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"", 12 | "build": "cd client && npm run build", 13 | "setup": "npm i && cd server && npm i && mkdir -p public && cd ../client && npm i --force", 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "author": "", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "concurrently": "^6.3.0" 20 | } 21 | } -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nftato-panel-server", 3 | "version": "1.0.0", 4 | "description": "Nftato防火墙规则多服务器管理面板后端", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "dev": "nodemon --config nodemon.json app.js", 9 | "dev:stable": "nodemon --config nodemon.json --ignore '**/*' app.js", 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "create-admin": "node scripts/createAdmin.js" 12 | }, 13 | "author": "", 14 | "license": "MIT", 15 | "dependencies": { 16 | "bcryptjs": "^2.4.3", 17 | "cors": "^2.8.5", 18 | "dotenv": "^10.0.0", 19 | "express": "^4.17.1", 20 | "jsonwebtoken": "^9.0.2", 21 | "nodemon": "^2.0.14", 22 | "socket.io": "^4.7.2", 23 | "ssh2": "^1.5.0", 24 | "uuid": "^8.3.2" 25 | }, 26 | "devDependencies": { 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Clawcloud.md: -------------------------------------------------------------------------------- 1 | 2 | # CLAWCLOUD Run 平台部署Gnftato教程 3 | 4 | ## 准备工作 5 | 6 | 7 | 如果您还没有注册CLAWCLOUD Run账号,请先[点击此处注册](https://console.run.claw.cloud/signin?link=9IOYACCW0AQ4) 8 | 9 | ## 部署步骤 10 | 11 | 1. 注册完成后,进入控制台(建议左上角选美国东部地区) 12 | 13 | 2. 在控制台中选择并点击 **App Launchpad** 14 | 15 | ![App Launchpad入口](deployimg/001.png) 16 | 17 | 3. 点击 **Create App** 按钮创建新应用 18 | 19 | ![创建应用界面](deployimg/002.png) 20 | 21 | 4. 在创建应用页面填写以下信息: 22 | - **Application Name**:填写 `gnftato` 23 | - **Image**:选择 **Public**,然后填入 `fiftonb/gnftato` 24 | - **Usage**:根据您的实际需求选择合适的配置 25 | - **Network**:确保开放端口 **3001** 26 | 27 | ![网络配置](deployimg/004.png) 28 | 29 | 5. 填写完所有信息后,滑动回页面顶部,点击 **Deploy Application** 按钮完成部署 30 | 31 | ![部署应用](deployimg/005.png) 32 | 33 | 6. 等待几分钟,系统将完成应用的部署 34 | 35 | ## 访问应用 36 | 37 | 部署成功后,您可以通过分配的URL访问您的Gnftato应用。URL通常显示在应用详情页面。 38 | 39 | ## 常见问题 40 | 41 | 如遇到部署问题,请检查: 42 | - 网络连接是否正常 43 | - 镜像名称是否正确 44 | - 端口是否已正确配置 45 | 46 | 希望本教程对您有所帮助! 47 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nftato-panel-client", 3 | "version": "1.0.0", 4 | "description": "Nftato防火墙规则多服务器管理面板前端", 5 | "private": true, 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.27.2", 13 | "core-js": "^3.25.0", 14 | "element-ui": "^2.15.10", 15 | "socket.io-client": "^4.7.2", 16 | "vue": "^2.7.10", 17 | "vue-router": "^3.6.5", 18 | "vuex": "^3.6.2" 19 | }, 20 | "devDependencies": { 21 | "@vue/cli-plugin-babel": "~5.0.8", 22 | "@vue/cli-plugin-eslint": "~5.0.8", 23 | "@vue/cli-plugin-router": "~5.0.8", 24 | "@vue/cli-plugin-vuex": "~5.0.8", 25 | "@vue/cli-service": "~5.0.8", 26 | "babel-eslint": "^10.1.0", 27 | "eslint": "^8.23.0", 28 | "eslint-plugin-vue": "^9.4.0", 29 | "sass": "^1.54.8", 30 | "sass-loader": "^13.0.2", 31 | "vue-template-compiler": "^2.7.10" 32 | } 33 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app 4 | 5 | # 设置默认环境变量 6 | ENV NODE_ENV=development 7 | ENV PORT=3001 8 | ENV JWT_SECRET=default_secret_please_change 9 | ENV CORS_ORIGIN=* 10 | 11 | # 复制package.json文件 12 | COPY package*.json ./ 13 | COPY server/package*.json ./server/ 14 | COPY client/package*.json ./client/ 15 | 16 | # 安装依赖 17 | RUN npm install 18 | RUN cd server && npm install --production=false 19 | RUN cd client && npm install 20 | 21 | # 复制应用程序代码 22 | COPY . . 23 | 24 | # 构建前端 25 | RUN npm run build 26 | 27 | # 给启动脚本添加执行权限 28 | RUN chmod +x /app/server/start.sh 29 | # 确保文件使用Unix格式的换行符 30 | RUN if [ -f /app/server/start.sh ]; then \ 31 | sed -i 's/\r$//' /app/server/start.sh; \ 32 | echo "start.sh文件存在并已修复换行符"; \ 33 | else \ 34 | echo "start.sh文件不存在!"; \ 35 | exit 1; \ 36 | fi 37 | 38 | # 暴露端口 39 | EXPOSE 3001 40 | 41 | # 设置工作目录 42 | WORKDIR /app/server 43 | 44 | # 安装jq工具以支持config.json解析 45 | RUN apk add --no-cache jq 46 | 47 | # 启动命令(使用绝对路径) 48 | CMD ["/bin/sh", "/app/server/start.sh"] -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import ElementUI from 'element-ui'; 3 | import 'element-ui/lib/theme-chalk/index.css'; 4 | import App from './App.vue'; 5 | import router from './router'; 6 | import store from './store'; 7 | import axios from 'axios'; 8 | 9 | // 设置axios默认配置 10 | axios.defaults.baseURL = process.env.VUE_APP_API_URL || ''; 11 | 12 | // 添加响应拦截器处理认证错误 13 | axios.interceptors.response.use( 14 | response => response, 15 | error => { 16 | if (error.response && error.response.status === 401) { 17 | // 如果接收到401错误,清除认证状态并重定向到登录页 18 | store.dispatch('logout'); 19 | router.push('/login'); 20 | } 21 | return Promise.reject(error); 22 | } 23 | ); 24 | 25 | // 如果已经有令牌,设置默认请求头 26 | const token = localStorage.getItem('token'); 27 | if (token) { 28 | axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; 29 | } 30 | 31 | Vue.prototype.$http = axios; 32 | Vue.use(ElementUI); 33 | Vue.config.productionTip = false; 34 | 35 | new Vue({ 36 | router, 37 | store, 38 | render: h => h(App) 39 | }).$mount('#app'); -------------------------------------------------------------------------------- /server/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 尝试加载环境变量,优先从.env文件加载 4 | if [ -f ../.env ]; then 5 | echo "从../.env加载环境变量" 6 | export $(cat ../.env | grep -v '#' | awk '/=/ {print $1}') 7 | elif [ -f ./.env ]; then 8 | echo "从./.env加载环境变量" 9 | export $(cat ./.env | grep -v '#' | awk '/=/ {print $1}') 10 | elif [ -f ./config.json ]; then 11 | echo "从config.json加载环境变量" 12 | # 使用jq工具解析JSON并设置环境变量(如果Docker容器中有jq) 13 | if command -v jq &> /dev/null; then 14 | while IFS="=" read -r key value; do 15 | export "$key"="$value" 16 | done < <(jq -r 'to_entries | map("\(.key)=\(.value)") | .[]' ./config.json) 17 | else 18 | echo "警告: 没有安装jq工具,无法解析config.json" 19 | fi 20 | else 21 | echo "警告: 找不到.env或config.json文件,将使用默认环境变量" 22 | fi 23 | 24 | # 创建初始管理员账户 25 | echo "检查并创建管理员账户..." 26 | node scripts/createAdmin.js 27 | 28 | # 根据环境变量选择启动模式 29 | if [ "$NODE_ENV" = "production" ]; then 30 | echo "以生产模式启动服务器..." 31 | npm run start 32 | elif [ "$STABLE_MODE" = "true" ]; then 33 | echo "以稳定开发模式启动服务器..." 34 | npm run dev:stable 35 | else 36 | echo "以开发模式启动服务器..." 37 | npm run dev 38 | fi -------------------------------------------------------------------------------- /client/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 29 | 30 | -------------------------------------------------------------------------------- /server/routes/serverRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const serverController = require('../controllers/serverController'); 3 | const router = express.Router(); 4 | const { protect } = require('../middlewares/authMiddleware'); 5 | 6 | // 应用认证中间件保护所有路由 7 | router.use(protect); 8 | 9 | // 获取所有服务器 10 | router.get('/', serverController.getAllServers); 11 | 12 | // 获取单个服务器 13 | router.get('/:id', serverController.getServer); 14 | 15 | // 添加服务器 16 | router.post('/', serverController.createServer); 17 | 18 | // 更新服务器 19 | router.put('/:id', serverController.updateServer); 20 | 21 | // 删除服务器 22 | router.delete('/:id', serverController.deleteServer); 23 | 24 | // 服务器连接路由 25 | router.post('/:id/connect', serverController.connectServer); 26 | router.post('/:id/disconnect', serverController.disconnectServer); 27 | router.post('/test-connection', serverController.testConnection); 28 | router.post('/:id/execute', serverController.executeCommand); 29 | router.get('/:id/status', serverController.checkServerStatus); 30 | router.get('/:id/logs', serverController.getServerLogs); 31 | 32 | // Nftato部署路由 33 | router.post('/:id/deploy', serverController.deployIptato); 34 | 35 | // 检查脚本是否存在路由 36 | router.get('/:id/checkScript', serverController.checkScriptExists); 37 | 38 | module.exports = router; -------------------------------------------------------------------------------- /Localdo.md: -------------------------------------------------------------------------------- 1 | ### 2. 安装依赖 2 | 3 | 安装nodejs环境(建议debian11+系统): 4 | 5 | ```bash 6 | apt-get remove nodejs npm 7 | rm -rf /usr/local/lib/node_modules 8 | rm -rf /usr/local/bin/npm 9 | rm -rf /usr/local/bin/node 10 | rm -rf ~/.npm 11 | source <(curl -L https://nodejs-install.netlify.app/install.sh) -v 22.2.0 12 | ``` 13 | 14 | 一键安装所有依赖: 15 | 16 | ```bash 17 | npm run setup 18 | ``` 19 | 20 | 可直接看第四步骤 21 | 22 | ### 3. 配置环境变量(项目自带可忽略) 23 | 24 | 复制`.env.example`文件为`.env`,或直接创建`.env`文件,并根据实际情况修改: 25 | 26 | ```bash 27 | cp .env.example .env 28 | ``` 29 | 30 | 配置示例: 31 | 32 | ``` 33 | # 服务器配置 34 | PORT=3001 35 | CORS_ORIGIN=http://localhost:8080 36 | 37 | # 数据目录配置 38 | DATA_DIR=./server/data 39 | 40 | # JWT配置 41 | JWT_SECRET=your-secret-key-change-this 42 | JWT_EXPIRES_IN=7d 43 | 44 | # 日志配置 45 | LOG_LEVEL=info 46 | LOG_DIR=./logs 47 | 48 | # 临时文件目录 49 | TMP_DIR=./tmp 50 | 51 | # 应用模式配置 52 | NODE_ENV=development 53 | # 设置为 true 可避免 nodemon 频繁重启 54 | STABLE_MODE=true 55 | ``` 56 | 57 | ### 4. 构建与启动 58 | 59 | 一键构建前端并启动服务: 60 | 61 | ```bash 62 | # 构建前端 (将构建结果输出到 server/public 目录) 63 | npm run build 64 | 65 | # 启动后端服务器 66 | npm start 67 | ``` 68 | 69 | 或使用一键启动脚本(仅开发模式): 70 | 71 | ```bash 72 | ./start-all.sh 73 | ``` 74 | 75 | ## 开发模式 76 | 77 | 同时启动前端和后端开发服务器: 78 | 79 | ```bash 80 | npm run dev 81 | ``` -------------------------------------------------------------------------------- /client/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import Home from '../views/Home.vue'; 4 | import Servers from '../views/Servers.vue'; 5 | import Rules from '../views/Rules.vue'; 6 | import Login from '../views/Login.vue'; 7 | import Profile from '../views/Profile.vue'; 8 | import store from '../store'; 9 | 10 | Vue.use(VueRouter); 11 | 12 | const routes = [ 13 | { 14 | path: '/', 15 | name: 'home', 16 | component: Home, 17 | meta: { requiresAuth: true } 18 | }, 19 | { 20 | path: '/servers', 21 | name: 'servers', 22 | component: Servers, 23 | meta: { requiresAuth: true } 24 | }, 25 | { 26 | path: '/rules/:serverId', 27 | name: 'rules', 28 | component: Rules, 29 | props: true, 30 | meta: { requiresAuth: true } 31 | }, 32 | { 33 | path: '/profile', 34 | name: 'profile', 35 | component: Profile, 36 | meta: { requiresAuth: true } 37 | }, 38 | { 39 | path: '/login', 40 | name: 'login', 41 | component: Login 42 | } 43 | ]; 44 | 45 | const router = new VueRouter({ 46 | mode: 'history', 47 | base: process.env.BASE_URL, 48 | routes 49 | }); 50 | 51 | // 全局前置守卫 52 | router.beforeEach((to, from, next) => { 53 | const requiresAuth = to.matched.some(record => record.meta.requiresAuth); 54 | const isAuthenticated = store.getters.isAuthenticated; 55 | 56 | if (requiresAuth && !isAuthenticated) { 57 | next('/login'); 58 | } else { 59 | next(); 60 | } 61 | }); 62 | 63 | export default router; -------------------------------------------------------------------------------- /server/middlewares/authMiddleware.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const User = require('../models/User'); 3 | 4 | // JWT密钥 5 | const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; 6 | 7 | // 验证令牌是否有效的中间件 8 | exports.protect = async (req, res, next) => { 9 | try { 10 | let token; 11 | 12 | // 检查Authorization请求头 13 | if ( 14 | req.headers.authorization && 15 | req.headers.authorization.startsWith('Bearer') 16 | ) { 17 | // 获取Bearer令牌 18 | token = req.headers.authorization.split(' ')[1]; 19 | } 20 | 21 | // 如果没有令牌,返回错误 22 | if (!token) { 23 | return res.status(401).json({ 24 | success: false, 25 | message: '请先登录以获取访问权限' 26 | }); 27 | } 28 | 29 | // 验证令牌 30 | const decoded = jwt.verify(token, JWT_SECRET); 31 | 32 | // 查找用户 33 | const currentUser = User.findUserByUsername(decoded.username); 34 | 35 | if (!currentUser) { 36 | return res.status(401).json({ 37 | success: false, 38 | message: '此令牌的用户不存在' 39 | }); 40 | } 41 | 42 | // 将用户信息添加到req对象 43 | req.user = { 44 | id: currentUser.id, 45 | username: currentUser.username 46 | }; 47 | 48 | next(); 49 | } catch (error) { 50 | if (error.name === 'JsonWebTokenError') { 51 | return res.status(401).json({ 52 | success: false, 53 | message: '无效的令牌' 54 | }); 55 | } 56 | 57 | if (error.name === 'TokenExpiredError') { 58 | return res.status(401).json({ 59 | success: false, 60 | message: '令牌已过期' 61 | }); 62 | } 63 | 64 | res.status(500).json({ 65 | success: false, 66 | message: '服务器错误', 67 | error: error.message 68 | }); 69 | } 70 | }; -------------------------------------------------------------------------------- /server/data/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "25f15b80-623f-49a9-b76f-9920f57e59d3": { 4 | "lastUpdate": "2025-05-01T14:20:26.794Z", 5 | "data": { 6 | "sshPortStatus": "文件存在 脚本不是初次运行\n\nSSH 端口为 22", 7 | "inboundIPs": [ 8 | { 9 | "ip": "1.1.1.1" 10 | } 11 | ], 12 | "inboundPorts": { 13 | "tcp": [ 14 | 22, 15 | 555 16 | ], 17 | "udp": [ 18 | 22, 19 | 555 20 | ] 21 | }, 22 | "defenseStatus": "文件存在 脚本不是初次运行\n============当前防御状态============\n\u001b[32m[已启用]\u001b[0m DDoS防御\n已阻止HTTP连接次数: 0\n已阻止HTTPS连接次数: 0\n\n\u001b[33m=== 当前保护的端口列表 ===\u001b[0m\n标准端口: 80(HTTP), 443(HTTPS)\n自定义端口: 777,999\n\n\u001b[33m=== 端口保护配置详情 ===\u001b[0m\n格式说明: [端口号] - [连接限制/分钟] [连接限制/秒] [每IP最大连接数] [最大并发连接数] [封禁时长]\n80(HTTP) - 500/分钟 300/秒 600 100000 1d (已阻止: 0)\n443(HTTPS) - 500/分钟 300/秒 600 100000 1d (已阻止: 0)\n777(TCP+UDP) - 500/分钟 300/秒 600 100000 12h (已阻止: 0)\n999(TCP+UDP) - 500/分钟 300/秒 600 100000 2d2h (已阻止: 0)\n\n\u001b[33m=== 总体防御情况 ===\u001b[0m\n总拦截连接数: 0\n当前白名单IPv4数量: 0\n当前白名单IPv6数量: 0\n当前黑名单IPv4数量: 0\n当前黑名单IPv6数量: 0\n==================================" 23 | } 24 | }, 25 | "09166f22-d6ae-46ab-8736-112abc960d82": { 26 | "lastUpdate": "2025-05-01T14:31:24.605Z", 27 | "data": { 28 | "inboundPorts": { 29 | "tcp": [ 30 | 22, 31 | 555 32 | ], 33 | "udp": [ 34 | 22, 35 | 555 36 | ] 37 | }, 38 | "sshPortStatus": "文件存在 脚本不是初次运行\n\nSSH 端口为 22", 39 | "inboundIPs": [ 40 | { 41 | "ip": "1.1.1.1" 42 | } 43 | ] 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /intcentos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "开始配置CentOS 9 nftables防火墙..." 5 | 6 | # 获取SSH端口 7 | SSH_PORT=$(ss -tlnp | grep sshd | awk '{print $4}' | awk -F ':' '{print $NF}' | head -1) 8 | [ -z "$SSH_PORT" ] && SSH_PORT=22 9 | echo "检测到SSH端口: $SSH_PORT" 10 | 11 | # 配置nftables 12 | echo "配置nftables规则..." 13 | 14 | # 创建规则文件 - 关键是使用正确的CentOS 9路径 15 | mkdir -p /etc/nftables 16 | cat > /etc/nftables/main.nft << EOF 17 | #!/usr/sbin/nft -f 18 | 19 | # 清空现有规则 20 | flush ruleset 21 | 22 | # 基本防火墙规则 23 | table inet filter { 24 | chain input { 25 | type filter hook input priority 0; policy drop; 26 | 27 | # 允许本地回环 28 | iifname "lo" accept 29 | 30 | # 允许已建立连接 31 | ct state established,related accept 32 | 33 | # 允许ICMP 34 | ip protocol icmp accept 35 | ip6 nexthdr icmpv6 accept 36 | 37 | # 允许SSH 38 | tcp dport ${SSH_PORT} accept 39 | } 40 | 41 | chain forward { 42 | type filter hook forward priority 0; policy drop; 43 | } 44 | 45 | chain output { 46 | type filter hook output priority 0; policy accept; 47 | } 48 | } 49 | EOF 50 | 51 | # 创建正确的配置文件 - CentOS 9中使用/etc/sysconfig/nftables.conf 52 | cat > /etc/sysconfig/nftables.conf << EOF 53 | # 加载主规则文件 54 | include "/etc/nftables/main.nft" 55 | EOF 56 | 57 | echo "应用nftables规则..." 58 | nft -f /etc/nftables/main.nft 59 | 60 | # 设置正确的文件权限和SELinux上下文 61 | chmod 600 /etc/nftables/main.nft 62 | chmod 600 /etc/sysconfig/nftables.conf 63 | restorecon -v /etc/nftables/main.nft || true 64 | restorecon -v /etc/sysconfig/nftables.conf || true 65 | 66 | # 重启nftables服务 67 | systemctl restart nftables 68 | 69 | # 确保服务开机启动 70 | systemctl enable nftables 71 | 72 | echo "检查nftables规则..." 73 | nft list ruleset 74 | 75 | echo "防火墙配置完成" 76 | echo "已放行SSH端口 ${SSH_PORT}" 77 | echo "出站流量不受限制" -------------------------------------------------------------------------------- /client/src/views/Profile.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 56 | 57 | -------------------------------------------------------------------------------- /server/scripts/createAdmin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 创建管理员账户的脚本 3 | * 4 | * 用法: node createAdmin.js 5 | */ 6 | 7 | const bcrypt = require('bcryptjs'); 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | require('dotenv').config({ path: path.join(__dirname, '../../.env') }); 11 | 12 | // 默认管理员配置 13 | const DEFAULT_ADMIN = { 14 | username: 'admin', 15 | password: 'admin123' 16 | }; 17 | 18 | // 数据目录 19 | let dataDir; 20 | if (process.env.DATA_DIR) { 21 | if (process.env.DATA_DIR.startsWith('./')) { 22 | dataDir = path.join(__dirname, '../..', process.env.DATA_DIR.substring(2)); 23 | } else { 24 | dataDir = path.resolve(process.env.DATA_DIR); 25 | } 26 | } else { 27 | dataDir = path.join(__dirname, '../data'); 28 | } 29 | 30 | // 用户数据文件路径 31 | const usersFilePath = path.join(dataDir, 'users.json'); 32 | 33 | // 检查并创建数据目录 34 | if (!fs.existsSync(dataDir)) { 35 | fs.mkdirSync(dataDir, { recursive: true }); 36 | console.log(`数据目录已创建: ${dataDir}`); 37 | } 38 | 39 | // 读取现有用户或创建空数组 40 | let users = []; 41 | if (fs.existsSync(usersFilePath)) { 42 | try { 43 | const data = fs.readFileSync(usersFilePath, 'utf8'); 44 | users = JSON.parse(data).users; 45 | } catch (error) { 46 | console.error('读取用户数据失败:', error); 47 | process.exit(1); 48 | } 49 | } 50 | 51 | // 检查是否已存在管理员 52 | const adminExists = users.some(user => user.username === DEFAULT_ADMIN.username); 53 | 54 | if (adminExists) { 55 | console.log(`管理员账户 '${DEFAULT_ADMIN.username}' 已存在,无需创建`); 56 | process.exit(0); 57 | } 58 | 59 | // 创建管理员账户 60 | async function createAdmin() { 61 | try { 62 | // 加密密码 63 | const salt = await bcrypt.genSalt(10); 64 | const hashedPassword = await bcrypt.hash(DEFAULT_ADMIN.password, salt); 65 | 66 | // 创建管理员用户 67 | const admin = { 68 | id: Date.now().toString(), 69 | username: DEFAULT_ADMIN.username, 70 | password: hashedPassword, 71 | isAdmin: true, 72 | createdAt: new Date().toISOString() 73 | }; 74 | 75 | // 添加到用户列表 76 | users.push(admin); 77 | 78 | // 保存到文件 79 | fs.writeFileSync(usersFilePath, JSON.stringify({ users }, null, 2)); 80 | 81 | console.log('管理员账户创建成功!'); 82 | console.log(`用户名: ${DEFAULT_ADMIN.username}`); 83 | console.log(`密码: ${DEFAULT_ADMIN.password}`); 84 | console.log('请登录后立即修改默认密码'); 85 | } catch (error) { 86 | console.error('创建管理员失败:', error); 87 | process.exit(1); 88 | } 89 | } 90 | 91 | createAdmin(); -------------------------------------------------------------------------------- /server/routes/rulesRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const rulesController = require('../controllers/rulesController'); 3 | const router = express.Router(); 4 | const { protect } = require('../middlewares/authMiddleware'); 5 | 6 | // 应用认证中间件保护所有路由 7 | router.use(protect); 8 | 9 | // 规则缓存路由 10 | router.get('/:serverId/cache', rulesController.getServerRulesCache); 11 | router.get('/:serverId/cache/last-update', rulesController.getCacheLastUpdate); 12 | router.delete('/:serverId/cache', rulesController.clearServerCache); 13 | router.put('/:serverId/cache/:key', rulesController.updateCacheItem); 14 | 15 | // 出网控制路由 16 | router.get('/:serverId/blocklist', rulesController.getBlockList); 17 | router.post('/:serverId/block/bt-pt', rulesController.blockBTPT); 18 | router.post('/:serverId/block/spam', rulesController.blockSPAM); 19 | router.post('/:serverId/block/all', rulesController.blockAll); 20 | router.post('/:serverId/block/ports', rulesController.blockCustomPorts); 21 | router.post('/:serverId/block/keyword', rulesController.blockCustomKeyword); 22 | router.post('/:serverId/unblock/bt-pt', rulesController.unblockBTPT); 23 | router.post('/:serverId/unblock/spam', rulesController.unblockSPAM); 24 | router.post('/:serverId/unblock/all', rulesController.unblockAll); 25 | router.post('/:serverId/unblock/ports', rulesController.unblockCustomPorts); 26 | router.post('/:serverId/unblock/keyword', rulesController.unblockCustomKeyword); 27 | router.post('/:serverId/unblock/all-keywords', rulesController.unblockAllKeywords); 28 | 29 | // 入网控制路由 30 | router.get('/:serverId/inbound/ports', rulesController.getInboundPorts); 31 | router.get('/:serverId/inbound/ips', rulesController.getInboundIPs); 32 | router.post('/:serverId/inbound/allow/ports', rulesController.allowInboundPorts); 33 | router.post('/:serverId/inbound/disallow/ports', rulesController.disallowInboundPorts); 34 | router.post('/:serverId/inbound/allow/ips', rulesController.allowInboundIPs); 35 | router.post('/:serverId/inbound/disallow/ips', rulesController.disallowInboundIPs); 36 | 37 | // DDoS防御路由 38 | router.post('/:serverId/ddos/protection', rulesController.setupDdosProtection); 39 | router.post('/:serverId/ddos/custom-port', rulesController.setupCustomPortProtection); 40 | router.post('/:serverId/ddos/ip-lists', rulesController.manageIpLists); 41 | router.get('/:serverId/ddos/status', rulesController.viewDefenseStatus); 42 | 43 | // 增强功能路由 44 | router.get('/:serverId/ssh-port', rulesController.getSSHPort); 45 | router.post('/:serverId/clear-all', rulesController.clearAllRules); 46 | 47 | module.exports = router; -------------------------------------------------------------------------------- /Dockerdo.md: -------------------------------------------------------------------------------- 1 | ### 5. 使用Docker部署 2 | 3 | 本项目支持使用Docker进行容器化部署,方便在不同环境中快速启动服务。 4 | 5 | #### 5.1 构建Docker镜像(不想构建直接从5.5看) 6 | 7 | ```bash 8 | # 在项目根目录下执行 9 | docker build -t nftato-app . 10 | ``` 11 | 12 | #### 5.2 运行Docker容器 13 | 14 | 基本运行命令: 15 | 16 | ```bash 17 | docker run -d -p 3001:3001 --name nftato-container nftato-app 18 | ``` 19 | 20 | #### 5.3 数据持久化 21 | 22 | 为了确保数据持久化(包括服务器配置、用户数据等),需要挂载数据目录: 23 | 24 | ```bash 25 | # 在项目根目录下 26 | # 确保数据目录存在 27 | mkdir -p $(pwd)/server/data 28 | 29 | # 运行容器并挂载数据目录 30 | docker run -d -p 3001:3001 \ 31 | -v $(pwd)/server/data:/app/server/data \ 32 | -v $(pwd)/server/config.json:/app/server/config.json \ 33 | --name nftato-container nftato-app 34 | ``` 35 | 36 | #### 5.4 使用Docker卷实现更好的数据管理(可选) 37 | 38 | ```bash 39 | # 创建数据卷 40 | docker volume create nftato-data 41 | 42 | # 使用数据卷运行容器 43 | docker run -d -p 3001:3001 \ 44 | -v nftato-data:/app/server/data \ 45 | -v $(pwd)/server/config.json:/app/server/config.json \ 46 | --name nftato-container nftato-app 47 | ``` 48 | 49 | #### 5.5 从Docker Hub直接拉取镜像(无需本地构建) 50 | 51 | 您也可以直接从Docker Hub拉取预构建的镜像: 52 | 53 | ```bash 54 | # 拉取镜像 55 | docker pull fiftonb/gnftato:latest 56 | 57 | # 运行容器 58 | docker run -d -p 3001:3001 \ 59 | -v $(pwd)/server/data:/app/server/data \ 60 | -v $(pwd)/server/config.json:/app/server/config.json \ 61 | --name nftato-container fiftonb/gnftato:latest 62 | ``` 63 | 64 | 这种方式无需在本地构建镜像,可以直接使用已发布的镜像运行应用。 65 | 66 | #### 5.6 设置环境变量 67 | 68 | 您可以通过环境变量自定义应用配置: 69 | 70 | ```bash 71 | # 使用-e选项设置单个环境变量 72 | docker run -d -p 3001:3001 \ 73 | -e NODE_ENV=production \ 74 | -e JWT_SECRET=your_custom_secret \ 75 | -v $(pwd)/server/data:/app/server/data \ 76 | --name nftato-container fiftonb/gnftato:latest 77 | ``` 78 | 79 | 或者使用环境变量文件: 80 | 81 | ```bash 82 | # 创建.env文件 83 | cat > .env << EOL 84 | NODE_ENV=production 85 | JWT_SECRET=your_custom_secret 86 | PORT=3001 87 | CORS_ORIGIN=* 88 | EOL 89 | 90 | # 使用环境变量文件运行容器 91 | docker run -d -p 3001:3001 \ 92 | --env-file .env \ 93 | -v $(pwd)/server/data:/app/server/data \ 94 | --name nftato-container fiftonb/gnftato:latest 95 | ``` 96 | 97 | 可用的环境变量及其说明: 98 | 99 | | 环境变量 | 说明 | 默认值 | 100 | |---------|------|--------| 101 | | PORT | 服务器监听端口 | 3001 | 102 | | HOST | 服务器监听地址 | 0.0.0.0 | 103 | | NODE_ENV | 运行模式 | development | 104 | | JWT_SECRET | JWT签名密钥 | iptato-secure-jwt-secret-key | 105 | | JWT_EXPIRES_IN | JWT过期时间 | 7d | 106 | | CORS_ORIGIN | CORS允许的源 | * | 107 | | STABLE_MODE | 稳定模式 | true | 108 | 109 | #### 5.7 查看容器日志 110 | 111 | ```bash 112 | docker logs nftato-container 113 | ``` 114 | 115 | #### 5.8 停止和重启容器 116 | 117 | ```bash 118 | # 停止容器 119 | docker stop nftato-container 120 | 121 | # 重启容器 122 | docker start nftato-container 123 | 124 | # 删除容器(数据卷不会被删除) 125 | docker rm nftato-container 126 | ``` -------------------------------------------------------------------------------- /projectStructure.txt: -------------------------------------------------------------------------------- 1 | Gnftato/ 2 | ├── client/ # 前端Vue项目 3 | │ ├── src/ 4 | │ │ ├── views/ # 页面组件 5 | │ │ │ ├── Home.vue # 主页/仪表盘,显示系统概况 6 | │ │ │ ├── Servers.vue # 服务器管理页面,用于添加/编辑/删除服务器 7 | │ │ │ ├── Rules.vue # 防火墙规则管理,配置nftables规则 8 | │ │ │ ├── Login.vue # 用户登录页面 9 | │ │ │ ├── Register.vue # 用户注册页面 10 | │ │ │ ├── Profile.vue # 用户资料设置页面 11 | │ │ ├── components/ # 通用组件 12 | │ │ │ ├── ServerForm.vue # 服务器添加/编辑表单组件 13 | │ │ │ ├── ChangePasswordForm.vue # 修改密码表单组件 14 | │ │ │ ├── rules/ # 规则相关子组件 15 | │ │ ├── router/ # Vue路由管理 16 | │ │ │ ├── index.js # 路由配置文件,定义应用所有路由 17 | │ │ ├── store/ # Vuex状态管理 18 | │ │ │ ├── index.js # Vuex入口,组装模块并导出store 19 | │ │ │ ├── modules/ # Vuex模块目录 20 | │ │ │ ├── auth.js # 认证状态管理模块 21 | │ │ │ ├── servers.js # 服务器状态管理模块 22 | │ │ │ ├── rules.js # 规则状态管理模块 23 | │ │ ├── App.vue # 应用主组件,根组件 24 | │ │ ├── main.js # 应用入口文件,初始化Vue实例 25 | │ ├── public/ # 静态资源 26 | │ │ ├── index.html # HTML模板文件 27 | │ ├── package.json # 前端依赖管理 28 | │ ├── vue.config.js # Vue配置文件 29 | │ ├── babel.config.js # Babel配置 30 | │ ├── .npmrc # NPM配置文件 31 | ├── server/ # 后端Express项目 32 | │ ├── controllers/ # 控制器 33 | │ │ ├── serverController.js # 服务器管理控制器,处理服务器CRUD操作 34 | │ │ ├── rulesController.js # 规则管理控制器,处理防火墙规则操作 35 | │ │ ├── authController.js # 认证控制器,处理登录/注册等操作 36 | │ ├── models/ # 数据模型 37 | │ │ ├── Server.js # 服务器模型,定义服务器数据结构 38 | │ │ ├── Rule.js # 规则模型,定义防火墙规则数据结构 39 | │ │ ├── User.js # 用户模型,定义用户数据结构及认证逻辑 40 | │ ├── services/ # 业务逻辑 41 | │ │ ├── sshService.js # SSH连接服务,处理与远程服务器的通信 42 | │ │ ├── nftablesService.js # nftables管理服务,处理nftables规则应用 43 | │ │ ├── cacheService.js # 缓存服务,缓存服务器状态和规则数据 44 | │ ├── routes/ # API路由 45 | │ │ ├── serverRoutes.js # 服务器相关API路由 46 | │ │ ├── rulesRoutes.js # 规则相关API路由 47 | │ │ ├── authRoutes.js # 认证相关API路由 48 | │ ├── middlewares/ # 中间件 49 | │ │ ├── authMiddleware.js # 认证中间件,处理API访问权限验证 50 | │ ├── scripts/ # 脚本文件 51 | │ │ ├── Nftato.sh # nftables核心主脚本备份 52 | │ │ ├── createAdmin.js # 创建管理员用户脚本 53 | │ ├── data/ # 数据存储目录,用于JSON文件存储 54 | │ ├── public/ # 静态资源 55 | │ ├── app.js # 主应用入口,Express服务器配置 56 | │ ├── config.json # 应用配置文件 57 | │ ├── start.sh # 后端启动脚本 58 | │ ├── nodemon.json # Nodemon配置,用于开发环境 59 | ├── Nftato.sh # nftables核心主脚本 60 | ├── intcentos.sh # CentOS系统安装脚本(仅测试) 61 | ├── start-all.sh # 前后端一键启动脚本 62 | ├── Dockerfile # Docker配置文件,用于容器化部署 63 | ├── .env # 环境变量配置 64 | ├── .env.example # 环境变量示例文件 65 | ├── package.json # 项目根目录依赖管理 66 | ├── package-lock.json # 依赖版本锁定文件 67 | ├── .gitignore # Git忽略配置 68 | ├── README.md # 项目说明文档 -------------------------------------------------------------------------------- /server/models/User.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const bcrypt = require('bcryptjs'); 4 | 5 | class User { 6 | constructor() { 7 | // 获取用户数据文件路径 8 | let dataDir; 9 | if (process.env.DATA_DIR) { 10 | if (process.env.DATA_DIR.startsWith('./')) { 11 | dataDir = path.join(__dirname, '../..', process.env.DATA_DIR.substring(2)); 12 | } else { 13 | dataDir = path.resolve(process.env.DATA_DIR); 14 | } 15 | } else { 16 | dataDir = path.join(__dirname, '../data'); 17 | } 18 | 19 | this.usersFilePath = path.join(dataDir, 'users.json'); 20 | 21 | // 如果用户数据文件不存在,创建一个空的 22 | if (!fs.existsSync(this.usersFilePath)) { 23 | fs.writeFileSync(this.usersFilePath, JSON.stringify({ users: [] }, null, 2)); 24 | } 25 | } 26 | 27 | // 获取所有用户 28 | getUsers() { 29 | const data = fs.readFileSync(this.usersFilePath, 'utf8'); 30 | return JSON.parse(data).users; 31 | } 32 | 33 | // 保存用户数据 34 | saveUsers(users) { 35 | fs.writeFileSync(this.usersFilePath, JSON.stringify({ users }, null, 2)); 36 | } 37 | 38 | // 通过用户名查找用户 39 | findUserByUsername(username) { 40 | const users = this.getUsers(); 41 | return users.find(user => user.username === username); 42 | } 43 | 44 | // 创建新用户 45 | async createUser(username, password) { 46 | // 检查用户名是否已存在 47 | if (this.findUserByUsername(username)) { 48 | throw new Error('用户名已存在'); 49 | } 50 | 51 | const users = this.getUsers(); 52 | 53 | // 加密密码 54 | const salt = await bcrypt.genSalt(10); 55 | const hashedPassword = await bcrypt.hash(password, salt); 56 | 57 | // 创建新用户 58 | const newUser = { 59 | id: Date.now().toString(), 60 | username, 61 | password: hashedPassword, 62 | createdAt: new Date().toISOString() 63 | }; 64 | 65 | users.push(newUser); 66 | this.saveUsers(users); 67 | 68 | // 返回不含密码的用户信息 69 | const { password: _, ...userWithoutPassword } = newUser; 70 | return userWithoutPassword; 71 | } 72 | 73 | // 验证用户登录 74 | async validateUser(username, password) { 75 | const user = this.findUserByUsername(username); 76 | 77 | if (!user) { 78 | return null; 79 | } 80 | 81 | const isMatch = await bcrypt.compare(password, user.password); 82 | 83 | if (!isMatch) { 84 | return null; 85 | } 86 | 87 | // 返回不含密码的用户信息 88 | const { password: _, ...userWithoutPassword } = user; 89 | return userWithoutPassword; 90 | } 91 | 92 | // 更新用户密码 93 | async updatePassword(userId, newPassword) { 94 | const users = this.getUsers(); 95 | const userIndex = users.findIndex(user => user.id === userId); 96 | 97 | if (userIndex === -1) { 98 | throw new Error('用户不存在'); 99 | } 100 | 101 | // 加密新密码 102 | const salt = await bcrypt.genSalt(10); 103 | const hashedPassword = await bcrypt.hash(newPassword, salt); 104 | 105 | // 更新用户密码 106 | users[userIndex].password = hashedPassword; 107 | users[userIndex].updatedAt = new Date().toISOString(); 108 | 109 | // 保存更新 110 | this.saveUsers(users); 111 | 112 | return true; 113 | } 114 | } 115 | 116 | module.exports = new User(); -------------------------------------------------------------------------------- /client/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 101 | 102 | -------------------------------------------------------------------------------- /client/src/store/modules/auth.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | // 初始状态 4 | const state = { 5 | token: localStorage.getItem('token') || null, 6 | user: null, 7 | loading: false 8 | }; 9 | 10 | // Getters 11 | const getters = { 12 | isAuthenticated: state => !!state.token, 13 | currentUser: state => state.user, 14 | isLoading: state => state.loading 15 | }; 16 | 17 | // Actions 18 | const actions = { 19 | // 登录 20 | async login({ commit }, credentials) { 21 | commit('SET_LOADING', true); 22 | try { 23 | const response = await axios.post('/api/auth/login', credentials); 24 | const { token, user } = response.data.data; 25 | 26 | // 存储令牌到本地存储和状态 27 | localStorage.setItem('token', token); 28 | commit('SET_TOKEN', token); 29 | commit('SET_USER', user); 30 | 31 | // 设置全局认证头 32 | axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; 33 | 34 | return response; 35 | } catch (error) { 36 | commit('SET_TOKEN', null); 37 | commit('SET_USER', null); 38 | localStorage.removeItem('token'); 39 | throw error; 40 | } finally { 41 | commit('SET_LOADING', false); 42 | } 43 | }, 44 | 45 | /** 46 | * 注册功能 - 仅供API调用,前端不使用 47 | * 保留此代码以便将来通过API工具或后台管理使用 48 | */ 49 | async register({ commit, dispatch }, credentials) { 50 | commit('SET_LOADING', true); 51 | try { 52 | const response = await axios.post('/api/auth/register', credentials); 53 | const { token, user } = response.data.data; 54 | 55 | // 存储令牌到本地存储和状态 56 | localStorage.setItem('token', token); 57 | commit('SET_TOKEN', token); 58 | commit('SET_USER', user); 59 | 60 | // 设置全局认证头 61 | axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; 62 | 63 | return response; 64 | } catch (error) { 65 | commit('SET_TOKEN', null); 66 | commit('SET_USER', null); 67 | localStorage.removeItem('token'); 68 | throw error; 69 | } finally { 70 | commit('SET_LOADING', false); 71 | } 72 | }, 73 | 74 | // 获取当前用户信息 75 | async getCurrentUser({ commit, state }) { 76 | if (!state.token) return; 77 | 78 | commit('SET_LOADING', true); 79 | try { 80 | const response = await axios.get('/api/auth/me'); 81 | commit('SET_USER', response.data.data.user); 82 | return response; 83 | } catch (error) { 84 | // 如果令牌无效或过期,清除认证状态 85 | if (error.response && error.response.status === 401) { 86 | commit('SET_TOKEN', null); 87 | commit('SET_USER', null); 88 | localStorage.removeItem('token'); 89 | } 90 | throw error; 91 | } finally { 92 | commit('SET_LOADING', false); 93 | } 94 | }, 95 | 96 | // 登出 97 | logout({ commit }) { 98 | commit('SET_TOKEN', null); 99 | commit('SET_USER', null); 100 | localStorage.removeItem('token'); 101 | delete axios.defaults.headers.common['Authorization']; 102 | } 103 | }; 104 | 105 | // Mutations 106 | const mutations = { 107 | SET_TOKEN(state, token) { 108 | state.token = token; 109 | }, 110 | SET_USER(state, user) { 111 | state.user = user; 112 | }, 113 | SET_LOADING(state, isLoading) { 114 | state.loading = isLoading; 115 | } 116 | }; 117 | 118 | export default { 119 | state, 120 | getters, 121 | actions, 122 | mutations 123 | }; -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 60 | 61 | -------------------------------------------------------------------------------- /client/src/components/ChangePasswordForm.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 112 | 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GNftato Panel - 多服务器防火墙规则管理面板 2 | 3 | 基于Nftato.sh脚本开发的可视化多服务器防火墙规则管理面板,支持通过SSH远程连接管理多台服务器的nftables规则。 4 | 5 | > 前端现在也不是很满意,但是,也就这样了(能用) 6 | > 另外关于测试用例覆盖啥的将就吧,精力不够,还是能用就行了 7 | > 有能力的自己二开吧,虽然代码像坨屎,能跑就行... 8 | 9 | ## 功能特色 10 | 11 | - **多服务器管理**:集中管理多台服务器的防火墙规则 12 | - **出网控制**:封禁/解封 SPAM端口、自定义端口 13 | - **入网控制**:管理入网端口和IP白名单 14 | - **SSH远程控制**:通过SSH安全连接到远程服务器执行命令 15 | - **可视化操作**:直观的界面操作替代复杂的命令行管理 16 | - **状态监控**:实时查看各服务器的连接状态和规则列表 17 | - **登录认证**:用户身份验证,保护管理界面安全 18 | - **DDOS防御**:借鉴Goedge防御规则实现的脚本防御 19 | 20 | > 需要注意,使用同类用到nftables命令的工具会使规则冲突。清除规则则可以夺回控制权。脚本首次运行默认只放行ssh端口,且ssh端口无法取消放行。 21 | 22 | ## TODO 23 | 24 | - [X] Debian11+ 脚本测试通过 25 | - [X] Ubuntu20+ 脚本测试通过 26 | - [X] Centos9+ 脚本测试通过 27 | - [X] 重写前端业务逻辑 28 | - [X] 优化部署脚本指令 29 | - [X] 自动更新核心代码功能 30 | - [ ] 一键清除黑白名单 31 | - [ ] 获取黑白名单IP列表 32 | - [ ] 批量导入IP添加黑白名单 33 | - [ ] 实现端口转发 34 | - [X] 完善部署文档 35 | - [X] 搭建预览链接 36 | 37 | ## 技术栈 38 | 39 | - **后端**:Node.js、Express、SSH2、本地JSON存储、JWT认证 40 | - **前端**:Vue.js 2.x、Element UI、Axios、Vuex状态管理 41 | - **通信**:RESTful API 42 | - **认证**:基于JWT的用户认证系统 43 | 44 | ## 系统要求 45 | 46 | - Node.js 12.x以上 47 | - 远程服务器需支持SSH连接 48 | 49 | ## 安装部署 50 | 51 | ### 1. 克隆项目 52 | 53 | ```bash 54 | git clone https://github.com/Fiftonb/Gnftato.git 55 | cd Gnftato 56 | ``` 57 | Clawcloud Run云平台部署教程=>[点击查看](https://github.com/Fiftonb/Gnftato/blob/main/Clawcloud.md) 58 | 59 | Docker部署教程=>[点击查看](https://github.com/Fiftonb/Gnftato/blob/main/Dockerdo.md) 60 | 61 | 本地环境部署教程=>[点击查看](https://github.com/Fiftonb/Gnftato/blob/main/Localdo.md) 62 | 63 | ## 用户认证 64 | 65 | 系统采用固定管理员模式,不支持开放注册。系统启动时会自动创建默认管理员账户: 66 | 67 | - **用户名**: admin 68 | - **密码**: admin123 69 | 70 | 您也可以通过命令行创建/重置管理员账户: 71 | 72 | ```bash 73 | cd server 74 | npm run create-admin 75 | ``` 76 | 77 | ## 服务访问 78 | 79 | - 前端界面: http://localhost:8080 (开发模式)或 http://localhost:3001 (生产模式) 80 | - 后端API: http://localhost:3001/api 81 | 82 | ## 使用演示 83 | 84 | 使用演示=>[点击查看](https://github.com/Fiftonb/Gnftato/blob/main/USE.md) 85 | 86 | ## 功能说明 87 | 88 | ### 放行IP与IP黑白名单的区别 89 | 90 | 系统提供两种IP管理功能,它们服务于不同的目的: 91 | 92 | 1. **放行IP (入网方向功能 - 第17项)** 93 | - 作用于基本防火墙层面,控制哪些IP可以访问服务器 94 | - 如果服务器防火墙默认策略是拒绝(DROP),只有被放行的IP才能建立连接 95 | - 未被放行的IP会被基本防火墙直接拒绝访问 96 | - 命令实现: `nft add rule inet filter input ip saddr $IP accept` 97 | 98 | 2. **IP黑白名单 (DDoS防御功能 - 第24项)** 99 | - 作用于DDoS防御层面,位于基本防火墙之后 100 | - **白名单IP**: 可以绕过DDoS防御检测,不受连接频率和数量限制 101 | - **黑名单IP**: 被直接拒绝,不论连接次数和频率 102 | - IP必须先通过基本防火墙(被放行或防火墙默认允许),才会到达DDoS防御层 103 | 104 | 使用建议: 105 | - 如果服务器设置为默认拒绝所有连接,需要先使用"放行IP"功能 106 | - 如果已开启DDoS防御,对于需要频繁访问的可信IP,建议添加到白名单 107 | - 如果只需简单的访问控制,使用"放行IP"即可 108 | - 如果需要防御DDoS攻击同时允许特定IP不受限制,应使用白名单功能 109 | 110 | ## 安全提示 111 | 112 | - 登录系统后请立即修改默认管理员密码 113 | - 确保JWT密钥安全,不要使用默认的密钥 114 | - 请确保使用安全的密码 115 | - 建议使用SSH密钥认证而非密码认证 116 | - 服务器连接信息(特别是密码和私钥)存储在本地JSON文件中 117 | 118 | 119 | ## 项目参考 120 | 121 | 本项目基于[GiPtato](https://github.com/Fiftonb/GiPtato)开发,内核脚本从iptables迁移到nftables的升级版本。 122 | > 使用nftables替代iptables实现更现代化的防火墙管理。 123 | 124 | 不使用面板只想使用脚本(完善后的脚本) 125 | 126 | ```bash 127 | wget -N --no-check-certificate https://raw.githubusercontent.com/Fiftonb/Gnftato/refs/heads/main/Nftato.sh && chmod +x Nftato.sh && bash Nftato.sh 128 | ``` 129 | 二次使用目录下执行 130 | ```sh 131 | ./Nftato.sh 132 | ``` 133 | 134 | ## 免责声明 135 | 136 | * 此项目开发目的为本人自用,因此本人不能保证向后兼容性。 137 | * 由于本人能力有限,不能保证所有功能的可用性,如果出现问题请在Issues反馈。 138 | * 本人不对任何人使用本项目造成的任何后果承担责任。 139 | * 本人比较多变,因此本项目可能会随想法或思路的变动随性更改项目结构或大规模重构代码,若不能接受请勿使用。 140 | 141 | ## 许可证 142 | 143 | MIT License 144 | 145 | ## Stargazers over time 146 | [![Stargazers over time](https://starchart.cc/Fiftonb/Gnftato.svg?variant=adaptive)](https://starchart.cc/Fiftonb/Gnftato) 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /server/controllers/authController.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/User'); 2 | const jwt = require('jsonwebtoken'); 3 | 4 | // JWT密钥 5 | const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; 6 | const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d'; 7 | 8 | // 生成JWT令牌 9 | const generateToken = (user) => { 10 | return jwt.sign( 11 | { id: user.id, username: user.username }, 12 | JWT_SECRET, 13 | { expiresIn: JWT_EXPIRES_IN } 14 | ); 15 | }; 16 | 17 | // 用户注册 18 | exports.register = async (req, res) => { 19 | try { 20 | const { username, password } = req.body; 21 | 22 | // 验证输入 23 | if (!username || !password) { 24 | return res.status(400).json({ 25 | success: false, 26 | message: '用户名和密码不能为空' 27 | }); 28 | } 29 | 30 | // 创建用户 31 | const user = await User.createUser(username, password); 32 | 33 | // 生成令牌 34 | const token = generateToken(user); 35 | 36 | res.status(201).json({ 37 | success: true, 38 | message: '用户注册成功', 39 | data: { 40 | user, 41 | token 42 | } 43 | }); 44 | } catch (error) { 45 | res.status(400).json({ 46 | success: false, 47 | message: error.message 48 | }); 49 | } 50 | }; 51 | 52 | // 用户登录 53 | exports.login = async (req, res) => { 54 | try { 55 | const { username, password } = req.body; 56 | 57 | // 验证输入 58 | if (!username || !password) { 59 | return res.status(400).json({ 60 | success: false, 61 | message: '用户名和密码不能为空' 62 | }); 63 | } 64 | 65 | // 验证用户 66 | const user = await User.validateUser(username, password); 67 | 68 | if (!user) { 69 | return res.status(401).json({ 70 | success: false, 71 | message: '用户名或密码错误' 72 | }); 73 | } 74 | 75 | // 生成令牌 76 | const token = generateToken(user); 77 | 78 | res.status(200).json({ 79 | success: true, 80 | message: '登录成功', 81 | data: { 82 | user, 83 | token 84 | } 85 | }); 86 | } catch (error) { 87 | res.status(500).json({ 88 | success: false, 89 | message: '服务器错误', 90 | error: error.message 91 | }); 92 | } 93 | }; 94 | 95 | // 获取当前用户信息 96 | exports.getCurrentUser = async (req, res) => { 97 | try { 98 | // 用户信息已经在身份验证中间件中添加到req对象 99 | res.status(200).json({ 100 | success: true, 101 | data: { 102 | user: req.user 103 | } 104 | }); 105 | } catch (error) { 106 | res.status(500).json({ 107 | success: false, 108 | message: '服务器错误', 109 | error: error.message 110 | }); 111 | } 112 | }; 113 | 114 | // 更新用户密码 115 | exports.updatePassword = async (req, res) => { 116 | try { 117 | const { currentPassword, newPassword } = req.body; 118 | 119 | // 验证输入 120 | if (!currentPassword || !newPassword) { 121 | return res.status(400).json({ 122 | success: false, 123 | message: '当前密码和新密码不能为空' 124 | }); 125 | } 126 | 127 | // 获取当前用户信息 128 | const userId = req.user.id; 129 | const username = req.user.username; 130 | 131 | // 验证当前密码 132 | const user = await User.validateUser(username, currentPassword); 133 | 134 | if (!user) { 135 | return res.status(401).json({ 136 | success: false, 137 | message: '当前密码错误' 138 | }); 139 | } 140 | 141 | // 更新密码 142 | await User.updatePassword(userId, newPassword); 143 | 144 | res.status(200).json({ 145 | success: true, 146 | message: '密码更新成功' 147 | }); 148 | } catch (error) { 149 | res.status(500).json({ 150 | success: false, 151 | message: '更新密码失败', 152 | error: error.message 153 | }); 154 | } 155 | }; -------------------------------------------------------------------------------- /server/services/cacheService.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const util = require('util'); 4 | 5 | const readFile = util.promisify(fs.readFile); 6 | const writeFile = util.promisify(fs.writeFile); 7 | 8 | const RULES_CACHE_PATH = path.join(__dirname, '../data/rules.json'); 9 | 10 | /** 11 | * 读取规则缓存数据 12 | * @returns {Promise} 规则缓存数据 13 | */ 14 | const getRulesCache = async () => { 15 | try { 16 | const data = await readFile(RULES_CACHE_PATH, 'utf8'); 17 | return JSON.parse(data); 18 | } catch (error) { 19 | console.error('读取规则缓存失败:', error); 20 | // 如果文件不存在或无法解析,返回默认结构 21 | return { rules: {} }; 22 | } 23 | }; 24 | 25 | /** 26 | * 写入规则缓存数据 27 | * @param {Object} data - 规则缓存数据 28 | * @returns {Promise} 是否写入成功 29 | */ 30 | const saveRulesCache = async (data) => { 31 | try { 32 | await writeFile(RULES_CACHE_PATH, JSON.stringify(data, null, 2), 'utf8'); 33 | return true; 34 | } catch (error) { 35 | console.error('写入规则缓存失败:', error); 36 | return false; 37 | } 38 | }; 39 | 40 | /** 41 | * 获取服务器规则缓存 42 | * @param {string} serverId - 服务器ID 43 | * @returns {Promise} 服务器规则缓存数据 44 | */ 45 | exports.getServerRulesCache = async (serverId) => { 46 | try { 47 | const cache = await getRulesCache(); 48 | return cache.rules[serverId] || null; 49 | } catch (error) { 50 | console.error(`获取服务器 ${serverId} 的规则缓存失败:`, error); 51 | return null; 52 | } 53 | }; 54 | 55 | /** 56 | * 保存服务器规则缓存 57 | * @param {string} serverId - 服务器ID 58 | * @param {Object} data - 服务器规则数据 59 | * @returns {Promise} 是否保存成功 60 | */ 61 | exports.saveServerRulesCache = async (serverId, data) => { 62 | try { 63 | const cache = await getRulesCache(); 64 | 65 | if (!cache.rules) { 66 | cache.rules = {}; 67 | } 68 | 69 | cache.rules[serverId] = { 70 | lastUpdate: new Date().toISOString(), 71 | data: data 72 | }; 73 | 74 | return await saveRulesCache(cache); 75 | } catch (error) { 76 | console.error(`保存服务器 ${serverId} 的规则缓存失败:`, error); 77 | return false; 78 | } 79 | }; 80 | 81 | /** 82 | * 更新服务器数据缓存项 83 | * @param {string} serverId - 服务器ID 84 | * @param {string} key - 数据项键名 85 | * @param {any} value - 数据项值 86 | * @returns {Promise} 是否更新成功 87 | */ 88 | exports.updateServerCacheItem = async (serverId, key, value) => { 89 | try { 90 | const cache = await getRulesCache(); 91 | 92 | if (!cache.rules[serverId]) { 93 | cache.rules[serverId] = { 94 | lastUpdate: new Date().toISOString(), 95 | data: {} 96 | }; 97 | } 98 | 99 | cache.rules[serverId].data[key] = value; 100 | cache.rules[serverId].lastUpdate = new Date().toISOString(); 101 | 102 | return await saveRulesCache(cache); 103 | } catch (error) { 104 | console.error(`更新服务器 ${serverId} 的缓存项 ${key} 失败:`, error); 105 | return false; 106 | } 107 | }; 108 | 109 | /** 110 | * 清除服务器规则缓存 111 | * @param {string} serverId - 服务器ID 112 | * @returns {Promise} 是否清除成功 113 | */ 114 | exports.clearServerRulesCache = async (serverId) => { 115 | try { 116 | const cache = await getRulesCache(); 117 | 118 | if (cache.rules && cache.rules[serverId]) { 119 | delete cache.rules[serverId]; 120 | return await saveRulesCache(cache); 121 | } 122 | 123 | return true; 124 | } catch (error) { 125 | console.error(`清除服务器 ${serverId} 的规则缓存失败:`, error); 126 | return false; 127 | } 128 | }; 129 | 130 | /** 131 | * 获取服务器缓存最后更新时间 132 | * @param {string} serverId - 服务器ID 133 | * @returns {Promise} 最后更新时间 134 | */ 135 | exports.getServerCacheLastUpdate = async (serverId) => { 136 | try { 137 | const cache = await getRulesCache(); 138 | 139 | if (cache.rules && cache.rules[serverId]) { 140 | return cache.rules[serverId].lastUpdate; 141 | } 142 | 143 | return null; 144 | } catch (error) { 145 | console.error(`获取服务器 ${serverId} 的缓存更新时间失败:`, error); 146 | return null; 147 | } 148 | }; -------------------------------------------------------------------------------- /client/src/components/ServerForm.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 125 | 126 | -------------------------------------------------------------------------------- /client/src/views/Register.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 126 | 127 | -------------------------------------------------------------------------------- /server/models/Server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { v4: uuidv4 } = require('uuid'); 4 | // 加载根目录的环境变量文件 5 | require('dotenv').config({ path: path.join(__dirname, '../../.env') }); 6 | 7 | // 使用环境变量中的DATA_DIR或默认路径,处理相对路径 8 | let dataDir; 9 | if (process.env.DATA_DIR) { 10 | // 处理相对路径,将它转换为相对于项目根目录的绝对路径 11 | if (process.env.DATA_DIR.startsWith('./')) { 12 | dataDir = path.join(__dirname, '../..', process.env.DATA_DIR.substring(2)); 13 | } else { 14 | dataDir = path.resolve(process.env.DATA_DIR); 15 | } 16 | } else { 17 | dataDir = path.join(__dirname, '../data'); 18 | } 19 | 20 | const dataFilePath = path.join(dataDir, 'servers.json'); 21 | 22 | console.log(`Server模型使用的数据文件路径: ${dataFilePath}`); 23 | 24 | // 确保data目录存在 25 | if (!fs.existsSync(dataDir)) { 26 | fs.mkdirSync(dataDir, { recursive: true }); 27 | console.log(`从Server模型创建的数据目录: ${dataDir}`); 28 | } 29 | 30 | // 确保JSON文件存在 31 | if (!fs.existsSync(dataFilePath)) { 32 | fs.writeFileSync(dataFilePath, JSON.stringify({ servers: [] }, null, 2)); 33 | console.log(`从Server模型创建的服务器数据文件: ${dataFilePath}`); 34 | } 35 | 36 | class Server { 37 | constructor(data) { 38 | this._id = data._id || uuidv4(); 39 | this.name = data.name; 40 | this.host = data.host; 41 | this.port = data.port || 22; 42 | this.username = data.username; 43 | this.authType = data.authType || 'password'; 44 | this.password = data.password; 45 | this.privateKey = data.privateKey; 46 | this.status = data.status || 'offline'; 47 | this.lastConnection = data.lastConnection || null; 48 | this.createdAt = data.createdAt || new Date().toISOString(); 49 | this.updatedAt = data.updatedAt || new Date().toISOString(); 50 | } 51 | 52 | // 获取所有服务器 53 | static find() { 54 | try { 55 | const data = JSON.parse(fs.readFileSync(dataFilePath, 'utf8')); 56 | return Promise.resolve(data.servers); 57 | } catch (error) { 58 | return Promise.reject(error); 59 | } 60 | } 61 | 62 | // 根据ID查找服务器 63 | static findById(id) { 64 | try { 65 | const data = JSON.parse(fs.readFileSync(dataFilePath, 'utf8')); 66 | const server = data.servers.find(server => server._id === id); 67 | return Promise.resolve(server || null); 68 | } catch (error) { 69 | return Promise.reject(error); 70 | } 71 | } 72 | 73 | // 创建新服务器 74 | static create(serverData) { 75 | try { 76 | const data = JSON.parse(fs.readFileSync(dataFilePath, 'utf8')); 77 | const newServer = new Server(serverData); 78 | data.servers.push(newServer); 79 | fs.writeFileSync(dataFilePath, JSON.stringify(data, null, 2)); 80 | return Promise.resolve(newServer); 81 | } catch (error) { 82 | return Promise.reject(error); 83 | } 84 | } 85 | 86 | // 更新服务器信息 87 | static findByIdAndUpdate(id, updateData) { 88 | try { 89 | const data = JSON.parse(fs.readFileSync(dataFilePath, 'utf8')); 90 | const serverIndex = data.servers.findIndex(server => server._id === id); 91 | 92 | if (serverIndex === -1) { 93 | return Promise.resolve(null); 94 | } 95 | 96 | const updatedServer = { 97 | ...data.servers[serverIndex], 98 | ...updateData, 99 | updatedAt: new Date().toISOString() 100 | }; 101 | 102 | data.servers[serverIndex] = updatedServer; 103 | fs.writeFileSync(dataFilePath, JSON.stringify(data, null, 2)); 104 | 105 | return Promise.resolve(updatedServer); 106 | } catch (error) { 107 | return Promise.reject(error); 108 | } 109 | } 110 | 111 | // 删除服务器 112 | static findByIdAndDelete(id) { 113 | try { 114 | const data = JSON.parse(fs.readFileSync(dataFilePath, 'utf8')); 115 | const serverIndex = data.servers.findIndex(server => server._id === id); 116 | 117 | if (serverIndex === -1) { 118 | return Promise.resolve(null); 119 | } 120 | 121 | const deletedServer = data.servers[serverIndex]; 122 | data.servers.splice(serverIndex, 1); 123 | fs.writeFileSync(dataFilePath, JSON.stringify(data, null, 2)); 124 | 125 | return Promise.resolve(deletedServer); 126 | } catch (error) { 127 | return Promise.reject(error); 128 | } 129 | } 130 | } 131 | 132 | module.exports = Server; -------------------------------------------------------------------------------- /server/models/Rule.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { v4: uuidv4 } = require('uuid'); 4 | // 加载根目录的环境变量文件 5 | require('dotenv').config({ path: path.join(__dirname, '../../.env') }); 6 | 7 | // 使用环境变量中的DATA_DIR或默认路径,处理相对路径 8 | let dataDir; 9 | if (process.env.DATA_DIR) { 10 | // 处理相对路径,将它转换为相对于项目根目录的绝对路径 11 | if (process.env.DATA_DIR.startsWith('./')) { 12 | dataDir = path.join(__dirname, '../..', process.env.DATA_DIR.substring(2)); 13 | } else { 14 | dataDir = path.resolve(process.env.DATA_DIR); 15 | } 16 | } else { 17 | dataDir = path.join(__dirname, '../data'); 18 | } 19 | 20 | const dataFilePath = path.join(dataDir, 'rules.json'); 21 | 22 | console.log(`Rule模型使用的数据文件路径: ${dataFilePath}`); 23 | 24 | // 确保data目录存在 25 | if (!fs.existsSync(dataDir)) { 26 | fs.mkdirSync(dataDir, { recursive: true }); 27 | console.log(`从Rule模型创建的数据目录: ${dataDir}`); 28 | } 29 | 30 | // 确保JSON文件存在 31 | if (!fs.existsSync(dataFilePath)) { 32 | fs.writeFileSync(dataFilePath, JSON.stringify({ rules: [] }, null, 2)); 33 | console.log(`从Rule模型创建的规则数据文件: ${dataFilePath}`); 34 | } 35 | 36 | class Rule { 37 | constructor(data) { 38 | this._id = data._id || uuidv4(); 39 | this.name = data.name; 40 | this.server = data.server; 41 | this.protocol = data.protocol || 'tcp'; 42 | this.sourceIP = data.sourceIP || 'any'; 43 | this.sourcePort = data.sourcePort || 'any'; 44 | this.destinationIP = data.destinationIP || 'any'; 45 | this.destinationPort = data.destinationPort || 'any'; 46 | this.action = data.action || 'ACCEPT'; 47 | this.chain = data.chain || 'INPUT'; 48 | this.priority = data.priority || 0; 49 | this.enabled = data.enabled !== undefined ? data.enabled : true; 50 | this.description = data.description || ''; 51 | this.createdAt = data.createdAt || new Date().toISOString(); 52 | this.updatedAt = data.updatedAt || new Date().toISOString(); 53 | } 54 | 55 | // 获取所有规则 56 | static find(query = {}) { 57 | try { 58 | const data = JSON.parse(fs.readFileSync(dataFilePath, 'utf8')); 59 | 60 | if (Object.keys(query).length === 0) { 61 | return Promise.resolve(data.rules); 62 | } 63 | 64 | // 过滤规则 65 | const filteredRules = data.rules.filter(rule => { 66 | for (const [key, value] of Object.entries(query)) { 67 | if (rule[key] !== value) { 68 | return false; 69 | } 70 | } 71 | return true; 72 | }); 73 | 74 | return Promise.resolve(filteredRules); 75 | } catch (error) { 76 | return Promise.reject(error); 77 | } 78 | } 79 | 80 | // 根据ID查找规则 81 | static findById(id) { 82 | try { 83 | const data = JSON.parse(fs.readFileSync(dataFilePath, 'utf8')); 84 | const rule = data.rules.find(rule => rule._id === id); 85 | return Promise.resolve(rule || null); 86 | } catch (error) { 87 | return Promise.reject(error); 88 | } 89 | } 90 | 91 | // 创建新规则 92 | static create(ruleData) { 93 | try { 94 | const data = JSON.parse(fs.readFileSync(dataFilePath, 'utf8')); 95 | const newRule = new Rule(ruleData); 96 | data.rules.push(newRule); 97 | fs.writeFileSync(dataFilePath, JSON.stringify(data, null, 2)); 98 | return Promise.resolve(newRule); 99 | } catch (error) { 100 | return Promise.reject(error); 101 | } 102 | } 103 | 104 | // 更新规则信息 105 | static findByIdAndUpdate(id, updateData) { 106 | try { 107 | const data = JSON.parse(fs.readFileSync(dataFilePath, 'utf8')); 108 | const ruleIndex = data.rules.findIndex(rule => rule._id === id); 109 | 110 | if (ruleIndex === -1) { 111 | return Promise.resolve(null); 112 | } 113 | 114 | const updatedRule = { 115 | ...data.rules[ruleIndex], 116 | ...updateData, 117 | updatedAt: new Date().toISOString() 118 | }; 119 | 120 | data.rules[ruleIndex] = updatedRule; 121 | fs.writeFileSync(dataFilePath, JSON.stringify(data, null, 2)); 122 | 123 | return Promise.resolve(updatedRule); 124 | } catch (error) { 125 | return Promise.reject(error); 126 | } 127 | } 128 | 129 | // 删除规则 130 | static findByIdAndDelete(id) { 131 | try { 132 | const data = JSON.parse(fs.readFileSync(dataFilePath, 'utf8')); 133 | const ruleIndex = data.rules.findIndex(rule => rule._id === id); 134 | 135 | if (ruleIndex === -1) { 136 | return Promise.resolve(null); 137 | } 138 | 139 | const deletedRule = data.rules[ruleIndex]; 140 | data.rules.splice(ruleIndex, 1); 141 | fs.writeFileSync(dataFilePath, JSON.stringify(data, null, 2)); 142 | 143 | return Promise.resolve(deletedRule); 144 | } catch (error) { 145 | return Promise.reject(error); 146 | } 147 | } 148 | 149 | // 根据服务器ID删除所有规则 150 | static deleteByServerId(serverId) { 151 | try { 152 | const data = JSON.parse(fs.readFileSync(dataFilePath, 'utf8')); 153 | const originalLength = data.rules.length; 154 | 155 | data.rules = data.rules.filter(rule => rule.server !== serverId); 156 | 157 | const deletedCount = originalLength - data.rules.length; 158 | 159 | if (deletedCount > 0) { 160 | fs.writeFileSync(dataFilePath, JSON.stringify(data, null, 2)); 161 | } 162 | 163 | return Promise.resolve({ deletedCount }); 164 | } catch (error) { 165 | return Promise.reject(error); 166 | } 167 | } 168 | } 169 | 170 | module.exports = Rule; -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const http = require('http'); 6 | const socketIO = require('socket.io'); 7 | 8 | // 尝试加载环境变量 9 | const dotenvPath = path.join(__dirname, '../.env'); 10 | if (fs.existsSync(dotenvPath)) { 11 | // 如果.env文件存在,使用dotenv加载 12 | require('dotenv').config({ path: dotenvPath }); 13 | console.log('已从.env文件加载环境变量'); 14 | } else { 15 | // 如果.env文件不存在,尝试从config.json加载 16 | const configPath = path.join(__dirname, 'config.json'); 17 | if (fs.existsSync(configPath)) { 18 | try { 19 | const config = require(configPath); 20 | console.log('从config.json加载环境变量'); 21 | // 将配置项添加到process.env 22 | Object.keys(config).forEach(key => { 23 | process.env[key] = config[key]; 24 | }); 25 | } catch (error) { 26 | console.error('加载config.json时出错:', error); 27 | } 28 | } else { 29 | console.warn('警告: 找不到.env文件或config.json文件,将使用默认环境变量'); 30 | } 31 | } 32 | 33 | // 打印环境变量 34 | console.log('===== 服务器配置 ====='); 35 | console.log(`NODE_ENV: ${process.env.NODE_ENV}`); 36 | console.log(`PORT: ${process.env.PORT || 3001}`); 37 | console.log(`CORS_ORIGIN: ${process.env.CORS_ORIGIN}`); 38 | console.log(`STABLE_MODE: ${process.env.STABLE_MODE}`); 39 | console.log('======================'); 40 | 41 | // 导入路由 42 | const serverRoutes = require('./routes/serverRoutes'); 43 | const rulesRoutes = require('./routes/rulesRoutes'); 44 | const authRoutes = require('./routes/authRoutes'); 45 | 46 | // 创建Express应用 47 | const app = express(); 48 | 49 | // 创建HTTP服务器 50 | const server = http.createServer(app); 51 | 52 | // CORS配置 53 | const corsOptions = { 54 | origin: process.env.CORS_ORIGIN === '*' 55 | ? true // 允许所有来源 56 | : (process.env.CORS_ORIGIN || 'http://localhost:8080').split(','), 57 | credentials: true, 58 | optionsSuccessStatus: 200 59 | }; 60 | console.log(`CORS配置: ${process.env.CORS_ORIGIN === '*' ? '允许所有来源' : '允许来源 ' + corsOptions.origin}`); 61 | 62 | // 中间件 63 | app.use(cors(corsOptions)); 64 | app.use(express.json()); 65 | app.use(express.urlencoded({ extended: true })); 66 | 67 | // 静态文件服务 68 | app.use(express.static(path.join(__dirname, 'public'))); 69 | 70 | // 使用环境变量中的DATA_DIR或默认路径,处理相对路径 71 | let dataDir; 72 | if (process.env.DATA_DIR) { 73 | // 处理相对路径,将它转换为相对于项目根目录的绝对路径 74 | if (process.env.DATA_DIR.startsWith('./')) { 75 | dataDir = path.join(__dirname, '..', process.env.DATA_DIR.substring(2)); 76 | } else { 77 | dataDir = path.resolve(process.env.DATA_DIR); 78 | } 79 | } else { 80 | dataDir = path.join(__dirname, 'data'); 81 | } 82 | 83 | console.log(`使用的数据目录: ${dataDir}`); 84 | 85 | // 检查并创建数据目录 86 | if (!fs.existsSync(dataDir)) { 87 | // 使用recursive确保所有必要的父目录也会被创建 88 | fs.mkdirSync(dataDir, { recursive: true }); 89 | console.log(`数据目录已创建: ${dataDir}`); 90 | } 91 | 92 | // 检查并创建服务器数据文件 93 | const serversDataPath = path.join(dataDir, 'servers.json'); 94 | if (!fs.existsSync(serversDataPath)) { 95 | fs.writeFileSync(serversDataPath, JSON.stringify({ servers: [] }, null, 2)); 96 | console.log(`服务器数据文件已创建: ${serversDataPath}`); 97 | } 98 | 99 | // 检查并创建规则数据文件 100 | const rulesDataPath = path.join(dataDir, 'rules.json'); 101 | if (!fs.existsSync(rulesDataPath)) { 102 | fs.writeFileSync(rulesDataPath, JSON.stringify({ rules: [] }, null, 2)); 103 | console.log(`规则数据文件已创建: ${rulesDataPath}`); 104 | } 105 | 106 | // 检查并创建用户数据文件 107 | const usersDataPath = path.join(dataDir, 'users.json'); 108 | if (!fs.existsSync(usersDataPath)) { 109 | fs.writeFileSync(usersDataPath, JSON.stringify({ users: [] }, null, 2)); 110 | console.log(`用户数据文件已创建: ${usersDataPath}`); 111 | } 112 | 113 | // 检查并创建脚本目录 114 | const scriptsDir = path.join(__dirname, 'scripts'); 115 | if (!fs.existsSync(scriptsDir)) { 116 | fs.mkdirSync(scriptsDir, { recursive: true }); 117 | } 118 | 119 | // 检查并复制脚本文件 120 | const sourceScriptPath = path.join(__dirname, '../Nftato.sh'); 121 | const targetScriptPath = path.join(scriptsDir, 'Nftato.sh'); 122 | if (fs.existsSync(sourceScriptPath) && !fs.existsSync(targetScriptPath)) { 123 | fs.copyFileSync(sourceScriptPath, targetScriptPath); 124 | } 125 | 126 | // 创建Socket.IO实例 127 | const io = socketIO(server, { 128 | cors: { 129 | origin: "*", 130 | methods: ["GET", "POST"] 131 | } 132 | }); 133 | 134 | // 将io实例添加到app对象,以便在路由中访问 135 | app.io = io; 136 | 137 | // WebSocket连接处理 138 | io.on('connection', (socket) => { 139 | console.log('新的WebSocket连接:', socket.id); 140 | 141 | // 处理心跳请求 142 | socket.on('heartbeat', (data) => { 143 | // 响应心跳,保持连接活跃 144 | socket.emit('heartbeat_response', { timestamp: Date.now() }); 145 | }); 146 | 147 | // 处理部署请求 148 | socket.on('start_deploy', async (data) => { 149 | console.log('收到部署请求:', data); 150 | const { serverId } = data; 151 | 152 | if (!serverId) { 153 | socket.emit('deploy_log', { 154 | type: 'error', 155 | message: '缺少服务器ID参数' 156 | }); 157 | socket.emit('deploy_complete', { success: false, error: '缺少服务器ID参数' }); 158 | return; 159 | } 160 | 161 | try { 162 | // 引入SSH服务 163 | const sshService = require('./services/sshService'); 164 | 165 | // 发送初始日志 166 | socket.emit('deploy_log', { 167 | type: 'log', 168 | message: `开始为服务器 ${serverId} 部署Nftato脚本...` 169 | }); 170 | 171 | // 创建日志回调函数 172 | const logCallback = (message, type = 'log') => { 173 | if (typeof message === 'string' && message.trim()) { 174 | socket.emit('deploy_log', { type, message: message.trim() }); 175 | } 176 | }; 177 | 178 | // 开始部署过程 179 | console.log('开始部署进程...'); 180 | const result = await sshService.deployIptatoWithLogs(serverId, logCallback); 181 | console.log('部署结果:', result); 182 | 183 | // 发送完成信号 184 | socket.emit('deploy_complete', { 185 | success: result.success, 186 | error: result.error 187 | }); 188 | 189 | } catch (error) { 190 | console.error('部署过程出错:', error); 191 | socket.emit('deploy_log', { 192 | type: 'error', 193 | message: `部署出错: ${error.message}` 194 | }); 195 | socket.emit('deploy_complete', { 196 | success: false, 197 | error: error.message 198 | }); 199 | } 200 | }); 201 | 202 | // 处理断开连接 203 | socket.on('disconnect', () => { 204 | console.log('WebSocket连接断开:', socket.id); 205 | }); 206 | }); 207 | 208 | // API路由 209 | app.use('/api/auth', authRoutes); 210 | app.use('/api/servers', serverRoutes); 211 | app.use('/api/rules', rulesRoutes); 212 | 213 | // 前端路由处理 214 | app.get('*', (req, res) => { 215 | res.sendFile(path.join(__dirname, 'public/index.html')); 216 | }); 217 | 218 | // 错误处理中间件 219 | app.use((err, req, res, next) => { 220 | console.error(err.stack); 221 | res.status(500).json({ 222 | success: false, 223 | message: '服务器内部错误', 224 | error: err.message 225 | }); 226 | }); 227 | 228 | // 启动服务器 229 | const PORT = process.env.PORT || 3001; 230 | const HOST = process.env.HOST || '0.0.0.0'; 231 | server.listen(PORT, HOST, () => { 232 | console.log(`服务器运行在 http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}`); 233 | console.log(`WebSocket服务已启动`); 234 | if (HOST === '0.0.0.0') { 235 | console.log(`在Docker或远程环境中,可通过服务器IP访问:http://服务器IP:${PORT}`); 236 | } 237 | }); -------------------------------------------------------------------------------- /server/public/css/app.6e665435.css: -------------------------------------------------------------------------------- 1 | body,html{margin:0;padding:0;font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#app,body,html{height:100%}.header{display:flex;justify-content:space-between;align-items:center;background-color:#409eff;color:#fff;padding:0 20px}.header-left h1{margin:0;font-size:18px}.header-right{display:flex;align-items:center}.user-dropdown{color:#fff;cursor:pointer}.logout-btn{color:#fff!important;font-weight:700;border:1px solid #fff;border-radius:4px;padding:5px 10px}.logout-btn:hover{background-color:hsla(0,0%,100%,.2)}.el-dialog{margin:0 auto!important;max-width:90%}.el-dialog__wrapper{display:flex;align-items:center;justify-content:center;overflow:auto}.server-dialog .el-dialog{margin:15vh auto!important}.ip-lists-dialog .el-dialog{margin:5vh auto!important}@media screen and (max-width:768px){.el-dialog{margin:10px auto!important;width:90%!important}.ip-lists-dialog .el-dialog{width:95%!important;margin:2vh auto!important}}.home[data-v-482b8d4c]{padding:20px}.dashboard[data-v-482b8d4c]{max-width:1200px;margin:0 auto}.welcome-card[data-v-482b8d4c]{margin-bottom:20px}.dashboard-content[data-v-482b8d4c]{padding:20px;text-align:center}ul[data-v-482b8d4c]{display:inline-block;text-align:left;margin:20px 0}li[data-v-482b8d4c]{margin:10px 0}.server-form[data-v-41c22c26]{max-width:600px;margin:0 auto}.servers-container[data-v-60870d5a]{padding:20px}.page-header[data-v-60870d5a]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.empty-state[data-v-60870d5a]{margin:40px 0;text-align:center}.batch-actions[data-v-60870d5a]{margin-top:20px}.status-container[data-v-60870d5a]{display:flex;align-items:center}.refresh-button[data-v-60870d5a]{margin-left:8px}.operation-buttons[data-v-60870d5a]{display:flex;flex-wrap:wrap;gap:5px}.status-time[data-v-60870d5a]{font-size:12px;color:#909399;margin-top:5px}.count-badge[data-v-60870d5a]{margin-left:3px}@keyframes highlight-row-60870d5a{0%{background-color:transparent}50%{background-color:rgba(255,230,0,.2)}to{background-color:transparent}}[data-v-60870d5a] .el-table__row.status-changed{animation:highlight-row-60870d5a 2s ease}.sync-warning[data-v-60870d5a]{margin-top:5px;text-align:center}.dialog-footer[data-v-60870d5a]{display:flex;justify-content:flex-end}.mobile-footer[data-v-60870d5a]{flex-direction:column;gap:10px}.mobile-footer .el-button[data-v-60870d5a]{margin-left:0!important;margin-top:5px}.mobile-server-cards[data-v-60870d5a]{margin:10px 0;display:flex;flex-direction:column;gap:15px}.mobile-server-card[data-v-60870d5a]{width:100%;margin-bottom:10px}.mobile-card-header[data-v-60870d5a]{display:flex;align-items:center;flex-wrap:wrap;gap:10px}.server-name[data-v-60870d5a]{font-weight:700;flex:1}.server-info[data-v-60870d5a]{margin:10px 0}.server-info p[data-v-60870d5a]{margin:5px 0;line-height:1.5}.mobile-operation-buttons[data-v-60870d5a]{display:flex;justify-content:space-around;flex-wrap:wrap;gap:8px;margin-top:15px;padding-top:10px;border-top:1px solid #ebeef5}.mobile-error-reason[data-v-60870d5a]{margin:10px 0;padding:8px;background-color:#fef0f0;border-radius:4px;color:#f56c6c;font-size:12px}.batch-buttons[data-v-60870d5a]{display:flex;gap:10px}.batch-button[data-v-60870d5a]{display:flex;align-items:center}.count-badge[data-v-60870d5a]{font-size:12px;margin-left:5px;background-color:hsla(0,0%,100%,.2);padding:2px 6px;border-radius:10px;display:inline-block}.mobile-batch-buttons[data-v-60870d5a]{flex-direction:column;gap:0}.mobile-batch-buttons .el-button[data-v-60870d5a]{margin-bottom:10px!important;margin-left:0!important;width:100%;display:flex;align-items:center;justify-content:flex-start;padding:12px 15px;border-radius:4px;height:auto;line-height:1.5}.mobile-batch-buttons .el-button[data-v-60870d5a]:last-child{margin-bottom:0!important}.mobile-batch-buttons .button-text[data-v-60870d5a]{flex:1;text-align:center;font-size:14px}.mobile-batch-buttons .el-button [class^=el-icon-][data-v-60870d5a]{margin-right:10px;font-size:16px}@media screen and (max-width:768px){.servers-container[data-v-60870d5a]{padding:10px}.page-header[data-v-60870d5a]{flex-direction:column;align-items:flex-start;gap:10px}.page-header h1[data-v-60870d5a]{margin-bottom:10px}.operation-buttons[data-v-60870d5a]{flex-direction:column;width:100%}.batch-actions[data-v-60870d5a]{margin-top:15px;margin-bottom:15px}[data-v-60870d5a] .server-dialog .el-dialog__body{padding:15px 10px}[data-v-60870d5a] .server-dialog .el-dialog{margin:5vh auto!important}[data-v-60870d5a] .server-dialog .el-form-item{margin-bottom:15px}[data-v-60870d5a] .server-dialog .dialog-footer{display:flex;flex-direction:column;gap:10px}[data-v-60870d5a] .server-dialog .el-button{width:100%;margin-left:0!important;margin-top:5px}}@media screen and (max-width:375px){.mobile-operation-buttons[data-v-60870d5a]{flex-wrap:wrap;justify-content:center}.mobile-operation-buttons .el-button[data-v-60870d5a]{margin:4px}.mobile-card-header[data-v-60870d5a]{flex-direction:column;align-items:flex-start}.mobile-card-header .el-tag[data-v-60870d5a]{margin-top:5px}}.deploy-terminal{margin:20px 0;border-radius:6px;overflow:hidden;border:1px solid #dcdfe6;background-color:#1e1e1e;color:#f0f0f0}.terminal-header{background-color:#2c2c2c;padding:8px 12px;justify-content:space-between;border-bottom:1px solid #3e3e3e}.terminal-body{max-height:400px;overflow-y:auto;font-family:Courier New,monospace}.log-line{margin:2px 0;white-space:pre-wrap;word-break:break-all}.log-line pre{margin:0;white-space:pre-wrap;font-family:Courier New,monospace}.error-line{color:#f56c6c}.success-line{color:#67c23a}.terminal-cursor{display:inline-block;width:8px;height:16px;background-color:#f0f0f0;animation:blink 1s infinite;vertical-align:middle}@keyframes blink{0%,to{opacity:1}50%{opacity:0}}.terminal-footer{padding:10px;background-color:#2c2c2c;border-top:1px solid #3e3e3e}.rules-container{padding:20px}.page-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.server-info{padding:15px;margin-bottom:20px}.server-info,.server-offline{background-color:#f5f7fa;border-radius:4px}.server-offline{text-align:center;margin:30px 0;padding:20px}.server-offline i{font-size:48px;color:#e6a23c;margin-bottom:15px}.offline-actions{display:flex;justify-content:center;margin-top:20px;gap:10px}.output{overflow-x:auto}.ip-manage-result,.output{padding:10px;background-color:#f5f7fa;border-radius:4px;font-family:monospace;white-space:pre-wrap}.ip-manage-result{max-height:200px;overflow-y:auto;margin-top:10px}.ip-manage-result pre{white-space:pre-wrap;margin:0}.form-item-tip{margin-left:10px;color:#909399;font-size:12px}.script-deploy-needed{margin:20px 0}.deploy-container{display:flex;align-items:center;justify-content:space-between;background-color:#f5f7fa;padding:20px;border-radius:4px}.deploy-intro{max-width:70%}.deploy-intro i{font-size:24px;color:#e6a23c;margin-bottom:10px}.loading-container{margin:30px 0}.loading-content{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:30px}.loading-content i{font-size:32px;color:#409eff}.ip-lists-dialog{display:flex;flex-direction:column;max-height:90vh;margin-top:0!important}.ip-lists-dialog .el-dialog__body{padding:15px 20px;overflow-y:auto}.ip-lists-dialog .el-dialog__header{padding:15px}.ip-lists-dialog .el-dialog__footer{padding:10px 20px 15px}.ip-tab-nav{display:flex;border-bottom:2px solid #ebeef5;margin-bottom:20px;flex-wrap:wrap}.ip-tab-item{padding:0 15px;height:40px;line-height:40px;cursor:pointer;transition:all .3s;text-align:center;font-size:14px;position:relative;white-space:nowrap}.ip-tab-item.active{color:#409eff;font-weight:700}.ip-tab-item.active:after{content:"";position:absolute;bottom:-2px;left:0;width:100%;height:2px;background-color:#409eff}.ip-form-wrapper{padding:0 5px}.form-group{margin-bottom:20px}.form-group label{display:block;margin-bottom:8px;font-weight:500;font-size:14px;color:#606266}.input-with-tip{display:flex;align-items:center}.form-tip{margin-left:10px;color:#909399;font-size:12px}.action-button{width:100%;margin-top:10px;height:40px;font-size:14px}.full-width{width:100%}.el-dialog{margin:15vh auto!important}@media screen and (max-width:768px){.mobile-tab-nav{flex-wrap:wrap;justify-content:space-between}.mobile-tab-nav .ip-tab-item{flex:1;min-width:45%;padding:0 5px;font-size:13px;margin-bottom:5px}.input-with-tip{flex-direction:column;align-items:flex-start}.form-tip{margin-left:0;margin-top:5px}.action-button{height:40px;font-size:15px}.form-group{margin-bottom:15px}}.login-container[data-v-4a046720]{display:flex;justify-content:center;align-items:center;height:100vh;background-color:#f5f7fa}.login-card[data-v-4a046720]{width:400px}.login-card h2[data-v-4a046720]{text-align:center;margin:0;color:#409eff}.login-tip[data-v-4a046720]{text-align:center;margin-top:10px;color:#909399}.el-form[data-v-94470f58]{max-width:500px}.profile-container[data-v-57c75397]{padding:20px}.page-header[data-v-57c75397]{margin-bottom:20px}.header-content[data-v-57c75397]{display:flex;justify-content:space-between;align-items:center}.password-card[data-v-57c75397],.profile-card[data-v-57c75397]{margin-bottom:20px}.profile-info[data-v-57c75397]{line-height:1.8} -------------------------------------------------------------------------------- /client/src/store/modules/servers.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const API_URL = '/api/servers'; 4 | 5 | const state = { 6 | servers: [], 7 | loading: false, 8 | error: null 9 | }; 10 | 11 | const getters = { 12 | getAllServers: state => state.servers, 13 | getServerById: state => id => state.servers.find(server => server._id === id), 14 | getLoading: state => state.loading, 15 | getError: state => state.error 16 | }; 17 | 18 | const actions = { 19 | async getAllServers({ commit }) { 20 | commit('setLoading', true); 21 | commit('setError', null); 22 | 23 | try { 24 | const response = await axios.get(API_URL); 25 | commit('setServers', response.data.data); 26 | return response.data; 27 | } catch (error) { 28 | commit('setError', error.response ? error.response.data.message : error.message); 29 | throw error; 30 | } finally { 31 | commit('setLoading', false); 32 | } 33 | }, 34 | 35 | async getServer({ commit }, id) { 36 | commit('setLoading', true); 37 | commit('setError', null); 38 | 39 | try { 40 | const response = await axios.get(`${API_URL}/${id}`); 41 | return response.data; 42 | } catch (error) { 43 | commit('setError', error.response ? error.response.data.message : error.message); 44 | throw error; 45 | } finally { 46 | commit('setLoading', false); 47 | } 48 | }, 49 | 50 | async createServer({ commit, dispatch }, serverData) { 51 | commit('setLoading', true); 52 | commit('setError', null); 53 | 54 | try { 55 | const response = await axios.post(API_URL, serverData); 56 | await dispatch('getAllServers'); 57 | return response.data; 58 | } catch (error) { 59 | commit('setError', error.response ? error.response.data.message : error.message); 60 | throw error; 61 | } finally { 62 | commit('setLoading', false); 63 | } 64 | }, 65 | 66 | async updateServer({ commit, dispatch }, { id, data }) { 67 | commit('setLoading', true); 68 | commit('setError', null); 69 | 70 | try { 71 | const response = await axios.put(`${API_URL}/${id}`, data); 72 | await dispatch('getAllServers'); 73 | return response.data; 74 | } catch (error) { 75 | commit('setError', error.response ? error.response.data.message : error.message); 76 | throw error; 77 | } finally { 78 | commit('setLoading', false); 79 | } 80 | }, 81 | 82 | async deleteServer({ commit, dispatch }, id) { 83 | commit('setLoading', true); 84 | commit('setError', null); 85 | 86 | try { 87 | const response = await axios.delete(`${API_URL}/${id}`); 88 | await dispatch('getAllServers'); 89 | return response.data; 90 | } catch (error) { 91 | commit('setError', error.response ? error.response.data.message : error.message); 92 | throw error; 93 | } finally { 94 | commit('setLoading', false); 95 | } 96 | }, 97 | 98 | async connectServer({ commit, dispatch }, id) { 99 | commit('setLoading', true); 100 | commit('setError', null); 101 | 102 | try { 103 | const response = await axios.post(`${API_URL}/${id}/connect`); 104 | if (response.data && response.data.serverStatus) { 105 | commit('updateServerStatus', { 106 | id, 107 | status: response.data.serverStatus, 108 | lastCheck: new Date().toISOString() 109 | }); 110 | } else { 111 | await dispatch('getAllServers'); 112 | } 113 | return response.data; 114 | } catch (error) { 115 | commit('setError', error.response ? error.response.data.message : error.message); 116 | throw error; 117 | } finally { 118 | commit('setLoading', false); 119 | } 120 | }, 121 | 122 | async disconnectServer({ commit, dispatch }, id) { 123 | commit('setLoading', true); 124 | commit('setError', null); 125 | 126 | try { 127 | const response = await axios.post(`${API_URL}/${id}/disconnect`); 128 | if (response.data && response.data.serverStatus) { 129 | commit('updateServerStatus', { 130 | id, 131 | status: response.data.serverStatus, 132 | lastCheck: new Date().toISOString() 133 | }); 134 | } else { 135 | await dispatch('getAllServers'); 136 | } 137 | return response.data; 138 | } catch (error) { 139 | commit('setError', error.response ? error.response.data.message : error.message); 140 | throw error; 141 | } finally { 142 | commit('setLoading', false); 143 | } 144 | }, 145 | 146 | async checkStatus({ commit }, id) { 147 | commit('setError', null); 148 | 149 | try { 150 | const response = await axios.get(`${API_URL}/${id}/status`); 151 | 152 | // 处理连接套接字正常但状态未知的情况 153 | if (response.data && response.data.data) { 154 | // 检查日志信息 155 | if (response.data.logs && 156 | (response.data.logs.includes('连接套接字正常') || 157 | response.data.logs.includes('SSH连接已就绪') || 158 | response.data.logs.includes('SSH连接建立成功'))) { 159 | // 覆盖状态为online 160 | response.data.data.status = 'online'; 161 | response.data.data.backendConnected = true; 162 | } 163 | 164 | // 更新服务器状态 165 | if (response.data.data.status) { 166 | commit('updateServerStatus', { 167 | id, 168 | status: response.data.data.status, 169 | lastCheck: new Date().toISOString(), 170 | backendConnected: response.data.data.backendConnected || false 171 | }); 172 | } 173 | } 174 | 175 | return response.data; 176 | } catch (error) { 177 | commit('setError', error.response ? error.response.data.message : error.message); 178 | throw error; 179 | } 180 | }, 181 | 182 | // 测试服务器连接 183 | async testConnection({ commit }, serverData) { 184 | commit('setLoading', true); 185 | commit('setError', null); 186 | 187 | try { 188 | const response = await axios.post(`${API_URL}/test-connection`, serverData); 189 | return response.data; 190 | } catch (error) { 191 | commit('setError', error.response ? error.response.data.message : error.message); 192 | throw error; 193 | } finally { 194 | commit('setLoading', false); 195 | } 196 | }, 197 | 198 | async executeCommand({ commit }, { serverId, command }) { 199 | commit('setLoading', true); 200 | commit('setError', null); 201 | 202 | try { 203 | const response = await axios.post(`${API_URL}/${serverId}/execute`, { command }); 204 | return response.data; 205 | } catch (error) { 206 | commit('setError', error.response ? error.response.data.message : error.message); 207 | throw error; 208 | } finally { 209 | commit('setLoading', false); 210 | } 211 | }, 212 | 213 | async deployIptato({ commit, dispatch }, id) { 214 | commit('setLoading', true); 215 | commit('setError', null); 216 | 217 | try { 218 | const response = await axios.post(`${API_URL}/${id}/deploy`); 219 | return response.data; 220 | } catch (error) { 221 | commit('setError', error.response ? error.response.data.message : error.message); 222 | throw error; 223 | } finally { 224 | commit('setLoading', false); 225 | } 226 | }, 227 | 228 | async getServerLogs({ commit }, id) { 229 | commit('setError', null); 230 | 231 | try { 232 | const response = await axios.get(`${API_URL}/${id}/logs`); 233 | return response.data; 234 | } catch (error) { 235 | commit('setError', error.response ? error.response.data.message : error.message); 236 | throw error; 237 | } 238 | }, 239 | 240 | /** 241 | * 检查服务器上是否已部署Nftato脚本 242 | */ 243 | async checkScriptExists({ commit }, id) { 244 | commit('setError', null); 245 | 246 | try { 247 | const response = await axios.get(`${API_URL}/${id}/checkScript`); 248 | return response.data; 249 | } catch (error) { 250 | commit('setError', error.response ? error.response.data.message : error.message); 251 | throw error; 252 | } 253 | }, 254 | 255 | /** 256 | * 使用WebSocket部署Nftato脚本 257 | */ 258 | async deployIptatoWithWebSocket({ commit }, id) { 259 | commit('setLoading', true); 260 | commit('setError', null); 261 | 262 | try { 263 | // 调用部署API,指示使用WebSocket 264 | const response = await axios.post(`${API_URL}/${id}/deploy`, { useWebSocket: true }); 265 | return response.data; 266 | } catch (error) { 267 | commit('setError', error.response ? error.response.data.message : error.message); 268 | throw error; 269 | } finally { 270 | commit('setLoading', false); 271 | } 272 | } 273 | }; 274 | 275 | const mutations = { 276 | setServers(state, servers) { 277 | state.servers = servers; 278 | }, 279 | setLoading(state, loading) { 280 | state.loading = loading; 281 | }, 282 | setError(state, error) { 283 | state.error = error; 284 | }, 285 | updateServerStatus(state, { id, status, lastCheck, backendConnected }) { 286 | const server = state.servers.find(s => s._id === id); 287 | if (server) { 288 | server.status = status; 289 | server.lastCheck = lastCheck; 290 | server.backendConnected = backendConnected; 291 | } 292 | } 293 | }; 294 | 295 | export default { 296 | namespaced: true, 297 | state, 298 | getters, 299 | actions, 300 | mutations 301 | }; -------------------------------------------------------------------------------- /client/src/store/modules/rules.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const API_URL = '/api/rules'; 4 | 5 | const state = { 6 | loading: false, 7 | error: null 8 | }; 9 | 10 | const getters = { 11 | getLoading: state => state.loading, 12 | getError: state => state.error 13 | }; 14 | 15 | const actions = { 16 | // 获取服务器规则缓存 17 | async getServerCache({ commit }, serverId) { 18 | commit('setLoading', true); 19 | commit('setError', null); 20 | 21 | try { 22 | const response = await axios.get(`${API_URL}/${serverId}/cache`); 23 | return response.data; 24 | } catch (error) { 25 | // 如果是404错误,说明缓存不存在,这不是错误 26 | if (error.response && error.response.status === 404) { 27 | return { success: false, error: '缓存不存在' }; 28 | } 29 | commit('setError', error.response ? error.response.data.message : error.message); 30 | throw error; 31 | } finally { 32 | commit('setLoading', false); 33 | } 34 | }, 35 | 36 | // 获取缓存最后更新时间 37 | async getCacheLastUpdate({ commit }, serverId) { 38 | commit('setLoading', true); 39 | commit('setError', null); 40 | 41 | try { 42 | const response = await axios.get(`${API_URL}/${serverId}/cache/last-update`); 43 | return response.data; 44 | } catch (error) { 45 | // 如果是404错误,说明缓存不存在,这不是错误 46 | if (error.response && error.response.status === 404) { 47 | return { success: false, error: '缓存不存在' }; 48 | } 49 | commit('setError', error.response ? error.response.data.message : error.message); 50 | throw error; 51 | } finally { 52 | commit('setLoading', false); 53 | } 54 | }, 55 | 56 | // 清除服务器规则缓存 57 | async clearServerCache({ commit }, serverId) { 58 | commit('setLoading', true); 59 | commit('setError', null); 60 | 61 | try { 62 | const response = await axios.delete(`${API_URL}/${serverId}/cache`); 63 | return response.data; 64 | } catch (error) { 65 | commit('setError', error.response ? error.response.data.message : error.message); 66 | throw error; 67 | } finally { 68 | commit('setLoading', false); 69 | } 70 | }, 71 | 72 | // 更新服务器缓存项 73 | async updateCacheItem({ commit }, { serverId, key, value }) { 74 | commit('setLoading', true); 75 | commit('setError', null); 76 | 77 | try { 78 | const response = await axios.put(`${API_URL}/${serverId}/cache/${key}`, { value }); 79 | return response.data; 80 | } catch (error) { 81 | commit('setError', error.response ? error.response.data.message : error.message); 82 | throw error; 83 | } finally { 84 | commit('setLoading', false); 85 | } 86 | }, 87 | 88 | // 获取封禁列表 89 | async getBlockList({ commit }, serverId) { 90 | commit('setLoading', true); 91 | commit('setError', null); 92 | 93 | try { 94 | const response = await axios.get(`${API_URL}/${serverId}/blocklist`); 95 | return response.data; 96 | } catch (error) { 97 | commit('setError', error.response ? error.response.data.message : error.message); 98 | throw error; 99 | } finally { 100 | commit('setLoading', false); 101 | } 102 | }, 103 | 104 | // 封禁SPAM 105 | async blockSPAMAction({ commit }, serverId) { 106 | commit('setLoading', true); 107 | commit('setError', null); 108 | 109 | try { 110 | const response = await axios.post(`${API_URL}/${serverId}/block/spam`); 111 | return response.data; 112 | } catch (error) { 113 | commit('setError', error.response ? error.response.data.message : error.message); 114 | throw error; 115 | } finally { 116 | commit('setLoading', false); 117 | } 118 | }, 119 | 120 | 121 | // 封禁自定义端口 122 | async blockCustomPortsAction({ commit }, { serverId, ports }) { 123 | commit('setLoading', true); 124 | commit('setError', null); 125 | 126 | try { 127 | const response = await axios.post(`${API_URL}/${serverId}/block/ports`, { ports }); 128 | return response.data; 129 | } catch (error) { 130 | commit('setError', error.response ? error.response.data.message : error.message); 131 | throw error; 132 | } finally { 133 | commit('setLoading', false); 134 | } 135 | }, 136 | 137 | 138 | // 解封SPAM 139 | async unblockSPAMAction({ commit }, serverId) { 140 | commit('setLoading', true); 141 | commit('setError', null); 142 | 143 | try { 144 | const response = await axios.post(`${API_URL}/${serverId}/unblock/spam`); 145 | return response.data; 146 | } catch (error) { 147 | commit('setError', error.response ? error.response.data.message : error.message); 148 | throw error; 149 | } finally { 150 | commit('setLoading', false); 151 | } 152 | }, 153 | 154 | // 解封自定义端口 155 | async unblockCustomPortsAction({ commit }, { serverId, ports }) { 156 | commit('setLoading', true); 157 | commit('setError', null); 158 | 159 | try { 160 | const response = await axios.post(`${API_URL}/${serverId}/unblock/ports`, { ports }); 161 | return response.data; 162 | } catch (error) { 163 | commit('setError', error.response ? error.response.data.message : error.message); 164 | throw error; 165 | } finally { 166 | commit('setLoading', false); 167 | } 168 | }, 169 | 170 | // 获取当前放行的入网端口 171 | async getInboundPorts({ commit }, serverId) { 172 | commit('setLoading', true); 173 | commit('setError', null); 174 | 175 | try { 176 | const response = await axios.get(`${API_URL}/${serverId}/inbound/ports`); 177 | return response.data; 178 | } catch (error) { 179 | commit('setError', error.response ? error.response.data.message : error.message); 180 | throw error; 181 | } finally { 182 | commit('setLoading', false); 183 | } 184 | }, 185 | 186 | // 获取当前放行的入网IP 187 | async getInboundIPs({ commit }, serverId) { 188 | commit('setLoading', true); 189 | commit('setError', null); 190 | 191 | try { 192 | const response = await axios.get(`${API_URL}/${serverId}/inbound/ips`); 193 | return response.data; 194 | } catch (error) { 195 | commit('setError', error.response ? error.response.data.message : error.message); 196 | throw error; 197 | } finally { 198 | commit('setLoading', false); 199 | } 200 | }, 201 | 202 | // 放行入网端口 203 | async allowInboundPortsAction({ commit }, { serverId, ports }) { 204 | commit('setLoading', true); 205 | commit('setError', null); 206 | 207 | try { 208 | const response = await axios.post(`${API_URL}/${serverId}/inbound/allow/ports`, { ports }); 209 | return response.data; 210 | } catch (error) { 211 | commit('setError', error.response ? error.response.data.message : error.message); 212 | throw error; 213 | } finally { 214 | commit('setLoading', false); 215 | } 216 | }, 217 | 218 | // 取消放行入网端口 219 | async disallowInboundPortsAction({ commit }, { serverId, ports }) { 220 | commit('setLoading', true); 221 | commit('setError', null); 222 | 223 | try { 224 | const response = await axios.post(`${API_URL}/${serverId}/inbound/disallow/ports`, { ports }); 225 | return response.data; 226 | } catch (error) { 227 | commit('setError', error.response ? error.response.data.message : error.message); 228 | throw error; 229 | } finally { 230 | commit('setLoading', false); 231 | } 232 | }, 233 | 234 | // 放行入网IP 235 | async allowInboundIPsAction({ commit }, { serverId, ips }) { 236 | commit('setLoading', true); 237 | commit('setError', null); 238 | 239 | try { 240 | const response = await axios.post(`${API_URL}/${serverId}/inbound/allow/ips`, { ips }); 241 | return response.data; 242 | } catch (error) { 243 | commit('setError', error.response ? error.response.data.message : error.message); 244 | throw error; 245 | } finally { 246 | commit('setLoading', false); 247 | } 248 | }, 249 | 250 | // 取消放行入网IP 251 | async disallowInboundIPsAction({ commit }, { serverId, ips }) { 252 | commit('setLoading', true); 253 | commit('setError', null); 254 | 255 | try { 256 | const response = await axios.post(`${API_URL}/${serverId}/inbound/disallow/ips`, { ips }); 257 | return response.data; 258 | } catch (error) { 259 | commit('setError', error.response ? error.response.data.message : error.message); 260 | throw error; 261 | } finally { 262 | commit('setLoading', false); 263 | } 264 | }, 265 | 266 | // 获取SSH端口 267 | async getSSHPort({ commit }, serverId) { 268 | commit('setLoading', true); 269 | commit('setError', null); 270 | 271 | try { 272 | const response = await axios.get(`${API_URL}/${serverId}/ssh-port`); 273 | return response.data; 274 | } catch (error) { 275 | commit('setError', error.response ? error.response.data.message : error.message); 276 | throw error; 277 | } finally { 278 | commit('setLoading', false); 279 | } 280 | }, 281 | 282 | // 清空所有规则 283 | async clearAllRulesAction({ commit }, serverId) { 284 | commit('setLoading', true); 285 | commit('setError', null); 286 | 287 | try { 288 | const response = await axios.post(`${API_URL}/${serverId}/clear-all`); 289 | return response.data; 290 | } catch (error) { 291 | commit('setError', error.response ? error.response.data.message : error.message); 292 | throw error; 293 | } finally { 294 | commit('setLoading', false); 295 | } 296 | }, 297 | 298 | // 配置DDoS防御规则 299 | async setupDdosProtection({ commit }, serverId) { 300 | commit('setLoading', true); 301 | commit('setError', null); 302 | 303 | try { 304 | const response = await axios.post(`${API_URL}/${serverId}/ddos/protection`); 305 | return response.data; 306 | } catch (error) { 307 | commit('setError', error.response ? error.response.data.message : error.message); 308 | throw error; 309 | } finally { 310 | commit('setLoading', false); 311 | } 312 | }, 313 | 314 | // 配置自定义端口DDoS防御 315 | async setupCustomPortProtection({ commit }, { serverId, data }) { 316 | commit('setLoading', true); 317 | commit('setError', null); 318 | 319 | try { 320 | const response = await axios.post(`${API_URL}/${serverId}/ddos/custom-port`, data); 321 | return response.data; 322 | } catch (error) { 323 | commit('setError', error.response ? error.response.data.message : error.message); 324 | throw error; 325 | } finally { 326 | commit('setLoading', false); 327 | } 328 | }, 329 | 330 | // 管理IP黑白名单 331 | async manageIpLists({ commit }, { serverId, data }) { 332 | commit('setLoading', true); 333 | commit('setError', null); 334 | 335 | console.log(`[Store调试] 开始manageIpLists请求: serverId=${serverId}`, data); 336 | 337 | try { 338 | const endpoint = `${API_URL}/${serverId}/ddos/ip-lists`; 339 | console.log(`[Store调试] 请求端点: ${endpoint}`); 340 | 341 | const response = await axios.post(endpoint, data); 342 | console.log(`[Store调试] 收到响应:`, response.data); 343 | return response.data; 344 | } catch (error) { 345 | console.error(`[Store调试] 请求错误:`, error); 346 | commit('setError', error.response ? error.response.data.message : error.message); 347 | throw error; 348 | } finally { 349 | commit('setLoading', false); 350 | } 351 | }, 352 | 353 | // 查看当前防御状态 354 | async getDefenseStatus({ commit }, serverId) { 355 | commit('setLoading', true); 356 | commit('setError', null); 357 | 358 | try { 359 | const response = await axios.get(`${API_URL}/${serverId}/ddos/status`); 360 | return response.data; 361 | } catch (error) { 362 | commit('setError', error.response ? error.response.data.message : error.message); 363 | throw error; 364 | } finally { 365 | commit('setLoading', false); 366 | } 367 | } 368 | }; 369 | 370 | const mutations = { 371 | setLoading(state, loading) { 372 | state.loading = loading; 373 | }, 374 | setError(state, error) { 375 | state.error = error; 376 | } 377 | }; 378 | 379 | export default { 380 | namespaced: true, 381 | state, 382 | getters, 383 | actions, 384 | mutations 385 | }; -------------------------------------------------------------------------------- /server/controllers/rulesController.js: -------------------------------------------------------------------------------- 1 | const nftablesService = require('../services/nftablesService'); 2 | const sshService = require('../services/sshService'); 3 | const cacheService = require('../services/cacheService'); 4 | 5 | /** 6 | * 检查服务器连接状态 7 | * @param {string} serverId - 服务器ID 8 | * @returns {object} - 连接状态 9 | */ 10 | const checkServerConnection = (serverId) => { 11 | const connection = sshService.connections[serverId]; 12 | if (!connection) { 13 | return { 14 | connected: false, 15 | message: '服务器未连接,请先连接服务器' 16 | }; 17 | } 18 | return { connected: true }; 19 | }; 20 | 21 | /** 22 | * 获取服务器规则缓存 23 | */ 24 | exports.getServerRulesCache = async (req, res) => { 25 | try { 26 | const serverId = req.params.serverId; 27 | const cache = await cacheService.getServerRulesCache(serverId); 28 | 29 | if (!cache) { 30 | return res.status(404).json({ 31 | success: false, 32 | message: '服务器规则缓存不存在' 33 | }); 34 | } 35 | 36 | res.status(200).json({ 37 | success: true, 38 | data: cache 39 | }); 40 | } catch (error) { 41 | res.status(500).json({ 42 | success: false, 43 | message: '获取服务器规则缓存失败', 44 | error: error.message 45 | }); 46 | } 47 | }; 48 | 49 | /** 50 | * 获取缓存最后更新时间 51 | */ 52 | exports.getCacheLastUpdate = async (req, res) => { 53 | try { 54 | const serverId = req.params.serverId; 55 | const lastUpdate = await cacheService.getServerCacheLastUpdate(serverId); 56 | 57 | if (!lastUpdate) { 58 | return res.status(404).json({ 59 | success: false, 60 | message: '服务器规则缓存不存在' 61 | }); 62 | } 63 | 64 | res.status(200).json({ 65 | success: true, 66 | data: { lastUpdate } 67 | }); 68 | } catch (error) { 69 | res.status(500).json({ 70 | success: false, 71 | message: '获取缓存最后更新时间失败', 72 | error: error.message 73 | }); 74 | } 75 | }; 76 | 77 | /** 78 | * 获取当前封禁列表 (修改以支持缓存) 79 | */ 80 | exports.getBlockList = async (req, res) => { 81 | try { 82 | const serverId = req.params.serverId; 83 | 84 | const result = await nftablesService.getBlockList(serverId); 85 | 86 | if (result.success) { 87 | // 如果请求成功,更新缓存 88 | await cacheService.updateServerCacheItem(serverId, 'blockList', result.data); 89 | } 90 | 91 | res.status(result.success ? 200 : 400).json({ 92 | success: result.success, 93 | data: result.data, 94 | error: result.error 95 | }); 96 | } catch (error) { 97 | res.status(500).json({ 98 | success: false, 99 | message: '获取封禁列表失败', 100 | error: error.message 101 | }); 102 | } 103 | }; 104 | 105 | /** 106 | * 封禁BT/PT协议 107 | */ 108 | exports.blockBTPT = async (req, res) => { 109 | try { 110 | const result = await nftablesService.blockBTPT(req.params.serverId); 111 | 112 | res.status(result.success ? 200 : 400).json({ 113 | success: result.success, 114 | data: result.data, 115 | error: result.error 116 | }); 117 | } catch (error) { 118 | res.status(500).json({ 119 | success: false, 120 | message: '封禁BT/PT协议失败', 121 | error: error.message 122 | }); 123 | } 124 | }; 125 | 126 | /** 127 | * 封禁垃圾邮件端口 128 | */ 129 | exports.blockSPAM = async (req, res) => { 130 | try { 131 | const result = await nftablesService.blockSPAM(req.params.serverId); 132 | 133 | res.status(result.success ? 200 : 400).json({ 134 | success: result.success, 135 | data: result.data, 136 | error: result.error 137 | }); 138 | } catch (error) { 139 | res.status(500).json({ 140 | success: false, 141 | message: '封禁垃圾邮件端口失败', 142 | error: error.message 143 | }); 144 | } 145 | }; 146 | 147 | /** 148 | * 封禁BT/PT和垃圾邮件 149 | */ 150 | exports.blockAll = async (req, res) => { 151 | try { 152 | const result = await nftablesService.blockAll(req.params.serverId); 153 | 154 | res.status(result.success ? 200 : 400).json({ 155 | success: result.success, 156 | data: result.data, 157 | error: result.error 158 | }); 159 | } catch (error) { 160 | res.status(500).json({ 161 | success: false, 162 | message: '封禁BT/PT和垃圾邮件失败', 163 | error: error.message 164 | }); 165 | } 166 | }; 167 | 168 | /** 169 | * 封禁自定义端口 170 | */ 171 | exports.blockCustomPorts = async (req, res) => { 172 | try { 173 | const { ports } = req.body; 174 | 175 | if (!ports) { 176 | return res.status(400).json({ 177 | success: false, 178 | message: '端口不能为空' 179 | }); 180 | } 181 | 182 | const result = await nftablesService.blockCustomPorts(req.params.serverId, ports); 183 | 184 | res.status(result.success ? 200 : 400).json({ 185 | success: result.success, 186 | data: result.data, 187 | error: result.error 188 | }); 189 | } catch (error) { 190 | res.status(500).json({ 191 | success: false, 192 | message: '封禁自定义端口失败', 193 | error: error.message 194 | }); 195 | } 196 | }; 197 | 198 | /** 199 | * 封禁自定义关键词 200 | */ 201 | exports.blockCustomKeyword = async (req, res) => { 202 | try { 203 | const { keyword } = req.body; 204 | 205 | if (!keyword) { 206 | return res.status(400).json({ 207 | success: false, 208 | message: '关键词不能为空' 209 | }); 210 | } 211 | 212 | const result = await nftablesService.blockCustomKeyword(req.params.serverId, keyword); 213 | 214 | res.status(result.success ? 200 : 400).json({ 215 | success: result.success, 216 | data: result.data, 217 | error: result.error 218 | }); 219 | } catch (error) { 220 | res.status(500).json({ 221 | success: false, 222 | message: '封禁自定义关键词失败', 223 | error: error.message 224 | }); 225 | } 226 | }; 227 | 228 | /** 229 | * 解封BT/PT协议 230 | */ 231 | exports.unblockBTPT = async (req, res) => { 232 | try { 233 | const result = await nftablesService.unblockBTPT(req.params.serverId); 234 | 235 | res.status(result.success ? 200 : 400).json({ 236 | success: result.success, 237 | data: result.data, 238 | error: result.error 239 | }); 240 | } catch (error) { 241 | res.status(500).json({ 242 | success: false, 243 | message: '解封BT/PT协议失败', 244 | error: error.message 245 | }); 246 | } 247 | }; 248 | 249 | /** 250 | * 解封垃圾邮件端口 251 | */ 252 | exports.unblockSPAM = async (req, res) => { 253 | try { 254 | const result = await nftablesService.unblockSPAM(req.params.serverId); 255 | 256 | res.status(result.success ? 200 : 400).json({ 257 | success: result.success, 258 | data: result.data, 259 | error: result.error 260 | }); 261 | } catch (error) { 262 | res.status(500).json({ 263 | success: false, 264 | message: '解封垃圾邮件端口失败', 265 | error: error.message 266 | }); 267 | } 268 | }; 269 | 270 | /** 271 | * 解封BT/PT和垃圾邮件 272 | */ 273 | exports.unblockAll = async (req, res) => { 274 | try { 275 | const result = await nftablesService.unblockAll(req.params.serverId); 276 | 277 | res.status(result.success ? 200 : 400).json({ 278 | success: result.success, 279 | data: result.data, 280 | error: result.error 281 | }); 282 | } catch (error) { 283 | res.status(500).json({ 284 | success: false, 285 | message: '解封BT/PT和垃圾邮件失败', 286 | error: error.message 287 | }); 288 | } 289 | }; 290 | 291 | /** 292 | * 解封自定义端口 293 | */ 294 | exports.unblockCustomPorts = async (req, res) => { 295 | try { 296 | const { ports } = req.body; 297 | 298 | if (!ports) { 299 | return res.status(400).json({ 300 | success: false, 301 | message: '端口不能为空' 302 | }); 303 | } 304 | 305 | const result = await nftablesService.unblockCustomPorts(req.params.serverId, ports); 306 | 307 | res.status(result.success ? 200 : 400).json({ 308 | success: result.success, 309 | data: result.data, 310 | error: result.error 311 | }); 312 | } catch (error) { 313 | res.status(500).json({ 314 | success: false, 315 | message: '解封自定义端口失败', 316 | error: error.message 317 | }); 318 | } 319 | }; 320 | 321 | /** 322 | * 解封自定义关键词 323 | */ 324 | exports.unblockCustomKeyword = async (req, res) => { 325 | try { 326 | const { keyword } = req.body; 327 | 328 | if (!keyword) { 329 | return res.status(400).json({ 330 | success: false, 331 | message: '关键词不能为空' 332 | }); 333 | } 334 | 335 | const result = await nftablesService.unblockCustomKeyword(req.params.serverId, keyword); 336 | 337 | res.status(result.success ? 200 : 400).json({ 338 | success: result.success, 339 | data: result.data, 340 | error: result.error 341 | }); 342 | } catch (error) { 343 | res.status(500).json({ 344 | success: false, 345 | message: '解封自定义关键词失败', 346 | error: error.message 347 | }); 348 | } 349 | }; 350 | 351 | /** 352 | * 解封所有关键词 353 | */ 354 | exports.unblockAllKeywords = async (req, res) => { 355 | try { 356 | const result = await nftablesService.unblockAllKeywords(req.params.serverId); 357 | 358 | res.status(result.success ? 200 : 400).json({ 359 | success: result.success, 360 | data: result.data, 361 | error: result.error 362 | }); 363 | } catch (error) { 364 | res.status(500).json({ 365 | success: false, 366 | message: '解封所有关键词失败', 367 | error: error.message 368 | }); 369 | } 370 | }; 371 | 372 | /** 373 | * 获取当前放行的入网端口 374 | */ 375 | exports.getInboundPorts = async (req, res) => { 376 | try { 377 | const serverId = req.params.serverId; 378 | const result = await nftablesService.getInboundPorts(serverId); 379 | 380 | if (result.success) { 381 | // 如果请求成功,确保缓存原始格式的数据 382 | await cacheService.updateServerCacheItem(serverId, 'inboundPorts', result.data); 383 | } 384 | 385 | res.status(result.success ? 200 : 400).json({ 386 | success: result.success, 387 | data: result.data, 388 | error: result.error 389 | }); 390 | } catch (error) { 391 | res.status(500).json({ 392 | success: false, 393 | message: '获取入网端口失败', 394 | error: error.message 395 | }); 396 | } 397 | }; 398 | 399 | /** 400 | * 获取当前放行的入网IP (修改以支持缓存) 401 | */ 402 | exports.getInboundIPs = async (req, res) => { 403 | try { 404 | const serverId = req.params.serverId; 405 | const result = await nftablesService.getInboundIPs(serverId); 406 | 407 | if (result.success) { 408 | // 如果请求成功,更新缓存 409 | await cacheService.updateServerCacheItem(serverId, 'inboundIPs', result.data); 410 | } 411 | 412 | res.status(result.success ? 200 : 400).json({ 413 | success: result.success, 414 | data: result.data, 415 | error: result.error 416 | }); 417 | } catch (error) { 418 | res.status(500).json({ 419 | success: false, 420 | message: '获取入网IP失败', 421 | error: error.message 422 | }); 423 | } 424 | }; 425 | 426 | /** 427 | * 放行入网端口 428 | */ 429 | exports.allowInboundPorts = async (req, res) => { 430 | try { 431 | const { ports } = req.body; 432 | 433 | if (!ports) { 434 | return res.status(400).json({ 435 | success: false, 436 | message: '端口不能为空' 437 | }); 438 | } 439 | 440 | const result = await nftablesService.allowInboundPorts(req.params.serverId, ports); 441 | 442 | res.status(result.success ? 200 : 400).json({ 443 | success: result.success, 444 | data: result.data, 445 | error: result.error 446 | }); 447 | } catch (error) { 448 | res.status(500).json({ 449 | success: false, 450 | message: '放行入网端口失败', 451 | error: error.message 452 | }); 453 | } 454 | }; 455 | 456 | /** 457 | * 取消放行入网端口 458 | */ 459 | exports.disallowInboundPorts = async (req, res) => { 460 | try { 461 | const serverId = req.params.serverId; 462 | const { ports } = req.body; 463 | 464 | if (!ports) { 465 | return res.status(400).json({ 466 | success: false, 467 | message: '端口不能为空' 468 | }); 469 | } 470 | 471 | // 验证端口格式 472 | const portPattern = /^(\d+(-\d+)?)(,\d+(-\d+)?)*$/; 473 | if (!portPattern.test(ports)) { 474 | return res.status(400).json({ 475 | success: false, 476 | message: '端口格式无效,请使用逗号分隔的端口或端口范围,如:80,443,8080-8090' 477 | }); 478 | } 479 | 480 | const result = await nftablesService.disallowInboundPorts(serverId, ports); 481 | 482 | // 如果请求成功且涉及缓存,更新缓存 483 | if (result.success) { 484 | try { 485 | // 获取并更新入网端口缓存 486 | const portsResult = await nftablesService.getInboundPorts(serverId); 487 | if (portsResult.success) { 488 | await cacheService.updateServerCacheItem(serverId, 'inboundPorts', portsResult.data); 489 | } 490 | } catch (cacheError) { 491 | console.error(`更新缓存失败: ${cacheError.message}`); 492 | // 更新缓存失败不影响主操作的成功 493 | } 494 | } 495 | 496 | if (result.error && result.error.includes('SSH')) { 497 | // SSH端口保护错误,返回更具体的错误信息 498 | return res.status(403).json({ 499 | success: false, 500 | message: '无法取消SSH端口的放行', 501 | error: result.error 502 | }); 503 | } 504 | 505 | res.status(result.success ? 200 : 400).json({ 506 | success: result.success, 507 | data: result.data, 508 | error: result.error 509 | }); 510 | } catch (error) { 511 | res.status(500).json({ 512 | success: false, 513 | message: '取消放行入网端口失败', 514 | error: error.message 515 | }); 516 | } 517 | }; 518 | 519 | /** 520 | * 放行入网IP 521 | */ 522 | exports.allowInboundIPs = async (req, res) => { 523 | try { 524 | const { ips } = req.body; 525 | 526 | if (!ips) { 527 | return res.status(400).json({ 528 | success: false, 529 | message: 'IP不能为空' 530 | }); 531 | } 532 | 533 | const result = await nftablesService.allowInboundIPs(req.params.serverId, ips); 534 | 535 | res.status(result.success ? 200 : 400).json({ 536 | success: result.success, 537 | data: result.data, 538 | error: result.error 539 | }); 540 | } catch (error) { 541 | res.status(500).json({ 542 | success: false, 543 | message: '放行入网IP失败', 544 | error: error.message 545 | }); 546 | } 547 | }; 548 | 549 | /** 550 | * 取消放行入网IP 551 | */ 552 | exports.disallowInboundIPs = async (req, res) => { 553 | try { 554 | const { ips } = req.body; 555 | 556 | if (!ips) { 557 | return res.status(400).json({ 558 | success: false, 559 | message: 'IP不能为空' 560 | }); 561 | } 562 | 563 | const result = await nftablesService.disallowInboundIPs(req.params.serverId, ips); 564 | 565 | res.status(result.success ? 200 : 400).json({ 566 | success: result.success, 567 | data: result.data, 568 | error: result.error 569 | }); 570 | } catch (error) { 571 | res.status(500).json({ 572 | success: false, 573 | message: '取消放行入网IP失败', 574 | error: error.message 575 | }); 576 | } 577 | }; 578 | 579 | /** 580 | * 获取当前SSH端口 (修改以支持缓存) 581 | */ 582 | exports.getSSHPort = async (req, res) => { 583 | try { 584 | const serverId = req.params.serverId; 585 | const result = await nftablesService.getSSHPort(serverId); 586 | 587 | if (result.success) { 588 | // 如果请求成功,更新缓存 589 | await cacheService.updateServerCacheItem(serverId, 'sshPortStatus', result.data); 590 | } 591 | 592 | res.status(result.success ? 200 : 400).json({ 593 | success: result.success, 594 | data: result.data, 595 | error: result.error 596 | }); 597 | } catch (error) { 598 | res.status(500).json({ 599 | success: false, 600 | message: '获取SSH端口失败', 601 | error: error.message 602 | }); 603 | } 604 | }; 605 | 606 | /** 607 | * 清空所有规则 608 | */ 609 | exports.clearAllRules = async (req, res) => { 610 | try { 611 | const result = await nftablesService.clearAllRules(req.params.serverId); 612 | 613 | res.status(result.success ? 200 : 400).json({ 614 | success: result.success, 615 | data: result.data, 616 | error: result.error 617 | }); 618 | } catch (error) { 619 | res.status(500).json({ 620 | success: false, 621 | message: '清空所有规则失败', 622 | error: error.message 623 | }); 624 | } 625 | }; 626 | 627 | /** 628 | * 清除服务器规则缓存 629 | */ 630 | exports.clearServerCache = async (req, res) => { 631 | try { 632 | const serverId = req.params.serverId; 633 | const success = await cacheService.clearServerRulesCache(serverId); 634 | 635 | res.status(200).json({ 636 | success: success, 637 | message: success ? '缓存清除成功' : '缓存清除失败' 638 | }); 639 | } catch (error) { 640 | res.status(500).json({ 641 | success: false, 642 | message: '清除缓存失败', 643 | error: error.message 644 | }); 645 | } 646 | }; 647 | 648 | /** 649 | * 更新服务器缓存项 650 | */ 651 | exports.updateCacheItem = async (req, res) => { 652 | try { 653 | const serverId = req.params.serverId; 654 | const key = req.params.key; 655 | const { value } = req.body; 656 | 657 | if (!key) { 658 | return res.status(400).json({ 659 | success: false, 660 | message: '缓存键名不能为空' 661 | }); 662 | } 663 | 664 | const success = await cacheService.updateServerCacheItem(serverId, key, value); 665 | 666 | res.status(200).json({ 667 | success: success, 668 | message: success ? `缓存项 ${key} 更新成功` : `缓存项 ${key} 更新失败` 669 | }); 670 | } catch (error) { 671 | res.status(500).json({ 672 | success: false, 673 | message: '更新缓存项失败', 674 | error: error.message 675 | }); 676 | } 677 | }; 678 | 679 | /** 680 | * 配置DDoS防御规则 681 | */ 682 | exports.setupDdosProtection = async (req, res) => { 683 | try { 684 | const serverId = req.params.serverId; 685 | 686 | const result = await nftablesService.setupDdosProtection(serverId); 687 | 688 | res.status(result.success ? 200 : 400).json({ 689 | success: result.success, 690 | data: result.data, 691 | error: result.error 692 | }); 693 | } catch (error) { 694 | res.status(500).json({ 695 | success: false, 696 | message: '配置DDoS防御规则失败', 697 | error: error.message 698 | }); 699 | } 700 | }; 701 | 702 | /** 703 | * 为自定义端口配置DDoS防御 704 | */ 705 | exports.setupCustomPortProtection = async (req, res) => { 706 | try { 707 | const serverId = req.params.serverId; 708 | const { port, protoType, maxConn, maxRateMin, maxRateSec, banHours } = req.body; 709 | 710 | if (!port) { 711 | return res.status(400).json({ 712 | success: false, 713 | message: '端口不能为空' 714 | }); 715 | } 716 | 717 | const result = await nftablesService.setupCustomPortProtection( 718 | serverId, 719 | port, 720 | protoType || 1, 721 | maxConn || 400, 722 | maxRateMin || 400, 723 | maxRateSec || 300, 724 | banHours || 24 725 | ); 726 | 727 | res.status(result.success ? 200 : 400).json({ 728 | success: result.success, 729 | data: result.data, 730 | error: result.error 731 | }); 732 | } catch (error) { 733 | res.status(500).json({ 734 | success: false, 735 | message: '配置自定义端口DDoS防御失败', 736 | error: error.message 737 | }); 738 | } 739 | }; 740 | 741 | /** 742 | * 管理IP黑白名单 743 | */ 744 | exports.manageIpLists = async (req, res) => { 745 | try { 746 | const serverId = req.params.serverId; 747 | const { actionType, ip, duration } = req.body; 748 | 749 | console.log("[DEBUG] manageIpLists - 请求体:", req.body); 750 | console.log(`[DEBUG] manageIpLists - 解构参数: actionType=${actionType}, ip=${ip}, duration=${duration}`); 751 | 752 | if (!actionType || !ip) { 753 | return res.status(400).json({ 754 | success: false, 755 | message: '操作类型和IP地址不能为空' 756 | }); 757 | } 758 | 759 | const result = await nftablesService.manageIpLists(serverId, actionType, ip, duration); 760 | 761 | res.status(result.success ? 200 : 400).json({ 762 | success: result.success, 763 | data: result.data, 764 | error: result.error 765 | }); 766 | } catch (error) { 767 | res.status(500).json({ 768 | success: false, 769 | message: '管理IP黑白名单失败', 770 | error: error.message 771 | }); 772 | } 773 | }; 774 | 775 | /** 776 | * 查看当前防御状态 777 | */ 778 | exports.viewDefenseStatus = async (req, res) => { 779 | try { 780 | const serverId = req.params.serverId; 781 | 782 | const result = await nftablesService.viewDefenseStatus(serverId); 783 | 784 | // 如果请求成功,更新缓存 785 | if (result.success) { 786 | await cacheService.updateServerCacheItem(serverId, 'defenseStatus', result.data); 787 | } 788 | 789 | res.status(result.success ? 200 : 400).json({ 790 | success: result.success, 791 | data: result.data, 792 | error: result.error 793 | }); 794 | } catch (error) { 795 | res.status(500).json({ 796 | success: false, 797 | message: '查看防御状态失败', 798 | error: error.message 799 | }); 800 | } 801 | }; -------------------------------------------------------------------------------- /server/controllers/serverController.js: -------------------------------------------------------------------------------- 1 | const Server = require('../models/Server'); 2 | const sshService = require('../services/sshService'); 3 | 4 | /** 5 | * 获取所有服务器 6 | */ 7 | exports.getAllServers = async (req, res) => { 8 | try { 9 | const servers = await Server.find(); 10 | 11 | // 手动过滤敏感字段 12 | const filteredServers = servers.map(server => { 13 | const { password, privateKey, ...filtered } = server; 14 | return filtered; 15 | }); 16 | 17 | res.status(200).json({ 18 | success: true, 19 | data: filteredServers 20 | }); 21 | } catch (error) { 22 | res.status(500).json({ 23 | success: false, 24 | message: '获取服务器列表失败', 25 | error: error.message 26 | }); 27 | } 28 | }; 29 | 30 | /** 31 | * 获取单个服务器 32 | */ 33 | exports.getServer = async (req, res) => { 34 | try { 35 | const server = await Server.findById(req.params.id); 36 | 37 | if (!server) { 38 | return res.status(404).json({ 39 | success: false, 40 | message: '服务器未找到' 41 | }); 42 | } 43 | 44 | // 手动过滤敏感字段 45 | const { password, privateKey, ...filteredServer } = server; 46 | 47 | res.status(200).json({ 48 | success: true, 49 | data: filteredServer 50 | }); 51 | } catch (error) { 52 | res.status(500).json({ 53 | success: false, 54 | message: '获取服务器信息失败', 55 | error: error.message 56 | }); 57 | } 58 | }; 59 | 60 | /** 61 | * 创建服务器 62 | */ 63 | exports.createServer = async (req, res) => { 64 | try { 65 | const { name, host, port, username, authType, password, privateKey } = req.body; 66 | 67 | // 创建服务器 68 | const server = await Server.create({ 69 | name, 70 | host, 71 | port, 72 | username, 73 | authType, 74 | password, 75 | privateKey 76 | }); 77 | 78 | // 手动过滤敏感字段 79 | const { password: pwd, privateKey: pk, ...filteredServer } = server; 80 | 81 | res.status(201).json({ 82 | success: true, 83 | data: filteredServer, 84 | message: '服务器添加成功' 85 | }); 86 | } catch (error) { 87 | res.status(500).json({ 88 | success: false, 89 | message: '添加服务器失败', 90 | error: error.message 91 | }); 92 | } 93 | }; 94 | 95 | /** 96 | * 更新服务器信息 97 | */ 98 | exports.updateServer = async (req, res) => { 99 | try { 100 | // 如果不更新敏感信息,移除这些字段 101 | if (!req.body.password) { 102 | delete req.body.password; 103 | } 104 | if (!req.body.privateKey) { 105 | delete req.body.privateKey; 106 | } 107 | 108 | const server = await Server.findByIdAndUpdate( 109 | req.params.id, 110 | req.body, 111 | { new: true } 112 | ); 113 | 114 | if (!server) { 115 | return res.status(404).json({ 116 | success: false, 117 | message: '服务器未找到' 118 | }); 119 | } 120 | 121 | // 手动过滤敏感字段 122 | const { password, privateKey, ...filteredServer } = server; 123 | 124 | res.status(200).json({ 125 | success: true, 126 | data: filteredServer, 127 | message: '服务器信息更新成功' 128 | }); 129 | } catch (error) { 130 | res.status(500).json({ 131 | success: false, 132 | message: '更新服务器信息失败', 133 | error: error.message 134 | }); 135 | } 136 | }; 137 | 138 | /** 139 | * 删除服务器 140 | */ 141 | exports.deleteServer = async (req, res) => { 142 | try { 143 | // 先检查是否有活动连接 144 | if (sshService.connections[req.params.id]) { 145 | // 断开连接 146 | await sshService.disconnect(req.params.id); 147 | } 148 | 149 | const server = await Server.findByIdAndDelete(req.params.id); 150 | 151 | if (!server) { 152 | return res.status(404).json({ 153 | success: false, 154 | message: '服务器未找到' 155 | }); 156 | } 157 | 158 | res.status(200).json({ 159 | success: true, 160 | message: '服务器已删除' 161 | }); 162 | } catch (error) { 163 | res.status(500).json({ 164 | success: false, 165 | message: '删除服务器失败', 166 | error: error.message 167 | }); 168 | } 169 | }; 170 | 171 | /** 172 | * 连接到服务器 173 | */ 174 | exports.connectServer = async (req, res) => { 175 | try { 176 | const serverId = req.params.id; 177 | console.log(`尝试连接服务器,ID: ${serverId}`); 178 | 179 | // 检查服务器是否存在 180 | const server = await Server.findById(serverId); 181 | if (!server) { 182 | console.error(`服务器不存在,ID: ${serverId}`); 183 | return res.status(404).json({ 184 | success: false, 185 | message: '服务器未找到' 186 | }); 187 | } 188 | 189 | // 如果已连接,返回成功 190 | if (sshService.connections[serverId]) { 191 | // 检查连接是否有效 192 | if (sshService.checkConnection(serverId)) { 193 | console.log(`服务器已连接且连接有效,ID: ${serverId}`); 194 | 195 | // 确保数据库状态与实际状态一致 196 | await Server.findByIdAndUpdate(serverId, { 197 | status: 'online', 198 | lastConnection: new Date(), 199 | updatedAt: new Date() 200 | }); 201 | 202 | return res.status(200).json({ 203 | success: true, 204 | message: '服务器已连接', 205 | serverStatus: 'online' 206 | }); 207 | } else { 208 | console.log(`服务器连接无效,尝试重新连接,ID: ${serverId}`); 209 | // 连接已失效,需要重新连接 210 | } 211 | } 212 | 213 | console.log(`开始建立SSH连接,服务器: ${server.name}, 主机: ${server.host}`); 214 | const result = await sshService.connect(serverId); 215 | console.log(`SSH连接建立成功,ID: ${serverId}`); 216 | 217 | // 增加连接后的状态信息 218 | const updatedServer = await Server.findById(serverId); 219 | 220 | res.status(200).json({ 221 | success: true, 222 | message: result.message, 223 | serverStatus: updatedServer.status, 224 | connectionTime: updatedServer.lastConnection 225 | }); 226 | } catch (error) { 227 | console.error(`连接服务器错误:`, error); 228 | 229 | // 确保服务器状态为错误 230 | try { 231 | await Server.findByIdAndUpdate(req.params.id, { 232 | status: 'error', 233 | updatedAt: new Date() 234 | }); 235 | } catch (updateError) { 236 | console.error('更新服务器状态出错:', updateError); 237 | } 238 | 239 | res.status(500).json({ 240 | success: false, 241 | message: '连接服务器失败', 242 | error: error.message, 243 | serverStatus: 'error', 244 | stack: process.env.NODE_ENV === 'production' ? undefined : error.stack 245 | }); 246 | } 247 | }; 248 | 249 | /** 250 | * 断开服务器连接 251 | */ 252 | exports.disconnectServer = async (req, res) => { 253 | try { 254 | const result = await sshService.disconnect(req.params.id); 255 | 256 | // 获取最新服务器状态 257 | const updatedServer = await Server.findById(req.params.id); 258 | 259 | res.status(200).json({ 260 | success: true, 261 | message: result.message, 262 | serverStatus: updatedServer.status 263 | }); 264 | } catch (error) { 265 | console.error(`断开服务器错误:`, error); 266 | 267 | // 确保服务器状态为离线或错误 268 | try { 269 | await Server.findByIdAndUpdate(req.params.id, { 270 | status: 'error', 271 | updatedAt: new Date() 272 | }); 273 | } catch (updateError) { 274 | console.error('更新服务器状态出错:', updateError); 275 | } 276 | 277 | res.status(500).json({ 278 | success: false, 279 | message: '断开服务器连接失败', 280 | error: error.message, 281 | serverStatus: 'offline' 282 | }); 283 | } 284 | }; 285 | 286 | /** 287 | * 在服务器上执行系统命令 288 | */ 289 | exports.executeCommand = async (req, res) => { 290 | try { 291 | const { command } = req.body; 292 | const serverId = req.params.id; 293 | 294 | console.log(`接收到执行命令请求,服务器ID: ${serverId}, 命令: ${command}`); 295 | 296 | if (!command) { 297 | console.error('命令为空'); 298 | return res.status(400).json({ 299 | success: false, 300 | message: '命令不能为空' 301 | }); 302 | } 303 | 304 | // 检查服务器是否存在 305 | const server = await Server.findById(serverId); 306 | if (!server) { 307 | console.error(`服务器不存在,ID: ${serverId}`); 308 | return res.status(404).json({ 309 | success: false, 310 | message: '服务器未找到' 311 | }); 312 | } 313 | 314 | // 检查连接状态 315 | if (!sshService.connections[serverId]) { 316 | console.error(`无有效连接,服务器ID: ${serverId}`); 317 | 318 | // 尝试重新连接 319 | try { 320 | console.log(`尝试重新连接服务器,ID: ${serverId}`); 321 | await sshService.connect(serverId); 322 | console.log(`服务器重新连接成功,ID: ${serverId}`); 323 | } catch (connError) { 324 | console.error(`重新连接失败: ${connError.message}`); 325 | return res.status(400).json({ 326 | success: false, 327 | message: '服务器未连接,请先连接服务器', 328 | error: connError.message 329 | }); 330 | } 331 | } 332 | 333 | // 确保连接有效 334 | if (!sshService.checkConnection(serverId)) { 335 | console.error(`SSH连接无效,服务器ID: ${serverId}`); 336 | return res.status(400).json({ 337 | success: false, 338 | message: 'SSH连接无效,请重新连接服务器' 339 | }); 340 | } 341 | 342 | console.log(`开始执行命令: ${command}`); 343 | const result = await sshService.executeCommand(serverId, command); 344 | console.log(`命令执行完成,退出码: ${result.code}`); 345 | 346 | res.status(200).json({ 347 | success: true, 348 | data: { 349 | stdout: result.stdout, 350 | stderr: result.stderr, 351 | code: result.code 352 | } 353 | }); 354 | } catch (error) { 355 | console.error(`执行命令出错: ${error.message}`, error); 356 | 357 | // 添加更详细的错误日志 358 | const errorDetails = { 359 | message: error.message, 360 | stack: error.stack, 361 | name: error.name, 362 | code: error.code, 363 | serverId: serverId 364 | }; 365 | console.error('详细错误信息:', JSON.stringify(errorDetails, null, 2)); 366 | 367 | res.status(500).json({ 368 | success: false, 369 | message: '执行命令失败', 370 | error: error.message, 371 | errorDetails: process.env.NODE_ENV === 'production' ? undefined : errorDetails, 372 | stack: process.env.NODE_ENV === 'production' ? undefined : error.stack 373 | }); 374 | } 375 | }; 376 | 377 | /** 378 | * 检测服务器状态 379 | */ 380 | exports.checkServerStatus = async (req, res) => { 381 | try { 382 | const serverId = req.params.id; 383 | const server = await Server.findById(serverId); 384 | 385 | if (!server) { 386 | return res.status(404).json({ 387 | success: false, 388 | message: '服务器未找到' 389 | }); 390 | } 391 | 392 | // 检查连接状态 393 | const isConnected = !!sshService.connections[serverId]; 394 | const isConnectionValid = isConnected ? sshService.checkConnection(serverId) : false; 395 | 396 | // 如果数据库状态与实际连接状态不符,更新数据库 397 | let actualStatus = server.status; 398 | if (isConnectionValid && server.status !== 'online') { 399 | actualStatus = 'online'; 400 | await Server.findByIdAndUpdate(serverId, { 401 | status: 'online', 402 | updatedAt: new Date() 403 | }); 404 | } else if (!isConnectionValid && server.status === 'online') { 405 | actualStatus = 'offline'; 406 | await Server.findByIdAndUpdate(serverId, { 407 | status: 'offline', 408 | updatedAt: new Date() 409 | }); 410 | } 411 | 412 | res.status(200).json({ 413 | success: true, 414 | data: { 415 | status: isConnectionValid ? 'online' : (server.status === 'error' ? 'error' : 'offline'), 416 | lastConnection: server.lastConnection, 417 | backendConnected: isConnected, // 后端实际是否有连接对象 418 | backendConnectionValid: isConnectionValid // 后端连接是否有效 419 | } 420 | }); 421 | } catch (error) { 422 | console.error('检查服务器状态失败:', error); 423 | res.status(500).json({ 424 | success: false, 425 | message: '检查服务器状态失败', 426 | error: error.message 427 | }); 428 | } 429 | }; 430 | 431 | /** 432 | * 部署Nftato脚本到服务器 433 | */ 434 | exports.deployIptato = async (req, res) => { 435 | // 创建一个独立的响应已发送标记,避免重复发送响应 436 | let responseSent = false; 437 | 438 | try { 439 | const serverId = req.params.id; 440 | // 检查服务器是否存在 441 | const server = await Server.findById(serverId); 442 | if (!server) { 443 | return res.status(404).json({ 444 | success: false, 445 | message: '服务器未找到' 446 | }); 447 | } 448 | 449 | // 检查服务器是否已连接 450 | if (!sshService.connections[serverId]) { 451 | return res.status(400).json({ 452 | success: false, 453 | message: '服务器未连接,请先连接服务器' 454 | }); 455 | } 456 | 457 | // 检查是否需要使用WebSocket 458 | const useWebSocket = req.query.useWebSocket === 'true' || req.body.useWebSocket === true; 459 | 460 | if (useWebSocket && req.app.io) { 461 | // 立即发送初始响应,表示部署已开始 462 | res.status(200).json({ 463 | success: true, 464 | message: '脚本部署已开始,请通过WebSocket接收进度', 465 | useWebSocket: true 466 | }); 467 | responseSent = true; 468 | 469 | // 创建一个WebSocket房间ID,基于serverId和时间戳 470 | const roomId = `deploy_${serverId}_${Date.now()}`; 471 | 472 | // 通知前端WebSocket连接信息 473 | req.app.io.emit('deploy_start', { 474 | serverId, 475 | roomId, 476 | message: '开始部署过程,请等待...' 477 | }); 478 | 479 | // 创建进度回调函数,通过WebSocket发送进度 480 | const progressCallback = (data) => { 481 | req.app.io.emit(roomId, data); 482 | }; 483 | 484 | // 开始部署过程 485 | try { 486 | await sshService.deployIptato(serverId, progressCallback); 487 | // 部署完成,发送最终状态 488 | req.app.io.emit(roomId, { 489 | type: 'complete', 490 | success: true, 491 | message: '脚本部署成功完成!' 492 | }); 493 | } catch (deployError) { 494 | console.error('部署过程中出错:', deployError); 495 | // 发送错误信息 496 | req.app.io.emit(roomId, { 497 | type: 'error', 498 | success: false, 499 | message: `部署失败: ${deployError.message}` 500 | }); 501 | } finally { 502 | // 发送关闭信号 503 | setTimeout(() => { 504 | req.app.io.emit(roomId, { type: 'close' }); 505 | }, 1000); 506 | } 507 | } else { 508 | // 常规非WebSocket部署,直接执行并等待结果 509 | await sshService.deployIptato(serverId); 510 | 511 | if (!responseSent) { 512 | res.status(200).json({ 513 | success: true, 514 | message: '脚本部署成功' 515 | }); 516 | } 517 | } 518 | } catch (error) { 519 | console.error('部署脚本错误:', error); 520 | 521 | // 添加更详细的错误日志 522 | const errorDetails = { 523 | message: error.message, 524 | stack: error.stack, 525 | name: error.name, 526 | code: error.code, 527 | serverId: req.params.id 528 | }; 529 | console.error('详细部署脚本错误信息:', JSON.stringify(errorDetails, null, 2)); 530 | 531 | if (!responseSent) { 532 | res.status(500).json({ 533 | success: false, 534 | message: '部署Nftato脚本失败', 535 | error: error.message, 536 | errorDetails: process.env.NODE_ENV === 'production' ? undefined : errorDetails 537 | }); 538 | } 539 | } 540 | }; 541 | 542 | /** 543 | * 获取服务器连接日志 544 | */ 545 | exports.getServerLogs = async (req, res) => { 546 | try { 547 | const serverId = req.params.id; 548 | const server = await Server.findById(serverId); 549 | 550 | if (!server) { 551 | return res.status(404).json({ 552 | success: false, 553 | message: '服务器未找到' 554 | }); 555 | } 556 | 557 | // 检查连接状态 558 | const isConnected = !!sshService.connections[serverId]; 559 | const isConnectionValid = isConnected ? sshService.checkConnection(serverId) : false; 560 | 561 | // 获取最近的系统日志(提取服务器连接相关的日志条目) 562 | let logs = []; 563 | 564 | // 添加连接状态信息 565 | logs.push(`[${new Date().toISOString()}] 服务器ID: ${serverId}`); 566 | logs.push(`[${new Date().toISOString()}] 服务器名称: ${server.name}`); 567 | logs.push(`[${new Date().toISOString()}] 数据库中状态: ${server.status}`); 568 | logs.push(`[${new Date().toISOString()}] 后端有连接对象: ${isConnected ? '是' : '否'}`); 569 | 570 | if (isConnected) { 571 | logs.push(`[${new Date().toISOString()}] 连接对象有效: ${isConnectionValid ? '是' : '否'}`); 572 | 573 | // 获取连接详细信息 574 | const conn = sshService.connections[serverId]; 575 | if (conn) { 576 | logs.push(`[${new Date().toISOString()}] 连接状态: ${conn._state || '未知'}`); 577 | logs.push(`[${new Date().toISOString()}] 套接字可读: ${conn._sock?.readable ? '是' : '否'}`); 578 | logs.push(`[${new Date().toISOString()}] 套接字可写: ${conn._sock?.writable ? '是' : '否'}`); 579 | } 580 | 581 | if (isConnectionValid) { 582 | logs.push(`[${new Date().toISOString()}] 服务器已连接且连接有效`); 583 | } else { 584 | logs.push(`[${new Date().toISOString()}] 服务器连接对象存在但可能无效`); 585 | } 586 | } else { 587 | logs.push(`[${new Date().toISOString()}] 当前没有活动的SSH连接`); 588 | } 589 | 590 | // 如果数据库状态为在线但实际连接检查显示不在线 591 | if (server.status === 'online' && !isConnectionValid) { 592 | logs.push(`[${new Date().toISOString()}] 状态不一致:数据库显示在线但连接检查显示不在线`); 593 | } 594 | 595 | // 如果数据库状态为离线但实际连接有效 596 | if ((server.status === 'offline' || server.status === 'error') && isConnectionValid) { 597 | logs.push(`[${new Date().toISOString()}] 状态不一致:数据库显示${server.status}但连接实际有效`); 598 | } 599 | 600 | // 返回系统日志 601 | res.status(200).json({ 602 | success: true, 603 | data: logs.join('\n'), 604 | connectionStatus: { 605 | databaseStatus: server.status, 606 | actualConnected: isConnected, 607 | connectionValid: isConnectionValid 608 | } 609 | }); 610 | } catch (error) { 611 | console.error('获取服务器日志失败:', error); 612 | res.status(500).json({ 613 | success: false, 614 | message: '获取服务器日志失败', 615 | error: error.message 616 | }); 617 | } 618 | }; 619 | 620 | /** 621 | * 检查Nftato脚本是否已存在于服务器 622 | */ 623 | exports.checkScriptExists = async (req, res) => { 624 | try { 625 | const serverId = req.params.id; 626 | console.log(`检查服务器 ${serverId} 是否已部署Nftato脚本`); 627 | 628 | // 检查服务器是否存在 629 | const server = await Server.findById(serverId); 630 | if (!server) { 631 | return res.status(404).json({ 632 | success: false, 633 | message: '服务器未找到' 634 | }); 635 | } 636 | 637 | // 检查服务器是否已连接 638 | if (!sshService.connections[serverId]) { 639 | return res.status(400).json({ 640 | success: false, 641 | message: '服务器未连接,请先连接服务器' 642 | }); 643 | } 644 | 645 | // 检查连接是否有效 646 | if (!sshService.checkConnection(serverId)) { 647 | return res.status(400).json({ 648 | success: false, 649 | message: 'SSH连接无效,请重新连接服务器' 650 | }); 651 | } 652 | 653 | // 执行检查命令 654 | const result = await sshService.executeCommand( 655 | serverId, 656 | 'test -f ~/Nftato.sh && echo "exists_home" || test -f /root/Nftato.sh && echo "exists_root" || echo "not_found"' 657 | ); 658 | 659 | const scriptExists = result.stdout.includes('exists_home') || result.stdout.includes('exists_root'); 660 | const location = result.stdout.includes('exists_home') ? '~/Nftato.sh' : 661 | result.stdout.includes('exists_root') ? '/root/Nftato.sh' : ''; 662 | 663 | // 如果脚本存在,运行升级命令 664 | let upgradeResult = null; 665 | if (scriptExists) { 666 | console.log(`在服务器 ${serverId} 上运行Nftato升级核心脚本命令`); 667 | try { 668 | const scriptPath = location === '~/Nftato.sh' ? '~/Nftato.sh' : '/root/Nftato.sh'; 669 | upgradeResult = await sshService.executeCommand(serverId, `bash ${scriptPath} 21`); 670 | console.log('升级核心脚本结果:', upgradeResult); 671 | } catch (upgradeError) { 672 | console.error('运行升级命令失败:', upgradeError); 673 | } 674 | } 675 | 676 | res.status(200).json({ 677 | success: true, 678 | exists: scriptExists, 679 | location: location, 680 | message: scriptExists ? 'Nftato脚本已部署' : 'Nftato脚本未部署', 681 | upgradeResult: upgradeResult ? { 682 | success: true, 683 | stdout: upgradeResult.stdout, 684 | stderr: upgradeResult.stderr 685 | } : null 686 | }); 687 | } catch (error) { 688 | console.error('检查脚本存在状态失败:', error); 689 | res.status(500).json({ 690 | success: false, 691 | message: '检查脚本存在状态失败', 692 | error: error.message 693 | }); 694 | } 695 | }; 696 | 697 | /** 698 | * 测试服务器连接 699 | */ 700 | exports.testConnection = async (req, res) => { 701 | try { 702 | // 获取请求中的服务器信息 703 | const serverData = req.body; 704 | 705 | console.log('接收到测试连接请求:', { 706 | host: serverData.host, 707 | port: serverData.port, 708 | username: serverData.username, 709 | authType: serverData.authType 710 | }); 711 | 712 | if (!serverData.host || !serverData.username) { 713 | return res.status(400).json({ 714 | success: false, 715 | message: '缺少必要的连接信息' 716 | }); 717 | } 718 | 719 | // 测试连接 720 | const result = await sshService.testConnection(serverData); 721 | 722 | res.status(200).json({ 723 | success: true, 724 | message: '连接测试成功', 725 | data: result 726 | }); 727 | } catch (error) { 728 | console.error('测试连接失败:', error); 729 | res.status(400).json({ 730 | success: false, 731 | message: '连接测试失败: ' + error.message 732 | }); 733 | } 734 | }; -------------------------------------------------------------------------------- /client/src/views/Rules.vue: -------------------------------------------------------------------------------- 1 | 425 | 426 | 431 | 432 | --------------------------------------------------------------------------------