├── 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 |
6 |
7 |
8 |
9 |
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 |
70 |
71 |
87 |
88 |
98 |
99 |
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 |
154 |
155 |
156 |
157 |
158 |
159 |
163 | {{ row.status }}
164 |
165 |
166 |
167 |
168 |
169 | {{ (row.client_name || '').slice(0, 8) }}
170 | 未知
171 |
172 |
173 |
174 |
175 | {{ row.team }}
176 | 未设置
177 |
178 |
179 |
180 |
181 | {{ formatTime(row.update_time) }}
182 |
183 |
184 |
185 |
186 | {{ row.output }}
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
199 |
200 | 刷新
201 |
202 |
203 |
204 |
205 |
206 |
207 |
221 |
222 |
223 |
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 |
--------------------------------------------------------------------------------