├── wails ├── bin │ └── backend.bin ├── wails.json ├── dev.sh └── app.go ├── electron ├── resources │ └── .gitkeep ├── build │ ├── icon.icns │ └── icon.png ├── package.json ├── electron-builder.yml └── src │ └── main.js ├── demo ├── flag_list.png ├── time_runner.png ├── exploit_edit_1.png ├── exploit_edit_2.png ├── exploit_runner.png ├── traffic_analyse.png ├── traffic_search.png └── DEMO.md ├── frontend ├── public │ ├── logo.png │ ├── favicon.ico │ ├── favicon.png │ ├── favicon-16.png │ └── favicon-32.png ├── tsconfig.app.json ├── tsconfig.node.json ├── env.d.ts ├── src │ ├── assets │ │ ├── main.css │ │ └── base.css │ ├── App.vue │ ├── main.ts │ └── components │ │ ├── CodeEditor.vue │ │ └── OutputTable.vue ├── tsconfig.json ├── index.html ├── package.json └── vite.config.ts ├── service ├── server │ └── register.go ├── update │ ├── sysprocattr_windows.go │ ├── sysprocattr_unix.go │ ├── register.go │ └── replace.go ├── webui │ ├── proxy.go │ ├── register.go │ ├── git.go │ ├── terminal.go │ ├── code_templates.go │ ├── log.go │ ├── flag.go │ └── action.go ├── route │ ├── register.go │ ├── monitor.go │ ├── flag.go │ ├── heartbeat.go │ └── exploit.go ├── windows │ ├── check_other.go │ └── check.go ├── git │ └── register.go ├── client │ ├── register.go │ ├── heartbeat.go │ ├── monitor.go │ └── monitor_pcap.go ├── udpcast │ └── udpcast.go ├── pcap │ ├── udp.go │ └── tcp.go ├── proxy │ ├── handler.go │ └── cache.go ├── database │ ├── database.go │ └── const.go └── config │ └── config.go ├── utils └── utils.go ├── .gitignore ├── README.md └── go.mod /wails/bin/backend.bin: -------------------------------------------------------------------------------- 1 | PLACEHOLDER -------------------------------------------------------------------------------- /electron/resources/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /demo/flag_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangzheng2016/0E7/HEAD/demo/flag_list.png -------------------------------------------------------------------------------- /demo/time_runner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangzheng2016/0E7/HEAD/demo/time_runner.png -------------------------------------------------------------------------------- /demo/exploit_edit_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangzheng2016/0E7/HEAD/demo/exploit_edit_1.png -------------------------------------------------------------------------------- /demo/exploit_edit_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangzheng2016/0E7/HEAD/demo/exploit_edit_2.png -------------------------------------------------------------------------------- /demo/exploit_runner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangzheng2016/0E7/HEAD/demo/exploit_runner.png -------------------------------------------------------------------------------- /demo/traffic_analyse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangzheng2016/0E7/HEAD/demo/traffic_analyse.png -------------------------------------------------------------------------------- /demo/traffic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangzheng2016/0E7/HEAD/demo/traffic_search.png -------------------------------------------------------------------------------- /electron/build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangzheng2016/0E7/HEAD/electron/build/icon.icns -------------------------------------------------------------------------------- /electron/build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangzheng2016/0E7/HEAD/electron/build/icon.png -------------------------------------------------------------------------------- /frontend/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangzheng2016/0E7/HEAD/frontend/public/logo.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangzheng2016/0E7/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangzheng2016/0E7/HEAD/frontend/public/favicon.png -------------------------------------------------------------------------------- /frontend/public/favicon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangzheng2016/0E7/HEAD/frontend/public/favicon-16.png -------------------------------------------------------------------------------- /frontend/public/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangzheng2016/0E7/HEAD/frontend/public/favicon-32.png -------------------------------------------------------------------------------- /service/server/register.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | var programs sync.Map 10 | 11 | func Register(router *gin.Engine) { 12 | go StartActionScheduler() 13 | } 14 | -------------------------------------------------------------------------------- /service/update/sysprocattr_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package update 5 | 6 | import ( 7 | "os/exec" 8 | ) 9 | 10 | // setUnixProcAttr 在 Windows 上为空实现(Windows 不需要设置进程组) 11 | func setUnixProcAttr(cmd *exec.Cmd) { 12 | // Windows 上不需要设置进程组 13 | } 14 | -------------------------------------------------------------------------------- /service/webui/proxy.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "0E7/service/proxy" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func proxy_cache_list(c *gin.Context) { 11 | list := proxy.ListCacheEntries() 12 | c.JSON(http.StatusOK, gin.H{ 13 | "success": true, 14 | "data": list, 15 | }) 16 | } 17 | 18 | 19 | -------------------------------------------------------------------------------- /service/update/sysprocattr_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package update 5 | 6 | import ( 7 | "os/exec" 8 | "syscall" 9 | ) 10 | 11 | // setUnixProcAttr 为 Unix/Linux 系统设置进程属性 12 | func setUnixProcAttr(cmd *exec.Cmd) { 13 | cmd.SysProcAttr = &syscall.SysProcAttr{ 14 | Setpgid: true, 15 | Pgid: 0, 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | ) 7 | 8 | func GetMd5FromString(str string) string { 9 | h := md5.New() 10 | h.Write([]byte(str)) 11 | return hex.EncodeToString(h.Sum(nil)) 12 | } 13 | 14 | func GetMd5FromBytes(b []byte) string { 15 | h := md5.New() 16 | h.Write(b) 17 | return hex.EncodeToString(h.Sum(nil)) 18 | } 19 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | }, 11 | "moduleResolution":"Node", 12 | "types": ["node"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Bundler", 14 | "types": ["node"], 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 0e7.log 2 | 0e7_* 3 | sqlite.db 4 | sqlite.db-shm 5 | sqlite.db-wal 6 | config.ini 7 | *.bak 8 | *.backup 9 | /frontend/node_modules/ 10 | /electron/node_modules/ 11 | /electron/release/ 12 | /electron/resources/bin/ 13 | /wails/build/ 14 | /wails/frontend/ 15 | /wails/bin/ 16 | .idea 17 | /0E7 18 | /flow 19 | /pcap 20 | /cert 21 | /dist 22 | /bleve 23 | /test 24 | /exploit 25 | /log 26 | /upload 27 | /git 28 | .DS_Store 29 | /test_* 30 | cpu.prof 31 | mem.prof 32 | -------------------------------------------------------------------------------- /service/route/register.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | var exploit_bucket sync.Map 10 | var exploit_mutex sync.Mutex 11 | 12 | func Register(router *gin.Engine) { 13 | router.POST("/api/heartbeat", heartbeat) 14 | router.GET("/api/exploit", exploit) 15 | router.GET("/api/exploit_download", exploit_download) 16 | router.POST("/api/exploit_output", exploit_output) 17 | router.POST("/api/flag", flag) 18 | router.POST("/api/monitor", monitor) 19 | } 20 | -------------------------------------------------------------------------------- /frontend/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } 8 | 9 | declare module '*.js' { 10 | const content: any 11 | export default content 12 | } 13 | 14 | // 确保 Vue 组件的类型声明 15 | declare module '@/components/*.vue' { 16 | import type { DefineComponent } from 'vue' 17 | const component: DefineComponent<{}, {}, any> 18 | export default component 19 | } -------------------------------------------------------------------------------- /demo/DEMO.md: -------------------------------------------------------------------------------- 1 | # 平台演示 2 | 3 | ## 定时计划 4 | 定时执行Exploit脚本,Flag提交器等 5 | 6 | 7 | 8 | 9 | ## Exploit管理 10 | 可以查看管理不同的攻击脚本 11 | 12 | 13 | ### 在线编辑 14 | 15 | 16 | 17 | ### 实时查看运行结果 18 | 19 | 20 | 21 | 22 | ## 流量分析 23 | 24 | ### 流量查询 25 | 可以根据Flag透出条件、IP端口、全文关键词模糊匹配(支持多关键词)搜索 26 | 27 | 28 | 29 | ### 在线分析流量 30 | 31 | 可以支持多种在线流量分析方式,自动重组TCP流,解析HTTP中的压缩流量 32 | 33 | 34 | 35 | ## Flag管理 36 | 查看不同Flag的提交情况,由哪些队伍,哪些脚本产生 37 | 38 | -------------------------------------------------------------------------------- /wails/wails.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://wails.io/schemas/config.v2.json", 3 | "name": "0E7 Desktop", 4 | "outputfilename": "0e7-desktop", 5 | "frontend": { 6 | "dir": "", 7 | "install": "", 8 | "build": "", 9 | "dev": "" 10 | }, 11 | "author": { 12 | "name": "HydrogenE7", 13 | "email": "huangzhengdoc@gmail.com" 14 | }, 15 | "info": { 16 | "productName": "0E7 Desktop", 17 | "productVersion": "1.0.0", 18 | "copyright": "Copyright © 2024", 19 | "comments": "0E7 For Security" 20 | }, 21 | "nsisType": "multiple", 22 | "obfuscated": false, 23 | "garbleargs": "" 24 | } 25 | 26 | -------------------------------------------------------------------------------- /frontend/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | 8 | font-weight: normal; 9 | } 10 | 11 | a, 12 | .green { 13 | text-decoration: none; 14 | color: hsla(160, 100%, 37%, 1); 15 | transition: 0.4s; 16 | } 17 | 18 | @media (hover: hover) { 19 | a:hover { 20 | background-color: hsla(160, 100%, 37%, 0.2); 21 | } 22 | } 23 | 24 | @media (min-width: 1024px) { 25 | body { 26 | display: flex; 27 | place-items: center; 28 | } 29 | 30 | #app { 31 | display: grid; 32 | grid-template-columns: 1fr; 33 | padding: 0 2rem; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /service/windows/check_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package windows 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "runtime" 10 | ) 11 | 12 | // CheckWindowsDependencies 在非Windows系统上的空实现 13 | func CheckWindowsDependencies() error { 14 | if runtime.GOOS == "windows" { 15 | log.Println("警告: 在Windows系统上使用了非Windows版本的检查函数") 16 | } 17 | log.Println("非Windows系统,跳过Windows依赖检查") 18 | return nil 19 | } 20 | 21 | // RequestAdminPrivileges 在非Windows系统上的空实现 22 | func RequestAdminPrivileges() error { 23 | return fmt.Errorf("此功能仅在Windows上可用") 24 | } 25 | 26 | // GetInstallationGuide 在非Windows系统上的空实现 27 | func GetInstallationGuide() string { 28 | return "此功能仅在Windows上可用" 29 | } 30 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": [ 6 | "src/*" 7 | ] 8 | }, 9 | "target": "ESNext", 10 | "useDefineForClassFields": true, 11 | "module": "ESNext", 12 | "moduleResolution": "Node", 13 | "strict": true, 14 | "jsx": "preserve", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "esModuleInterop": true, 18 | "lib": [ 19 | "ESNext", 20 | "DOM" 21 | ], 22 | "types": [ 23 | "node" 24 | ], 25 | "skipLibCheck": true, 26 | "noEmit": true 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "src/**/*.d.ts", 31 | "src/**/*.tsx", 32 | "src/**/*.vue" 33 | ], 34 | "references": [ 35 | { 36 | "path": "./tsconfig.node.json" 37 | } 38 | ], 39 | } 40 | -------------------------------------------------------------------------------- /electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "0e7-desktop", 3 | "version": "1.0.0", 4 | "description": "0E7 Desktop Version", 5 | "homepage": "https://0e7.cn", 6 | "author": { 7 | "name": "HydrogenE7", 8 | "email": "huangzhengdoc@gmail.com" 9 | }, 10 | "private": true, 11 | "main": "src/main.js", 12 | "scripts": { 13 | "dev": "cross-env NODE_ENV=development electron src/main.js", 14 | "build:mac": "electron-builder --config electron-builder.yml --mac", 15 | "build:win": "electron-builder --config electron-builder.yml --win", 16 | "build:linux": "electron-builder --config electron-builder.yml --linux" 17 | }, 18 | "dependencies": { 19 | "sudo-prompt": "^9.2.1", 20 | "wait-on": "^7.2.0" 21 | }, 22 | "devDependencies": { 23 | "cross-env": "^7.0.3", 24 | "electron": "^32.3.3", 25 | "electron-builder": "^25.1.8" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 0E7工具箱 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /wails/dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" 6 | 7 | echo "==> 启动 Wails 开发模式" 8 | 9 | # 检查 Wails CLI 10 | if ! command -v wails &> /dev/null; then 11 | echo "错误: 未找到 wails 命令" 12 | echo "请先安装 Wails CLI:" 13 | echo " go install github.com/wailsapp/wails/v2/cmd/wails@latest" 14 | exit 1 15 | fi 16 | 17 | # 确保前端依赖已安装 18 | if [ ! -d "${ROOT_DIR}/frontend/node_modules" ]; then 19 | echo "==> 安装前端依赖" 20 | npm --prefix "${ROOT_DIR}/frontend" install --loglevel=error --fund=false 21 | fi 22 | 23 | # 检查 Wails Go 依赖 24 | cd "${ROOT_DIR}" 25 | if ! go list -m github.com/wailsapp/wails/v2 &> /dev/null; then 26 | echo "==> 安装 Wails Go 依赖" 27 | go get github.com/wailsapp/wails/v2 28 | fi 29 | 30 | # 切换到 wails 目录执行开发模式 31 | cd "${WAILS_DIR}" 32 | 33 | # 启动 Wails 开发模式 34 | wails dev -skipbindings 35 | 36 | -------------------------------------------------------------------------------- /electron/electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.0e7.desktop 2 | productName: 0E7 Desktop 3 | directories: 4 | output: release 5 | buildResources: build 6 | artifactName: 0e7_electron_${os}_${arch}.${ext} 7 | icon: build/icon.png 8 | generateUpdatesFilesForAllChannels: false 9 | files: 10 | - filter: 11 | - package.json 12 | - node_modules/**/* 13 | - src/**/* 14 | extraResources: 15 | - from: resources/bin 16 | to: bin 17 | mac: 18 | category: public.app-category.utilities 19 | icon: build/icon.icns 20 | target: 21 | - dmg 22 | dmg: 23 | title: ${productName} 24 | win: 25 | icon: build/icon.png 26 | target: 27 | - nsis 28 | nsis: 29 | oneClick: false 30 | allowToChangeInstallationDirectory: true 31 | uninstallDisplayName: 0E7 Desktop 32 | linux: 33 | category: Utility 34 | icon: build/icon.png 35 | target: 36 | - deb 37 | publish: null 38 | 39 | -------------------------------------------------------------------------------- /service/update/register.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | var ( 8 | allowedPlatforms = map[string]bool{ 9 | "darwin": true, 10 | "linux": true, 11 | "windows": true, 12 | } 13 | allowedArchs = map[string]bool{ 14 | "amd64": true, 15 | "arm64": true, 16 | "386": true, 17 | } 18 | ) 19 | 20 | func Register(router *gin.Engine) { 21 | router.POST("/api/update", func(c *gin.Context) { 22 | platform := c.PostForm("platform") 23 | arch := c.PostForm("arch") 24 | 25 | // 验证平台和架构是否在白名单中 26 | if !allowedPlatforms[platform] || !allowedArchs[arch] { 27 | c.JSON(400, gin.H{"error": "invalid platform or arch"}) 28 | return 29 | } 30 | 31 | fileName := "0e7_" + platform + "_" + arch 32 | if platform == "windows" { 33 | fileName += ".exe" 34 | } 35 | 36 | c.Header("Content-Disposition", "attachment; filename="+fileName) 37 | c.Header("Content-Type", "application/octet-stream") 38 | c.File(fileName) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 30 | 31 | 54 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pypro", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "run-p type-check build-only", 8 | "preview": "vite preview", 9 | "build-only": "vite build", 10 | "rebuild": "vite build", 11 | "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false" 12 | }, 13 | "dependencies": { 14 | "@codemirror/autocomplete": "^6.18.7", 15 | "@codemirror/commands": "^6.8.1", 16 | "@codemirror/lang-javascript": "^6.2.4", 17 | "@codemirror/lang-python": "^6.2.1", 18 | "@codemirror/lint": "^6.8.5", 19 | "@codemirror/search": "^6.5.11", 20 | "@codemirror/state": "^6.5.2", 21 | "@codemirror/theme-one-dark": "^6.1.3", 22 | "@codemirror/view": "^6.38.3", 23 | "@element-plus/icons-vue": "^2.1.0", 24 | "@xterm/addon-fit": "^0.10.0", 25 | "@xterm/addon-search": "^0.15.0", 26 | "@xterm/addon-web-links": "^0.11.0", 27 | "@xterm/xterm": "^5.5.0", 28 | "element-plus": "^2.3.9", 29 | "vue": "^3.5.22", 30 | "vue-codemirror": "^6.1.1", 31 | "vuex": "^4.1.0" 32 | }, 33 | "devDependencies": { 34 | "@tsconfig/node18": "^18.2.0", 35 | "@types/node": "^24.9.2", 36 | "@vitejs/plugin-vue": "^6.0.1", 37 | "@vue/tsconfig": "^0.8.1", 38 | "npm-run-all": "^4.1.5", 39 | "typescript": "^5.9.3", 40 | "vite": "^7.1.12", 41 | "vue-tsc": "^3.1.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /service/route/monitor.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "0E7/service/config" 5 | "0E7/service/database" 6 | "log" 7 | "strconv" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func monitor(c *gin.Context) { 13 | client_id_str := c.PostForm("client_id") 14 | if client_id_str == "" { 15 | c.JSON(400, gin.H{ 16 | "message": "fail", 17 | "error": "missing parameters", 18 | "result": "", 19 | }) 20 | c.Abort() 21 | return 22 | } 23 | 24 | // 转换uuid为int 25 | client_id, err := strconv.Atoi(client_id_str) 26 | if err != nil { 27 | c.JSON(400, gin.H{ 28 | "message": "fail", 29 | "error": "invalid uuid: " + err.Error(), 30 | "result": "", 31 | }) 32 | log.Println("Invalid uuid:", err) 33 | c.Abort() 34 | return 35 | } 36 | 37 | var monitors []database.Monitor 38 | err = config.Db.Where("client_id = ?", client_id).Find(&monitors).Error 39 | if err != nil { 40 | c.JSON(400, gin.H{ 41 | "message": "fail", 42 | "error": err.Error(), 43 | "result": []interface{}{}, 44 | }) 45 | return 46 | } 47 | 48 | var ret []map[string]interface{} 49 | found := false 50 | for _, monitor := range monitors { 51 | element := map[string]interface{}{ 52 | "id": monitor.ID, 53 | "types": monitor.Types, 54 | "data": monitor.Data, 55 | "interval": monitor.Interval, 56 | } 57 | ret = append(ret, element) 58 | found = true 59 | } 60 | if !found { 61 | c.JSON(202, gin.H{ 62 | "message": "success", 63 | "error": "", 64 | "result": []interface{}{}, 65 | }) 66 | return 67 | } 68 | c.JSON(200, gin.H{ 69 | "message": "success", 70 | "error": "", 71 | "result": ret, 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /service/route/flag.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "0E7/service/config" 5 | "0E7/service/database" 6 | "strconv" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func flag(c *gin.Context) { 12 | exploit_id_str := c.PostForm("exploit_id") 13 | exploit_id, err := strconv.Atoi(exploit_id_str) 14 | if err != nil { 15 | c.JSON(400, gin.H{ 16 | "message": "fail", 17 | "error": "exploit_id error", 18 | }) 19 | return 20 | } 21 | exploit_flag := c.PostForm("flag") 22 | team := c.PostForm("team") 23 | 24 | var count int64 25 | err = config.Db.Model(&database.Flag{}).Where("flag = ?", exploit_flag).Count(&count).Error 26 | if err != nil { 27 | c.JSON(400, gin.H{ 28 | "message": "fail", 29 | "error": err.Error(), 30 | }) 31 | return 32 | } 33 | 34 | if count == 0 { 35 | flag := database.Flag{ 36 | ExploitId: exploit_id, 37 | Flag: exploit_flag, 38 | Status: "QUEUE", 39 | Team: team, 40 | } 41 | err = config.Db.Create(&flag).Error 42 | if err != nil { 43 | c.JSON(400, gin.H{ 44 | "message": "fail", 45 | "error": err.Error(), 46 | }) 47 | return 48 | } 49 | c.JSON(200, gin.H{ 50 | "message": "success", 51 | "error": "", 52 | }) 53 | } else { 54 | flag := database.Flag{ 55 | ExploitId: exploit_id, 56 | Flag: exploit_flag, 57 | Status: "SKIPPED", 58 | Team: team, 59 | } 60 | err = config.Db.Create(&flag).Error 61 | if err != nil { 62 | c.JSON(400, gin.H{ 63 | "message": "fail", 64 | "error": err.Error(), 65 | }) 66 | return 67 | } 68 | c.JSON(202, gin.H{ 69 | "message": "skipped", 70 | "error": "", 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /service/git/register.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "log" 5 | "os/exec" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // CheckGitCommand 检查系统中是否有 git 命令 11 | func CheckGitCommand() bool { 12 | _, err := exec.LookPath("git") 13 | return err == nil 14 | } 15 | 16 | // CheckAndWarnGit 检查 git 命令并在缺失时警告 17 | func CheckAndWarnGit() { 18 | if !CheckGitCommand() { 19 | log.Println("警告: 未检测到 git 命令,Git HTTP 服务可能无法正常工作") 20 | log.Println("提示: 请安装 Git 以启用 Git 仓库功能") 21 | log.Println(" 安装方法:") 22 | log.Println(" - macOS: brew install git") 23 | log.Println(" - Ubuntu/Debian: sudo apt-get install git") 24 | log.Println(" - CentOS/RHEL: sudo yum install git") 25 | log.Println(" - Windows: https://git-scm.com/download/win") 26 | } else { 27 | // 验证 git 版本 28 | cmd := exec.Command("git", "--version") 29 | output, err := cmd.Output() 30 | if err == nil { 31 | log.Printf("Git 命令已就绪: %s", string(output)) 32 | } 33 | } 34 | } 35 | 36 | // Register 注册 Git HTTP 服务路由 37 | func Register(router *gin.Engine) { 38 | // Git Smart HTTP Protocol 路由 39 | // 格式: /git/{仓库名}/info/refs?service=git-upload-pack 或 git-receive-pack 40 | // handleInfoRefs 会根据是否有 service 参数来选择处理方式 41 | router.GET("/git/:repo/info/refs", func(c *gin.Context) { 42 | service := c.Query("service") 43 | if service != "" { 44 | handleInfoRefs(c) 45 | } else { 46 | handleInfoRefsOld(c) 47 | } 48 | }) 49 | 50 | // Git upload pack (用于 clone/fetch) 51 | router.POST("/git/:repo/git-upload-pack", handleUploadPack) 52 | 53 | // Git receive pack (用于 push) 54 | router.POST("/git/:repo/git-receive-pack", handleReceivePack) 55 | 56 | // 其他 Git HTTP 支持 57 | router.GET("/git/:repo/HEAD", handleHead) 58 | 59 | // Objects 静态文件服务(用于旧版协议) 60 | router.GET("/git/:repo/objects/:hash1/:hash2", handleObjects) 61 | } 62 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | vue(), 10 | ], 11 | base: '/', 12 | resolve: { 13 | alias: { 14 | '@': fileURLToPath(new URL('./src', import.meta.url)) 15 | } 16 | }, 17 | server: { 18 | proxy: { 19 | '/webui': { 20 | target: 'http://localhost:6102', 21 | changeOrigin: true, 22 | secure: false, 23 | ws: true // 支持WebSocket代理 24 | }, 25 | '/api': { 26 | target: 'http://localhost:6102', 27 | changeOrigin: true, 28 | secure: false 29 | } 30 | } 31 | }, 32 | build:{ 33 | outDir: '../dist', 34 | sourcemap: false, 35 | chunkSizeWarningLimit: 1500, 36 | emptyOutDir: true, 37 | rollupOptions: { 38 | output: { 39 | entryFileNames: 'static/[name].[hash].js', 40 | chunkFileNames: 'static/[name].[hash].js', 41 | assetFileNames: (assetInfo) => { 42 | let fileName = assetInfo.name || '[name]'; 43 | if(fileName[0]==='.') fileName = fileName.slice(1); 44 | return `static/${fileName}.[hash][extname]`; 45 | }, 46 | manualChunks(id) { 47 | // 只对 node_modules 中的依赖进行拆分 48 | if (id.includes('node_modules')) { 49 | // 将大型库拆分出来 50 | if (id.includes('element-plus')) { 51 | return 'element-plus'; 52 | } 53 | if (id.includes('vuex')) { 54 | return 'vuex'; 55 | } 56 | // CodeMirror 相关包合并到 vendor,避免初始化顺序问题 57 | // 其他依赖合并到 vendor 58 | return 'vendor'; 59 | } 60 | } 61 | } 62 | } 63 | } 64 | }) 65 | -------------------------------------------------------------------------------- /service/client/register.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "0E7/service/config" 5 | "context" 6 | "log" 7 | "sync" 8 | "time" 9 | 10 | "golang.org/x/sync/semaphore" 11 | ) 12 | 13 | var set_pipreqs sync.Map 14 | var programs sync.Map 15 | 16 | type Tmonitor struct { 17 | types string 18 | data string 19 | interval int 20 | } 21 | 22 | var monitor_list sync.Map 23 | 24 | func Register() { 25 | workerSemaphore = semaphore.NewWeighted(int64(config.Client_worker)) 26 | 27 | go heartbeat() 28 | if !config.Client_only_monitor { 29 | go exploitLoop() 30 | } 31 | 32 | if config.Client_monitor { 33 | go monitorLoop() 34 | } 35 | } 36 | 37 | // exploitLoop 独立运行 exploit,根据配置的时间间隔执行 38 | func exploitLoop() { 39 | defer func() { 40 | if err := recover(); err != nil { 41 | log.Println("Exploit Loop Error:", err) 42 | go exploitLoop() 43 | } 44 | }() 45 | 46 | // 启动 goroutine 监听 jobsChan,处理 exploit 执行 47 | go func() { 48 | for range jobsChan { 49 | go func() { 50 | workerSemaphore.Acquire(context.Background(), 1) 51 | defer workerSemaphore.Release(1) 52 | exploit() 53 | }() 54 | } 55 | }() 56 | 57 | // 根据配置的时间间隔循环调用 exploit 58 | interval := time.Duration(config.Client_exploit_interval) * time.Second 59 | ticker := time.NewTicker(interval) 60 | defer ticker.Stop() 61 | 62 | for range ticker.C { 63 | go func() { 64 | workerSemaphore.Acquire(context.Background(), 1) 65 | defer workerSemaphore.Release(1) 66 | exploit() 67 | }() 68 | } 69 | } 70 | 71 | func monitorLoop() { 72 | defer func() { 73 | if err := recover(); err != nil { 74 | log.Println("Monitor Loop Error:", err) 75 | go monitorLoop() 76 | } 77 | }() 78 | 79 | interval := 5 * time.Second 80 | ticker := time.NewTicker(interval) 81 | defer ticker.Stop() 82 | 83 | for range ticker.C { 84 | monitor() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /service/udpcast/udpcast.go: -------------------------------------------------------------------------------- 1 | package udpcast 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "strings" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | func Udp_sent(server_tls bool, server_port string) { 12 | defer func() { 13 | if err := recover(); err != nil { 14 | log.Println("UDPCAST ERROR:", err) 15 | go Udp_sent(server_tls, server_port) 16 | } 17 | }() 18 | 19 | // 创建一次连接,然后复用 20 | broadcastIP := net.IPv4(255, 255, 255, 255) 21 | port := 6102 22 | conn, err := net.DialUDP("udp", nil, &net.UDPAddr{ 23 | IP: broadcastIP, 24 | Port: port, 25 | }) 26 | if err != nil { 27 | log.Printf("UDPCAST ERROR: %s\n", err.Error()) 28 | return 29 | } 30 | defer conn.Close() // 确保函数结束时关闭连接 31 | 32 | var message []byte 33 | if server_tls { 34 | message = []byte("0E7" + "s" + server_port) 35 | } else { 36 | message = []byte("0E7" + "n" + server_port) 37 | } 38 | 39 | for { 40 | _, err = conn.Write(message) 41 | if err != nil { 42 | log.Printf("UDPCAST ERROR: %s\n", err.Error()) 43 | return 44 | } 45 | //log.Println("UDPCAST SENT") 46 | time.Sleep(time.Second) 47 | } 48 | } 49 | func Udp_receive(wg *sync.WaitGroup, server_url *string) { 50 | defer wg.Done() 51 | log.Println("SERVER NOT FOUND,WAIT FOR UDP CAST") 52 | port := 6102 53 | conn, err := net.ListenUDP("udp", &net.UDPAddr{ 54 | Port: port, 55 | }) 56 | if err != nil { 57 | log.Printf("UDPCAST ERROR: %s\n", err.Error()) 58 | return 59 | } 60 | defer conn.Close() 61 | 62 | timeout := 120 * time.Second 63 | conn.SetDeadline(time.Now().Add(timeout)) 64 | 65 | buffer := make([]byte, 1024) 66 | n, addr, err := conn.ReadFromUDP(buffer) 67 | if err != nil { 68 | log.Printf("UDPCAST ERROR: %s\n", err.Error()) 69 | return 70 | } 71 | message := string(buffer[:n]) 72 | if strings.HasPrefix(message, "0E7") { 73 | log.Println("SERVER IP REVEIVED") 74 | if message[3] == 's' { 75 | *server_url = "https://" + addr.IP.String() + ":" + message[4:] 76 | } else { 77 | *server_url = "http://" + addr.IP.String() + ":" + message[4:] 78 | } 79 | } else { 80 | return 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /service/webui/register.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func Register(router *gin.Engine) { 10 | router.POST("/webui/exploit", exploit) 11 | router.POST("/webui/exploit_rename", exploit_rename) 12 | router.GET("/webui/exploit_show", exploit_show) 13 | router.GET("/webui/exploit_show_output", exploit_show_output) 14 | router.GET("/webui/exploit_get_by_id", exploit_get_by_id) 15 | router.POST("/webui/exploit_delete", exploit_delete) 16 | router.POST("/webui/action", action) 17 | router.POST("/webui/action_show", action_show) 18 | router.GET("/webui/action_get_by_id", action_get_by_id) 19 | router.POST("/webui/action_delete", action_delete) 20 | router.POST("/webui/action_execute", action_execute) 21 | 22 | router.POST("/webui/pcap_upload", pcap_upload) 23 | router.GET("/webui/pcap_show", pcap_show) 24 | router.GET("/webui/pcap_get_by_id", pcap_get_by_id) 25 | router.GET("/webui/pcap_download", pcap_download) 26 | 27 | // 搜索相关路由 28 | router.GET("/webui/search_pcap", search_pcap) 29 | 30 | // Flag管理相关路由 31 | router.GET("/webui/flag_show", GetFlagList) 32 | router.POST("/webui/flag_submit", SubmitFlag) 33 | router.POST("/webui/flag_delete", DeleteFlag) 34 | 35 | // Flag检测和配置相关路由 36 | router.GET("/webui/flag_config", GetCurrentFlagConfig) 37 | router.POST("/webui/flag_config_update", UpdateFlagConfig) 38 | 39 | // 代码生成相关路由 40 | router.GET("/webui/pcap_generate_code", pcap_generate_code) 41 | 42 | // Proxy 缓存监控 43 | router.GET("/webui/proxy_cache_list", proxy_cache_list) 44 | 45 | // Git 仓库管理 46 | router.GET("/webui/git_repo_list", git_repo_list) 47 | router.POST("/webui/git_repo_update_description", git_repo_update_description) 48 | router.POST("/webui/git_repo_delete", git_repo_delete) 49 | 50 | // 终端管理相关API 51 | router.GET("/webui/clients", getClients) 52 | router.POST("/webui/traffic_collection", createTrafficCollection) 53 | router.POST("/webui/client_monitors", getClientMonitors) 54 | router.POST("/webui/delete_monitor", deleteMonitor) 55 | 56 | // 日志流式传输WebSocket 57 | router.GET("/webui/log/ws", handleLogWebSocket) 58 | } 59 | -------------------------------------------------------------------------------- /frontend/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | font-weight: normal; 59 | } 60 | 61 | body { 62 | min-height: 100vh; 63 | transition: color 0.5s, background-color 0.5s; 64 | line-height: 1.6; 65 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 66 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 67 | font-size: 15px; 68 | text-rendering: optimizeLegibility; 69 | -webkit-font-smoothing: antialiased; 70 | -moz-osx-font-smoothing: grayscale; 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 0E7 Security 2 | 3 | > **警告**:本项目的部分代码由本人生成,并仅由AI维护。作者本人并不会写代码,也不懂安全,长期直接vibe代替思考。如果存在AI相关PR,pls show me the talk。 4 | 5 | 6 | [查看平台图文示例](demo/DEMO.md) 7 | 8 | > 在少量比赛中完成了测试, 使用Sqlite+Bleve的快捷部署方式能承受约14队伍,8小时,6G流量的拷打,再长没有进行相关测试。如果要长期使用建议使用Mysql+Elasticsearch的方案(未完全测试) 9 | 10 | 如果不想自己编译,可以[在此下载已发布的预编译版本](https://github.com/huangzheng2016/0E7/releases) 11 | 12 | ## AWD攻防演练工具箱 13 | 14 | 专为AWD(Attack With Defense)攻防演练比赛设计的综合性工具箱,集成漏洞利用、流量监控、自动化攻击等功能 15 | 16 | ## 功能特性 17 | 18 | ### 1. 漏洞利用管理 19 | - **Exploit管理**: 支持多种编程语言的漏洞利用脚本 20 | - **多语言执行**: 支持Python、Go等脚本的执行 21 | - **定时任务**: 支持定时执行和周期性任务 22 | - **参数化配置**: 支持环境变量、命令行参数等灵活配置 23 | - **参数注入**: 支持将BUCKET值批量注入利用(队伍批量攻击) 24 | - **结果收集**: 自动收集执行结果和输出信息 25 | - **团队协作**: 支持多用户并行使用 26 | 27 | ### 2. 流量监控分析 28 | - **PCAP解析**: 支持多种网络协议的数据包分析 29 | - **实时监控**: 实时捕获和分析网络流量 30 | - **流量可视化**: 提供直观的流量数据展示 31 | - **协议识别**: 自动识别并解析H2、WebSocket、TCP、UDP协议 32 | - **全文检索**:支持Bleve和Elasticsearch两种全文检索引擎 33 | 34 | ### 3. 客户端管理 35 | - **多平台支持**: 支持Windows、Linux、macOS等主流操作系统 36 | - **自动注册**: 客户端自动向服务器注册和心跳保持 37 | - **任务分发**: 服务器自动向客户端分发执行任务 38 | - **状态监控**: 实时监控客户端状态和执行进度 39 | 40 | ### 4. 实用工具 41 | - **代理管理**: 支持多端定时缓各类数据文件 42 | - **Git 仓库管理**: 内置 HTTP 协议的 Git 仓库服务 43 | 44 | ## 快速开始 45 | 46 | ### 环境要求 47 | 48 | 确保您的系统已安装以下环境: 49 | 50 | - **Go 1.25**: 后端开发环境 51 | - **Node.js 24+**: 前端开发环境 52 | - **npm**: 包管理工具 53 | 54 | ### 构建方式 55 | 56 | #### 方式一:基础构建(推荐) 57 | ```bash 58 | # 构建当前系统版本 59 | chmod +x build.sh 60 | ./build.sh 61 | ``` 62 | 63 | #### 方式二:高级构建(请自行准备跨平台编译工具链) 64 | ```bash 65 | # 查看所有选项 66 | ./build-advanced.sh -h 67 | 68 | # 构建当前系统版本 69 | ./build-advanced.sh 70 | 71 | # 构建所有支持的平台 72 | ./build-advanced.sh -a 73 | 74 | # 发布模式构建(优化文件大小) 75 | ./build-advanced.sh -r 76 | 77 | # 构建指定平台 78 | ./build-advanced.sh -p windows 79 | ./build-advanced.sh -p linux 80 | ./build-advanced.sh -p darwin 81 | ``` 82 | 83 | ### 运行方式 84 | 85 | #### 服务器模式 86 | ```bash 87 | # 正常启动(使用默认配置文件) 88 | ./0e7__ 89 | 90 | # 指定配置文件路径 91 | ./0e7__ -config 92 | 93 | # 服务器模式启动(自动生成默认配置) 94 | ./0e7__ --server 95 | 96 | # 服务器模式启动并指定配置文件 97 | ./0e7__ --server -config 98 | 99 | # 显示帮助信息 100 | ./0e7__ --help 101 | 102 | # 启用CPU性能分析并输出至文件 103 | ./0e7__ --cpu-profile cpu.prof 104 | 105 | # 同时启用CPU与内存性能分析 106 | ./0e7__ --cpu-profile cpu.prof --mem-profile mem.prof 107 | ``` 108 | 109 | ## 许可证 110 | 111 | 本项目采用 AGPL-3.0 许可证,详情请查看 [LICENSE](LICENSE) 文件。 112 | 113 | --- 114 | -------------------------------------------------------------------------------- /service/route/heartbeat.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "0E7/service/config" 5 | "0E7/service/database" 6 | "0E7/service/update" 7 | "strconv" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func heartbeat(c *gin.Context) { 13 | client_id_str := c.PostForm("client_id") 14 | client_id, err := strconv.ParseInt(client_id_str, 10, 64) 15 | if err != nil { 16 | client_id = 0 17 | } 18 | client_name := c.PostForm("name") 19 | hostname := c.PostForm("hostname") 20 | platform := c.PostForm("platform") 21 | arch := c.PostForm("arch") 22 | cpu := c.PostForm("cpu") 23 | cpu_use := c.PostForm("cpu_use") 24 | memory_use := c.PostForm("memory_use") 25 | memory_max := c.PostForm("memory_max") 26 | pcap := c.PostForm("pcap") 27 | 28 | if hostname == "" || platform == "" || arch == "" || cpu == "" || cpu_use == "" || memory_use == "" || memory_max == "" { 29 | c.JSON(400, gin.H{ 30 | "message": "fail", 31 | "error": "missing parameters", 32 | "sha256": update.Sha256Hash, 33 | }) 34 | c.Abort() 35 | return 36 | } 37 | 38 | var client database.Client 39 | var found bool = false 40 | 41 | // 如果client_id存在且不为0,先根据ID查询 42 | if client_id > 0 { 43 | err = config.Db.Where("id = ?", client_id).First(&client).Error 44 | if err == nil { 45 | found = true 46 | } 47 | } 48 | 49 | // 如果根据ID没找到,且client_name不为空,根据name查询 50 | if !found && client_name != "" { 51 | err = config.Db.Where("name = ? AND platform = ? AND arch = ?", client_name, platform, arch).First(&client).Error 52 | if err == nil { 53 | found = true 54 | client_id = int64(client.ID) // 更新client_id为找到的记录的ID 55 | } 56 | } 57 | 58 | if found { 59 | // 更新现有记录 60 | err = config.Db.Model(&client).Updates(map[string]interface{}{ 61 | "hostname": hostname, 62 | "platform": platform, 63 | "arch": arch, 64 | "cpu": cpu, 65 | "cpu_use": cpu_use, 66 | "memory_use": memory_use, 67 | "memory_max": memory_max, 68 | "pcap": pcap, 69 | }).Error 70 | if err != nil { 71 | c.JSON(400, gin.H{ 72 | "message": "fail", 73 | "error": err.Error(), 74 | "sha256": update.Sha256Hash, 75 | }) 76 | c.Abort() 77 | return 78 | } 79 | client_id = int64(client.ID) // 确保返回正确的ID 80 | } else { 81 | // 创建新记录 82 | client = database.Client{ 83 | Name: client_name, 84 | Hostname: hostname, 85 | Platform: platform, 86 | Arch: arch, 87 | CPU: cpu, 88 | CPUUse: cpu_use, 89 | MemoryUse: memory_use, 90 | MemoryMax: memory_max, 91 | Pcap: pcap, 92 | } 93 | err = config.Db.Create(&client).Error 94 | if err != nil { 95 | c.JSON(400, gin.H{ 96 | "message": "fail", 97 | "error": err.Error(), 98 | "sha256": update.Sha256Hash, 99 | }) 100 | c.Abort() 101 | return 102 | } 103 | client_id = int64(client.ID) // 获取新创建记录的ID 104 | } 105 | c.JSON(200, gin.H{ 106 | "message": "success", 107 | "error": "", 108 | "sha256": update.Sha256Hash, 109 | "id": client_id, 110 | }) 111 | } 112 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import "./assets/main.css"; 2 | 3 | import { createApp } from "vue"; 4 | // @ts-ignore 5 | import { createStore } from "vuex"; 6 | // @ts-ignore 7 | import App from "./App.vue"; 8 | import ElementPlus, { ElNotification } from "element-plus"; 9 | import "element-plus/dist/index.css"; 10 | import * as ElementPlusIconsVue from "@element-plus/icons-vue"; 11 | import zhCn from 'element-plus/es/locale/lang/zh-cn'; 12 | 13 | 14 | const store = createStore({ 15 | state() { 16 | return { 17 | workerQueue: [], 18 | totalItems: 0 19 | }; 20 | }, 21 | mutations: {}, 22 | actions: { 23 | fetchResults( 24 | { 25 | state 26 | }: { 27 | state: any; 28 | }, 29 | payload: { page?: number; pageSize?: number; exploit_id?: string } = {} 30 | ) { 31 | const { page = 1, pageSize = 20, exploit_id } = payload; 32 | // 如果没有传入exploit_id,则从URL参数获取 33 | const finalExploitId = exploit_id || new URLSearchParams(window.location.search).get('exploit_id'); 34 | 35 | // 当exploit_id为空时,不查询output,直接返回空结果 36 | if (!finalExploitId) { 37 | state.workerQueue = []; 38 | state.totalItems = 0; 39 | return Promise.resolve({ 40 | message: "success", 41 | total: 0, 42 | result: [] 43 | }); 44 | } 45 | 46 | const params = new URLSearchParams() 47 | params.append('exploit_id', finalExploitId.toString()) 48 | params.append('page', page.toString()) 49 | params.append('page_size', pageSize.toString()) 50 | 51 | return fetch(`/webui/exploit_show_output?${params.toString()}`, { 52 | method: "GET" 53 | }) 54 | .then((res) => res.json()) 55 | .then((res) => { 56 | console.log('API响应数据:', res); // 调试信息 57 | if (res.result && Array.isArray(res.result)) { 58 | // 直接替换整个 workerQueue(只显示当前页的数据) 59 | state.workerQueue = res.result.map((item: any) => { 60 | return { 61 | id: item.id, 62 | exploit_id: item.exploit_id, 63 | client_id: item.client_id, 64 | client_name: item.client_name, 65 | team: item.team, 66 | status: item.status, 67 | output: item.output, 68 | update_time: item.update_time 69 | }; 70 | }); 71 | } else { 72 | // 当没有数据时,清空队列 73 | state.workerQueue = []; 74 | } 75 | 76 | // 使用后端返回的总条数 77 | if (res.total !== undefined) { 78 | state.totalItems = res.total; 79 | } else { 80 | state.totalItems = 0; 81 | } 82 | 83 | return res; 84 | }) 85 | .catch((error) => { 86 | console.error("获取结果失败:", error); 87 | throw error; 88 | }); 89 | } 90 | }, 91 | getters: {}, 92 | }); 93 | 94 | const app = createApp(App); 95 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 96 | app.component(key, component); 97 | } 98 | app.use(store); 99 | app.use(ElementPlus, { 100 | locale: zhCn, 101 | }); 102 | app.mount("#app"); 103 | -------------------------------------------------------------------------------- /service/client/heartbeat.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "0E7/service/config" 5 | "0E7/service/update" 6 | "bytes" 7 | "encoding/json" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | "net/url" 12 | "runtime" 13 | "time" 14 | 15 | "github.com/shirou/gopsutil/v3/cpu" 16 | "github.com/shirou/gopsutil/v3/host" 17 | "github.com/shirou/gopsutil/v3/mem" 18 | ) 19 | 20 | func heartbeat() { 21 | defer func() { 22 | if err := recover(); err != nil { 23 | log.Println("Heartbeat Error:", err) 24 | go heartbeat() 25 | } 26 | }() 27 | 28 | interval := 5 * time.Second 29 | ticker := time.NewTicker(interval) 30 | defer ticker.Stop() 31 | 32 | for range ticker.C { 33 | cpuInfo, err := cpu.Info() 34 | if err != nil { 35 | log.Println("Failed to get cpuInfo:", err) 36 | } 37 | if len(cpuInfo) == 0 { 38 | log.Println("No CPU info available") 39 | } 40 | 41 | memInfo, err := mem.VirtualMemory() 42 | if err != nil { 43 | log.Println("Failed to get memInfo:", err) 44 | } 45 | 46 | cpuPercent, err := cpu.Percent(time.Second, false) 47 | if err != nil { 48 | log.Println("Failed to get cpuPercent:", err) 49 | } 50 | if len(cpuPercent) == 0 { 51 | log.Println("No CPU percent available") 52 | } 53 | 54 | hostname, err := host.Info() 55 | if err != nil { 56 | log.Println("Failed to get hostname:", err) 57 | } 58 | 59 | pcap := moniter_pcap_device() 60 | values := url.Values{} 61 | values.Set("client_id", fmt.Sprintf("%d", config.Client_id)) 62 | values.Set("name", config.Client_name) 63 | values.Set("hostname", hostname.Hostname) 64 | values.Set("platform", runtime.GOOS) 65 | values.Set("arch", runtime.GOARCH) 66 | values.Set("cpu", cpuInfo[0].ModelName) 67 | values.Set("cpu_use", fmt.Sprintf("%.2f", cpuPercent[0])) 68 | values.Set("memory_use", fmt.Sprintf("%d", memInfo.Used/1024/1024)) 69 | values.Set("memory_max", fmt.Sprintf("%d", memInfo.Total/1024/1024)) 70 | values.Set("pcap", pcap) 71 | 72 | requestBody := bytes.NewBufferString(values.Encode()) 73 | request, err := http.NewRequest("POST", config.Server_url+"/api/heartbeat", requestBody) 74 | if err != nil { 75 | log.Println(err) 76 | continue 77 | } 78 | request.Header.Set("Content-Type", "application/x-www-form-urlencoded") 79 | response, err := client.Do(request) 80 | if err != nil { 81 | log.Println(err) 82 | continue 83 | } 84 | defer response.Body.Close() // 确保关闭响应体 85 | if response.StatusCode == 200 || response.StatusCode == 400 { 86 | if response.StatusCode == 400 { 87 | log.Println("Try to update manually") 88 | } 89 | var result map[string]interface{} 90 | err = json.NewDecoder(response.Body).Decode(&result) 91 | if err != nil { 92 | log.Println(err) 93 | } 94 | 95 | // 更新client_id 96 | if result["id"] != nil { 97 | if newId, ok := result["id"].(float64); ok { 98 | err := config.UpdateConfigClientId(int(newId)) 99 | if err != nil { 100 | log.Printf("Failed to update config file: %v", err) 101 | } 102 | } 103 | } 104 | 105 | found := false 106 | if result["sha256"] == nil { 107 | found = true 108 | } else { 109 | for _, hash := range result["sha256"].([]interface{}) { 110 | if hash == update.Sha256Hash[0] { 111 | found = true 112 | break 113 | } 114 | } 115 | } 116 | if !found && config.Client_update { 117 | log.Println("Try to update") 118 | go update.Replace() 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /frontend/src/components/CodeEditor.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 100 | 101 | -------------------------------------------------------------------------------- /service/client/monitor.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "0E7/service/config" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "net/url" 11 | "strconv" 12 | "time" 13 | ) 14 | 15 | func monitor() { 16 | defer func() { 17 | if err := recover(); err != nil { 18 | log.Println("Monitor error: ", err) 19 | } 20 | }() 21 | if !config.Client_monitor { 22 | return 23 | } 24 | values := url.Values{} 25 | values.Set("client_id", fmt.Sprintf("%d", config.Client_id)) 26 | requestBody := bytes.NewBufferString(values.Encode()) 27 | request, err := http.NewRequest("POST", config.Server_url+"/api/monitor", requestBody) 28 | if err != nil { 29 | log.Println(err) 30 | return 31 | } 32 | request.Header.Set("Content-Type", "application/x-www-form-urlencoded") 33 | response, err := client.Do(request) 34 | if err != nil { 35 | log.Println(err) 36 | return 37 | } 38 | defer response.Body.Close() // 确保关闭响应体 39 | if response.StatusCode == 200 { 40 | var result map[string]interface{} 41 | err = json.NewDecoder(response.Body).Decode(&result) 42 | if err != nil { 43 | log.Println(err) 44 | } 45 | id_list := []int{} 46 | for _, item := range result["result"].([]interface{}) { 47 | itemMap := item.(map[string]interface{}) 48 | id := int(itemMap["id"].(float64)) 49 | id_list = append(id_list, id) 50 | types := itemMap["types"].(string) 51 | data := itemMap["data"].(string) 52 | 53 | // 处理interval字段,可能是string或float64 54 | var interval int 55 | switch v := itemMap["interval"].(type) { 56 | case string: 57 | interval, err = strconv.Atoi(v) 58 | if err != nil { 59 | log.Printf("解析interval字符串失败: %v,使用默认值60", err) 60 | interval = 60 61 | } 62 | case float64: 63 | interval = int(v) 64 | case int: 65 | interval = v 66 | default: 67 | log.Printf("未知的interval类型: %T,使用默认值60", v) 68 | interval = 60 69 | } 70 | 71 | new := Tmonitor{types: types, data: data, interval: interval} 72 | if oldValue, exists := monitor_list.Load(id); !exists || oldValue.(Tmonitor) != new { 73 | monitor_list.Store(id, new) 74 | go monitor_run(id) 75 | } 76 | } 77 | monitor_list.Range(func(key, value interface{}) bool { 78 | id := key.(int) 79 | found := false 80 | for _, listId := range id_list { 81 | if id == listId { 82 | found = true 83 | break 84 | } 85 | } 86 | if !found { 87 | log.Printf("监控任务ID %d 已从服务器删除,停止本地任务", id) 88 | monitor_list.Delete(id) 89 | } 90 | return true 91 | }) 92 | } 93 | } 94 | 95 | func monitor_run(id int) { 96 | defer func() { 97 | if err := recover(); err != nil { 98 | log.Printf("Monitor run error (ID: %d): %v", id, err) 99 | } 100 | }() 101 | 102 | value, exists := monitor_list.Load(id) 103 | if !exists { 104 | return 105 | } 106 | old := value.(Tmonitor) 107 | if old.types == "pcap" { 108 | type item struct { 109 | Name string `json:"name"` 110 | Description string `json:"description"` 111 | Bpf string `json:"bpf"` 112 | } 113 | var device item 114 | err := json.Unmarshal([]byte(old.data), &device) 115 | if err != nil { 116 | log.Printf("监控任务ID %d 解析设备配置失败: %v", id, err) 117 | return 118 | } 119 | 120 | log.Printf("监控任务ID %d 开始执行,设备: %s (%s), BPF过滤器: %s, 采集间隔: %d秒", 121 | id, device.Name, device.Description, device.Bpf, old.interval) 122 | 123 | for { 124 | // 在每次循环开始时检查任务是否还存在 125 | currentValue, exists := monitor_list.Load(id) 126 | if !exists || currentValue.(Tmonitor) != old || old.interval == 0 { 127 | log.Printf("监控任务ID %d 已停止或配置已更改", id) 128 | break 129 | } 130 | 131 | startTime := time.Now() 132 | log.Printf("监控任务ID %d 开始采集流量,开始时间: %s", id, startTime.Format("2006-01-02 15:04:05")) 133 | 134 | moniter_pcap(device.Name, device.Description, device.Bpf, time.Duration(old.interval)*time.Second) 135 | 136 | endTime := time.Now() 137 | duration := endTime.Sub(startTime) 138 | log.Printf("监控任务ID %d 采集完成,结束时间: %s,采集耗时: %v", 139 | id, endTime.Format("2006-01-02 15:04:05"), duration) 140 | 141 | // 如果采集时间小于间隔时间,则等待剩余时间 142 | if duration < time.Duration(old.interval)*time.Second { 143 | sleepTime := time.Duration(old.interval)*time.Second - duration 144 | log.Printf("监控任务ID %d 等待 %v 后进行下次采集", id, sleepTime) 145 | 146 | // 分段等待,每5秒检查一次任务是否还存在 147 | waitStart := time.Now() 148 | for time.Since(waitStart) < sleepTime { 149 | // 检查任务是否还存在 150 | currentValue, exists := monitor_list.Load(id) 151 | if !exists || currentValue.(Tmonitor) != old { 152 | log.Printf("监控任务ID %d 在等待期间被删除,停止等待", id) 153 | return 154 | } 155 | 156 | // 等待5秒或剩余时间(取较小值) 157 | remainingTime := sleepTime - time.Since(waitStart) 158 | if remainingTime <= 0 { 159 | break 160 | } 161 | 162 | checkInterval := 5 * time.Second 163 | if remainingTime < checkInterval { 164 | checkInterval = remainingTime 165 | } 166 | 167 | time.Sleep(checkInterval) 168 | } 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /service/webui/git.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "0E7/service/config" 5 | "0E7/service/git" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/gin-gonic/gin" 14 | ) 15 | 16 | // GitRepoInfo Git 仓库信息 17 | type GitRepoInfo struct { 18 | Name string `json:"name"` 19 | URL string `json:"url"` 20 | Description string `json:"description"` 21 | } 22 | 23 | // git_repo_list 获取所有 Git 仓库列表 24 | func git_repo_list(c *gin.Context) { 25 | gitDir := "git" 26 | 27 | // 检查 git 目录是否存在 28 | if _, err := os.Stat(gitDir); os.IsNotExist(err) { 29 | c.JSON(200, gin.H{ 30 | "status": "success", 31 | "data": []GitRepoInfo{}, 32 | }) 33 | return 34 | } 35 | 36 | var repos []GitRepoInfo 37 | 38 | // 获取服务器 URL 39 | serverURL := config.Server_url 40 | if serverURL == "" { 41 | // 如果没有配置,使用默认值 42 | if config.Server_tls { 43 | serverURL = fmt.Sprintf("https://localhost:%s", config.Server_port) 44 | } else { 45 | serverURL = fmt.Sprintf("http://localhost:%s", config.Server_port) 46 | } 47 | } 48 | 49 | // 遍历 git 目录 50 | entries, err := os.ReadDir(gitDir) 51 | if err != nil { 52 | log.Printf("读取 git 目录失败: %v", err) 53 | c.JSON(500, gin.H{ 54 | "status": "error", 55 | "msg": "读取仓库列表失败", 56 | }) 57 | return 58 | } 59 | 60 | for _, entry := range entries { 61 | if !entry.IsDir() { 62 | continue 63 | } 64 | 65 | repoPath := filepath.Join(gitDir, entry.Name()) 66 | headPath := filepath.Join(repoPath, "HEAD") 67 | objectsPath := filepath.Join(repoPath, "objects") 68 | 69 | // 检查是否是有效的 bare 仓库 70 | if _, err := os.Stat(headPath); err != nil { 71 | continue 72 | } 73 | if _, err := os.Stat(objectsPath); err != nil { 74 | continue 75 | } 76 | 77 | // 读取描述文件 78 | descriptionPath := filepath.Join(repoPath, "description") 79 | description := "Unnamed repository; edit this file 'description' to name the repository." 80 | if descData, err := ioutil.ReadFile(descriptionPath); err == nil { 81 | desc := strings.TrimSpace(string(descData)) 82 | if desc != "" && desc != "Unnamed repository; edit this file 'description' to name the repository." { 83 | description = desc 84 | } else { 85 | description = "" // 如果还是默认值,显示为空 86 | } 87 | } 88 | 89 | // 构建仓库 URL 90 | repoURL := fmt.Sprintf("%s/git/%s", serverURL, entry.Name()) 91 | 92 | repos = append(repos, GitRepoInfo{ 93 | Name: entry.Name(), 94 | URL: repoURL, 95 | Description: description, 96 | }) 97 | } 98 | 99 | c.JSON(200, gin.H{ 100 | "status": "success", 101 | "data": repos, 102 | }) 103 | } 104 | 105 | // git_repo_update_description 更新仓库描述 106 | func git_repo_update_description(c *gin.Context) { 107 | repoName := c.PostForm("name") 108 | description := c.PostForm("description") 109 | 110 | if repoName == "" { 111 | c.JSON(400, gin.H{ 112 | "status": "error", 113 | "msg": "仓库名称不能为空", 114 | }) 115 | return 116 | } 117 | 118 | // 如果以 .git 结尾,去除后缀 119 | repoName = strings.TrimSuffix(repoName, ".git") 120 | 121 | // 验证仓库名称(使用统一的验证函数) 122 | if !git.ValidateRepoName(repoName) { 123 | c.JSON(400, gin.H{ 124 | "status": "error", 125 | "msg": "无效的仓库名称,只能包含字母、数字、连字符(-)和下划线(_),且不能以连字符或下划线开头或结尾", 126 | }) 127 | return 128 | } 129 | 130 | repoPath := filepath.Join("git", repoName) 131 | descriptionPath := filepath.Join(repoPath, "description") 132 | 133 | // 检查仓库是否存在 134 | if _, err := os.Stat(repoPath); os.IsNotExist(err) { 135 | c.JSON(404, gin.H{ 136 | "status": "error", 137 | "msg": "仓库不存在", 138 | }) 139 | return 140 | } 141 | 142 | // 写入描述文件 143 | if err := ioutil.WriteFile(descriptionPath, []byte(description), 0644); err != nil { 144 | log.Printf("更新仓库描述失败: %v", err) 145 | c.JSON(500, gin.H{ 146 | "status": "error", 147 | "msg": "更新描述失败", 148 | }) 149 | return 150 | } 151 | 152 | c.JSON(200, gin.H{ 153 | "status": "success", 154 | "msg": "描述更新成功", 155 | }) 156 | } 157 | 158 | // git_repo_delete 删除仓库 159 | func git_repo_delete(c *gin.Context) { 160 | repoName := c.PostForm("name") 161 | 162 | if repoName == "" { 163 | c.JSON(400, gin.H{ 164 | "status": "error", 165 | "msg": "仓库名称不能为空", 166 | }) 167 | return 168 | } 169 | 170 | // 如果以 .git 结尾,去除后缀 171 | repoName = strings.TrimSuffix(repoName, ".git") 172 | 173 | // 验证仓库名称(使用统一的验证函数) 174 | if !git.ValidateRepoName(repoName) { 175 | c.JSON(400, gin.H{ 176 | "status": "error", 177 | "msg": "无效的仓库名称,只能包含字母、数字、连字符(-)和下划线(_),且不能以连字符或下划线开头或结尾", 178 | }) 179 | return 180 | } 181 | 182 | repoPath := filepath.Join("git", repoName) 183 | 184 | // 检查仓库是否存在 185 | if _, err := os.Stat(repoPath); os.IsNotExist(err) { 186 | c.JSON(404, gin.H{ 187 | "status": "error", 188 | "msg": "仓库不存在", 189 | }) 190 | return 191 | } 192 | 193 | // 删除仓库目录 194 | if err := os.RemoveAll(repoPath); err != nil { 195 | log.Printf("删除仓库失败: %v", err) 196 | c.JSON(500, gin.H{ 197 | "status": "error", 198 | "msg": "删除仓库失败", 199 | }) 200 | return 201 | } 202 | 203 | c.JSON(200, gin.H{ 204 | "status": "success", 205 | "msg": "仓库删除成功", 206 | }) 207 | } 208 | 209 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module 0E7 2 | 3 | go 1.25 4 | 5 | require ( 6 | github.com/andybalholm/brotli v1.0.5 7 | github.com/blevesearch/bleve/v2 v2.3.10 8 | github.com/elastic/go-elasticsearch/v8 v8.11.1 9 | github.com/fsnotify/fsnotify v1.9.0 10 | github.com/gin-contrib/gzip v0.0.6 11 | github.com/gin-gonic/gin v1.9.1 12 | github.com/google/gopacket v1.1.19 13 | github.com/google/uuid v1.6.0 14 | github.com/gorilla/websocket v1.5.3 15 | github.com/patrickmn/go-cache v2.1.0+incompatible 16 | github.com/shirou/gopsutil/v3 v3.23.9 17 | github.com/traefik/yaegi v0.15.1 18 | github.com/wailsapp/wails/v2 v2.11.0 19 | golang.org/x/net v0.46.0 20 | golang.org/x/sync v0.17.0 21 | golang.org/x/sys v0.37.0 22 | gopkg.in/ini.v1 v1.67.0 23 | gorm.io/driver/mysql v1.5.2 24 | gorm.io/driver/sqlite v1.6.0 25 | gorm.io/gorm v1.30.0 26 | ) 27 | 28 | require ( 29 | github.com/RoaringBitmap/roaring v1.2.3 // indirect 30 | github.com/bep/debounce v1.2.1 // indirect 31 | github.com/bits-and-blooms/bitset v1.2.0 // indirect 32 | github.com/blevesearch/bleve_index_api v1.0.6 // indirect 33 | github.com/blevesearch/geo v0.1.18 // indirect 34 | github.com/blevesearch/go-porterstemmer v1.0.3 // indirect 35 | github.com/blevesearch/gtreap v0.1.1 // indirect 36 | github.com/blevesearch/mmap-go v1.0.4 // indirect 37 | github.com/blevesearch/scorch_segment_api/v2 v2.1.6 // indirect 38 | github.com/blevesearch/segment v0.9.1 // indirect 39 | github.com/blevesearch/snowballstem v0.9.0 // indirect 40 | github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect 41 | github.com/blevesearch/vellum v1.0.10 // indirect 42 | github.com/blevesearch/zapx/v11 v11.3.10 // indirect 43 | github.com/blevesearch/zapx/v12 v12.3.10 // indirect 44 | github.com/blevesearch/zapx/v13 v13.3.10 // indirect 45 | github.com/blevesearch/zapx/v14 v14.3.10 // indirect 46 | github.com/blevesearch/zapx/v15 v15.3.13 // indirect 47 | github.com/bytedance/sonic v1.9.1 // indirect 48 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 49 | github.com/elastic/elastic-transport-go/v8 v8.3.0 // indirect 50 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 51 | github.com/gin-contrib/sse v0.1.0 // indirect 52 | github.com/go-ole/go-ole v1.3.0 // indirect 53 | github.com/go-playground/locales v0.14.1 // indirect 54 | github.com/go-playground/universal-translator v0.18.1 // indirect 55 | github.com/go-playground/validator/v10 v10.14.0 // indirect 56 | github.com/go-sql-driver/mysql v1.7.0 // indirect 57 | github.com/goccy/go-json v0.10.2 // indirect 58 | github.com/godbus/dbus/v5 v5.1.0 // indirect 59 | github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect 60 | github.com/golang/protobuf v1.5.0 // indirect 61 | github.com/golang/snappy v0.0.1 // indirect 62 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect 63 | github.com/jinzhu/inflection v1.0.0 // indirect 64 | github.com/jinzhu/now v1.1.5 // indirect 65 | github.com/json-iterator/go v1.1.12 // indirect 66 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 67 | github.com/kr/pretty v0.3.1 // indirect 68 | github.com/labstack/echo/v4 v4.13.3 // indirect 69 | github.com/labstack/gommon v0.4.2 // indirect 70 | github.com/leaanthony/go-ansi-parser v1.6.1 // indirect 71 | github.com/leaanthony/gosod v1.0.4 // indirect 72 | github.com/leaanthony/slicer v1.6.0 // indirect 73 | github.com/leaanthony/u v1.1.1 // indirect 74 | github.com/leodido/go-urn v1.2.4 // indirect 75 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 76 | github.com/mattn/go-colorable v0.1.13 // indirect 77 | github.com/mattn/go-isatty v0.0.20 // indirect 78 | github.com/mattn/go-sqlite3 v1.14.22 // indirect 79 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 80 | github.com/modern-go/reflect2 v1.0.2 // indirect 81 | github.com/mschoch/smat v0.2.0 // indirect 82 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 83 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 84 | github.com/pkg/errors v0.9.1 // indirect 85 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 86 | github.com/rivo/uniseg v0.4.7 // indirect 87 | github.com/rogpeppe/go-internal v1.10.0 // indirect 88 | github.com/samber/lo v1.49.1 // indirect 89 | github.com/shoenig/go-m1cpu v0.1.7 // indirect 90 | github.com/stretchr/testify v1.11.1 // indirect 91 | github.com/tklauser/go-sysconf v0.3.12 // indirect 92 | github.com/tklauser/numcpus v0.6.1 // indirect 93 | github.com/tkrajina/go-reflector v0.5.8 // indirect 94 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 95 | github.com/ugorji/go/codec v1.2.11 // indirect 96 | github.com/valyala/bytebufferpool v1.0.0 // indirect 97 | github.com/valyala/fasttemplate v1.2.2 // indirect 98 | github.com/wailsapp/go-webview2 v1.0.22 // indirect 99 | github.com/wailsapp/mimetype v1.4.1 // indirect 100 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 101 | go.etcd.io/bbolt v1.3.7 // indirect 102 | golang.org/x/arch v0.3.0 // indirect 103 | golang.org/x/crypto v0.43.0 // indirect 104 | golang.org/x/text v0.30.0 // indirect 105 | google.golang.org/protobuf v1.36.10 // indirect 106 | gopkg.in/yaml.v3 v3.0.1 // indirect 107 | ) 108 | -------------------------------------------------------------------------------- /service/client/monitor_pcap.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "0E7/service/config" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "mime/multipart" 12 | "net/http" 13 | "sync" 14 | "time" 15 | 16 | "github.com/google/gopacket" 17 | "github.com/google/gopacket/pcap" 18 | "github.com/google/gopacket/pcapgo" 19 | ) 20 | 21 | func moniter_pcap_device() string { 22 | devices, err := pcap.FindAllDevs() 23 | if err != nil { 24 | return "{}" 25 | } 26 | type item struct { 27 | Name string `json:"name"` 28 | Description string `json:"description"` 29 | } 30 | result := []item{} 31 | for _, device := range devices { 32 | result = append(result, item{Name: device.Name, Description: device.Description}) 33 | } 34 | device, err := json.Marshal(result) 35 | if err != nil { 36 | return "{}" 37 | } 38 | return string(device) 39 | } 40 | 41 | func moniter_pcap(device string, desc string, bpf string, timeout time.Duration) { 42 | var wg sync.WaitGroup 43 | if device != "" { 44 | log.Printf("开始采集设备 %s (%s) 的流量,BPF过滤器: %s,采集时长: %v", device, desc, bpf, timeout) 45 | wg.Add(1) 46 | go capture(device, desc, bpf, timeout, &wg) 47 | } else { 48 | log.Printf("开始采集所有设备的流量,BPF过滤器: %s,采集时长: %v", bpf, timeout) 49 | devices, err := pcap.FindAllDevs() 50 | if err != nil { 51 | log.Printf("查找网络设备失败: %v", err) 52 | return 53 | } 54 | log.Printf("找到 %d 个网络设备", len(devices)) 55 | for _, device := range devices { 56 | wg.Add(1) 57 | go capture(device.Name, device.Description, bpf, timeout, &wg) 58 | } 59 | } 60 | wg.Wait() 61 | log.Printf("所有设备流量采集完成") 62 | } 63 | 64 | func capture(device string, desc string, bpf string, timeout time.Duration, wg *sync.WaitGroup) (err error) { 65 | defer wg.Done() 66 | 67 | log.Printf("设备 %s 开始初始化流量采集", device) 68 | handle, err := pcap.OpenLive(device, 65536, true, timeout) 69 | if err != nil { 70 | log.Printf("设备 %s 打开失败: %v", device, err) 71 | return err 72 | } 73 | defer handle.Close() 74 | log.Printf("设备 %s 初始化成功,链路类型: %v", device, handle.LinkType()) 75 | 76 | buffer := new(bytes.Buffer) 77 | writer_pcap := pcapgo.NewWriter(buffer) 78 | if err != nil { 79 | log.Printf("设备 %s 创建PCAP写入器失败: %v", device, err) 80 | return err 81 | } 82 | 83 | err = writer_pcap.WriteFileHeader(65536, handle.LinkType()) 84 | if err != nil { 85 | log.Printf("设备 %s 写入PCAP文件头失败: %v", device, err) 86 | return err 87 | } 88 | 89 | if bpf != "" { 90 | err = handle.SetBPFFilter(bpf) 91 | if err != nil { 92 | log.Printf("设备 %s 设置BPF过滤器 '%s' 失败: %v", device, bpf, err) 93 | return err 94 | } 95 | log.Printf("设备 %s BPF过滤器设置成功: %s", device, bpf) 96 | } else { 97 | log.Printf("设备 %s 未设置BPF过滤器,将采集所有数据包", device) 98 | } 99 | 100 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 101 | defer cancel() 102 | 103 | packetSource := gopacket.NewPacketSource(handle, handle.LinkType()) 104 | packetCount := 0 105 | startTime := time.Now() 106 | 107 | log.Printf("设备 %s 开始采集数据包,采集时长: %v", device, timeout) 108 | 109 | for { 110 | select { 111 | case packet := <-packetSource.Packets(): 112 | packetCount++ 113 | err = writer_pcap.WritePacket(packet.Metadata().CaptureInfo, packet.Data()) 114 | if err != nil { 115 | log.Printf("设备 %s 写入数据包失败: %v", device, err) 116 | return err 117 | } 118 | case <-ctx.Done(): 119 | endTime := time.Now() 120 | duration := endTime.Sub(startTime) 121 | 122 | // 修复文件名生成 123 | fileName := fmt.Sprintf("%d_%s_%d.pcap", 124 | config.Client_id, 125 | device, 126 | startTime.Unix()) 127 | 128 | // 检查是否采集到数据包 129 | if packetCount == 0 { 130 | log.Printf("设备 %s 采集完成,但未采集到任何数据包,跳过上传", device) 131 | return nil 132 | } 133 | 134 | log.Printf("设备 %s 采集完成,采集时长: %v,数据包数量: %d,文件名: %s,文件大小: %d 字节", 135 | device, duration, packetCount, fileName, buffer.Len()) 136 | 137 | body := &bytes.Buffer{} 138 | writer_file := multipart.NewWriter(body) 139 | fileWriter, err := writer_file.CreateFormFile("file", fileName) 140 | if err != nil { 141 | log.Printf("设备 %s 创建文件写入器失败: %v", device, err) 142 | return err 143 | } 144 | 145 | _, err = buffer.WriteTo(fileWriter) 146 | if err != nil { 147 | log.Printf("设备 %s 写入文件数据失败: %v", device, err) 148 | return err 149 | } 150 | 151 | err = writer_file.Close() 152 | if err != nil { 153 | log.Printf("设备 %s 关闭文件写入器失败: %v", device, err) 154 | return err 155 | } 156 | 157 | log.Printf("设备 %s 开始上传文件 %s,上传数据大小: %d 字节", device, fileName, body.Len()) 158 | 159 | request, err := http.NewRequest("POST", config.Server_url+"/webui/pcap_upload", body) 160 | if err != nil { 161 | log.Printf("设备 %s 创建上传请求失败: %v", device, err) 162 | return err 163 | } 164 | request.Header.Set("Content-Type", writer_file.FormDataContentType()) 165 | 166 | response, err := client.Do(request) 167 | if err != nil { 168 | log.Printf("设备 %s 上传文件失败: %v", device, err) 169 | return err 170 | } 171 | defer response.Body.Close() 172 | 173 | if response.StatusCode != 200 { 174 | log.Printf("设备 %s 上传文件失败,状态码: %d", device, response.StatusCode) 175 | return errors.New("upload failed") 176 | } 177 | 178 | log.Printf("设备 %s 文件上传成功", device) 179 | return nil 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /service/pcap/udp.go: -------------------------------------------------------------------------------- 1 | package pcap 2 | 3 | import ( 4 | "encoding/base64" 5 | "sync" 6 | "time" 7 | 8 | "github.com/google/gopacket" 9 | "github.com/google/gopacket/layers" 10 | ) 11 | 12 | // UDP流结构 13 | type udpStream struct { 14 | net, transport gopacket.Flow 15 | source string 16 | FlowItems []FlowItem 17 | src_port layers.UDPPort 18 | dst_port layers.UDPPort 19 | total_size int 20 | num_packets int 21 | last_seen time.Time 22 | timeout time.Duration 23 | reassemblyCallback func(FlowEntry) 24 | // 保存所有原始数据包,用于Wireshark分析 25 | originalPackets [][]byte 26 | linktype layers.LinkType 27 | sync.Mutex 28 | } 29 | 30 | // UDP流工厂 31 | type udpStreamFactory struct { 32 | source string 33 | reassemblyCallback func(FlowEntry) 34 | streams map[string]*udpStream 35 | timeout time.Duration 36 | sync.Mutex 37 | linktype layers.LinkType 38 | } 39 | 40 | // 创建新的UDP流工厂 41 | func newUDPStreamFactory(source string, reassemblyCallback func(FlowEntry)) *udpStreamFactory { 42 | return &udpStreamFactory{ 43 | source: source, 44 | reassemblyCallback: reassemblyCallback, 45 | streams: make(map[string]*udpStream), 46 | timeout: 30 * time.Second, // UDP流超时时间 47 | } 48 | } 49 | 50 | // 获取或创建UDP流 51 | func (factory *udpStreamFactory) getOrCreateStream(net, transport gopacket.Flow, udp *layers.UDP) *udpStream { 52 | factory.Lock() 53 | defer factory.Unlock() 54 | 55 | // 创建流的唯一标识符 56 | streamKey := net.String() + ":" + transport.String() 57 | 58 | stream, exists := factory.streams[streamKey] 59 | if !exists { 60 | stream = &udpStream{ 61 | net: net, 62 | transport: transport, 63 | source: factory.source, 64 | FlowItems: []FlowItem{}, 65 | src_port: udp.SrcPort, 66 | dst_port: udp.DstPort, 67 | reassemblyCallback: factory.reassemblyCallback, 68 | timeout: factory.timeout, 69 | last_seen: time.Now(), 70 | linktype: factory.linktype, 71 | } 72 | factory.streams[streamKey] = stream 73 | } 74 | 75 | stream.last_seen = time.Now() 76 | return stream 77 | } 78 | 79 | // 处理UDP数据包 80 | func (factory *udpStreamFactory) ProcessPacket(packet gopacket.Packet) { 81 | udpLayer := packet.Layer(layers.LayerTypeUDP) 82 | if udpLayer == nil { 83 | return 84 | } 85 | 86 | udp := udpLayer.(*layers.UDP) 87 | netLayer := packet.NetworkLayer() 88 | if netLayer == nil { 89 | return 90 | } 91 | 92 | net := netLayer.NetworkFlow() 93 | transport := gopacket.NewFlow(layers.EndpointUDPPort, []byte{byte(udp.SrcPort >> 8), byte(udp.SrcPort)}, []byte{byte(udp.DstPort >> 8), byte(udp.DstPort)}) 94 | 95 | stream := factory.getOrCreateStream(net, transport, udp) 96 | stream.addPacket(packet, udp) 97 | } 98 | 99 | // 向UDP流添加数据包 100 | func (s *udpStream) addPacket(packet gopacket.Packet, udp *layers.UDP) { 101 | s.Lock() 102 | defer s.Unlock() 103 | 104 | s.num_packets++ 105 | 106 | // 获取数据包时间戳 107 | timestamp := packet.Metadata().CaptureInfo.Timestamp 108 | 109 | // 确定数据方向:基于当前数据包与流初始方向对比 110 | var from string 111 | if nl := packet.NetworkLayer(); nl != nil { 112 | curNet := nl.NetworkFlow() 113 | if curNet.Src().String() == s.net.Src().String() { 114 | from = "c" // 客户端到服务器 115 | } else { 116 | from = "s" // 服务器到客户端 117 | } 118 | } else { 119 | from = "c" 120 | } 121 | 122 | // 获取UDP载荷数据 123 | payload := udp.Payload 124 | if len(payload) == 0 { 125 | return 126 | } 127 | 128 | // 保存原始数据包到流中(设置上限以避免内存过大) 129 | const maxOriginalPackets = 1000 130 | if len(s.originalPackets) < maxOriginalPackets { 131 | dataCopy := make([]byte, len(packet.Data())) 132 | copy(dataCopy, packet.Data()) 133 | s.originalPackets = append(s.originalPackets, dataCopy) 134 | } 135 | 136 | flowItem := FlowItem{ 137 | B64: base64.StdEncoding.EncodeToString(payload), 138 | From: from, 139 | Time: int(timestamp.UnixNano() / 1000000), 140 | } 141 | 142 | s.FlowItems = append(s.FlowItems, flowItem) 143 | s.total_size += len(payload) 144 | } 145 | 146 | // 检查并清理超时的UDP流 147 | func (factory *udpStreamFactory) cleanupExpiredStreams() { 148 | factory.Lock() 149 | defer factory.Unlock() 150 | 151 | now := time.Now() 152 | for key, stream := range factory.streams { 153 | if now.Sub(stream.last_seen) > stream.timeout { 154 | // 流已超时,处理并删除 155 | stream.finalize() 156 | delete(factory.streams, key) 157 | } 158 | } 159 | } 160 | 161 | // 完成UDP流处理 162 | func (s *udpStream) finalize() { 163 | if len(s.FlowItems) == 0 { 164 | return 165 | } 166 | 167 | // 找到最小和最大时间戳来计算准确的持续时间 168 | minTime := s.FlowItems[0].Time 169 | maxTime := s.FlowItems[0].Time 170 | 171 | for _, item := range s.FlowItems { 172 | if item.Time < minTime { 173 | minTime = item.Time 174 | } 175 | if item.Time > maxTime { 176 | maxTime = item.Time 177 | } 178 | } 179 | 180 | time := minTime 181 | duration := maxTime - minTime 182 | 183 | // 获取网络端点信息 184 | src, dst := s.net.Endpoints() 185 | 186 | // 创建FlowEntry 187 | entry := FlowEntry{ 188 | SrcPort: int(s.src_port), 189 | DstPort: int(s.dst_port), 190 | SrcIp: src.String(), 191 | DstIp: dst.String(), 192 | Time: time, 193 | Duration: duration, 194 | NumPackets: s.num_packets, 195 | Blocked: false, 196 | Filename: s.source, 197 | Flow: s.FlowItems, 198 | Size: s.total_size, 199 | OriginalPackets: s.originalPackets, 200 | LinkType: s.linktype, 201 | Tags: []string{"UDP"}, 202 | } 203 | 204 | // 调用重组回调函数 205 | s.reassemblyCallback(entry) 206 | } 207 | 208 | // 读取工厂的链路类型(简化访问) 209 | // 删除占位方法(不再需要) 210 | 211 | // 强制完成所有UDP流 212 | func (factory *udpStreamFactory) FlushAll() { 213 | factory.Lock() 214 | defer factory.Unlock() 215 | 216 | for _, stream := range factory.streams { 217 | stream.finalize() 218 | } 219 | factory.streams = make(map[string]*udpStream) 220 | } 221 | -------------------------------------------------------------------------------- /service/webui/terminal.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "0E7/service/config" 5 | "0E7/service/database" 6 | "encoding/json" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | // 获取所有在线客户端 14 | func getClients(c *gin.Context) { 15 | var clients []database.Client 16 | 17 | // 获取最近1分钟内有心跳的客户端 18 | oneMinuteAgo := time.Now().Add(-1 * time.Minute) 19 | err := config.Db.Where("updated_at > ?", oneMinuteAgo).Find(&clients).Error 20 | if err != nil { 21 | c.JSON(500, gin.H{ 22 | "message": "fail", 23 | "error": err.Error(), 24 | "result": []interface{}{}, 25 | }) 26 | return 27 | } 28 | 29 | // 解析每个客户端的网卡信息 30 | result := make([]map[string]interface{}, 0) // 确保始终是数组而不是 nil 31 | for _, client := range clients { 32 | clientInfo := map[string]interface{}{ 33 | "id": client.ID, 34 | "name": client.Name, 35 | "hostname": client.Hostname, 36 | "platform": client.Platform, 37 | "arch": client.Arch, 38 | "cpu": client.CPU, 39 | "cpu_use": client.CPUUse, 40 | "memory_use": client.MemoryUse, 41 | "memory_max": client.MemoryMax, 42 | "updated_at": client.UpdatedAt, 43 | "interfaces": []map[string]interface{}{}, 44 | } 45 | 46 | // 解析网卡信息 47 | if client.Pcap != "" { 48 | var interfaces []map[string]interface{} 49 | err := json.Unmarshal([]byte(client.Pcap), &interfaces) 50 | if err == nil { 51 | clientInfo["interfaces"] = interfaces 52 | } 53 | } 54 | 55 | result = append(result, clientInfo) 56 | } 57 | 58 | c.JSON(200, gin.H{ 59 | "message": "success", 60 | "error": "", 61 | "result": result, 62 | }) 63 | } 64 | 65 | // 下发流量采集任务 66 | func createTrafficCollection(c *gin.Context) { 67 | clientIdStr := c.PostForm("client_id") 68 | interfaceName := c.PostForm("interface_name") // 网卡名称,为空表示所有网卡 69 | bpf := c.PostForm("bpf") // BPF过滤器,为空表示采集所有流量 70 | intervalStr := c.PostForm("interval") // 采集间隔,默认60秒 71 | description := c.PostForm("description") // 任务描述 72 | 73 | if clientIdStr == "" { 74 | c.JSON(400, gin.H{ 75 | "message": "fail", 76 | "error": "missing client_id parameter", 77 | "result": "", 78 | }) 79 | return 80 | } 81 | 82 | clientId, err := strconv.Atoi(clientIdStr) 83 | if err != nil { 84 | c.JSON(400, gin.H{ 85 | "message": "fail", 86 | "error": "invalid client_id: " + err.Error(), 87 | "result": "", 88 | }) 89 | return 90 | } 91 | 92 | // 解析采集间隔 93 | interval := 60 // 默认60秒 94 | if intervalStr != "" { 95 | if parsedInterval, err := strconv.Atoi(intervalStr); err == nil && parsedInterval > 0 { 96 | interval = parsedInterval 97 | } 98 | } 99 | 100 | // 构建监控任务数据 101 | taskData := map[string]interface{}{ 102 | "name": interfaceName, 103 | "description": description, 104 | "bpf": bpf, 105 | } 106 | 107 | taskDataJSON, err := json.Marshal(taskData) 108 | if err != nil { 109 | c.JSON(500, gin.H{ 110 | "message": "fail", 111 | "error": "failed to marshal task data: " + err.Error(), 112 | "result": "", 113 | }) 114 | return 115 | } 116 | 117 | // 创建监控任务 118 | monitor := database.Monitor{ 119 | ClientId: clientId, 120 | Name: interfaceName, 121 | Types: "pcap", 122 | Data: string(taskDataJSON), 123 | Interval: interval, 124 | } 125 | 126 | err = config.Db.Create(&monitor).Error 127 | if err != nil { 128 | c.JSON(500, gin.H{ 129 | "message": "fail", 130 | "error": "failed to create monitor task: " + err.Error(), 131 | "result": "", 132 | }) 133 | return 134 | } 135 | 136 | c.JSON(200, gin.H{ 137 | "message": "success", 138 | "error": "", 139 | "result": map[string]interface{}{ 140 | "id": monitor.ID, 141 | "client_id": monitor.ClientId, 142 | "name": monitor.Name, 143 | "types": monitor.Types, 144 | "data": monitor.Data, 145 | "interval": monitor.Interval, 146 | }, 147 | }) 148 | } 149 | 150 | // 获取客户端的监控任务列表 151 | func getClientMonitors(c *gin.Context) { 152 | clientIdStr := c.PostForm("client_id") 153 | if clientIdStr == "" { 154 | c.JSON(400, gin.H{ 155 | "message": "fail", 156 | "error": "missing client_id parameter", 157 | "result": []interface{}{}, 158 | }) 159 | return 160 | } 161 | 162 | clientId, err := strconv.Atoi(clientIdStr) 163 | if err != nil { 164 | c.JSON(400, gin.H{ 165 | "message": "fail", 166 | "error": "invalid client_id: " + err.Error(), 167 | "result": []interface{}{}, 168 | }) 169 | return 170 | } 171 | 172 | var monitors []database.Monitor 173 | err = config.Db.Where("client_id = ?", clientId).Find(&monitors).Error 174 | if err != nil { 175 | c.JSON(500, gin.H{ 176 | "message": "fail", 177 | "error": err.Error(), 178 | "result": []interface{}{}, 179 | }) 180 | return 181 | } 182 | 183 | result := make([]map[string]interface{}, 0) // 确保始终是数组而不是 nil 184 | for _, monitor := range monitors { 185 | result = append(result, map[string]interface{}{ 186 | "id": monitor.ID, 187 | "client_id": monitor.ClientId, 188 | "name": monitor.Name, 189 | "types": monitor.Types, 190 | "data": monitor.Data, 191 | "interval": monitor.Interval, 192 | "created_at": monitor.CreatedAt, 193 | "updated_at": monitor.UpdatedAt, 194 | }) 195 | } 196 | 197 | c.JSON(200, gin.H{ 198 | "message": "success", 199 | "error": "", 200 | "result": result, 201 | }) 202 | } 203 | 204 | // 删除监控任务 205 | func deleteMonitor(c *gin.Context) { 206 | monitorIdStr := c.PostForm("monitor_id") 207 | if monitorIdStr == "" { 208 | c.JSON(400, gin.H{ 209 | "message": "fail", 210 | "error": "missing monitor_id parameter", 211 | "result": "", 212 | }) 213 | return 214 | } 215 | 216 | monitorId, err := strconv.Atoi(monitorIdStr) 217 | if err != nil { 218 | c.JSON(400, gin.H{ 219 | "message": "fail", 220 | "error": "invalid monitor_id: " + err.Error(), 221 | "result": "", 222 | }) 223 | return 224 | } 225 | 226 | err = config.Db.Delete(&database.Monitor{}, monitorId).Error 227 | if err != nil { 228 | c.JSON(500, gin.H{ 229 | "message": "fail", 230 | "error": "failed to delete monitor: " + err.Error(), 231 | "result": "", 232 | }) 233 | return 234 | } 235 | 236 | c.JSON(200, gin.H{ 237 | "message": "success", 238 | "error": "", 239 | "result": "monitor deleted successfully", 240 | }) 241 | } 242 | 243 | -------------------------------------------------------------------------------- /service/proxy/handler.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "0E7/service/config" 5 | "bytes" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | var manager *Manager 18 | 19 | func initManager() { 20 | if manager == nil { 21 | // default expiration is not used directly; we pass per-request TTL. 22 | manager = NewManager(0, 1*time.Minute, config.Proxy_retain_duration) 23 | } 24 | } 25 | 26 | func RegisterRoutes(r *gin.Engine) { 27 | initManager() 28 | r.Any("/proxy/:ttl/*upstream", handleProxy) 29 | } 30 | 31 | func ListCacheEntries() []CacheEntryMeta { 32 | initManager() 33 | return manager.List() 34 | } 35 | 36 | func handleProxy(c *gin.Context) { 37 | 38 | // parse TTL duration 39 | ttlStr := c.Param("ttl") 40 | if ttlStr == "" { 41 | c.String(http.StatusBadRequest, "missing ttl") 42 | return 43 | } 44 | var ttl time.Duration 45 | var err error 46 | if ttlStr == "0s" || ttlStr == "0" { 47 | ttl = 0 48 | } else { 49 | ttl, err = time.ParseDuration(ttlStr) 50 | if err != nil || ttl < 0 { 51 | c.String(http.StatusBadRequest, "invalid ttl") 52 | return 53 | } 54 | } 55 | 56 | upstreamRaw := strings.TrimPrefix(c.Param("upstream"), "/") 57 | if upstreamRaw == "" { 58 | c.String(http.StatusBadRequest, "missing upstream url") 59 | return 60 | } 61 | // ensure it's a full URL 62 | if !strings.HasPrefix(upstreamRaw, "http://") && !strings.HasPrefix(upstreamRaw, "https://") { 63 | upstreamRaw = "http://" + upstreamRaw 64 | } 65 | upstreamURL, err := url.Parse(upstreamRaw) 66 | if err != nil { 67 | c.String(http.StatusBadRequest, "invalid upstream url") 68 | return 69 | } 70 | 71 | // read body for cache key and forwarding 72 | bodyBytes, _ := ioutil.ReadAll(c.Request.Body) 73 | c.Request.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) 74 | 75 | method := c.Request.Method 76 | 77 | // Try cache 78 | if ttl > 0 { 79 | if resp, _, ok := manager.Get(method, upstreamURL.String(), bodyBytes); ok && resp != nil { 80 | // If expired but within stale window, refresh in background 81 | if time.Now().After(resp.ExpiresAt) { 82 | go func(method string, u *url.URL, body []byte, ttl time.Duration) { 83 | _ = refreshOnce(method, u, body, ttl) 84 | }(method, upstreamURL, bodyBytes, ttl) 85 | } 86 | // increment hits when actually serving from cache 87 | manager.Increment(method, upstreamURL.String(), bodyBytes) 88 | // add cache meta headers 89 | h := cloneHeader(resp.Header) 90 | remain := int(time.Until(resp.ExpiresAt).Seconds()) 91 | if remain < 0 { 92 | remain = 0 93 | } 94 | h.Set("proxy-cache-ttl", strconv.Itoa(remain)) 95 | h.Set("proxy-cache-expire", resp.ExpiresAt.UTC().Format(time.RFC3339)) 96 | writeResponse(c, resp.StatusCode, h, bytes.NewReader(resp.Body)) 97 | return 98 | } 99 | } 100 | 101 | // Forward request 102 | client := &http.Client{Timeout: time.Duration(config.Global_timeout_http) * time.Second} 103 | forwardReq, err := http.NewRequestWithContext(c.Request.Context(), method, upstreamURL.String(), bytes.NewReader(bodyBytes)) 104 | if err != nil { 105 | c.String(http.StatusBadGateway, "forward request build failed") 106 | return 107 | } 108 | copyHeaders(c.Request.Header, &forwardReq.Header) 109 | forwardReq.Host = upstreamURL.Host 110 | 111 | resp, err := client.Do(forwardReq) 112 | if err != nil { 113 | c.String(http.StatusBadGateway, err.Error()) 114 | return 115 | } 116 | defer resp.Body.Close() 117 | body, _ := io.ReadAll(resp.Body) 118 | // cache only if enabled and status allowed 119 | shouldCache := ttl > 0 && (!config.Proxy_cache_2xx_only || (resp.StatusCode >= 200 && resp.StatusCode < 300)) 120 | hdr := cloneHeader(resp.Header) 121 | if shouldCache { 122 | now := time.Now() 123 | cr := &CachedResponse{ 124 | StatusCode: resp.StatusCode, 125 | Header: cloneHeader(resp.Header), 126 | Body: body, 127 | CachedAt: now, 128 | TTL: ttl, 129 | ExpiresAt: now.Add(ttl), 130 | } 131 | manager.Set(method, upstreamURL.String(), bodyBytes, cr) 132 | remain := int(time.Until(cr.ExpiresAt).Seconds()) 133 | if remain < 0 { 134 | remain = 0 135 | } 136 | hdr.Set("proxy-cache-ttl", strconv.Itoa(remain)) 137 | hdr.Set("proxy-cache-expire", cr.ExpiresAt.UTC().Format(time.RFC3339)) 138 | } else { 139 | // 即使未缓存(如 ttl=0 或状态不满足策略),也返回提示头 140 | remain := int(ttl.Seconds()) 141 | if remain < 0 { 142 | remain = 0 143 | } 144 | hdr.Set("proxy-cache-ttl", strconv.Itoa(remain)) 145 | hdr.Set("proxy-cache-expire", time.Now().Add(ttl).UTC().Format(time.RFC3339)) 146 | } 147 | writeResponse(c, resp.StatusCode, hdr, bytes.NewReader(body)) 148 | } 149 | 150 | func refreshOnce(method string, u *url.URL, body []byte, ttl time.Duration) error { 151 | client := &http.Client{Timeout: time.Duration(config.Global_timeout_http) * time.Second} 152 | req, err := http.NewRequest(method, u.String(), bytes.NewReader(body)) 153 | if err != nil { 154 | return err 155 | } 156 | resp, err := client.Do(req) 157 | if err != nil { 158 | return err 159 | } 160 | defer resp.Body.Close() 161 | b, _ := io.ReadAll(resp.Body) 162 | shouldCache := ttl > 0 && (!config.Proxy_cache_2xx_only || (resp.StatusCode >= 200 && resp.StatusCode < 300)) 163 | if shouldCache { 164 | cr := &CachedResponse{ 165 | StatusCode: resp.StatusCode, 166 | Header: cloneHeader(resp.Header), 167 | Body: b, 168 | CachedAt: time.Now(), 169 | TTL: ttl, 170 | ExpiresAt: time.Now().Add(ttl), 171 | } 172 | manager.Set(method, u.String(), body, cr) 173 | } 174 | return nil 175 | } 176 | 177 | func writeResponse(c *gin.Context, status int, hdr http.Header, body io.Reader) { 178 | for k, vals := range hdr { 179 | for _, v := range vals { 180 | c.Writer.Header().Add(k, v) 181 | } 182 | } 183 | c.Status(status) 184 | if body != nil { 185 | io.Copy(c.Writer, body) 186 | } 187 | } 188 | 189 | func copyHeaders(src http.Header, dst *http.Header) { 190 | for k, v := range src { 191 | // skip hop-by-hop headers 192 | lk := strings.ToLower(k) 193 | if lk == "connection" || lk == "proxy-connection" || lk == "keep-alive" || lk == "proxy-authenticate" || lk == "proxy-authorization" || lk == "te" || lk == "trailers" || lk == "transfer-encoding" || lk == "upgrade" { 194 | continue 195 | } 196 | for _, vv := range v { 197 | dst.Add(k, vv) 198 | } 199 | } 200 | } 201 | 202 | func cloneHeader(h http.Header) http.Header { 203 | cl := http.Header{} 204 | for k, v := range h { 205 | vv := make([]string, len(v)) 206 | copy(vv, v) 207 | cl[k] = vv 208 | } 209 | return cl 210 | } 211 | -------------------------------------------------------------------------------- /service/proxy/cache.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "net/http" 7 | "sync" 8 | "sync/atomic" 9 | "time" 10 | 11 | cache "github.com/patrickmn/go-cache" 12 | ) 13 | 14 | type CachedResponse struct { 15 | StatusCode int 16 | Header http.Header 17 | Body []byte 18 | CachedAt time.Time 19 | TTL time.Duration 20 | ExpiresAt time.Time 21 | StaleUntil time.Time 22 | } 23 | 24 | type EntryStatus string 25 | 26 | const ( 27 | StatusActive EntryStatus = "active" 28 | StatusStale EntryStatus = "stale" 29 | StatusExpired EntryStatus = "expired" 30 | ) 31 | 32 | type CacheEntryMeta struct { 33 | Key string 34 | Method string 35 | URL string 36 | BodyHash string 37 | Status EntryStatus 38 | CachedAt time.Time 39 | ExpiresAt time.Time 40 | StaleUntil time.Time 41 | TTL time.Duration 42 | StatusCode int 43 | Hits int64 // 导出以便 JSON 返回 44 | BodySnapshot []byte 45 | HeaderSnapshot http.Header 46 | } 47 | 48 | type Manager struct { 49 | data *cache.Cache 50 | mu sync.RWMutex 51 | metas map[string]*CacheEntryMeta 52 | retain time.Duration 53 | stopCh chan struct{} 54 | } 55 | 56 | func NewManager(defaultExpiration, cleanupInterval, retain time.Duration) *Manager { 57 | m := &Manager{ 58 | data: cache.New(defaultExpiration, cleanupInterval), 59 | metas: make(map[string]*CacheEntryMeta), 60 | retain: retain, 61 | stopCh: make(chan struct{}), 62 | } 63 | go m.metaJanitor(5 * time.Minute) 64 | return m 65 | } 66 | 67 | func (m *Manager) makeKey(method, url string, body []byte) (string, string) { 68 | // key: METHOD + URL + sha256(body) 69 | h := sha256.Sum256(body) 70 | bodyHash := hex.EncodeToString(h[:]) 71 | return method + " " + url + " " + bodyHash, bodyHash 72 | } 73 | 74 | func (m *Manager) Get(method, url string, body []byte) (*CachedResponse, *CacheEntryMeta, bool) { 75 | key, _ := m.makeKey(method, url, body) 76 | if v, found := m.data.Get(key); found { 77 | resp := v.(*CachedResponse) 78 | m.mu.Lock() 79 | if meta, ok := m.metas[key]; ok { 80 | meta.Status = StatusActive 81 | meta.StatusCode = resp.StatusCode 82 | } 83 | m.mu.Unlock() 84 | return resp, m.snapshotMeta(key), true 85 | } 86 | // miss: check if meta exists and within stale period 87 | m.mu.RLock() 88 | meta, ok := m.metas[key] 89 | m.mu.RUnlock() 90 | if ok && time.Now().Before(meta.StaleUntil) { 91 | if v, found := m.data.Get(key); found { 92 | return v.(*CachedResponse), m.snapshotMeta(key), true 93 | } 94 | // serve from meta snapshot (stale) 95 | return &CachedResponse{ 96 | StatusCode: meta.StatusCode, 97 | Header: cloneHeader(meta.HeaderSnapshot), 98 | Body: append([]byte(nil), meta.BodySnapshot...), 99 | CachedAt: meta.CachedAt, 100 | TTL: meta.TTL, 101 | ExpiresAt: meta.ExpiresAt, 102 | }, m.snapshotMeta(key), true 103 | } 104 | return nil, nil, false 105 | } 106 | 107 | func (m *Manager) Set(method, url string, body []byte, resp *CachedResponse) { 108 | key, bodyHash := m.makeKey(method, url, body) 109 | m.data.Set(key, resp, resp.TTL) 110 | m.mu.Lock() 111 | defer m.mu.Unlock() 112 | if existing, ok := m.metas[key]; ok { 113 | // 刷新:更新动态字段,保留首次时间与初始 TTL 114 | existing.Status = StatusActive 115 | existing.CachedAt = resp.CachedAt 116 | existing.ExpiresAt = resp.ExpiresAt 117 | existing.StaleUntil = resp.ExpiresAt.Add(m.retain) 118 | existing.TTL = resp.TTL 119 | existing.StatusCode = resp.StatusCode 120 | existing.BodySnapshot = append(existing.BodySnapshot[:0], resp.Body...) 121 | existing.HeaderSnapshot = cloneHeader(resp.Header) 122 | } else { 123 | meta := &CacheEntryMeta{ 124 | Key: key, 125 | Method: method, 126 | URL: url, 127 | BodyHash: bodyHash, 128 | Status: StatusActive, 129 | CachedAt: resp.CachedAt, 130 | ExpiresAt: resp.ExpiresAt, 131 | StaleUntil: resp.ExpiresAt.Add(m.retain), 132 | TTL: resp.TTL, 133 | StatusCode: resp.StatusCode, 134 | BodySnapshot: append([]byte(nil), resp.Body...), 135 | HeaderSnapshot: cloneHeader(resp.Header), 136 | } 137 | m.metas[key] = meta 138 | } 139 | } 140 | 141 | func (m *Manager) MarkStale(key string, until time.Time) { 142 | m.mu.Lock() 143 | defer m.mu.Unlock() 144 | if meta, ok := m.metas[key]; ok { 145 | meta.Status = StatusStale 146 | meta.StaleUntil = until 147 | } 148 | } 149 | 150 | func (m *Manager) SetExpired(key string) { 151 | m.mu.Lock() 152 | defer m.mu.Unlock() 153 | if meta, ok := m.metas[key]; ok { 154 | meta.Status = StatusExpired 155 | } 156 | } 157 | 158 | func (m *Manager) snapshotMeta(key string) *CacheEntryMeta { 159 | m.mu.RLock() 160 | defer m.mu.RUnlock() 161 | if meta, ok := m.metas[key]; ok { 162 | copy := *meta 163 | return © 164 | } 165 | return nil 166 | } 167 | 168 | func (m *Manager) List() []CacheEntryMeta { 169 | m.mu.RLock() 170 | defer m.mu.RUnlock() 171 | list := make([]CacheEntryMeta, 0, len(m.metas)) 172 | for _, v := range m.metas { 173 | copy := *v 174 | copy.Hits = atomic.LoadInt64(&v.Hits) 175 | list = append(list, copy) 176 | } 177 | return list 178 | } 179 | 180 | // Close 停止后台清理(预留) 181 | func (m *Manager) Close() { 182 | close(m.stopCh) 183 | } 184 | 185 | // 根据策略清理:now > ExpiresAt + 2*TTL 则删除元信息与缓存值 186 | func (m *Manager) metaJanitor(interval time.Duration) { 187 | if interval <= 0 { 188 | interval = 5 * time.Minute 189 | } 190 | ticker := time.NewTicker(interval) 191 | defer ticker.Stop() 192 | for { 193 | select { 194 | case <-ticker.C: 195 | now := time.Now() 196 | m.mu.Lock() 197 | for k, meta := range m.metas { 198 | // 清理阈值:now > ExpiresAt + TTL 199 | ttl := meta.TTL 200 | if ttl < 0 { 201 | ttl = 0 202 | } 203 | cutoff := meta.ExpiresAt.Add(ttl) 204 | if now.After(cutoff) { 205 | // 从 go-cache 与 metas 中删除 206 | m.data.Delete(k) 207 | delete(m.metas, k) 208 | } 209 | } 210 | m.mu.Unlock() 211 | case <-m.stopCh: 212 | return 213 | } 214 | } 215 | } 216 | 217 | // Increment increases hit counter for the given request identity and returns the new value. 218 | func (m *Manager) Increment(method, url string, body []byte) int64 { 219 | key, _ := m.makeKey(method, url, body) 220 | m.mu.RLock() 221 | meta, ok := m.metas[key] 222 | m.mu.RUnlock() 223 | if !ok { 224 | return 0 225 | } 226 | return atomic.AddInt64(&meta.Hits, 1) 227 | } 228 | 229 | // GetHits returns current hit counter snapshot. 230 | func (m *Manager) GetHits(method, url string, body []byte) int64 { 231 | key, _ := m.makeKey(method, url, body) 232 | m.mu.RLock() 233 | meta, ok := m.metas[key] 234 | m.mu.RUnlock() 235 | if !ok { 236 | return 0 237 | } 238 | return atomic.LoadInt64(&meta.Hits) 239 | } 240 | -------------------------------------------------------------------------------- /service/update/replace.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "0E7/service/config" 5 | "bytes" 6 | "crypto/tls" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "os/exec" 15 | "path/filepath" 16 | "runtime" 17 | "sync" 18 | "time" 19 | ) 20 | 21 | var ( 22 | client = &http.Client{Timeout: time.Duration(config.Global_timeout_http) * time.Second, 23 | Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} 24 | 25 | replaceMutex sync.Mutex 26 | lastFailureTime time.Time 27 | failureCount int 28 | maxBackoffDelay = time.Hour 29 | initialBackoff = time.Minute 30 | ) 31 | 32 | func downloadFile(filepath string) error { 33 | values := url.Values{} 34 | values.Set("platform", runtime.GOOS) 35 | values.Set("arch", runtime.GOARCH) 36 | requestBody := bytes.NewBufferString(values.Encode()) 37 | request, err := http.NewRequest("POST", config.Server_url+"/api/update", requestBody) 38 | if err != nil { 39 | return err 40 | } 41 | request.Header.Set("Content-Type", "application/x-www-form-urlencoded") 42 | response, err := client.Do(request) 43 | if err != nil { 44 | return err 45 | } 46 | defer response.Body.Close() 47 | 48 | // 检查响应状态码 49 | if response.StatusCode != http.StatusOK { 50 | return fmt.Errorf("server returned status code %d", response.StatusCode) 51 | } 52 | 53 | newFilePath := "new_" + filepath 54 | out, err := os.Create(newFilePath) 55 | if err != nil { 56 | return err 57 | } 58 | defer out.Close() 59 | 60 | copied, err := io.Copy(out, response.Body) 61 | if err != nil { 62 | os.Remove(newFilePath) // 下载失败时清理不完整的文件 63 | return err 64 | } 65 | 66 | // 验证文件大小(至少应该有内容) 67 | if copied == 0 { 68 | os.Remove(newFilePath) 69 | return errors.New("downloaded file is empty") 70 | } 71 | 72 | // 在非 Windows 系统上设置执行权限 73 | if runtime.GOOS != "windows" { 74 | err = os.Chmod(newFilePath, 0755) 75 | if err != nil { 76 | log.Printf("Warning: Failed to set executable permission: %v", err) 77 | } 78 | } 79 | 80 | log.Printf("Downloaded %s successfully, size: %d bytes", newFilePath, copied) 81 | return nil 82 | } 83 | 84 | // calculateBackoffDelay 计算指数退避延时 85 | func calculateBackoffDelay(count int) time.Duration { 86 | delay := initialBackoff 87 | for i := 0; i < count; i++ { 88 | delay *= 2 89 | if delay > maxBackoffDelay { 90 | delay = maxBackoffDelay 91 | break 92 | } 93 | } 94 | return delay 95 | } 96 | 97 | func Replace() { 98 | // 尝试获取锁,如果获取不到(已经有其他 Replace() 在运行)则直接退出 99 | if !replaceMutex.TryLock() { 100 | return 101 | } 102 | defer replaceMutex.Unlock() 103 | 104 | // 在执行前判断时间,如果还没到重试时间就直接返回 105 | if !lastFailureTime.IsZero() { 106 | nextRetryTime := lastFailureTime.Add(calculateBackoffDelay(failureCount)) 107 | if time.Now().Before(nextRetryTime) { 108 | return 109 | } 110 | } 111 | 112 | defer func() { 113 | if err := recover(); err != nil { 114 | log.Printf("Replace Error: %v", err) 115 | } 116 | }() 117 | 118 | var filePath string 119 | filePath = "0e7_" + runtime.GOOS + "_" + runtime.GOARCH 120 | if runtime.GOOS == "windows" { 121 | filePath += ".exe" 122 | } 123 | 124 | log.Printf("Starting update process, downloading file: %s", filePath) 125 | err := downloadFile(filePath) 126 | if err != nil { 127 | log.Println("File download error", err) 128 | // 下载失败,记录失败时间和失败次数 129 | lastFailureTime = time.Now() 130 | failureCount++ 131 | return 132 | } 133 | 134 | // 下载成功,重置失败计数 135 | lastFailureTime = time.Time{} 136 | failureCount = 0 137 | 138 | execPath, err := os.Executable() 139 | if err != nil { 140 | log.Printf("Failed to get executable path: %v", err) 141 | // 回退到使用当前工作目录 142 | execPath, _ = os.Getwd() 143 | } 144 | wdPath := filepath.Dir(execPath) 145 | 146 | newFilePath := filepath.Join(wdPath, "new_"+filePath) 147 | // 等待文件写入完成(下载可能刚完成,文件可能还在写入) 148 | time.Sleep(100 * time.Millisecond) 149 | 150 | // 验证新文件是否存在且可执行 151 | maxRetries := 5 152 | for i := 0; i < maxRetries; i++ { 153 | info, err := os.Stat(newFilePath) 154 | if err != nil { 155 | if i < maxRetries-1 { 156 | log.Printf("New file not found (attempt %d/%d): %v, retrying...", i+1, maxRetries, err) 157 | time.Sleep(200 * time.Millisecond) 158 | continue 159 | } 160 | log.Printf("New file not found after %d attempts: %v", maxRetries, err) 161 | return 162 | } 163 | 164 | // 检查文件模式是否有执行权限(仅非Windows系统) 165 | if runtime.GOOS != "windows" { 166 | if info.Mode()&0111 == 0 { 167 | log.Println("Warning: File does not have execute permission, attempting to fix...") 168 | err = os.Chmod(newFilePath, 0755) 169 | if err != nil { 170 | log.Printf("Failed to set execute permission: %v", err) 171 | return 172 | } 173 | // 重新检查权限 174 | info, _ = os.Stat(newFilePath) 175 | if info.Mode()&0111 == 0 { 176 | log.Printf("Failed to set execute permission after retry") 177 | return 178 | } 179 | } 180 | } 181 | 182 | // 验证文件大小不为0 183 | if info.Size() == 0 { 184 | log.Printf("New file is empty, download may have failed") 185 | return 186 | } 187 | break 188 | } 189 | 190 | // 使用绝对路径确保能找到文件 191 | absNewFilePath, err := filepath.Abs(newFilePath) 192 | if err != nil { 193 | log.Printf("Failed to get absolute path: %v", err) 194 | return 195 | } 196 | 197 | // 验证文件确实存在 198 | if _, err := os.Stat(absNewFilePath); err != nil { 199 | log.Printf("New file does not exist at %s: %v", absNewFilePath, err) 200 | return 201 | } 202 | 203 | var cmd *exec.Cmd 204 | if runtime.GOOS == "windows" { 205 | // Windows: 使用绝对路径启动,cmd.Dir已设置工作目录 206 | // start /b 在后台启动程序,不等待其退出 207 | cmd = exec.Command("cmd.exe", "/C", "start", "/b", absNewFilePath) 208 | } else { 209 | // Unix-like: 直接执行程序并设置后台运行 210 | cmd = exec.Command(absNewFilePath) 211 | // 设置进程组,使新进程独立于当前进程组 212 | setUnixProcAttr(cmd) 213 | } 214 | 215 | cmd.Dir = wdPath 216 | // 分离标准输入输出,避免继承 217 | cmd.Stdin = nil 218 | cmd.Stdout = nil 219 | cmd.Stderr = nil 220 | 221 | log.Printf("Starting new process: %s (working dir: %s)", absNewFilePath, wdPath) 222 | err = cmd.Start() 223 | if err != nil { 224 | log.Printf("Failed to start new process: %v", err) 225 | return 226 | } 227 | 228 | // 延长等待时间并改进启动验证 229 | done := make(chan error, 1) 230 | go func() { 231 | done <- cmd.Wait() 232 | }() 233 | 234 | // 使用更长的等待时间(2秒),给程序足够的初始化时间 235 | select { 236 | case <-time.After(2 * time.Second): 237 | // 进程仍在运行,说明启动成功 238 | log.Println("New process started successfully") 239 | cmd.Process.Release() // 释放资源,不再等待进程退出 240 | case err := <-done: 241 | if err != nil { 242 | log.Printf("New process exited immediately with error: %v", err) 243 | // 检查进程是否真的启动失败 244 | // 在某些情况下,程序可能快速启动并退出,这也是正常的 245 | log.Println("Warning: Process exited quickly, but update may still succeed") 246 | // 不直接返回,继续执行退出流程 247 | } else { 248 | log.Println("New process exited normally") 249 | } 250 | } 251 | 252 | // 给新进程一些时间来处理更新(复制文件等操作) 253 | // 这样新进程在尝试替换旧文件时,旧进程可能已经退出了 254 | log.Println("Waiting a moment for new process to process the update...") 255 | time.Sleep(1 * time.Second) 256 | 257 | log.Println("Exiting current process to complete update...") 258 | os.Exit(0) 259 | } 260 | -------------------------------------------------------------------------------- /service/webui/code_templates.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "0E7/service/config" 5 | "0E7/service/database" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | // 代码生成模板类型 16 | type CodeTemplateType string 17 | 18 | const ( 19 | TemplateRequests CodeTemplateType = "requests" 20 | TemplatePwntools CodeTemplateType = "pwntools" 21 | TemplateCurl CodeTemplateType = "curl" 22 | ) 23 | 24 | // 代码生成请求结构 25 | type CodeGenerateRequest struct { 26 | PcapId int `json:"pcap_id"` 27 | Template CodeTemplateType `json:"template"` 28 | FlowData []FlowItem `json:"flow_data"` 29 | } 30 | 31 | // 流量项结构 32 | type FlowItem struct { 33 | F string `json:"f"` // from: 'c' for client, 's' for server 34 | B string `json:"b"` // base64 data 35 | T int64 `json:"t"` // time 36 | } 37 | 38 | // 生成代码 39 | func generateCode(pcapId int, templateType CodeTemplateType, flowData []FlowItem) (string, error) { 40 | // 获取pcap信息 41 | var pcap database.Pcap 42 | err := config.Db.Where("id = ?", pcapId).First(&pcap).Error 43 | if err != nil { 44 | return "", fmt.Errorf("pcap not found: %v", err) 45 | } 46 | 47 | // 解析流量数据,只处理客户端到服务器的请求 48 | var requestFlow *FlowItem 49 | for _, flow := range flowData { 50 | if flow.F == "c" { // 客户端请求 51 | requestFlow = &flow 52 | break 53 | } 54 | } 55 | 56 | if requestFlow == nil { 57 | return "", fmt.Errorf("no client request found in flow data") 58 | } 59 | 60 | // 解码base64数据 61 | decodedData, err := base64.StdEncoding.DecodeString(requestFlow.B) 62 | if err != nil { 63 | return "", fmt.Errorf("failed to decode base64 data: %v", err) 64 | } 65 | 66 | // 解析HTTP请求 67 | httpData := string(decodedData) 68 | lines := strings.Split(httpData, "\n") 69 | 70 | var path string 71 | var headers = make(map[string]string) 72 | var body string 73 | var inBody bool 74 | 75 | for i, line := range lines { 76 | line = strings.TrimRight(line, "\r") 77 | 78 | if i == 0 { 79 | // 解析请求行 80 | parts := strings.Split(line, " ") 81 | if len(parts) >= 2 { 82 | path = parts[1] 83 | } 84 | } else if line == "" { 85 | // 空行,开始解析body 86 | inBody = true 87 | } else if !inBody { 88 | // 解析头部 89 | parts := strings.SplitN(line, ":", 2) 90 | if len(parts) == 2 { 91 | key := strings.TrimSpace(parts[0]) 92 | value := strings.TrimSpace(parts[1]) 93 | headers[key] = value 94 | } 95 | } else { 96 | // body内容 97 | body += line + "\n" 98 | } 99 | } 100 | 101 | // 构建URL 102 | url := fmt.Sprintf("http://%s:%s%s", pcap.DstIP, pcap.DstPort, path) 103 | host := pcap.DstIP 104 | port, _ := strconv.Atoi(pcap.DstPort) 105 | 106 | // 根据模板类型生成代码 107 | // 对于pwntools模板,传递原始数据而不是base64编码的数据 108 | var dataToPass string 109 | if templateType == TemplatePwntools { 110 | dataToPass = string(decodedData) 111 | } else { 112 | dataToPass = requestFlow.B 113 | } 114 | 115 | // 原始数据应该只包含HTTP请求的body部分,不包含请求行和headers 116 | rawBodyData := strings.TrimSpace(body) 117 | 118 | return generateCodeFromTemplate(templateType, url, host, port, headers, dataToPass, rawBodyData) 119 | } 120 | 121 | // 从数据库模板生成代码 122 | func generateCodeFromTemplate(templateType CodeTemplateType, url, host string, port int, headers map[string]string, base64Data string, rawData string) (string, error) { 123 | // 根据模板类型获取模板名称 124 | var templateName string 125 | switch templateType { 126 | case TemplateRequests: 127 | templateName = "requests_template" 128 | case TemplatePwntools: 129 | templateName = "pwntools_template" 130 | case TemplateCurl: 131 | templateName = "curl_template" 132 | default: 133 | return "", fmt.Errorf("unsupported template type: %s", templateType) 134 | } 135 | 136 | // 从数据库获取模板 137 | var action database.Action 138 | err := config.Db.Where("name = ? AND is_deleted = ?", templateName, false).First(&action).Error 139 | if err != nil { 140 | return "", fmt.Errorf("template not found: %v", err) 141 | } 142 | 143 | // 解码模板代码 144 | templateCode, err := decodeActionCode(action.Code) 145 | if err != nil { 146 | return "", fmt.Errorf("failed to decode template: %v", err) 147 | } 148 | 149 | // 准备模板数据 150 | headersJson, err := json.MarshalIndent(headers, "", " ") 151 | if err != nil { 152 | return "", err 153 | } 154 | 155 | // 为curl生成headers字符串 156 | var headersCurl strings.Builder 157 | for key, value := range headers { 158 | headersCurl.WriteString(fmt.Sprintf(" -H \"%s: %s\" \\\n", key, value)) 159 | } 160 | 161 | // 对原始数据进行转义,确保在模板中正确显示 162 | escapedRawData := strings.ReplaceAll(rawData, "\\", "\\\\") // 转义反斜杠 163 | escapedRawData = strings.ReplaceAll(escapedRawData, "\"", "\\\"") // 转义双引号 164 | escapedRawData = strings.ReplaceAll(escapedRawData, "\n", "\\n") // 转义换行符 165 | escapedRawData = strings.ReplaceAll(escapedRawData, "\r", "\\r") // 转义回车符 166 | escapedRawData = strings.ReplaceAll(escapedRawData, "\t", "\\t") // 转义制表符 167 | 168 | // 构建模板变量 169 | templateData := map[string]interface{}{ 170 | "URL": url, 171 | "Host": host, 172 | "Port": port, 173 | "Headers": string(headersJson), 174 | "Data": base64Data, 175 | "RawData": escapedRawData, // 转义后的原始数据 176 | "HeadersMap": headers, 177 | "HeadersCurl": headersCurl.String(), 178 | } 179 | 180 | // 简单的模板替换 181 | result := templateCode 182 | for key, value := range templateData { 183 | placeholder := fmt.Sprintf("{{.%s}}", key) 184 | result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", value)) 185 | } 186 | 187 | return result, nil 188 | } 189 | 190 | // 解码Action代码 191 | func decodeActionCode(code string) (string, error) { 192 | // 检查是否是base64编码的代码 193 | if strings.HasPrefix(code, "data:code/python3;base64,") { 194 | base64Data := strings.TrimPrefix(code, "data:code/python3;base64,") 195 | decoded, err := base64.StdEncoding.DecodeString(base64Data) 196 | if err != nil { 197 | return "", err 198 | } 199 | return string(decoded), nil 200 | } 201 | // 如果不是base64格式,直接返回 202 | return code, nil 203 | } 204 | 205 | // 代码生成API接口 206 | func pcap_generate_code(c *gin.Context) { 207 | pcapIdStr := c.Query("pcap_id") 208 | templateType := c.Query("template") 209 | flowDataStr := c.Query("flow_data") 210 | 211 | if pcapIdStr == "" || templateType == "" || flowDataStr == "" { 212 | c.JSON(400, gin.H{ 213 | "message": "fail", 214 | "error": "缺少必要参数", 215 | }) 216 | return 217 | } 218 | 219 | pcapId, err := strconv.Atoi(pcapIdStr) 220 | if err != nil { 221 | c.JSON(400, gin.H{ 222 | "message": "fail", 223 | "error": "无效的pcap_id", 224 | }) 225 | return 226 | } 227 | 228 | // 解析流量数据 229 | var flowData []FlowItem 230 | err = json.Unmarshal([]byte(flowDataStr), &flowData) 231 | if err != nil { 232 | c.JSON(400, gin.H{ 233 | "message": "fail", 234 | "error": "无效的流量数据格式", 235 | }) 236 | return 237 | } 238 | 239 | // 生成代码 240 | code, err := generateCode(pcapId, CodeTemplateType(templateType), flowData) 241 | if err != nil { 242 | c.JSON(500, gin.H{ 243 | "message": "fail", 244 | "error": "代码生成失败: " + err.Error(), 245 | }) 246 | return 247 | } 248 | 249 | c.JSON(200, gin.H{ 250 | "message": "success", 251 | "result": gin.H{ 252 | "code": code, 253 | "template": templateType, 254 | }, 255 | }) 256 | } 257 | -------------------------------------------------------------------------------- /service/webui/log.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "regexp" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/gorilla/websocket" 14 | ) 15 | 16 | const ( 17 | // 日志缓冲区大小 18 | logBufferSize = 100 19 | // WebSocket写入超时 20 | writeWait = 10 * time.Second 21 | // WebSocket读取超时 22 | pongWait = 60 * time.Second 23 | // WebSocket ping间隔 24 | pingPeriod = (pongWait * 9) / 10 25 | // WebSocket最大消息大小 26 | maxMessageSize = 512 27 | ) 28 | 29 | var ( 30 | // WebSocket升级器 31 | upgrader = websocket.Upgrader{ 32 | ReadBufferSize: 1024, 33 | WriteBufferSize: 1024, 34 | CheckOrigin: func(r *http.Request) bool { 35 | return true // 允许所有来源,生产环境应该限制 36 | }, 37 | } 38 | 39 | // 日志广播器 40 | logBroadcaster *LogBroadcaster 41 | broadcasterOnce sync.Once 42 | ) 43 | 44 | // LogBroadcaster 日志广播器,使用worker模式 45 | type LogBroadcaster struct { 46 | // 主channel,接收所有日志 47 | logChan chan string 48 | 49 | // 所有连接的WebSocket客户端 50 | clients map[*LogClient]bool 51 | 52 | // 客户端注册/注销channel 53 | register chan *LogClient 54 | unregister chan *LogClient 55 | 56 | // 日志缓冲区(最新的100条) 57 | buffer []string 58 | bufferMu sync.RWMutex 59 | 60 | // 广播器运行状态 61 | running bool 62 | mu sync.RWMutex 63 | } 64 | 65 | // LogClient WebSocket客户端 66 | type LogClient struct { 67 | // WebSocket连接 68 | conn *websocket.Conn 69 | 70 | // 该客户端的日志channel 71 | send chan string 72 | 73 | // 客户端ID(用于调试) 74 | id string 75 | } 76 | 77 | // GetLogBroadcaster 获取日志广播器单例 78 | func GetLogBroadcaster() *LogBroadcaster { 79 | broadcasterOnce.Do(func() { 80 | logBroadcaster = &LogBroadcaster{ 81 | logChan: make(chan string, 1000), // 缓冲1000条日志 82 | clients: make(map[*LogClient]bool), 83 | register: make(chan *LogClient), 84 | unregister: make(chan *LogClient), 85 | buffer: make([]string, 0, logBufferSize), 86 | running: false, 87 | } 88 | go logBroadcaster.run() 89 | }) 90 | return logBroadcaster 91 | } 92 | 93 | // run 运行广播器worker 94 | func (lb *LogBroadcaster) run() { 95 | lb.mu.Lock() 96 | lb.running = true 97 | lb.mu.Unlock() 98 | 99 | defer func() { 100 | lb.mu.Lock() 101 | lb.running = false 102 | lb.mu.Unlock() 103 | }() 104 | 105 | for { 106 | select { 107 | case client := <-lb.register: 108 | lb.clients[client] = true 109 | // 发送缓存的日志给新客户端 110 | // 使用goroutine异步发送,避免阻塞主循环 111 | go func() { 112 | lb.bufferMu.RLock() 113 | cachedLogs := make([]string, len(lb.buffer)) 114 | copy(cachedLogs, lb.buffer) 115 | lb.bufferMu.RUnlock() 116 | 117 | // 逐条发送缓存的日志 118 | for _, logMsg := range cachedLogs { 119 | select { 120 | case client.send <- logMsg: 121 | // 发送成功,继续下一条 122 | default: 123 | // 如果channel满了,跳过剩余的日志 124 | return 125 | } 126 | } 127 | }() 128 | 129 | case client := <-lb.unregister: 130 | if _, ok := lb.clients[client]; ok { 131 | delete(lb.clients, client) 132 | close(client.send) 133 | } 134 | 135 | case logMsg := <-lb.logChan: 136 | // 添加到缓冲区 137 | lb.bufferMu.Lock() 138 | lb.buffer = append(lb.buffer, logMsg) 139 | // 保持缓冲区大小不超过100条 140 | if len(lb.buffer) > logBufferSize { 141 | lb.buffer = lb.buffer[len(lb.buffer)-logBufferSize:] 142 | } 143 | lb.bufferMu.Unlock() 144 | 145 | // 广播给所有客户端 146 | for client := range lb.clients { 147 | select { 148 | case client.send <- logMsg: 149 | default: 150 | // 如果客户端channel满了,关闭连接 151 | delete(lb.clients, client) 152 | close(client.send) 153 | } 154 | } 155 | } 156 | } 157 | } 158 | 159 | // BroadcastLog 广播日志消息 160 | func (lb *LogBroadcaster) BroadcastLog(message string) { 161 | lb.mu.RLock() 162 | running := lb.running 163 | lb.mu.RUnlock() 164 | 165 | if !running { 166 | return 167 | } 168 | 169 | select { 170 | case lb.logChan <- message: 171 | default: 172 | // 如果channel满了,丢弃这条日志 173 | } 174 | } 175 | 176 | // readPump 从WebSocket读取消息(主要用于处理ping/pong) 177 | func (c *LogClient) readPump() { 178 | defer func() { 179 | c.conn.Close() 180 | }() 181 | 182 | c.conn.SetReadDeadline(time.Now().Add(pongWait)) 183 | c.conn.SetReadLimit(maxMessageSize) 184 | c.conn.SetPongHandler(func(string) error { 185 | c.conn.SetReadDeadline(time.Now().Add(pongWait)) 186 | return nil 187 | }) 188 | 189 | for { 190 | _, _, err := c.conn.ReadMessage() 191 | if err != nil { 192 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 193 | log.Printf("WebSocket error: %v", err) 194 | } 195 | break 196 | } 197 | } 198 | } 199 | 200 | // writePump 向WebSocket写入消息 201 | func (c *LogClient) writePump() { 202 | ticker := time.NewTicker(pingPeriod) 203 | defer func() { 204 | ticker.Stop() 205 | c.conn.Close() 206 | }() 207 | 208 | for { 209 | select { 210 | case message, ok := <-c.send: 211 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 212 | if !ok { 213 | // 通道已关闭 214 | c.conn.WriteMessage(websocket.CloseMessage, []byte{}) 215 | return 216 | } 217 | 218 | // 收集要发送的消息(包括第一条) 219 | messages := []string{message} 220 | 221 | // 批量收集队列中的其他消息(最多收集10条,避免单次发送过多) 222 | maxBatch := 10 223 | for i := 0; i < maxBatch; i++ { 224 | select { 225 | case msg := <-c.send: 226 | messages = append(messages, msg) 227 | default: 228 | // 没有更多消息了,跳出循环 229 | goto sendMessages 230 | } 231 | } 232 | 233 | sendMessages: 234 | 235 | // 逐条发送消息,确保每条消息都单独发送 236 | for _, msg := range messages { 237 | if err := c.conn.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil { 238 | return 239 | } 240 | } 241 | 242 | case <-ticker.C: 243 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 244 | if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { 245 | return 246 | } 247 | } 248 | } 249 | } 250 | 251 | // handleLogWebSocket 处理WebSocket连接 252 | func handleLogWebSocket(c *gin.Context) { 253 | conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) 254 | if err != nil { 255 | log.Printf("WebSocket upgrade failed: %v", err) 256 | return 257 | } 258 | 259 | broadcaster := GetLogBroadcaster() 260 | 261 | client := &LogClient{ 262 | conn: conn, 263 | send: make(chan string, 256), 264 | id: c.ClientIP(), 265 | } 266 | 267 | broadcaster.register <- client 268 | 269 | // 启动读写goroutines 270 | go client.writePump() 271 | go client.readPump() 272 | } 273 | 274 | // LogWriter 日志拦截Writer,将日志输出同时写入原始Writer和广播器 275 | type LogWriter struct { 276 | original io.Writer 277 | broadcaster *LogBroadcaster 278 | } 279 | 280 | // Write 实现io.Writer接口 281 | func (lw *LogWriter) Write(p []byte) (n int, err error) { 282 | // 写入原始Writer 283 | n, err = lw.original.Write(p) 284 | if err != nil { 285 | return n, err 286 | } 287 | 288 | // 清理日志消息:去除ANSI转义码、多余的空白字符和末尾换行符 289 | message := cleanLogMessage(string(p)) 290 | if len(message) > 0 { 291 | lw.broadcaster.BroadcastLog(message) 292 | } 293 | 294 | return n, nil 295 | } 296 | 297 | // cleanLogMessage 清理日志消息,去除ANSI转义码和多余的空白字符 298 | func cleanLogMessage(msg string) string { 299 | // 去除末尾的换行符和空白字符 300 | msg = strings.TrimRight(msg, " \t\n\r") 301 | if len(msg) == 0 { 302 | return "" 303 | } 304 | 305 | // 去除ANSI转义码(颜色码等) 306 | ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) 307 | msg = ansiRegex.ReplaceAllString(msg, "") 308 | 309 | // 将所有制表符替换为空格 310 | msg = strings.ReplaceAll(msg, "\t", " ") 311 | 312 | // 去除行首的所有空白字符(包括空格和制表符) 313 | msg = strings.TrimLeft(msg, " \t") 314 | 315 | // 将多个连续空格(2个或更多)替换为单个空格 316 | spaceRegex := regexp.MustCompile(` +`) 317 | msg = spaceRegex.ReplaceAllString(msg, " ") 318 | 319 | // 去除行尾空白 320 | msg = strings.TrimRight(msg, " \t") 321 | 322 | return msg 323 | } 324 | 325 | // NewLogWriter 创建新的日志拦截Writer 326 | func NewLogWriter(original io.Writer) io.Writer { 327 | broadcaster := GetLogBroadcaster() 328 | return &LogWriter{ 329 | original: original, 330 | broadcaster: broadcaster, 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /service/webui/flag.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "0E7/service/config" 5 | "0E7/service/database" 6 | "0E7/service/flag" 7 | "fmt" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/gin-gonic/gin" 13 | "gopkg.in/ini.v1" 14 | ) 15 | 16 | // GetFlagList 获取flag列表 17 | func GetFlagList(c *gin.Context) { 18 | // 获取分页参数 19 | pageStr := c.Query("page") 20 | pageSizeStr := c.Query("page_size") 21 | 22 | page := 1 23 | pageSize := 20 24 | 25 | if pageStr != "" { 26 | if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { 27 | page = p 28 | } 29 | } 30 | 31 | if pageSizeStr != "" { 32 | if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 && ps <= 100 { 33 | pageSize = ps 34 | } 35 | } 36 | 37 | // 获取搜索条件 38 | flag := c.Query("flag") 39 | team := c.Query("team") 40 | status := c.Query("status") 41 | exploitIdStr := c.Query("exploit_id") 42 | 43 | // 构建查询条件 44 | query := config.Db.Model(&database.Flag{}) 45 | 46 | if flag != "" { 47 | query = query.Where("flag LIKE ?", "%"+flag+"%") 48 | } 49 | if team != "" { 50 | query = query.Where("team LIKE ?", "%"+team+"%") 51 | } 52 | if status != "" { 53 | query = query.Where("status = ?", status) 54 | } 55 | if exploitIdStr != "" { 56 | if exploitId, err := strconv.Atoi(exploitIdStr); err == nil { 57 | query = query.Where("exploit_id = ?", exploitId) 58 | } 59 | } 60 | 61 | // 获取总数 62 | var total int64 63 | err := query.Count(&total).Error 64 | if err != nil { 65 | c.JSON(500, gin.H{ 66 | "message": "fail", 67 | "error": "获取flag总数失败: " + err.Error(), 68 | }) 69 | return 70 | } 71 | 72 | // 获取分页数据 73 | var flags []database.Flag 74 | 75 | // 确保分页参数正确 76 | if pageSize <= 0 { 77 | pageSize = 20 78 | } 79 | if page <= 0 { 80 | page = 1 81 | } 82 | 83 | // 计算offset 84 | offset := (page - 1) * pageSize 85 | 86 | // 执行查询 87 | err = query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&flags).Error 88 | if err != nil { 89 | c.JSON(500, gin.H{ 90 | "message": "fail", 91 | "error": "获取flag列表失败: " + err.Error(), 92 | }) 93 | return 94 | } 95 | 96 | // 获取exploit名称 97 | // 收集唯一的 ExploitId 98 | exploitIDSet := make(map[int]struct{}) 99 | for i := range flags { 100 | if flags[i].ExploitId > 0 { 101 | exploitIDSet[flags[i].ExploitId] = struct{}{} 102 | } 103 | } 104 | 105 | // 如果有需要查询的 ExploitId,则批量查询 106 | if len(exploitIDSet) > 0 { 107 | ids := make([]int, 0, len(exploitIDSet)) 108 | for id := range exploitIDSet { 109 | ids = append(ids, id) 110 | } 111 | 112 | var exploits []database.Exploit 113 | if err := config.Db.Where("id IN ?", ids).Find(&exploits).Error; err == nil { 114 | // 构建 id -> name 映射 115 | idToName := make(map[int]string, len(exploits)) 116 | for i := range exploits { 117 | idToName[exploits[i].ID] = exploits[i].Name 118 | } 119 | // 回填 120 | for i := range flags { 121 | if name, ok := idToName[flags[i].ExploitId]; ok { 122 | flags[i].ExploitName = name 123 | } 124 | } 125 | } 126 | } 127 | 128 | c.JSON(200, gin.H{ 129 | "message": "success", 130 | "result": gin.H{ 131 | "flags": flags, 132 | "total": total, 133 | }, 134 | }) 135 | } 136 | 137 | // SubmitFlag 提交flag 138 | func SubmitFlag(c *gin.Context) { 139 | flagValue := c.PostForm("flag") 140 | team := c.PostForm("team") 141 | flagRegex := c.PostForm("flag_regex") 142 | 143 | if flagValue == "" { 144 | c.JSON(400, gin.H{ 145 | "message": "fail", 146 | "error": "flag不能为空", 147 | }) 148 | return 149 | } 150 | 151 | // 确定使用的flag正则表达式 152 | regexPattern := flagRegex 153 | if regexPattern == "" { 154 | regexPattern = config.Server_flag 155 | } 156 | 157 | if regexPattern == "" { 158 | c.JSON(400, gin.H{ 159 | "message": "fail", 160 | "error": "未设置flag正则表达式", 161 | }) 162 | return 163 | } 164 | 165 | // 编译正则表达式 166 | regex, err := regexp.Compile(regexPattern) 167 | if err != nil { 168 | c.JSON(400, gin.H{ 169 | "message": "fail", 170 | "error": "flag正则表达式无效: " + err.Error(), 171 | }) 172 | return 173 | } 174 | 175 | // 使用正则表达式匹配flag 176 | var flags []string 177 | matches := regex.FindAllString(flagValue, -1) 178 | for _, match := range matches { 179 | match = strings.TrimSpace(match) 180 | if match != "" { 181 | flags = append(flags, match) 182 | } 183 | } 184 | 185 | // 限制数量 186 | if len(flags) > 999 { 187 | flags = flags[:999] 188 | } 189 | 190 | if len(flags) == 0 { 191 | c.JSON(400, gin.H{ 192 | "message": "fail", 193 | "error": "没有匹配到有效的flag", 194 | }) 195 | return 196 | } 197 | 198 | // 统计结果 199 | var total, success, skipped, error int 200 | 201 | // 批量处理flag 202 | for _, flag := range flags { 203 | total++ 204 | 205 | // 检查是否已存在 206 | var count int64 207 | err := config.Db.Model(&database.Flag{}).Where("flag = ?", flag).Count(&count).Error 208 | if err != nil { 209 | error++ 210 | continue 211 | } 212 | 213 | // 创建flag记录 214 | flagRecord := database.Flag{ 215 | Flag: flag, 216 | Team: team, 217 | Status: "QUEUE", 218 | } 219 | 220 | if count > 0 { 221 | flagRecord.Status = "SKIPPED" 222 | skipped++ 223 | } else { 224 | success++ 225 | } 226 | 227 | err = config.Db.Create(&flagRecord).Error 228 | if err != nil { 229 | error++ 230 | success-- 231 | continue 232 | } 233 | } 234 | 235 | c.JSON(200, gin.H{ 236 | "message": "success", 237 | "result": gin.H{ 238 | "total": total, 239 | "success": success, 240 | "skipped": skipped, 241 | "error": error, 242 | }, 243 | }) 244 | } 245 | 246 | // DeleteFlag 删除flag 247 | func DeleteFlag(c *gin.Context) { 248 | idStr := c.PostForm("id") 249 | if idStr == "" { 250 | c.JSON(400, gin.H{ 251 | "message": "fail", 252 | "error": "id不能为空", 253 | }) 254 | return 255 | } 256 | 257 | id, err := strconv.Atoi(idStr) 258 | if err != nil { 259 | c.JSON(400, gin.H{ 260 | "message": "fail", 261 | "error": "无效的id", 262 | }) 263 | return 264 | } 265 | 266 | err = config.Db.Delete(&database.Flag{}, id).Error 267 | if err != nil { 268 | c.JSON(500, gin.H{ 269 | "message": "fail", 270 | "error": "删除flag失败: " + err.Error(), 271 | }) 272 | return 273 | } 274 | 275 | c.JSON(200, gin.H{ 276 | "message": "success", 277 | "result": "flag删除成功", 278 | }) 279 | } 280 | 281 | // UpdateFlagConfig 更新flag配置 282 | func UpdateFlagConfig(c *gin.Context) { 283 | newPattern := c.PostForm("pattern") 284 | if newPattern == "" { 285 | c.JSON(400, gin.H{ 286 | "message": "fail", 287 | "error": "flag模式不能为空", 288 | }) 289 | return 290 | } 291 | 292 | // 检查模式是否发生变化 293 | if config.Server_flag == newPattern { 294 | c.JSON(200, gin.H{ 295 | "message": "success", 296 | "result": "flag配置未发生变化", 297 | }) 298 | return 299 | } 300 | 301 | // 更新config.ini文件 302 | err := updateFlagConfigInFile(newPattern) 303 | if err != nil { 304 | c.JSON(500, gin.H{ 305 | "message": "fail", 306 | "error": "更新flag配置失败: " + err.Error(), 307 | }) 308 | return 309 | } 310 | 311 | // 更新内存中的配置 312 | config.Server_flag = newPattern 313 | 314 | // 触发重新索引 315 | flagDetector := flag.GetFlagDetector() 316 | flagDetector.TriggerReindex() 317 | 318 | c.JSON(200, gin.H{ 319 | "message": "success", 320 | "result": "flag配置更新成功,正在重新索引历史数据", 321 | }) 322 | } 323 | 324 | // updateFlagConfigInFile 更新config.ini文件中的flag配置 325 | func updateFlagConfigInFile(newPattern string) error { 326 | cfg, err := ini.Load("config.ini") 327 | if err != nil { 328 | return fmt.Errorf("failed to load config.ini: %v", err) 329 | } 330 | 331 | // 更新server section中的flag值 332 | serverSection := cfg.Section("server") 333 | serverSection.Key("flag").SetValue(newPattern) 334 | 335 | // 保存文件 336 | err = cfg.SaveTo("config.ini") 337 | if err != nil { 338 | return fmt.Errorf("failed to save config.ini: %v", err) 339 | } 340 | 341 | return nil 342 | } 343 | 344 | // GetCurrentFlagConfig 获取当前flag配置 345 | func GetCurrentFlagConfig(c *gin.Context) { 346 | c.JSON(200, gin.H{ 347 | "message": "success", 348 | "result": gin.H{ 349 | "current_pattern": config.Server_flag, 350 | }, 351 | }) 352 | } 353 | -------------------------------------------------------------------------------- /frontend/src/components/OutputTable.vue: -------------------------------------------------------------------------------- 1 | 152 | 153 | 224 | 225 | -------------------------------------------------------------------------------- /service/windows/check.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package windows 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "os" 10 | "os/exec" 11 | "runtime" 12 | "strings" 13 | "unsafe" 14 | 15 | "github.com/google/gopacket/pcap" 16 | "golang.org/x/sys/windows" 17 | ) 18 | 19 | // Windows API constants 20 | const ( 21 | TOKEN_QUERY = 0x0008 22 | ) 23 | 24 | var ( 25 | advapi32 = windows.NewLazyDLL("advapi32.dll") 26 | procOpenProcessToken = advapi32.NewProc("OpenProcessToken") 27 | procGetTokenInformation = advapi32.NewProc("GetTokenInformation") 28 | procCloseHandle = windows.NewLazyDLL("kernel32.dll").NewProc("CloseHandle") 29 | ) 30 | 31 | // TokenElevation represents the elevation status of a token 32 | type TokenElevation struct { 33 | TokenIsElevated uint32 34 | } 35 | 36 | // CheckWindowsDependencies 检查Windows下的依赖项 37 | func CheckWindowsDependencies() error { 38 | if runtime.GOOS != "windows" { 39 | return nil // 非Windows系统跳过检查 40 | } 41 | 42 | log.Println("正在检查Windows依赖项...") 43 | 44 | // 检查管理员权限(仅提示,不阻止运行) 45 | if err := checkAdminPrivileges(); err != nil { 46 | log.Printf("警告 权限检查: %v", err) 47 | log.Println("提示: 没有管理员权限,实时网络监控功能可能受限,但pcap文件分析功能仍可正常使用") 48 | log.Println("建议: 如需完整功能,请右键以管理员身份运行程序") 49 | } else { 50 | log.Println("成功 具有管理员权限,所有功能可用") 51 | } 52 | 53 | // 检查pcap库(仅提示,不阻止运行) 54 | if err := checkPcapLibrary(); err != nil { 55 | log.Printf("警告 PCAP库检查: %v", err) 56 | log.Println("提示: 未检测到WinPcap/Npcap,实时网络监控功能不可用,但pcap文件分析功能仍可正常使用") 57 | showDownloadLinks() 58 | } else { 59 | log.Println("成功 PCAP库可用,网络监控功能正常") 60 | } 61 | 62 | // 检查网络适配器(仅提示) 63 | if err := checkNetworkAdapters(); err != nil { 64 | log.Printf("信息 网络适配器: %v", err) 65 | log.Println("提示: 网络适配器检查失败,但不影响pcap文件分析功能") 66 | } 67 | 68 | log.Println("成功 Windows依赖检查完成,程序可以正常运行") 69 | return nil 70 | } 71 | 72 | // showDownloadLinks 显示下载链接 73 | func showDownloadLinks() { 74 | log.Println("如需完整功能,请下载安装:") 75 | log.Println(" Npcap (推荐): https://npcap.com/") 76 | log.Println(" WinPcap: https://www.winpcap.org/install/") 77 | log.Println(" Nmap (包含WinPcap): https://nmap.org/download.html") 78 | log.Println("") 79 | log.Println("安装提示:") 80 | log.Println(" 1. 推荐使用Npcap (支持Windows 10/11)") 81 | log.Println(" 2. 右键以管理员身份运行安装程序") 82 | log.Println(" 3. 安装完成后重启程序") 83 | log.Println(" 4. 如遇问题,可尝试关闭杀毒软件后安装") 84 | } 85 | 86 | // checkAdminPrivileges 检查是否具有管理员权限 87 | func checkAdminPrivileges() error { 88 | log.Println("检查管理员权限...") 89 | 90 | // 方法1: 检查当前进程的令牌 91 | if isElevated, err := isProcessElevated(); err == nil { 92 | if isElevated { 93 | log.Println("成功 当前进程具有管理员权限") 94 | return nil 95 | } else { 96 | log.Println("警告 当前进程没有管理员权限") 97 | return fmt.Errorf("需要管理员权限以访问网络适配器") 98 | } 99 | } 100 | 101 | // 方法2: 尝试打开需要管理员权限的资源 102 | if err := testAdminAccess(); err != nil { 103 | log.Println("警告 无法访问需要管理员权限的资源") 104 | return fmt.Errorf("权限不足: %v", err) 105 | } 106 | 107 | log.Println("成功 权限检查通过") 108 | return nil 109 | } 110 | 111 | // isProcessElevated 检查当前进程是否具有提升的权限 112 | func isProcessElevated() (bool, error) { 113 | handle, err := windows.GetCurrentProcess() 114 | if err != nil { 115 | return false, err 116 | } 117 | 118 | var token windows.Token 119 | err = windows.OpenProcessToken(handle, TOKEN_QUERY, &token) 120 | if err != nil { 121 | return false, err 122 | } 123 | defer token.Close() 124 | 125 | var elevation TokenElevation 126 | var returnedLen uint32 127 | err = windows.GetTokenInformation(token, windows.TokenElevation, (*byte)(unsafe.Pointer(&elevation)), uint32(unsafe.Sizeof(elevation)), &returnedLen) 128 | if err != nil { 129 | return false, err 130 | } 131 | 132 | return elevation.TokenIsElevated != 0, nil 133 | } 134 | 135 | // testAdminAccess 测试管理员访问权限 136 | func testAdminAccess() error { 137 | // 尝试访问需要管理员权限的注册表项 138 | cmd := exec.Command("reg", "query", "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "/s", "/f", "WinPcap") 139 | output, err := cmd.Output() 140 | if err != nil { 141 | return fmt.Errorf("无法访问注册表: %v", err) 142 | } 143 | 144 | // 如果能执行到这里,说明有足够的权限 145 | _ = output 146 | return nil 147 | } 148 | 149 | // checkPcapLibrary 检查pcap库是否可用 150 | func checkPcapLibrary() error { 151 | log.Println("检查PCAP库...") 152 | 153 | // 检查WinPcap 154 | if isWinPcapInstalled() { 155 | log.Println("成功 检测到WinPcap已安装") 156 | return nil 157 | } 158 | 159 | // 检查Npcap 160 | if isNpcapInstalled() { 161 | log.Println("成功 检测到Npcap已安装") 162 | return nil 163 | } 164 | 165 | // 尝试使用pcap库进行基本测试 166 | devices, err := pcap.FindAllDevs() 167 | if err != nil { 168 | log.Printf("警告 pcap库测试失败: %v", err) 169 | log.Println("提示: 这不会影响pcap文件分析功能,只是实时网络监控功能不可用") 170 | return fmt.Errorf("pcap库不可用,实时网络监控功能受限") 171 | } 172 | 173 | if len(devices) == 0 { 174 | log.Println("警告 未找到网络设备") 175 | log.Println("提示: 这不会影响pcap文件分析功能,只是实时网络监控功能不可用") 176 | return fmt.Errorf("未找到网络设备,实时网络监控功能受限") 177 | } 178 | 179 | log.Printf("成功 找到 %d 个网络设备", len(devices)) 180 | return nil 181 | } 182 | 183 | // isWinPcapInstalled 检查WinPcap是否安装 184 | func isWinPcapInstalled() bool { 185 | // 检查注册表 186 | cmd := exec.Command("reg", "query", "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "/s", "/f", "WinPcap") 187 | output, err := cmd.Output() 188 | if err == nil && strings.Contains(string(output), "WinPcap") { 189 | return true 190 | } 191 | 192 | // 检查文件系统 193 | winPcapPaths := []string{ 194 | "C:\\Windows\\System32\\wpcap.dll", 195 | "C:\\Windows\\System32\\packet.dll", 196 | "C:\\Program Files\\WinPcap", 197 | "C:\\Program Files (x86)\\WinPcap", 198 | } 199 | 200 | for _, path := range winPcapPaths { 201 | if _, err := os.Stat(path); err == nil { 202 | return true 203 | } 204 | } 205 | 206 | return false 207 | } 208 | 209 | // isNpcapInstalled 检查Npcap是否安装 210 | func isNpcapInstalled() bool { 211 | // 检查注册表 212 | cmd := exec.Command("reg", "query", "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "/s", "/f", "Npcap") 213 | output, err := cmd.Output() 214 | if err == nil && strings.Contains(string(output), "Npcap") { 215 | return true 216 | } 217 | 218 | // 检查文件系统 219 | npcapPaths := []string{ 220 | "C:\\Windows\\System32\\Npcap", 221 | "C:\\Program Files\\Npcap", 222 | "C:\\Program Files (x86)\\Npcap", 223 | } 224 | 225 | for _, path := range npcapPaths { 226 | if _, err := os.Stat(path); err == nil { 227 | return true 228 | } 229 | } 230 | 231 | return false 232 | } 233 | 234 | // checkNetworkAdapters 检查网络适配器 235 | func checkNetworkAdapters() error { 236 | log.Println("检查网络适配器...") 237 | 238 | devices, err := pcap.FindAllDevs() 239 | if err != nil { 240 | return fmt.Errorf("无法获取网络设备列表: %v", err) 241 | } 242 | 243 | if len(devices) == 0 { 244 | return fmt.Errorf("未找到可用的网络适配器") 245 | } 246 | 247 | log.Printf("成功 找到 %d 个网络适配器:", len(devices)) 248 | for i, device := range devices { 249 | if i < 3 { // 只显示前3个设备 250 | log.Printf(" - %s: %s", device.Name, device.Description) 251 | } 252 | } 253 | if len(devices) > 3 { 254 | log.Printf(" ... 还有 %d 个设备", len(devices)-3) 255 | } 256 | 257 | return nil 258 | } 259 | 260 | // RequestAdminPrivileges 请求管理员权限(重新启动程序) 261 | func RequestAdminPrivileges() error { 262 | if runtime.GOOS != "windows" { 263 | return fmt.Errorf("此功能仅在Windows上可用") 264 | } 265 | 266 | log.Println("正在请求管理员权限...") 267 | 268 | // 获取当前程序路径 269 | exePath, err := os.Executable() 270 | if err != nil { 271 | return fmt.Errorf("无法获取程序路径: %v", err) 272 | } 273 | 274 | // 使用runas命令以管理员身份重新启动 275 | cmd := exec.Command("runas", "/user:Administrator", exePath) 276 | cmd.Stdin = os.Stdin 277 | cmd.Stdout = os.Stdout 278 | cmd.Stderr = os.Stderr 279 | 280 | if err := cmd.Start(); err != nil { 281 | return fmt.Errorf("无法以管理员身份启动程序: %v", err) 282 | } 283 | 284 | // 退出当前进程 285 | os.Exit(0) 286 | return nil 287 | } 288 | 289 | // GetInstallationGuide 获取安装指南 290 | func GetInstallationGuide() string { 291 | guide := ` 292 | Windows PCAP库安装指南: 293 | 294 | 1. Npcap (推荐 - 支持Windows 10/11): 295 | 下载地址: https://npcap.com/ 296 | 支持Windows 10/11 297 | 支持现代网络功能 298 | 更好的性能和稳定性 299 | 300 | 2. WinPcap (传统选择 - 支持旧系统): 301 | 下载地址: https://www.winpcap.org/install/ 302 | 支持Windows XP/7/8 303 | 不支持Windows 10/11的某些功能 304 | 305 | 3. 通过Nmap安装 (包含WinPcap): 306 | 下载地址: https://nmap.org/download.html 307 | 包含WinPcap组件 308 | 同时获得网络扫描工具 309 | 310 | 安装步骤: 311 | 1. 下载对应版本的安装包 312 | 2. 右键以管理员身份运行安装程序 313 | 3. 按照安装向导完成安装 314 | 4. 重启程序以启用完整功能 315 | 316 | 注意事项: 317 | - 安装时需要管理员权限 318 | - 某些杀毒软件可能会阻止安装 319 | - 建议关闭杀毒软件实时保护后再安装 320 | - 安装后可能需要重启计算机 321 | 322 | 故障排除: 323 | - 如果安装失败,请检查是否有其他网络监控软件冲突 324 | - 确保系统版本与安装包兼容 325 | - 可以尝试以兼容模式运行安装程序 326 | ` 327 | return guide 328 | } 329 | -------------------------------------------------------------------------------- /service/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "encoding/base64" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "time" 10 | 11 | "gopkg.in/ini.v1" 12 | "gorm.io/driver/mysql" 13 | "gorm.io/driver/sqlite" 14 | "gorm.io/gorm" 15 | "gorm.io/gorm/logger" 16 | ) 17 | 18 | func Init_database(section *ini.Section) (db *gorm.DB, err error) { 19 | engine := section.Key("db_engine").String() 20 | switch engine { 21 | case "mysql": 22 | host := section.Key("db_host").String() 23 | port := section.Key("db_port").String() 24 | username := section.Key("db_username").String() 25 | password := section.Key("db_password").String() 26 | tables := section.Key("db_tables").String() 27 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local&compress=True", 28 | username, password, host, port, tables) 29 | db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ 30 | SkipDefaultTransaction: true, 31 | Logger: logger.Default.LogMode(logger.Silent), 32 | }) 33 | default: 34 | engine = "sqlite" 35 | dsn := "file:sqlite.db?mode=rwc" + 36 | "&_journal_mode=WAL" + 37 | "&_synchronous=NORMAL" + // 改为 NORMAL 提高性能 38 | "&_cache_size=-4000" + // 4GB 缓存 39 | "&_auto_vacuum=FULL" + 40 | "&_page_size=4096" + 41 | "&_mmap_size=536870912" + // 512MB 内存映射 42 | "&_temp_store=2" + 43 | "&_busy_timeout=10000" + // 10秒超时 44 | "&_foreign_keys=1" + 45 | "&_secure_delete=OFF" + 46 | "&_journal_size_limit=67108864" + // 64MB WAL 限制 47 | "&_wal_autocheckpoint=1000" // WAL 检查点 48 | db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{ 49 | SkipDefaultTransaction: true, 50 | Logger: logger.Default.LogMode(logger.Silent), 51 | }) 52 | if err == nil { 53 | // 连接池限制 54 | sqlDB, _ := db.DB() 55 | sqlDB.SetMaxOpenConns(1) 56 | 57 | // 确保元信息表一次性创建 58 | db.Exec("CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value INTEGER)") 59 | 60 | var vacuumFlag = flag.Bool("db_vacuum", false, "run VACUUM on startup (sqlite only)") 61 | forceVacuum := *vacuumFlag || os.Getenv("DB_VACUUM") == "1" || os.Getenv("DB_VACUUM") == "true" 62 | 63 | shouldVacuum := false 64 | if forceVacuum { 65 | shouldVacuum = true 66 | } else { 67 | // 读取最近一次 VACUUM 时间 68 | var lastVacUnix int64 69 | row := db.Raw("SELECT value FROM meta WHERE key = 'last_vacuum' LIMIT 1").Row() 70 | if scanErr := row.Scan(&lastVacUnix); scanErr == nil { 71 | lastVacTime := time.Unix(lastVacUnix, 0) 72 | if time.Since(lastVacTime) >= 24*time.Hour { 73 | shouldVacuum = true 74 | } 75 | } else { 76 | // 没有该记录/表:初始化 last_vacuum 为当前时间 77 | nowUnix := time.Now().Unix() 78 | db.Exec("INSERT INTO meta(key, value) VALUES('last_vacuum', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", nowUnix) 79 | } 80 | } 81 | 82 | if shouldVacuum { 83 | db.Exec("VACUUM;") 84 | // 记录本次 VACUUM 时间 85 | nowUnix := time.Now().Unix() 86 | db.Exec("INSERT INTO meta(key, value) VALUES('last_vacuum', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", nowUnix) 87 | } 88 | } 89 | } 90 | 91 | if err != nil { 92 | log.Println("Failed to open database: ", err) 93 | return db, err 94 | } 95 | 96 | sqlDB, err := db.DB() 97 | if err != nil { 98 | log.Println("Failed to get underlying sql.DB:", err) 99 | return db, err 100 | } 101 | 102 | err = sqlDB.Ping() 103 | if err != nil { 104 | log.Println("Failed to connect to database:", err) 105 | os.Exit(1) 106 | } 107 | 108 | log.Println("Connected to database:", engine) 109 | 110 | err = init_database_client(db, engine) 111 | return db, err 112 | } 113 | 114 | func init_database_client(db *gorm.DB, engine string) error { 115 | // 自动迁移所有表结构 116 | err := db.AutoMigrate( 117 | &Client{}, 118 | &Exploit{}, 119 | &Flag{}, 120 | &ExploitOutput{}, 121 | &Action{}, 122 | &PcapFile{}, 123 | &Monitor{}, 124 | &Pcap{}, 125 | ) 126 | if err != nil { 127 | log.Println("Failed to migrate database tables:", err) 128 | return err 129 | } 130 | 131 | // 插入默认的 action 数据 132 | var count int64 133 | db.Model(&Action{}).Count(&count) 134 | if count == 0 { 135 | actions := []Action{ 136 | { 137 | ID: 1, 138 | Name: "flag_submiter", 139 | Code: CodeToBase64("code/python3", 140 | `import sys 141 | import json 142 | if len(sys.argv) != 2: 143 | print(json.dumps([])) 144 | sys.exit(0) 145 | 146 | data = json.loads(sys.argv[1]) 147 | result = [] 148 | for item in data: 149 | result.append({ 150 | "flag": item, 151 | "status": "SUCCESS", 152 | "msg": "", 153 | "score": 10.0 154 | }) 155 | print(json.dumps(result))`), 156 | Config: "{\"type\":\"flag_submiter\",\"num\":20}", 157 | Interval: 5, 158 | }, 159 | { 160 | ID: 2, 161 | Name: "ipbucket_default", 162 | Code: CodeToBase64("code/python3", 163 | `import json 164 | team = [] 165 | for i in range(1,10): 166 | team.append({ 167 | "team": f"Team {i}", 168 | "value": f"192.168.1.{i}" 169 | }) 170 | print(json.dumps(team))`), 171 | Interval: -1, 172 | NextRun: time.Date(1999, 1, 1, 0, 0, 0, 0, time.UTC), 173 | }, 174 | { 175 | ID: 3, 176 | Name: "run_exploit_1", 177 | Code: "", 178 | Config: "{\"type\":\"exec_script\",\"num\":1,\"script_id\":1}", 179 | Interval: -1, // 默认不启用 180 | NextRun: time.Date(2037, 1, 1, 0, 0, 0, 0, time.UTC), 181 | }, 182 | } 183 | for _, action := range actions { 184 | db.Create(&action) 185 | } 186 | 187 | // 插入代码生成模板 188 | codeTemplates := []Action{ 189 | { 190 | ID: 4, 191 | Name: "requests_template", 192 | Code: CodeToBase64("code/python3", 193 | `import requests 194 | import base64 195 | 196 | # 禁用SSL验证和保持连接 197 | session = requests.Session() 198 | session.verify = False 199 | session.headers.update({ 200 | 'Connection': 'close' # 不保持连接 201 | }) 202 | 203 | # 请求数据 204 | url = "{{.URL}}" 205 | headers = {{.Headers}} 206 | 207 | # 使用base64解码的数据(默认) 208 | data = base64.b64decode("{{.Data}}").decode('utf-8', errors='ignore') 209 | 210 | # 或者直接使用原始数据(取消注释下面一行,注释上面一行) 211 | # data = "{{.RawData}}" 212 | 213 | # 发送请求 214 | response = session.post(url, headers=headers, data=data) 215 | print(f"Status: {response.status_code}") 216 | print(f"Response: {response.text}")`), 217 | Config: "{\"type\": \"template\"}", 218 | Interval: -1, // 默认不启用 219 | NextRun: time.Date(2037, 1, 1, 0, 0, 0, 0, time.UTC), 220 | }, 221 | { 222 | ID: 5, 223 | Name: "pwntools_template", 224 | Code: CodeToBase64("code/python3", 225 | `from pwn import * 226 | 227 | # 连接设置 228 | context.log_level = 'debug' 229 | 230 | # 连接信息 231 | host = "{{.Host}}" 232 | port = {{.Port}} 233 | 234 | # 原始数据 - 用户可以直接修改这里的数据 235 | raw_data = "{{.RawData}}" 236 | 237 | # 建立连接 238 | conn = remote(host, port) 239 | 240 | # 发送原始数据 241 | conn.send(raw_data.encode()) 242 | 243 | # 接收响应 244 | response = conn.recvall() 245 | print(response.decode('utf-8', errors='ignore')) 246 | 247 | conn.close()`), 248 | Config: "{\"type\": \"template\"}", 249 | Interval: -1, // 默认不启用 250 | NextRun: time.Date(2037, 1, 1, 0, 0, 0, 0, time.UTC), 251 | }, 252 | { 253 | ID: 6, 254 | Name: "curl_template", 255 | Code: CodeToBase64("code/bash", 256 | `#!/bin/bash 257 | 258 | # 请求数据 259 | URL="{{.URL}}" 260 | DATA="{{.Data}}" 261 | 262 | # 解码base64数据(默认) 263 | DECODED_DATA=$(echo "$DATA" | base64 -d) 264 | 265 | # 或者直接使用原始数据(取消注释下面一行,注释上面一行) 266 | # DECODED_DATA="{{.RawData}}" 267 | 268 | # 构建curl命令 269 | curl -X POST \\ 270 | --insecure \\ 271 | --no-keepalive \\ 272 | {{.HeadersCurl}} --data "$DECODED_DATA" \\ 273 | "$URL"`), 274 | Config: "{\"type\": \"template\"}", 275 | Interval: -1, // 默认不启用 276 | NextRun: time.Date(2037, 1, 1, 0, 0, 0, 0, time.UTC), 277 | }, 278 | } 279 | for _, template := range codeTemplates { 280 | db.Create(&template) 281 | } 282 | } 283 | db.Model(&Exploit{}).Count(&count) 284 | if count == 0 { 285 | exploits := []Exploit{ 286 | { 287 | ID: 1, 288 | Name: "rand_flag", 289 | Filename: CodeToBase64("code/python3", 290 | `import sys 291 | import uuid 292 | ip = "127.0.0.1" 293 | if len(sys.argv) == 2: 294 | ip = sys.argv[1] 295 | print(f"ip:{ip} \nflag{{{str(uuid.uuid4())}}}")`), 296 | Timeout: "15", 297 | Times: "0", 298 | Flag: "flag{.*}", 299 | Argv: "{ipbucket_default}", 300 | IsDeleted: false, 301 | }, 302 | } 303 | for _, exploit := range exploits { 304 | db.Create(&exploit) 305 | } 306 | } 307 | 308 | log.Println("Database tables migrated successfully.") 309 | return nil 310 | } 311 | 312 | func CodeToBase64(codeType string, code string) string { 313 | return fmt.Sprintf("data:%s;base64,%s", codeType, base64.StdEncoding.EncodeToString([]byte(code))) 314 | } 315 | -------------------------------------------------------------------------------- /wails/app.go: -------------------------------------------------------------------------------- 1 | //go:build wails 2 | // +build wails 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "io" 10 | "net" 11 | "net/http" 12 | "os" 13 | "os/exec" 14 | "os/signal" 15 | "path/filepath" 16 | "runtime" 17 | "syscall" 18 | "time" 19 | 20 | "github.com/wailsapp/wails/v2" 21 | "github.com/wailsapp/wails/v2/pkg/options" 22 | "github.com/wailsapp/wails/v2/pkg/options/assetserver" 23 | "github.com/wailsapp/wails/v2/pkg/options/mac" 24 | winOptions "github.com/wailsapp/wails/v2/pkg/options/windows" 25 | ) 26 | 27 | // 不需要嵌入前端资源,因为我们使用 AssetServer.Handler 代理到后端服务器 28 | 29 | // App struct 30 | type App struct { 31 | ctx context.Context 32 | backendCmd *exec.Cmd 33 | backendPort int 34 | } 35 | 36 | // NewApp creates a new App application struct 37 | func NewApp() *App { 38 | return &App{} 39 | } 40 | 41 | // startup is called when the app starts. The context is saved 42 | // so we can call the runtime methods 43 | func (a *App) startup(ctx context.Context) { 44 | a.ctx = ctx 45 | } 46 | 47 | // shutdown is called when the app is closing 48 | func (a *App) shutdown(ctx context.Context) { 49 | // 清理后端进程 50 | if a.backendCmd != nil && a.backendCmd.Process != nil { 51 | a.backendCmd.Process.Kill() 52 | a.backendCmd.Wait() 53 | } 54 | } 55 | 56 | // getUserDataDir 获取用户数据目录:~/.0e7/ 57 | func getUserDataDir() string { 58 | homeDir, err := os.UserHomeDir() 59 | if err != nil { 60 | homeDir = "." 61 | } 62 | dataDir := filepath.Join(homeDir, ".0e7") 63 | if err := os.MkdirAll(dataDir, 0755); err != nil { 64 | return "." 65 | } 66 | return dataDir 67 | } 68 | 69 | // findFreePort 查找可用端口(参考 electron 的逻辑:45000-55000) 70 | func findFreePort(min, max, retries int) (int, error) { 71 | for i := 0; i < retries; i++ { 72 | rng := time.Now().UnixNano() + int64(i) 73 | port := min + int(rng%int64(max-min+1)) 74 | 75 | listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) 76 | if err == nil { 77 | addr := listener.Addr().(*net.TCPAddr) 78 | port := addr.Port 79 | listener.Close() 80 | return port, nil 81 | } 82 | } 83 | return 0, fmt.Errorf("无法找到可用端口") 84 | } 85 | 86 | // resolveBinaryPath 解析二进制文件路径(参考 electron 的逻辑) 87 | func resolveBinaryPath() (string, error) { 88 | var binaryName string 89 | 90 | switch runtime.GOOS { 91 | case "darwin": 92 | if runtime.GOARCH == "arm64" { 93 | binaryName = "0e7_darwin_arm64" 94 | } else { 95 | binaryName = "0e7_darwin_amd64" 96 | } 97 | case "linux": 98 | binaryName = "0e7_linux_amd64" 99 | case "windows": 100 | binaryName = "0e7_windows_amd64.exe" 101 | default: 102 | return "", fmt.Errorf("不支持的平台: %s", runtime.GOOS) 103 | } 104 | 105 | // 开发模式下,从项目根目录查找(相对于 wails 目录) 106 | binaryPathInRoot := filepath.Join("..", binaryName) 107 | if _, err := os.Stat(binaryPathInRoot); err == nil { 108 | absPath, _ := filepath.Abs(binaryPathInRoot) 109 | return absPath, nil 110 | } 111 | 112 | // 打包后,从 Resources/bin 目录查找(参考 electron 的结构) 113 | exePath, err := os.Executable() 114 | if err != nil { 115 | return "", err 116 | } 117 | exeDir := filepath.Dir(exePath) 118 | 119 | // macOS: 从 .app/Contents/Resources/bin 查找 120 | // Windows/Linux: 从可执行文件同目录的 bin 查找 121 | var resourcesBinDir string 122 | if runtime.GOOS == "darwin" { 123 | // macOS: 可执行文件在 .app/Contents/MacOS/,Resources 在 .app/Contents/Resources/ 124 | resourcesBinDir = filepath.Join(exeDir, "..", "Resources", "bin") 125 | } else { 126 | // Windows/Linux: 从可执行文件同目录的 bin 查找 127 | resourcesBinDir = filepath.Join(exeDir, "bin") 128 | } 129 | 130 | resourcesBinDir, _ = filepath.Abs(resourcesBinDir) 131 | binaryPath := filepath.Join(resourcesBinDir, binaryName) 132 | 133 | if _, err := os.Stat(binaryPath); err == nil { 134 | return binaryPath, nil 135 | } 136 | 137 | // 如果找不到,尝试从可执行文件同目录查找 138 | binaryPath = filepath.Join(exeDir, binaryName) 139 | if _, err := os.Stat(binaryPath); err == nil { 140 | return binaryPath, nil 141 | } 142 | 143 | return "", fmt.Errorf("未找到二进制文件: %s (已查找: %s, %s)", binaryName, filepath.Join(resourcesBinDir, binaryName), binaryPath) 144 | } 145 | 146 | // launchBackend 启动后端进程(参考 electron 的逻辑) 147 | func launchBackend(port int) (*exec.Cmd, error) { 148 | binaryPath, err := resolveBinaryPath() 149 | if err != nil { 150 | return nil, fmt.Errorf("解析二进制路径失败: %v", err) 151 | } 152 | 153 | userDataDir := getUserDataDir() 154 | configPath := filepath.Join(userDataDir, "config.ini") 155 | 156 | args := []string{ 157 | "--server", 158 | "--config", configPath, 159 | "--server-port", fmt.Sprintf("%d", port), 160 | } 161 | 162 | cmd := exec.Command(binaryPath, args...) 163 | cmd.Dir = userDataDir 164 | cmd.Env = append(os.Environ(), fmt.Sprintf("OE7_SERVER_PORT=%d", port)) 165 | 166 | // 重定向输出 167 | cmd.Stdout = os.Stdout 168 | cmd.Stderr = os.Stderr 169 | 170 | if err := cmd.Start(); err != nil { 171 | return nil, fmt.Errorf("启动后端进程失败: %v", err) 172 | } 173 | 174 | return cmd, nil 175 | } 176 | 177 | // waitForServer 等待服务器启动 178 | func waitForServer(port int, timeout time.Duration) error { 179 | deadline := time.Now().Add(timeout) 180 | url := fmt.Sprintf("http://127.0.0.1:%d", port) 181 | 182 | for time.Now().Before(deadline) { 183 | conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 1*time.Second) 184 | if err == nil { 185 | conn.Close() 186 | // 再等待一下确保服务器完全启动 187 | time.Sleep(500 * time.Millisecond) 188 | return nil 189 | } 190 | time.Sleep(200 * time.Millisecond) 191 | } 192 | 193 | return fmt.Errorf("等待服务器启动超时: %s", url) 194 | } 195 | 196 | func main() { 197 | // 查找可用端口(参考 electron:45000-55000) 198 | port, err := findFreePort(45000, 55000, 50) 199 | if err != nil { 200 | fmt.Fprintf(os.Stderr, "无法找到可用端口: %v\n", err) 201 | os.Exit(1) 202 | } 203 | 204 | // 启动后端进程 205 | backendCmd, err := launchBackend(port) 206 | if err != nil { 207 | fmt.Fprintf(os.Stderr, "启动后端失败: %v\n", err) 208 | os.Exit(1) 209 | } 210 | 211 | // 设置信号处理,确保程序退出时关闭后端进程 212 | sigChan := make(chan os.Signal, 1) 213 | signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) 214 | 215 | go func() { 216 | <-sigChan 217 | if backendCmd != nil && backendCmd.Process != nil { 218 | backendCmd.Process.Kill() 219 | backendCmd.Wait() 220 | } 221 | os.Exit(0) 222 | }() 223 | 224 | // 等待服务器启动 225 | if err := waitForServer(port, 60*time.Second); err != nil { 226 | fmt.Fprintf(os.Stderr, "%v\n", err) 227 | if backendCmd != nil && backendCmd.Process != nil { 228 | backendCmd.Process.Kill() 229 | } 230 | os.Exit(1) 231 | } 232 | 233 | // 创建应用 234 | app := NewApp() 235 | app.backendCmd = backendCmd 236 | app.backendPort = port 237 | 238 | // 创建应用选项 239 | backendURL := fmt.Sprintf("http://127.0.0.1:%d", port) 240 | appOptions := &options.App{ 241 | Title: "0E7 Desktop", 242 | Width: 1366, 243 | Height: 900, 244 | MinWidth: 1200, 245 | MinHeight: 720, 246 | // 使用 AssetServer.Handler 代理所有请求到后端服务器 247 | AssetServer: &assetserver.Options{ 248 | Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 249 | // 创建代理请求 250 | proxyReq, err := http.NewRequest(r.Method, backendURL+r.URL.Path+"?"+r.URL.RawQuery, r.Body) 251 | if err != nil { 252 | http.Error(w, err.Error(), http.StatusInternalServerError) 253 | return 254 | } 255 | // 复制请求头 256 | for key, values := range r.Header { 257 | for _, value := range values { 258 | proxyReq.Header.Add(key, value) 259 | } 260 | } 261 | // 执行请求 262 | client := &http.Client{Timeout: 30 * time.Second} 263 | resp, err := client.Do(proxyReq) 264 | if err != nil { 265 | http.Error(w, err.Error(), http.StatusBadGateway) 266 | return 267 | } 268 | defer resp.Body.Close() 269 | // 复制响应头 270 | for key, values := range resp.Header { 271 | for _, value := range values { 272 | w.Header().Add(key, value) 273 | } 274 | } 275 | w.WriteHeader(resp.StatusCode) 276 | // 复制响应体 277 | io.Copy(w, resp.Body) 278 | }), 279 | }, 280 | BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255}, 281 | OnStartup: app.startup, 282 | OnShutdown: app.shutdown, 283 | } 284 | 285 | // macOS 特定配置:确保关闭按钮不遮挡内容 286 | if runtime.GOOS == "darwin" { 287 | appOptions.Mac = &mac.Options{ 288 | TitleBar: &mac.TitleBar{ 289 | TitlebarAppearsTransparent: true, 290 | HideTitle: false, 291 | FullSizeContent: true, // 允许内容延伸到标题栏下方 292 | UseToolbar: false, 293 | }, 294 | Appearance: mac.NSAppearanceNameAqua, 295 | WebviewIsTransparent: false, 296 | WindowIsTranslucent: false, 297 | } 298 | } 299 | 300 | // Windows 特定配置 301 | if runtime.GOOS == "windows" { 302 | appOptions.Windows = &winOptions.Options{ 303 | WebviewIsTransparent: false, 304 | WindowIsTranslucent: false, 305 | DisableWindowIcon: false, 306 | } 307 | } 308 | 309 | // 创建并运行应用 310 | if err := wails.Run(appOptions); err != nil { 311 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 312 | if backendCmd != nil && backendCmd.Process != nil { 313 | backendCmd.Process.Kill() 314 | } 315 | os.Exit(1) 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /service/pcap/tcp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Google, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | // The pcapdump binary implements a tcpdump-like command line tool with gopacket 8 | // using pcap as a backend data collection mechanism. 9 | package pcap 10 | 11 | import ( 12 | "encoding/base64" 13 | "sync" 14 | "time" 15 | 16 | "github.com/google/gopacket" 17 | "github.com/google/gopacket/layers" 18 | "github.com/google/gopacket/reassembly" 19 | ) 20 | 21 | var allowmissinginit = true 22 | var verbose = false 23 | var debug = false 24 | var quiet = true 25 | 26 | const closeTimeout time.Duration = time.Hour * 24 // Closing inactive: UNTODO: from CLI 27 | const timeout time.Duration = time.Minute * 5 // Pending bytes: UNTODO: from CLI 28 | const streamdoc_limit int = 6_000_000 - 0x1000 // 16 MB (6 + (4/3)*6) - some overhead 29 | 30 | /* 31 | * The TCP factory: returns a new Stream 32 | */ 33 | type tcpStreamFactory struct { 34 | // The source of every tcp stream in this batch. 35 | // Traditionally, this would be the pcap file name 36 | source string 37 | reassemblyCallback func(FlowEntry) 38 | wg sync.WaitGroup 39 | linktype layers.LinkType 40 | } 41 | 42 | func (factory *tcpStreamFactory) New(net, transport gopacket.Flow, tcp *layers.TCP, ac reassembly.AssemblerContext) reassembly.Stream { 43 | fsmOptions := reassembly.TCPSimpleFSMOptions{ 44 | SupportMissingEstablishment: true, 45 | } 46 | stream := &tcpStream{ 47 | net: net, 48 | transport: transport, 49 | tcpstate: reassembly.NewTCPSimpleFSM(fsmOptions), 50 | optchecker: reassembly.NewTCPOptionCheck(), 51 | source: factory.source, 52 | FlowItems: []FlowItem{}, 53 | src_port: tcp.SrcPort, 54 | dst_port: tcp.DstPort, 55 | reassemblyCallback: factory.reassemblyCallback, 56 | linkType: factory.linktype, 57 | } 58 | return stream 59 | } 60 | 61 | func (factory *tcpStreamFactory) WaitGoRoutines() { 62 | factory.wg.Wait() 63 | } 64 | 65 | /* 66 | * The assembler context 67 | */ 68 | type Context struct { 69 | CaptureInfo gopacket.CaptureInfo 70 | // 保存原始数据包信息 71 | OriginalPacket gopacket.Packet 72 | } 73 | 74 | func (c *Context) GetCaptureInfo() gopacket.CaptureInfo { 75 | return c.CaptureInfo 76 | } 77 | 78 | /* 79 | * TCP stream 80 | */ 81 | 82 | /* It's a connection (bidirectional) */ 83 | type tcpStream struct { 84 | tcpstate *reassembly.TCPSimpleFSM 85 | fsmerr bool 86 | optchecker reassembly.TCPOptionCheck 87 | net, transport gopacket.Flow 88 | sync.Mutex 89 | // RDJ; These field are added to make mongo convertion easier 90 | source string 91 | reassemblyCallback func(FlowEntry) 92 | FlowItems []FlowItem 93 | src_port layers.TCPPort 94 | dst_port layers.TCPPort 95 | total_size int 96 | num_packets int 97 | // 保存所有原始数据包,用于Wireshark分析 98 | originalPackets [][]byte 99 | linkType layers.LinkType 100 | } 101 | 102 | func (t *tcpStream) Accept(tcp *layers.TCP, ci gopacket.CaptureInfo, dir reassembly.TCPFlowDirection, nextSeq reassembly.Sequence, start *bool, ac reassembly.AssemblerContext) bool { 103 | // FSM 104 | if !t.tcpstate.CheckState(tcp, dir) { 105 | if !t.fsmerr { 106 | t.fsmerr = true 107 | } 108 | if !nonstrict { 109 | return false 110 | } 111 | } 112 | 113 | // 保存原始数据包信息(只有在有载荷数据时才保存) 114 | if context, ok := ac.(*Context); ok && context.OriginalPacket != nil { 115 | // 检查TCP载荷是否为空,只有非空载荷才保存原始数据包 116 | if len(tcp.Payload) > 0 { 117 | // 限制数量,避免内存占用过大 118 | const maxOriginalPackets = 1000 119 | if len(t.originalPackets) < maxOriginalPackets { 120 | dataCopy := make([]byte, len(context.OriginalPacket.Data())) 121 | copy(dataCopy, context.OriginalPacket.Data()) 122 | t.originalPackets = append(t.originalPackets, dataCopy) 123 | } 124 | } 125 | } 126 | 127 | // We just ignore the Checksum 128 | return true 129 | } 130 | 131 | // ReassembledSG is called zero or more times. 132 | // ScatterGather is reused after each Reassembled call, 133 | // so it's important to copy anything you need out of it, 134 | // especially bytes (or use KeepFrom()) 135 | func (t *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.AssemblerContext) { 136 | dir, _, _, _ := sg.Info() 137 | length, _ := sg.Lengths() 138 | capInfo := ac.GetCaptureInfo() 139 | timestamp := capInfo.Timestamp 140 | t.num_packets += 1 141 | 142 | // Don't add empty streams to the DB 143 | if length == 0 { 144 | return 145 | } 146 | 147 | data := sg.Fetch(length) 148 | 149 | // We have to make sure to stay under the document limit 150 | t.total_size += length 151 | bytes_available := streamdoc_limit - t.total_size 152 | if length > bytes_available { 153 | length = bytes_available 154 | } 155 | if length < 0 { 156 | length = 0 157 | } 158 | string_data := string(data[:length]) 159 | 160 | var from string 161 | if dir == reassembly.TCPDirClientToServer { 162 | from = "c" 163 | } else { 164 | from = "s" 165 | } 166 | 167 | l := len(t.FlowItems) 168 | if l > 0 && t.FlowItems[l-1].From == from { 169 | existingData, err := base64.StdEncoding.DecodeString(t.FlowItems[l-1].B64) 170 | if err == nil { 171 | startsNewHTTP := hasHTTPStart([]byte(string_data)) 172 | endsWithHeader := endsWithDoubleCRLF(existingData) 173 | existingIsHTTP := hasHTTPStart(existingData) 174 | 175 | if existingIsHTTP { 176 | if !startsNewHTTP { 177 | combinedData := append(existingData, []byte(string_data)...) 178 | t.FlowItems[l-1].B64 = base64.StdEncoding.EncodeToString(combinedData) 179 | return 180 | } 181 | } else if !startsNewHTTP && !endsWithHeader { 182 | combinedData := append(existingData, []byte(string_data)...) 183 | t.FlowItems[l-1].B64 = base64.StdEncoding.EncodeToString(combinedData) 184 | return 185 | } 186 | } 187 | } 188 | 189 | // Add a FlowItem based on the data we just reassembled 190 | t.FlowItems = append(t.FlowItems, FlowItem{ 191 | B64: base64.StdEncoding.EncodeToString([]byte(string_data)), 192 | From: from, 193 | Time: int(timestamp.UnixNano() / 1000000), // UNTODO; maybe use int64? 194 | }) 195 | 196 | } 197 | 198 | // 粗略检测是否为 HTTP 报文起始(请求或响应) 199 | func hasHTTPStart(b []byte) bool { 200 | if len(b) < 4 { 201 | return false 202 | } 203 | // 常见方法/响应前缀 204 | prefixes := [][]byte{ 205 | []byte("GET "), []byte("POST "), []byte("HEAD "), []byte("PUT "), []byte("DELETE "), 206 | []byte("OPTIONS "), []byte("TRACE "), []byte("PATCH "), []byte("CONNECT "), 207 | []byte("HTTP/1."), []byte("HTTP/2"), 208 | } 209 | for _, p := range prefixes { 210 | if len(b) >= len(p) && string(b[:len(p)]) == string(p) { 211 | return true 212 | } 213 | } 214 | return false 215 | } 216 | 217 | func endsWithDoubleCRLF(b []byte) bool { 218 | if len(b) < 4 { 219 | return false 220 | } 221 | n := len(b) 222 | // \r\n\r\n 223 | return b[n-4] == '\r' && b[n-3] == '\n' && b[n-2] == '\r' && b[n-1] == '\n' 224 | } 225 | 226 | // ReassemblyComplete is called when assembly decides there is 227 | // no more data for this Stream, either because a FIN or RST packet 228 | // was seen, or because the stream has timed out without any new 229 | // packet data (due to a call to FlushCloseOlderThan). 230 | // It should return true if the connection should be removed from the pool 231 | // It can return false if it want to see subsequent packets with Accept(), e.g. to 232 | // see FIN-ACK, for deeper state-machine analysis. 233 | func (t *tcpStream) ReassemblyComplete(ac reassembly.AssemblerContext) bool { 234 | 235 | // Insert the stream into the mogodb. 236 | 237 | /* 238 | { 239 | "src_port": 32858, 240 | "dst_ip": "10.10.3.1", 241 | "contains_flag": false, 242 | "flow": [{}], 243 | "filename": "services/test_pcap/dump-2018-06-27_13:25:31.pcap", 244 | "src_ip": "10.10.3.126", 245 | "dst_port": 8080, 246 | "time": 1530098789655, 247 | "duration": 96, 248 | "inx": 0, 249 | } 250 | */ 251 | src, dst := t.net.Endpoints() 252 | var time, duration int 253 | if len(t.FlowItems) == 0 { 254 | // No point in inserting this element, it has no data and even if we wanted to, 255 | // we can't timestamp it so the front-end can't display it either 256 | return false 257 | } 258 | 259 | // 找到最小和最大时间戳来计算准确的持续时间 260 | minTime := t.FlowItems[0].Time 261 | maxTime := t.FlowItems[0].Time 262 | 263 | for _, item := range t.FlowItems { 264 | if item.Time < minTime { 265 | minTime = item.Time 266 | } 267 | if item.Time > maxTime { 268 | maxTime = item.Time 269 | } 270 | } 271 | 272 | time = minTime 273 | duration = maxTime - minTime 274 | 275 | entry := FlowEntry{ 276 | SrcPort: int(t.src_port), 277 | DstPort: int(t.dst_port), 278 | SrcIp: src.String(), 279 | DstIp: dst.String(), 280 | Time: time, 281 | Duration: duration, 282 | NumPackets: t.num_packets, 283 | Blocked: false, 284 | Filename: t.source, 285 | Flow: t.FlowItems, 286 | Size: t.total_size, 287 | OriginalPackets: t.originalPackets, 288 | LinkType: t.linkType, 289 | } 290 | 291 | t.reassemblyCallback(entry) 292 | 293 | return false 294 | } 295 | -------------------------------------------------------------------------------- /service/webui/action.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "0E7/service/config" 5 | "0E7/service/database" 6 | "encoding/json" 7 | "math" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/gin-gonic/gin" 14 | "gorm.io/gorm" 15 | ) 16 | 17 | func action(c *gin.Context) { 18 | var err error 19 | id := c.PostForm("id") 20 | name := c.PostForm("name") 21 | code := c.PostForm("code") 22 | output := c.PostForm("output") 23 | interval := c.PostForm("interval") 24 | timeout := c.PostForm("timeout") 25 | configStr := c.PostForm("config") 26 | 27 | if code != "" { 28 | match := regexp.MustCompile(`^data:(code\/(?:python2|python3|golang|bash));base64,(.*)$`).FindStringSubmatch(code) 29 | if match == nil { 30 | c.JSON(400, gin.H{ 31 | "message": "fail", 32 | "error": "code format error", 33 | }) 34 | c.Abort() 35 | return 36 | } 37 | } 38 | 39 | intervalInt, _ := strconv.Atoi(interval) 40 | timeoutInt, _ := strconv.Atoi(timeout) 41 | 42 | // 限制超时时间在 0-60 秒之间 43 | if timeoutInt < 0 { 44 | timeoutInt = 0 45 | } else if timeoutInt > 60 { 46 | timeoutInt = 60 47 | } 48 | 49 | actionRecord := database.Action{ 50 | Name: name, 51 | Code: code, 52 | Output: output, 53 | Config: configStr, 54 | Status: "PENDING", 55 | Interval: intervalInt, 56 | Timeout: timeoutInt, 57 | } 58 | 59 | if id == "" { 60 | err = config.Db.Create(&actionRecord).Error 61 | } else { 62 | idInt, _ := strconv.Atoi(id) 63 | actionRecord.ID = idInt 64 | err = config.Db.Save(&actionRecord).Error 65 | } 66 | 67 | if err != nil { 68 | c.JSON(400, gin.H{ 69 | "message": "fail", 70 | "error": err.Error(), 71 | }) 72 | c.Abort() 73 | return 74 | } 75 | c.JSON(200, gin.H{ 76 | "message": "success", 77 | "error": "", 78 | }) 79 | } 80 | 81 | func action_show(c *gin.Context) { 82 | var err error 83 | id := c.PostForm("id") 84 | name := c.PostForm("name") 85 | code := c.PostForm("code") 86 | output := c.PostForm("output") 87 | page_size := c.PostForm("page_size") 88 | page_num := c.PostForm("page") 89 | offset := 1 90 | if page_num != "" { 91 | offset, err = strconv.Atoi(page_num) 92 | if err != nil { 93 | c.JSON(400, gin.H{ 94 | "message": "fail", 95 | "error": err.Error(), 96 | "total": 0, 97 | "result": []interface{}{}, 98 | }) 99 | return 100 | } 101 | if offset <= 0 { 102 | offset = 1 103 | } 104 | } 105 | multi := 20 106 | if page_size != "" { 107 | multi, err = strconv.Atoi(page_size) 108 | if err != nil { 109 | c.JSON(400, gin.H{ 110 | "message": "fail", 111 | "error": err.Error(), 112 | "total": 0, 113 | "result": []interface{}{}, 114 | }) 115 | return 116 | } 117 | if multi <= 0 { 118 | multi = 1 119 | } 120 | } 121 | 122 | // 构建查询条件 123 | var filter_argv []interface{} 124 | var filter_sql string 125 | 126 | if name != "" { 127 | filter_sql = filter_sql + " AND name LIKE ?" 128 | filter_argv = append(filter_argv, "%"+name+"%") 129 | } 130 | if code != "" { 131 | filter_sql = filter_sql + " AND code LIKE ?" 132 | filter_argv = append(filter_argv, "%"+code+"%") 133 | } 134 | if output != "" { 135 | filter_sql = filter_sql + " AND output LIKE ?" 136 | filter_argv = append(filter_argv, "%"+output+"%") 137 | } 138 | 139 | // 构建基础查询,过滤已删除的记录 140 | baseQuery := config.Db.Model(&database.Action{}).Where("is_deleted = ?", false) 141 | if filter_sql != "" { 142 | // 移除开头的 " AND " 143 | filter_sql = strings.TrimPrefix(filter_sql, " AND ") 144 | baseQuery = baseQuery.Where(filter_sql, filter_argv...) 145 | } 146 | 147 | var count int64 148 | err = baseQuery.Count(&count).Error 149 | if err != nil { 150 | c.JSON(400, gin.H{ 151 | "message": "fail", 152 | "error": err.Error(), 153 | "total": 0, 154 | "result": []interface{}{}, 155 | }) 156 | return 157 | } 158 | page_count := 1 159 | if count > 0 { 160 | page_count = int(math.Ceil(float64(count) / float64(multi))) 161 | } 162 | 163 | // 当没有数据时,直接返回空结果,而不是报错 164 | if count == 0 { 165 | c.JSON(200, gin.H{ 166 | "message": "success", 167 | "error": "", 168 | "total": count, 169 | "result": []interface{}{}, 170 | }) 171 | return 172 | } 173 | 174 | if page_count < offset { 175 | c.JSON(400, gin.H{ 176 | "message": "fail", 177 | "error": "Page Error", 178 | "total": count, 179 | "result": []interface{}{}, 180 | }) 181 | return 182 | } 183 | 184 | var actions []database.Action 185 | if id == "" { 186 | query := baseQuery.Order("id DESC").Limit(multi).Offset((offset - 1) * multi) 187 | err = query.Find(&actions).Error 188 | } else { 189 | err = config.Db.Where("id = ?", id).Order("id DESC").Limit(multi).Offset((offset - 1) * multi).Find(&actions).Error 190 | } 191 | if err != nil { 192 | c.JSON(400, gin.H{ 193 | "message": "fail", 194 | "error": err.Error(), 195 | "total": 0, 196 | "result": []interface{}{}, 197 | }) 198 | return 199 | } 200 | var ret []map[string]interface{} 201 | for _, action := range actions { 202 | code := action.Code 203 | output := action.Output 204 | if len(code) > 10240 { 205 | code = code[:10240] 206 | } 207 | if len(output) > 10240 { 208 | output = output[:10240] 209 | } 210 | 211 | element := map[string]interface{}{ 212 | "id": action.ID, 213 | "name": action.Name, 214 | "code": code, 215 | "output": output, 216 | "error": action.Error, 217 | "config": action.Config, 218 | "interval": action.Interval, 219 | "timeout": action.Timeout, 220 | "status": action.Status, 221 | "next_run": action.NextRun.Format(time.DateTime), 222 | "updated": action.UpdatedAt.Format(time.DateTime), 223 | } 224 | ret = append(ret, element) 225 | } 226 | c.JSON(200, gin.H{ 227 | "message": "success", 228 | "error": "", 229 | "total": count, 230 | "result": ret, 231 | }) 232 | } 233 | 234 | func action_delete(c *gin.Context) { 235 | action_id := c.PostForm("id") 236 | if action_id == "" { 237 | c.JSON(400, gin.H{"message": "fail", "error": "id is required"}) 238 | return 239 | } 240 | 241 | // 软删除:将is_deleted设置为true 242 | result := config.Db.Model(&database.Action{}).Where("id = ?", action_id).Update("is_deleted", true) 243 | if result.Error != nil { 244 | c.JSON(500, gin.H{"message": "fail", "error": result.Error.Error()}) 245 | return 246 | } 247 | 248 | if result.RowsAffected == 0 { 249 | c.JSON(404, gin.H{"message": "fail", "error": "action not found"}) 250 | return 251 | } 252 | 253 | c.JSON(200, gin.H{ 254 | "message": "success", 255 | "error": "", 256 | }) 257 | } 258 | 259 | func action_execute(c *gin.Context) { 260 | action_id := c.PostForm("id") 261 | if action_id == "" { 262 | c.JSON(400, gin.H{"message": "fail", "error": "id is required"}) 263 | return 264 | } 265 | 266 | // 查找Action 267 | var action database.Action 268 | err := config.Db.Where("id = ? AND is_deleted = ?", action_id, false).First(&action).Error 269 | if err != nil { 270 | c.JSON(404, gin.H{"message": "fail", "error": "action not found"}) 271 | return 272 | } 273 | 274 | // 解析配置,检查是否为exec_script类型 275 | var actionConfig struct { 276 | Type string `json:"type"` 277 | } 278 | if action.Config != "" { 279 | json.Unmarshal([]byte(action.Config), &actionConfig) 280 | } 281 | 282 | // 检查是否有代码(exec_script类型不需要代码) 283 | if action.Code == "" && actionConfig.Type != "exec_script" { 284 | c.JSON(400, gin.H{"message": "fail", "error": "action has no code"}) 285 | return 286 | } 287 | 288 | // 设置next_run为1999年1月1日,这样会被立即执行 289 | action.NextRun = time.Date(1999, 1, 1, 0, 0, 0, 0, time.UTC) 290 | 291 | // 更新数据库 292 | err = config.Db.Save(&action).Error 293 | if err != nil { 294 | c.JSON(500, gin.H{"message": "fail", "error": err.Error()}) 295 | return 296 | } 297 | 298 | c.JSON(200, gin.H{ 299 | "message": "success", 300 | "error": "", 301 | }) 302 | } 303 | 304 | // action_get_by_id 根据ID获取Action详情 305 | func action_get_by_id(c *gin.Context) { 306 | action_id := c.Query("id") 307 | if action_id == "" { 308 | c.JSON(400, gin.H{ 309 | "message": "fail", 310 | "error": "ID参数不能为空", 311 | }) 312 | return 313 | } 314 | 315 | var action database.Action 316 | err := config.Db.Where("id = ? AND is_deleted = ?", action_id, false).First(&action).Error 317 | if err != nil { 318 | if err == gorm.ErrRecordNotFound { 319 | c.JSON(404, gin.H{ 320 | "message": "fail", 321 | "error": "定时计划不存在", 322 | }) 323 | } else { 324 | c.JSON(500, gin.H{ 325 | "message": "fail", 326 | "error": "查询失败: " + err.Error(), 327 | }) 328 | } 329 | return 330 | } 331 | 332 | // 格式化next_run时间 333 | var nextRunStr string 334 | if !action.NextRun.IsZero() { 335 | nextRunStr = action.NextRun.Format("2006-01-02 15:04:05") 336 | } 337 | 338 | element := map[string]interface{}{ 339 | "id": action.ID, 340 | "name": action.Name, 341 | "code": action.Code, 342 | "output": action.Output, 343 | "error": action.Error, 344 | "config": action.Config, 345 | "interval": action.Interval, 346 | "timeout": action.Timeout, 347 | "status": action.Status, 348 | "next_run": nextRunStr, 349 | "created_at": action.CreatedAt.Format("2006-01-02 15:04:05"), 350 | "updated_at": action.UpdatedAt.Format("2006-01-02 15:04:05"), 351 | } 352 | 353 | c.JSON(200, gin.H{ 354 | "message": "success", 355 | "result": element, 356 | }) 357 | } 358 | -------------------------------------------------------------------------------- /service/database/const.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Client 客户端信息表 8 | type Client struct { 9 | ID int `json:"id" gorm:"column:id;primary_key;auto_increment;"` 10 | Name string `json:"name" gorm:"column:name;type:varchar(255);not null;default:'';index;"` 11 | Hostname string `json:"hostname" gorm:"column:hostname;type:varchar(255);not null;default:'';index;"` 12 | Platform string `json:"platform" gorm:"column:platform;type:varchar(255);not null;default:'';"` 13 | Arch string `json:"arch" gorm:"column:arch;type:varchar(255);not null;default:'';"` 14 | CPU string `json:"cpu" gorm:"column:cpu;type:varchar(255);not null;default:'';"` 15 | CPUUse string `json:"cpu_use" gorm:"column:cpu_use;type:varchar(255);not null;default:'';"` 16 | MemoryUse string `json:"memory_use" gorm:"column:memory_use;type:varchar(255);not null;default:'';"` 17 | MemoryMax string `json:"memory_max" gorm:"column:memory_max;type:varchar(255);not null;default:'';"` 18 | Pcap string `json:"pcap" gorm:"column:pcap;type:varchar(255);not null;default:'';"` 19 | CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index;"` 20 | UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index;"` 21 | } 22 | 23 | func (Client) TableName() string { 24 | return "0e7_client" 25 | } 26 | 27 | // Exploit 漏洞利用表 28 | type Exploit struct { 29 | ID int `json:"id" gorm:"column:id;primary_key;auto_increment;"` 30 | Name string `json:"name" gorm:"column:name;type:varchar(255);not null;index;"` 31 | Filename string `json:"filename" gorm:"column:filename;type:varchar(255);"` 32 | Environment string `json:"environment" gorm:"column:environment;type:varchar(255);"` 33 | Command string `json:"command" gorm:"column:command;type:varchar(255);"` 34 | Argv string `json:"argv" gorm:"column:argv;type:varchar(255);"` 35 | Platform string `json:"platform" gorm:"column:platform;type:varchar(255);index;"` 36 | Arch string `json:"arch" gorm:"column:arch;type:varchar(255);index;"` 37 | Filter string `json:"filter" gorm:"column:filter;type:varchar(255);"` 38 | Timeout string `json:"timeout" gorm:"column:timeout;type:varchar(255);"` 39 | Times string `json:"times" gorm:"column:times;type:varchar(255);not null;default:'0';index;"` 40 | Flag string `json:"flag" gorm:"column:flag;type:varchar(255);"` 41 | Team string `json:"team" gorm:"column:team;type:varchar(255);index;"` 42 | IsDeleted bool `json:"is_deleted" gorm:"column:is_deleted;type:boolean;default:false;index;"` 43 | 44 | CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index;"` 45 | UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` 46 | } 47 | 48 | func (Exploit) TableName() string { 49 | return "0e7_exploit" 50 | } 51 | 52 | // Flag 标志表 53 | type Flag struct { 54 | ID int `json:"id" gorm:"column:id;primary_key;auto_increment;"` 55 | ExploitId int `json:"exploit_id" gorm:"column:exploit_id;type:int;not null;default:0;index;"` 56 | Team string `json:"team" gorm:"column:team;type:varchar(255);not null;default:'';index;"` 57 | Flag string `json:"flag" gorm:"column:flag;type:varchar(255);not null;default:'';index;"` 58 | Status string `json:"status" gorm:"column:status;type:varchar(255);index;"` 59 | Msg string `json:"msg" gorm:"column:msg;type:text;"` // 提交结果消息 60 | Score float64 `json:"score" gorm:"column:score;type:float;not null;default:0;index;"` // 提交分数 61 | ExploitName string `json:"exploit_name" gorm:"-"` // 不存储到数据库,仅用于显示 62 | CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index;"` 63 | UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` 64 | } 65 | 66 | func (Flag) TableName() string { 67 | return "0e7_flag" 68 | } 69 | 70 | // ExploitOutput 漏洞利用输出表 71 | type ExploitOutput struct { 72 | ID int `json:"id" gorm:"column:id;primary_key;auto_increment;"` 73 | ExploitId int `json:"exploit_id" gorm:"column:exploit_id;type:int;not null;default:0;index;"` 74 | ClientId int `json:"client_id" gorm:"column:client_id;type:int;not null;default:0;index;"` 75 | Team string `json:"team" gorm:"column:team;type:varchar(255);not null;default:'';index;"` 76 | Output string `json:"output" gorm:"column:output;type:text;"` 77 | Status string `json:"status" gorm:"column:status;type:varchar(255);index;"` 78 | CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index;"` 79 | UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` 80 | } 81 | 82 | func (ExploitOutput) TableName() string { 83 | return "0e7_exploit_output" 84 | } 85 | 86 | // Action 动作表 87 | type Action struct { 88 | ID int `json:"id" gorm:"column:id;primary_key;auto_increment;"` 89 | Name string `json:"name" gorm:"column:name;type:varchar(255);not null;default:'';index;"` 90 | Code string `json:"code" gorm:"column:code;type:text;"` 91 | Output string `json:"output" gorm:"column:output;type:text;"` 92 | Error string `json:"error" gorm:"column:error;type:text;"` 93 | Config string `json:"config" gorm:"column:config;type:text;"` 94 | Timeout int `json:"timeout" gorm:"column:timeout;type:int;default:60;"` // 超时时间(秒),默认60秒,最多60秒 95 | Status string `json:"status" gorm:"column:status;type:varchar(50);default:'pending';not null;index;"` // 任务状态:pending, running, completed, timeout, error 96 | IsDeleted bool `json:"is_deleted" gorm:"column:is_deleted;type:boolean;default:false;index:idx_action_deleted_nextrun_interval;index;"` // 用于复合索引优化查询(索引顺序:is_deleted, next_run, interval) 97 | NextRun time.Time `json:"next_run" gorm:"column:next_run;type:datetime;index:idx_action_deleted_nextrun_interval;index;"` // 下次执行时间,用于复合索引 98 | Interval int `json:"interval" gorm:"column:interval;type:int;index:idx_action_deleted_nextrun_interval;"` // 用于复合索引 99 | CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index;"` 100 | UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` 101 | } 102 | 103 | func (Action) TableName() string { 104 | return "0e7_action" 105 | } 106 | 107 | // PcapFile PCAP文件表 108 | type PcapFile struct { 109 | ID int `json:"id" gorm:"column:id;primary_key;auto_increment;"` 110 | Filename string `json:"filename" gorm:"column:filename;type:varchar(255);index;"` 111 | ModTime time.Time `json:"mod_time" gorm:"column:mod_time;type:datetime;index;"` 112 | FileSize int64 `json:"file_size" gorm:"column:file_size;type:bigint;"` 113 | MD5 string `json:"md5" gorm:"column:md5;type:varchar(32);index;"` 114 | CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index;"` 115 | UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` 116 | } 117 | 118 | func (PcapFile) TableName() string { 119 | return "0e7_pcapfile" 120 | } 121 | 122 | // Monitor 监控表 123 | type Monitor struct { 124 | ID int `json:"id" gorm:"column:id;primary_key;auto_increment;"` 125 | ClientId int `json:"client_id" gorm:"column:client_id;type:int;not null;default:0;index;"` 126 | Name string `json:"name" gorm:"column:name;type:varchar(255);index;"` 127 | Types string `json:"types" gorm:"column:types;type:varchar(255);index;"` 128 | Data string `json:"data" gorm:"column:data;type:text;"` 129 | Interval int `json:"interval" gorm:"column:interval;type:int;"` 130 | CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index;"` 131 | UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` 132 | } 133 | 134 | func (Monitor) TableName() string { 135 | return "0e7_monitor" 136 | } 137 | 138 | // Pcap PCAP数据表 139 | type Pcap struct { 140 | ID int `json:"id" gorm:"column:id;primary_key;auto_increment;"` 141 | SrcPort string `json:"src_port" gorm:"column:src_port;type:varchar(255);index;"` 142 | DstPort string `json:"dst_port" gorm:"column:dst_port;type:varchar(255);index;"` 143 | SrcIP string `json:"src_ip" gorm:"column:src_ip;type:varchar(255);index;"` 144 | DstIP string `json:"dst_ip" gorm:"column:dst_ip;type:varchar(255);index;"` 145 | Time int `json:"time" gorm:"column:time;type:int;index;"` 146 | Duration int `json:"duration" gorm:"column:duration;type:int;"` 147 | NumPackets int `json:"num_packets" gorm:"column:num_packets;type:int;"` 148 | Blocked string `json:"blocked" gorm:"column:blocked;type:varchar(255);index;"` 149 | Filename string `json:"filename" gorm:"column:filename;type:varchar(255);index;"` 150 | FlowFile string `json:"flow_file" gorm:"column:flow_file;type:text;"` // 大文件路径(前端通过有无判断是否需要点击加载) 151 | FlowData string `json:"flow_data,omitempty" gorm:"column:flow_data;type:longtext;"` // 小文件时的JSON字符串(前端自己解析) 152 | PcapFile string `json:"pcap_file" gorm:"column:pcap_file;type:varchar(255);"` // pcap文件路径 153 | PcapData string `json:"-" gorm:"column:pcap_data;type:longblob;"` // 不返回(小pcap数据,base64编码) 154 | Tags string `json:"tags" gorm:"column:tags;type:text;index;"` 155 | ClientContent string `json:"-" gorm:"column:client_content;type:text;index;"` // 不返回(用于搜索) 156 | ServerContent string `json:"-" gorm:"column:server_content;type:text;index;"` // 不返回(用于搜索) 157 | Size int `json:"size" gorm:"column:size;type:int;"` 158 | CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index;"` 159 | UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` 160 | } 161 | 162 | func (Pcap) TableName() string { 163 | return "0e7_pcap" 164 | } 165 | -------------------------------------------------------------------------------- /service/route/exploit.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "0E7/service/config" 5 | "0E7/service/database" 6 | "crypto/md5" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "log" 12 | "math/rand/v2" 13 | "os" 14 | "regexp" 15 | "strconv" 16 | "strings" 17 | 18 | "github.com/gin-gonic/gin" 19 | "gorm.io/gorm" 20 | ) 21 | 22 | type Bucket struct { 23 | Team string `json:"team"` 24 | Value string `json:"value"` 25 | } 26 | 27 | func exploit(c *gin.Context) { 28 | exploit_mutex.Lock() 29 | defer exploit_mutex.Unlock() 30 | 31 | client_id_str := c.Query("client_id") 32 | platform := c.Query("platform") 33 | arch := c.Query("arch") 34 | 35 | var exploit database.Exploit 36 | err := config.Db.Where("(filter = '' OR filter LIKE ?) AND (platform = '' OR platform LIKE ?) AND (arch = '' OR arch LIKE ?) AND (times <= -2 OR times >= 1) AND is_deleted = ?", 37 | "%"+client_id_str+"%", "%"+platform+"%", "%"+arch+"%", false). 38 | Order("RANDOM()"). 39 | First(&exploit).Error 40 | if err != nil { 41 | if err == gorm.ErrRecordNotFound { 42 | c.JSON(202, gin.H{ 43 | "message": "no task", 44 | "error": "", 45 | "name": "", 46 | "filename": "", 47 | "environment": "", 48 | "command": "", 49 | "argv": "", 50 | "flag": "", 51 | "team": "", 52 | "timeout": "", 53 | }) 54 | return 55 | } else { 56 | c.JSON(400, gin.H{ 57 | "message": "fail", 58 | "error": err.Error(), 59 | "name": "", 60 | "filename": "", 61 | "environment": "", 62 | "command": "", 63 | "argv": "", 64 | "flag": "", 65 | "team": "", 66 | "timeout": "", 67 | }) 68 | return 69 | } 70 | } 71 | if exploit.Name == "" { 72 | c.JSON(202, gin.H{ 73 | "message": "no task", 74 | "error": "", 75 | "name": "", 76 | "filename": "", 77 | "environment": "", 78 | "command": "", 79 | "argv": "", 80 | "flag": "", 81 | "team": "", 82 | "timeout": "", 83 | }) 84 | } else { 85 | costTime := false 86 | 87 | reg := regexp.MustCompile(`\{[^}]+\}`) 88 | matches := reg.FindAllString(exploit.Argv, -1) 89 | if len(matches) == 0 { 90 | costTime = true 91 | } 92 | for _, match := range matches { 93 | action := match[1 : len(match)-1] 94 | if strings.HasPrefix(action, "ipbucket_") || strings.HasPrefix(action, "cache_") { 95 | var backet []Bucket 96 | if value, ok := exploit_bucket.Load(exploit.ID); !ok { 97 | var actionRecord database.Action 98 | err = config.Db.Where("name = ? AND is_deleted = ?", action, false).First(&actionRecord).Error 99 | if err != nil { 100 | actionRecord.Output = "" 101 | } 102 | err = json.Unmarshal([]byte(actionRecord.Output), &backet) 103 | if err != nil { 104 | lines := strings.Split(actionRecord.Output, "\n") 105 | for _, line := range lines { 106 | line = strings.TrimSpace(line) 107 | if line == "" { 108 | continue 109 | } 110 | backet = append(backet, Bucket{ 111 | Team: line, 112 | Value: line, 113 | }) 114 | } 115 | } 116 | if strings.HasPrefix(action, "ipbucket_") { 117 | rand.Shuffle(len(backet), func(i, j int) { 118 | backet[i], backet[j] = backet[j], backet[i] 119 | }) 120 | } 121 | } else { 122 | backet = value.([]Bucket) 123 | } 124 | if len(backet) == 0 { 125 | backet = []Bucket{ 126 | { 127 | Team: "NOTHING_IN_BUCKET", 128 | Value: "NOTHING_IN_BUCKET", 129 | }, 130 | } 131 | } 132 | first := backet[0] 133 | backet = backet[1:] 134 | if len(backet) == 0 { 135 | exploit_bucket.Delete(exploit.ID) 136 | costTime = true 137 | } else { 138 | exploit_bucket.Store(exploit.ID, backet) 139 | } 140 | exploit.Argv = strings.Replace(exploit.Argv, match, first.Value, 1) 141 | exploit.Team = first.Team 142 | } else { 143 | var actionRecord database.Action 144 | err = config.Db.Where("name = ? AND is_deleted = ?", action, false).First(&actionRecord).Error 145 | if err == nil { 146 | exploit.Argv = strings.Replace(exploit.Argv, match, actionRecord.Output, 1) 147 | } 148 | costTime = true 149 | } 150 | } 151 | if exploit.Flag == "" { 152 | exploit.Flag = config.Server_flag 153 | } 154 | 155 | times, _ := strconv.Atoi(exploit.Times) 156 | if times >= 0 && costTime { 157 | times -= 1 158 | err = config.Db.Model(&exploit).Where("id = ?", exploit.ID).Update("times", fmt.Sprintf("%d", times)).Error 159 | if err != nil { 160 | c.JSON(400, gin.H{ 161 | "message": "fail", 162 | "error": err.Error(), 163 | "name": "", 164 | "filename": "", 165 | "environment": "", 166 | "command": "", 167 | "argv": "", 168 | "flag": "", 169 | "team": "", 170 | "timeout": "", 171 | }) 172 | c.Abort() 173 | } 174 | } 175 | 176 | log.Printf("任务 %s 已分配给客户端 %s", exploit.Name, client_id_str) 177 | 178 | c.JSON(200, gin.H{ 179 | "message": "success", 180 | "error": "", 181 | "id": exploit.ID, 182 | "name": exploit.Name, 183 | "filename": exploit.Filename, 184 | "environment": exploit.Environment, 185 | "command": exploit.Command, 186 | "argv": exploit.Argv, 187 | "flag": config.Server_flag, 188 | "flag_regex": exploit.Flag, 189 | "team": exploit.Team, 190 | "timeout": exploit.Timeout, 191 | }) 192 | } 193 | } 194 | 195 | // calculateFileMD5 计算文件的MD5值 196 | func calculateFileMD5(filePath string) (string, error) { 197 | file, err := os.Open(filePath) 198 | if err != nil { 199 | return "", err 200 | } 201 | defer file.Close() 202 | 203 | hash := md5.New() 204 | if _, err := io.Copy(hash, file); err != nil { 205 | return "", err 206 | } 207 | 208 | return hex.EncodeToString(hash.Sum(nil)), nil 209 | } 210 | 211 | func exploit_download(c *gin.Context) { 212 | exploit_id := c.Query("id") 213 | filename := c.Query("filename") 214 | filePath := "upload/" + exploit_id + "/" + filename 215 | _, err := os.Stat(filePath) 216 | if err != nil { 217 | c.JSON(404, gin.H{ 218 | "message": "fail", 219 | "error": err.Error(), 220 | }) 221 | c.Abort() 222 | return 223 | } 224 | 225 | // 计算文件MD5并添加到响应头 226 | fileMD5, err := calculateFileMD5(filePath) 227 | if err == nil { 228 | c.Header("X-File-MD5", fileMD5) 229 | } 230 | 231 | c.Header("Content-Disposition", "attachment; filename="+filename) 232 | c.Header("Content-Type", "application/octet-stream") 233 | 234 | // 如果是HEAD请求,只返回响应头,不返回文件内容 235 | if c.Request.Method == "HEAD" { 236 | fileInfo, _ := os.Stat(filePath) 237 | if fileInfo != nil { 238 | c.Header("Content-Length", fmt.Sprintf("%d", fileInfo.Size())) 239 | } 240 | c.Status(200) 241 | return 242 | } 243 | 244 | c.File(filePath) 245 | } 246 | func exploit_output(c *gin.Context) { 247 | var err error 248 | id := c.PostForm("id") 249 | exploit_id_str := c.PostForm("exploit_id") 250 | client_id_str := c.PostForm("client_id") 251 | output := c.PostForm("output") 252 | status := c.PostForm("status") 253 | team := c.PostForm("team") 254 | 255 | // 转换exploit_id为int 256 | exploit_id, err := strconv.Atoi(exploit_id_str) 257 | if err != nil { 258 | c.JSON(400, gin.H{ 259 | "message": "fail", 260 | "error": "invalid exploit_id: " + err.Error(), 261 | "id": "", 262 | }) 263 | log.Println("Invalid exploit_id:", err) 264 | return 265 | } 266 | 267 | // 转换client_id为int 268 | client_id, err := strconv.Atoi(client_id_str) 269 | if err != nil { 270 | c.JSON(400, gin.H{ 271 | "message": "fail", 272 | "error": "invalid client_id: " + err.Error(), 273 | "id": "", 274 | }) 275 | log.Println("Invalid client_id:", err) 276 | return 277 | } 278 | 279 | if id == "" { 280 | // 获取exploit名称用于日志 281 | var exploit database.Exploit 282 | config.Db.First(&exploit, exploit_id) 283 | 284 | log.Printf("开始执行任务 %s (ID: %d)", exploit.Name, exploit_id) 285 | 286 | // 如果传递了team参数,使用传递的team,否则使用exploit的team 287 | teamValue := team 288 | if teamValue == "" { 289 | teamValue = exploit.Team 290 | } 291 | 292 | exploitOutput := database.ExploitOutput{ 293 | ExploitId: exploit_id, 294 | ClientId: client_id, 295 | Team: teamValue, 296 | Output: output, 297 | Status: status, 298 | } 299 | err := config.Db.Create(&exploitOutput).Error 300 | if err != nil { 301 | c.JSON(400, gin.H{ 302 | "message": "fail", 303 | "error": err.Error(), 304 | "id": "", 305 | }) 306 | log.Println(err) 307 | return 308 | } 309 | c.JSON(200, gin.H{ 310 | "message": "success", 311 | "error": "", 312 | "id": fmt.Sprintf("%d", exploitOutput.ID), 313 | }) 314 | } else { 315 | var exploitOutput database.ExploitOutput 316 | err = config.Db.First(&exploitOutput, id).Error 317 | if err != nil { 318 | c.JSON(400, gin.H{ 319 | "message": "fail", 320 | "error": err.Error(), 321 | "id": "", 322 | }) 323 | log.Println(err) 324 | return 325 | } 326 | 327 | if status == "RUNNING" { 328 | exploitOutput.Output += output 329 | } else { 330 | exploitOutput.Output = output 331 | // 任务完成时记录日志 332 | var exploit database.Exploit 333 | config.Db.First(&exploit, exploitOutput.ExploitId) 334 | log.Printf("任务 %s (ID: %d) 执行%s", exploit.Name, exploitOutput.ExploitId, status) 335 | } 336 | exploitOutput.Status = status 337 | 338 | err = config.Db.Save(&exploitOutput).Error 339 | if err != nil { 340 | c.JSON(400, gin.H{ 341 | "message": "fail", 342 | "error": err.Error(), 343 | "id": "", 344 | }) 345 | log.Println(err) 346 | return 347 | } 348 | c.JSON(200, gin.H{ 349 | "message": "update", 350 | "error": "", 351 | "id": id, 352 | }) 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /electron/src/main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, dialog, Menu } = require('electron'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const os = require('os'); 5 | const { spawn, exec } = require('child_process'); 6 | const net = require('net'); 7 | const waitOn = require('wait-on'); 8 | const sudo = require('sudo-prompt'); 9 | 10 | let backendProcess = null; 11 | 12 | const isDev = !app.isPackaged || process.env.NODE_ENV === 'development'; 13 | 14 | // 获取用户数据目录:~/.0e7/ 15 | function getUserDataDir() { 16 | const homeDir = os.homedir(); 17 | const dataDir = path.join(homeDir, '.0e7'); 18 | 19 | // 确保目录存在 20 | if (!fs.existsSync(dataDir)) { 21 | fs.mkdirSync(dataDir, { recursive: true }); 22 | } 23 | 24 | return dataDir; 25 | } 26 | 27 | const PLATFORM_MAP = { 28 | darwin: { 29 | arm64: '0e7_darwin_arm64', 30 | x64: '0e7_darwin_amd64' 31 | }, 32 | linux: { 33 | x64: '0e7_linux_amd64' 34 | }, 35 | win32: { 36 | x64: '0e7_windows_amd64.exe' 37 | } 38 | }; 39 | 40 | function resolveBinaryName() { 41 | const arch = process.arch === 'arm64' ? 'arm64' : 'x64'; 42 | const platformBinaries = PLATFORM_MAP[process.platform]; 43 | if (!platformBinaries) { 44 | throw new Error(`当前平台 ${process.platform} 暂不支持 Electron 打包`); 45 | } 46 | const binary = platformBinaries[arch] || platformBinaries.x64; 47 | if (!binary) { 48 | throw new Error(`未找到平台 ${process.platform}/${arch} 的二进制名称映射`); 49 | } 50 | return binary; 51 | } 52 | 53 | async function findFreePort(min = 45000, max = 55000, retries = 50) { 54 | const tryPort = () => 55 | Math.floor(Math.random() * (max - min + 1)) + min; 56 | 57 | return new Promise((resolve, reject) => { 58 | const attempt = (remaining) => { 59 | if (remaining <= 0) { 60 | reject(new Error('无法找到可用端口')); 61 | return; 62 | } 63 | const port = tryPort(); 64 | const server = net.createServer(); 65 | server.unref(); 66 | server.on('error', () => { 67 | server.close(); 68 | attempt(remaining - 1); 69 | }); 70 | server.listen(port, () => { 71 | server.close(() => resolve(port)); 72 | }); 73 | }; 74 | attempt(retries); 75 | }); 76 | } 77 | 78 | function resolveBinaryPath() { 79 | const binaryName = resolveBinaryName(); 80 | if (isDev) { 81 | return path.resolve(__dirname, '..', '..', binaryName); 82 | } 83 | return path.join(process.resourcesPath, 'bin', binaryName); 84 | } 85 | 86 | function resolveConfigPath() { 87 | // 配置文件放在用户目录的 .0e7/ 目录中 88 | return path.join(getUserDataDir(), 'config.ini'); 89 | } 90 | 91 | function attachLogging(child, label) { 92 | if (!child) { 93 | return; 94 | } 95 | child.stdout?.on('data', (data) => { 96 | console.log(`[${label}] ${data}`.trim()); 97 | }); 98 | child.stderr?.on('data', (data) => { 99 | console.error(`[${label}][err] ${data}`.trim()); 100 | }); 101 | } 102 | 103 | // 检测当前是否有管理员权限 104 | async function checkAdminPrivileges() { 105 | return new Promise((resolve) => { 106 | const platform = process.platform; 107 | 108 | if (platform === 'win32') { 109 | // Windows: 使用 net session 命令检测 110 | exec('net session', (error) => { 111 | resolve(error === null); 112 | }); 113 | } else if (platform === 'darwin' || platform === 'linux') { 114 | // macOS/Linux: 使用 id -u 检测是否为 root (uid 0) 115 | exec('id -u', (error, stdout) => { 116 | if (error) { 117 | resolve(false); 118 | } else { 119 | resolve(stdout.trim() === '0'); 120 | } 121 | }); 122 | } else { 123 | resolve(false); 124 | } 125 | }); 126 | } 127 | 128 | // 尝试以管理员权限启动后端 129 | async function launchBackendWithElevation(port) { 130 | const binaryPath = resolveBinaryPath(); 131 | if (!fs.existsSync(binaryPath)) { 132 | throw new Error(`未找到 0E7 二进制文件: ${binaryPath}`); 133 | } 134 | 135 | const userDataDir = getUserDataDir(); 136 | const configPath = resolveConfigPath(); 137 | const args = ['--server', '--config', configPath, '--server-port', port.toString()]; 138 | 139 | const platform = process.platform; 140 | let command; 141 | 142 | if (platform === 'win32') { 143 | // Windows: 使用 PowerShell 的 Start-Process 以管理员权限运行 144 | // 注意:需要转义路径和参数 145 | const escapedBinaryPath = binaryPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); 146 | const escapedArgs = args.map(arg => `"${arg.replace(/"/g, '\\"')}"`).join(' '); 147 | const escapedCwd = userDataDir.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); 148 | command = `powershell -Command "Start-Process -FilePath '${escapedBinaryPath}' -ArgumentList '${escapedArgs}' -WorkingDirectory '${escapedCwd}' -Verb RunAs -WindowStyle Hidden"`; 149 | } else { 150 | // macOS/Linux: 使用 sudo,设置工作目录和环境变量 151 | const envVars = `OE7_SERVER_PORT=${port.toString()}`; 152 | command = `cd "${userDataDir}" && ${envVars} sudo ${binaryPath} ${args.join(' ')}`; 153 | } 154 | 155 | return new Promise((resolve, reject) => { 156 | const options = { 157 | name: '0E7 Desktop', 158 | icns: platform === 'darwin' ? path.join(__dirname, '..', 'build', 'icon.icns') : undefined 159 | }; 160 | 161 | console.log('正在请求管理员权限启动后端...'); 162 | sudo.exec(command, options, (error, stdout, stderr) => { 163 | if (error) { 164 | // 用户取消或权限提升失败 165 | if (error.message && error.message.includes('User did not grant permission')) { 166 | console.log('用户取消了权限提升请求'); 167 | } else { 168 | console.warn('无法以管理员权限启动后端:', error.message); 169 | } 170 | reject(error); 171 | } else { 172 | console.log('后端已以管理员权限启动'); 173 | if (stdout) console.log('输出:', stdout); 174 | if (stderr) console.warn('错误:', stderr); 175 | resolve(); 176 | } 177 | }); 178 | }); 179 | } 180 | 181 | async function launchBackend(port) { 182 | const binaryPath = resolveBinaryPath(); 183 | if (!fs.existsSync(binaryPath)) { 184 | throw new Error(`未找到 0E7 二进制文件: ${binaryPath}`); 185 | } 186 | 187 | // 使用用户目录的 .0e7/ 作为工作目录和配置文件路径 188 | const userDataDir = getUserDataDir(); 189 | const configPath = resolveConfigPath(); 190 | 191 | const args = ['--server', '--config', configPath, '--server-port', port.toString()]; 192 | 193 | // 首先尝试检测是否有管理员权限 194 | const hasAdmin = await checkAdminPrivileges(); 195 | 196 | if (!hasAdmin) { 197 | // 如果没有管理员权限,尝试提升权限 198 | console.log('检测到没有管理员权限,尝试以管理员权限启动后端...'); 199 | try { 200 | await launchBackendWithElevation(port); 201 | // 如果成功以管理员权限启动,等待一下让进程启动 202 | await new Promise(resolve => setTimeout(resolve, 2000)); 203 | // 注意:使用 sudo-prompt 启动的进程无法直接控制,所以这里只是尝试 204 | // 如果提升失败,会继续使用普通权限启动 205 | return; 206 | } catch (error) { 207 | console.warn('无法以管理员权限启动,使用普通权限启动:', error.message); 208 | // 继续使用普通权限启动 209 | } 210 | } else { 211 | console.log('当前已有管理员权限'); 212 | } 213 | 214 | // 使用普通权限启动(或已有管理员权限) 215 | backendProcess = spawn(binaryPath, args, { 216 | env: { 217 | ...process.env, 218 | OE7_SERVER_PORT: port.toString() 219 | }, 220 | cwd: userDataDir, // 工作目录设置为用户目录的 .0e7/ 221 | stdio: ['ignore', 'pipe', 'pipe'], 222 | windowsHide: true 223 | }); 224 | 225 | attachLogging(backendProcess, '0E7'); 226 | 227 | backendProcess.on('exit', (code, signal) => { 228 | if (code !== 0 && signal !== 'SIGTERM') { 229 | dialog.showErrorBox('0E7 已退出', `0E7 服务异常退出,退出码 ${code ?? '未知'}`); 230 | app.quit(); 231 | } 232 | }); 233 | } 234 | 235 | const debugPortPromise = findFreePort(35000, 44000) 236 | .then((port) => { 237 | app.commandLine.appendSwitch('remote-debugging-port', port.toString()); 238 | return port; 239 | }) 240 | .catch((err) => { 241 | console.warn('无法设置远程调试端口', err); 242 | return null; 243 | }); 244 | 245 | async function createWindow() { 246 | await debugPortPromise; 247 | const appPort = await findFreePort(); 248 | 249 | await launchBackend(appPort); 250 | 251 | await waitOn({ 252 | resources: [`http://127.0.0.1:${appPort}`], 253 | timeout: 60000, 254 | interval: 500, 255 | validateStatus: (status) => status >= 200 && status < 500 256 | }); 257 | 258 | const mainWindow = new BrowserWindow({ 259 | width: 1366, 260 | height: 900, 261 | minWidth: 1200, 262 | minHeight: 720, 263 | webPreferences: { 264 | contextIsolation: true, 265 | nodeIntegration: false, 266 | devTools: true // 允许开发者工具 267 | } 268 | }); 269 | 270 | mainWindow.loadURL(`http://127.0.0.1:${appPort}`); 271 | 272 | // 移除菜单栏,但保留开发者工具快捷键 273 | Menu.setApplicationMenu(null); 274 | 275 | // 注册快捷键打开开发者工具(F12 或 Cmd+Option+I / Ctrl+Shift+I) 276 | mainWindow.webContents.on('before-input-event', (event, input) => { 277 | // F12 键 278 | if (input.key === 'F12') { 279 | mainWindow.webContents.toggleDevTools(); 280 | event.preventDefault(); 281 | } 282 | // Cmd+Option+I (macOS) 或 Ctrl+Shift+I (Windows/Linux) 283 | if ((input.control || input.meta) && input.shift && input.key.toLowerCase() === 'i') { 284 | mainWindow.webContents.toggleDevTools(); 285 | event.preventDefault(); 286 | } 287 | }); 288 | 289 | if (isDev) { 290 | mainWindow.webContents.openDevTools(); 291 | } 292 | } 293 | 294 | function cleanupBackend() { 295 | if (backendProcess && !backendProcess.killed) { 296 | backendProcess.kill(); 297 | } 298 | backendProcess = null; 299 | } 300 | 301 | app.whenReady().then(createWindow).catch((err) => { 302 | dialog.showErrorBox('启动失败', err?.message || '未知错误'); 303 | console.error(err); 304 | app.quit(); 305 | }); 306 | 307 | app.on('window-all-closed', () => { 308 | cleanupBackend(); 309 | if (process.platform !== 'darwin') { 310 | app.quit(); 311 | } 312 | }); 313 | 314 | app.on('before-quit', cleanupBackend); 315 | app.on('activate', () => { 316 | if (BrowserWindow.getAllWindows().length === 0) { 317 | createWindow().catch((err) => { 318 | dialog.showErrorBox('启动失败', err?.message || '未知错误'); 319 | console.error(err); 320 | }); 321 | } 322 | }); 323 | process.on('exit', cleanupBackend); 324 | process.on('SIGINT', () => { 325 | cleanupBackend(); 326 | process.exit(0); 327 | }); 328 | process.on('SIGTERM', () => { 329 | cleanupBackend(); 330 | process.exit(0); 331 | }); 332 | 333 | -------------------------------------------------------------------------------- /service/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "0E7/service/database" 5 | "0E7/service/udpcast" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/x509" 9 | "encoding/pem" 10 | "fmt" 11 | "io/ioutil" 12 | "log" 13 | "math/big" 14 | "os" 15 | "strings" 16 | "sync" 17 | "time" 18 | 19 | "github.com/google/uuid" 20 | "gopkg.in/ini.v1" 21 | "gorm.io/gorm" 22 | ) 23 | 24 | var ( 25 | Global_timeout_http int 26 | Global_timeout_download int 27 | Global_debug bool 28 | Db *gorm.DB 29 | Server_mode bool 30 | Server_tls bool 31 | Server_port string 32 | Server_url string 33 | Server_flag string 34 | Server_pcap_zip bool 35 | Server_pcap_workers int 36 | Client_mode bool 37 | Client_name string 38 | Client_id int 39 | Client_pypi string 40 | Client_update bool 41 | Client_worker int 42 | Client_monitor bool 43 | Client_only_monitor bool 44 | Client_exploit_interval int 45 | Client_monitor_interval int 46 | Search_engine string 47 | Search_elasticsearch_url string 48 | Search_elasticsearch_username string 49 | Search_elasticsearch_password string 50 | Db_engine string 51 | Db_host string 52 | Db_port string 53 | Db_username string 54 | Db_password string 55 | Db_tables string 56 | 57 | // Proxy/Cache related 58 | Proxy_cache_2xx_only bool 59 | Proxy_retain_duration time.Duration 60 | Client_proxy_enable bool 61 | Client_proxy_port string 62 | ) 63 | 64 | func Init_conf(configFile string) error { 65 | cfg, err := ini.Load(configFile) 66 | if err != nil { 67 | file, err := os.Create(configFile) 68 | if err != nil { 69 | log.Println("Create error", err) 70 | os.Exit(1) 71 | } 72 | defer file.Close() 73 | cfg, err = ini.Load(configFile) 74 | if err != nil { 75 | log.Println("Failed to load config file:", err) 76 | os.Exit(1) 77 | } 78 | } 79 | Server_url = "" 80 | section := cfg.Section("global") 81 | Global_timeout_http, err = section.Key("timeout_http").Int() 82 | if err != nil { 83 | Global_timeout_http = 5 84 | } 85 | Global_timeout_download, err = section.Key("timeout_download").Int() 86 | if err != nil { 87 | Global_timeout_download = 60 88 | } 89 | Global_debug, err = section.Key("debug").Bool() 90 | if err != nil { 91 | Global_debug = false 92 | } 93 | 94 | section = cfg.Section("client") 95 | Client_mode, err = section.Key("enable").Bool() 96 | if err != nil { 97 | Client_mode = true 98 | } 99 | 100 | section = cfg.Section("server") 101 | Server_mode, err = section.Key("enable").Bool() 102 | if err != nil { 103 | Server_mode = false 104 | } 105 | if Server_mode { 106 | Server_port = section.Key("port").String() 107 | if Server_port == "" { 108 | Server_port = "6102" 109 | } 110 | Server_url = section.Key("server_url").String() 111 | Server_flag = section.Key("flag").String() 112 | Server_tls, err = section.Key("tls").Bool() 113 | if err != nil { 114 | Server_tls = true 115 | } 116 | if Server_tls { 117 | generator_key() 118 | Server_url = strings.Replace(Server_url, "http://", "https://", 1) 119 | } else { 120 | Server_url = strings.Replace(Server_url, "https://", "http://", 1) 121 | } 122 | Server_pcap_zip, err = section.Key("pcap_zip").Bool() 123 | if err != nil { 124 | Server_pcap_zip = true 125 | } 126 | Server_pcap_workers, err = section.Key("pcap_workers").Int() 127 | if err != nil || Server_pcap_workers <= 0 { 128 | Server_pcap_workers = 0 // 0 表示使用 CPU 核心数 129 | } 130 | 131 | // Proxy/cache settings (server side) 132 | Proxy_cache_2xx_only, err = section.Key("proxy_cache_2xx_only").Bool() 133 | if err != nil { 134 | Proxy_cache_2xx_only = true 135 | } 136 | retainStr := section.Key("proxy_retain_duration").String() 137 | if retainStr == "" { 138 | retainStr = "15m" 139 | } 140 | Proxy_retain_duration, err = time.ParseDuration(retainStr) 141 | if err != nil || Proxy_retain_duration < 0 { 142 | Proxy_retain_duration = 15 * time.Minute 143 | } 144 | 145 | // 读取数据库配置 146 | Db_engine = section.Key("db_engine").String() 147 | if Db_engine == "" { 148 | Db_engine = "sqlite3" // 默认使用sqlite3 149 | } 150 | Db_host = section.Key("db_host").String() 151 | if Db_host == "" { 152 | Db_host = "localhost" 153 | } 154 | Db_port = section.Key("db_port").String() 155 | if Db_port == "" { 156 | Db_port = "3306" 157 | } 158 | Db_username = section.Key("db_username").String() 159 | Db_password = section.Key("db_password").String() 160 | Db_tables = section.Key("db_tables").String() 161 | 162 | Db, err = database.Init_database(section) 163 | if err != nil { 164 | log.Println("Failed to init database:", err) 165 | os.Exit(1) 166 | } 167 | } 168 | 169 | section = cfg.Section("client") 170 | var wg sync.WaitGroup 171 | if Client_mode { 172 | if Server_url == "" { 173 | Server_url = section.Key("server_url").String() 174 | } 175 | if Server_url == "" { 176 | args := os.Args 177 | if len(args) == 2 { 178 | Server_url = args[1] 179 | } else if Server_mode == false { 180 | wg.Add(1) 181 | go udpcast.Udp_receive(&wg, &Server_url) 182 | } else { 183 | if Server_tls == true { 184 | Server_url = "https://localhost:" + Server_port 185 | } else { 186 | Server_url = "http://localhost:" + Server_port 187 | } 188 | } 189 | if Server_url != "" { 190 | section.Key("server_url").SetValue(Server_url) 191 | } 192 | } 193 | Client_id, err = section.Key("id").Int() 194 | if err != nil { 195 | Client_id = 0 196 | } 197 | Client_name = section.Key("name").String() 198 | if Client_name == "" { 199 | Client_name = uuid.New().String() 200 | section.Key("name").SetValue(Client_name) 201 | } 202 | 203 | Client_pypi = section.Key("pypi").String() 204 | if Client_pypi == "" { 205 | Client_pypi = "https://pypi.tuna.tsinghua.edu.cn/simple" 206 | } 207 | 208 | Client_update, err = section.Key("update").Bool() 209 | if err != nil { 210 | Client_update = false 211 | } 212 | 213 | Client_worker, err = section.Key("worker").Int() 214 | if err != nil { 215 | Client_worker = 20 216 | } 217 | 218 | Client_monitor, err = section.Key("monitor").Bool() 219 | if err != nil { 220 | Client_monitor = true 221 | } 222 | 223 | Client_only_monitor, err = section.Key("only_monitor").Bool() 224 | if err != nil { 225 | Client_only_monitor = false 226 | } 227 | 228 | Client_exploit_interval, err = section.Key("exploit_interval").Int() 229 | if err != nil || Client_exploit_interval <= 0 { 230 | Client_exploit_interval = 1 // 默认 1 秒 231 | } 232 | 233 | Client_proxy_enable, err = section.Key("proxy_enable").Bool() 234 | if err != nil { 235 | Client_proxy_enable = false 236 | } 237 | Client_proxy_port = section.Key("proxy_port").String() 238 | if Client_proxy_port == "" { 239 | Client_proxy_port = "6102" 240 | } 241 | } 242 | 243 | // 读取搜索引擎配置 244 | section = cfg.Section("search") 245 | Search_engine = section.Key("search_engine").String() 246 | if Search_engine == "" { 247 | Search_engine = "bleve" // 默认使用bleve 248 | } 249 | Search_elasticsearch_url = section.Key("search_elasticsearch_url").String() 250 | if Search_elasticsearch_url == "" { 251 | Search_elasticsearch_url = "http://localhost:9200" // 默认Elasticsearch地址 252 | } 253 | Search_elasticsearch_username = section.Key("search_elasticsearch_username").String() 254 | Search_elasticsearch_password = section.Key("search_elasticsearch_password").String() 255 | 256 | wg.Wait() 257 | if Client_mode && Server_url == "" { 258 | log.Println("Server not found") 259 | os.Exit(1) 260 | } 261 | 262 | err = cfg.SaveTo("config.ini") 263 | if err != nil { 264 | log.Println("Failed to save config file:", err) 265 | return err 266 | } 267 | 268 | return nil 269 | } 270 | 271 | func generator_key() { 272 | if _, err := os.Stat("cert"); os.IsNotExist(err) { 273 | err := os.Mkdir("cert", os.ModePerm) 274 | if err != nil { 275 | log.Println("Error to create cert folder:", err) 276 | } 277 | } 278 | _, err1 := os.Stat("cert/private.key") 279 | _, err2 := os.Stat("cert/certificate.crt") 280 | if err1 != nil || err2 != nil { 281 | privateKey, err := rsa.GenerateKey(rand.Reader, 2048) 282 | if err != nil { 283 | log.Fatal(err) 284 | } 285 | template := x509.Certificate{ 286 | SerialNumber: big.NewInt(1), 287 | NotBefore: time.Now(), 288 | NotAfter: time.Now().AddDate(10, 0, 0), // 有效期为十年 289 | BasicConstraintsValid: true, 290 | } 291 | derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) 292 | if err != nil { 293 | log.Fatal(err) 294 | } 295 | err = ioutil.WriteFile("cert/private.key", encodePrivateKeyToPEM(privateKey), os.ModePerm) 296 | if err != nil { 297 | log.Fatal(err) 298 | } 299 | err = ioutil.WriteFile("cert/certificate.crt", pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), os.ModePerm) 300 | if err != nil { 301 | log.Fatal(err) 302 | } 303 | } 304 | } 305 | 306 | func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { 307 | privateKeyPEM := &pem.Block{ 308 | Type: "RSA PRIVATE KEY", 309 | Bytes: x509.MarshalPKCS1PrivateKey(privateKey), 310 | } 311 | return pem.EncodeToMemory(privateKeyPEM) 312 | } 313 | 314 | // UpdateConfigClientId 更新config.ini文件中的client_id,如果ID发生变化则更新 315 | func UpdateConfigClientId(clientId int) error { 316 | // 检查ID是否发生变化 317 | if Client_id == clientId { 318 | return nil // ID没有变化,不需要更新 319 | } 320 | log.Printf("更新客户端 ID: %d", clientId) 321 | cfg, err := ini.Load("config.ini") 322 | if err != nil { 323 | return fmt.Errorf("failed to load config.ini: %v", err) 324 | } 325 | 326 | // 更新client section中的id值 327 | clientSection := cfg.Section("client") 328 | clientSection.Key("id").SetValue(fmt.Sprintf("%d", clientId)) 329 | 330 | // 保存文件 331 | err = cfg.SaveTo("config.ini") 332 | if err != nil { 333 | return fmt.Errorf("failed to save config.ini: %v", err) 334 | } 335 | 336 | // 更新内存中的Client_id 337 | Client_id = clientId 338 | 339 | return nil 340 | } 341 | --------------------------------------------------------------------------------