mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-11-15 01:04:22 +08:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f2150828d | |||
| c76bb77b93 | |||
| 01d5c25df1 | |||
| a19e8d2bcb | |||
| 76f443f900 | |||
| 47b1663938 | |||
| 406f5bb380 | |||
| 85ea6c17a4 | |||
| 3420dc5460 | |||
| 3d7a0b697d | |||
| 065b23d9c3 | |||
| 7919dfde59 | |||
| 565d732967 | |||
| bf88b0dd7e | |||
| 20dc151aec | |||
| 630ed969d3 | |||
| e94aa01213 | |||
| 67d8f7a381 | |||
| 0c7b7deaf5 | |||
| a4def9c5f3 | |||
| 6582348da2 | |||
| 3a14b22ebc | |||
| d7b44916bf | |||
| f8a0bda3fd | |||
| 634562e56d | |||
| d06e02998b | |||
| 169df953d8 | |||
| 7bc33f63ba | |||
| 5925a84a6d | |||
| bbffe5872c | |||
| 98db0e6a5e | |||
| 30591a008a | |||
| 52885d1821 | |||
| 124cf1ced2 | |||
| 36ba2f95ef | |||
| 858f9ef1d5 | |||
| 218084b542 | |||
| a9c304546e | |||
| 85250aa2d3 | |||
| aeeb57b89d | |||
| 4e163163cb | |||
| eb5464511a | |||
| efa6c0e322 | |||
| fef4c28777 | |||
| 9ab5140258 | |||
| 11dc21e46f | |||
| 576654fc5a | |||
| a1454a3549 | |||
| 1c72fb233a | |||
| ac1c2fa6bf | |||
| 4af2186e35 | |||
| 9fe9798677 | |||
| cfee6092c7 | |||
| 7ab6545594 | |||
| 2e2c182a0d |
@@ -0,0 +1,43 @@
|
||||
name: Deploy Docs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- beta
|
||||
paths:
|
||||
- 'doc/**'
|
||||
- 'mkdocs.yml'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Configure Git Credentials
|
||||
run: |
|
||||
git config user.name github-actions[bot]
|
||||
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
key: mkdocs-material-${{ env.cache_id }}
|
||||
path: .cache
|
||||
restore-keys: |
|
||||
mkdocs-material-
|
||||
|
||||
- name: Install MkDocs and Material Theme
|
||||
run: pip install mkdocs-material
|
||||
|
||||
- name: Build and Deploy
|
||||
run: mkdocs gh-deploy --force
|
||||
@@ -5,17 +5,15 @@
|
||||
**Branch:** beta
|
||||
|
||||
## OVERVIEW
|
||||
Flux Panel is a traffic forwarding management system built on a forked GOST v3 stack. It ships as Dockerized Spring Boot (admin API) + Vite/React UI + Go forwarding agent, with optional mobile WebView wrappers.
|
||||
FLVX (formerly Flux Panel) is a traffic forwarding management system built on a forked GOST v3 stack. It ships as a Go-based admin API (SQLite) + Vite/React UI + Go forwarding agent, with optional mobile WebView wrappers.
|
||||
|
||||
## STRUCTURE
|
||||
```
|
||||
./
|
||||
├── go-gost/ # Go forwarding agent (forked gost + local x/)
|
||||
│ └── x/ # Local fork of github.com/go-gost/x (replace => ./x)
|
||||
├── springboot-backend/ # Java/Spring Boot admin API (SQLite/MyBatis)
|
||||
├── go-backend/ # Go Admin API (SQLite, net/http)
|
||||
├── vite-frontend/ # React/Vite dashboard (HeroUI + Tailwind)
|
||||
├── android-app/ # Android WebView wrapper (optional)
|
||||
├── ios-app/ # iOS WebView wrapper (optional)
|
||||
├── docker-compose-v4.yml # Panel deploy (IPv4-only bridge)
|
||||
├── docker-compose-v6.yml # Panel deploy (IPv6-enabled bridge)
|
||||
├── panel_install.sh # Panel installer/upgrader (downloads compose)
|
||||
@@ -30,13 +28,19 @@ Flux Panel is a traffic forwarding management system built on a forked GOST v3 s
|
||||
| **Deploy (IPv6)** | `docker-compose-v6.yml` | Same as v4 + IPv6-enabled bridge |
|
||||
| **Panel install** | `panel_install.sh` | Picks v4/v6, generates `JWT_SECRET`, downloads compose |
|
||||
| **Node install** | `install.sh` | Installs `/etc/flux_agent/flux_agent` + writes `config.json`/`gost.json` + systemd `flux_agent.service` |
|
||||
| **Admin API entry** | `springboot-backend/src/main/java/com/admin/AdminApplication.java` | Spring Boot app |
|
||||
| **Admin API routes** | `springboot-backend/src/main/java/com/admin/controller/` | Mostly `/api/v1/*` controllers |
|
||||
| **Admin auth** | `springboot-backend/src/main/java/com/admin/common/interceptor/JwtInterceptor.java` | Checks `Authorization` header |
|
||||
| **Web UI routing** | `vite-frontend/src/App.tsx` | React Router v6 + ProtectedRoute/H5 layouts |
|
||||
| **Web UI API client** | `vite-frontend/src/api/network.ts` | Axios `baseURL` + `Authorization` header |
|
||||
| **Go agent entry** | `go-gost/main.go` | Reads panel `config.json` + starts gost services |
|
||||
| **Go x fork** | `go-gost/x/` | Handlers/listeners/dialers + management API |
|
||||
| **Admin API** | `go-backend/` | Go Admin API (SQLite) |
|
||||
| **Web UI** | `vite-frontend/` | React/Vite dashboard (HeroUI + Tailwind) |
|
||||
| **Go Agent** | `go-gost/` | Forwarding agent (forked gost + local x/) |
|
||||
| **Go Core** | `go-gost/x/` | Handlers/listeners/dialers + management API |
|
||||
|
||||
## CODE MAP
|
||||
| Symbol | Type | Location | Role |
|
||||
|--------|------|----------|------|
|
||||
| `flvx` | Project | `.` | Root directory |
|
||||
| `main` | Func | `go-backend/cmd/paneld/main.go` | Backend Entry |
|
||||
| `App` | Component | `vite-frontend/src/App.tsx` | Frontend Entry |
|
||||
| `main` | Func | `go-gost/main.go` | Agent Entry |
|
||||
|
||||
|
||||
## CONVENTIONS
|
||||
- `Authorization` header carries the raw JWT token (no `Bearer` prefix) between `vite-frontend/` and `springboot-backend/`.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# flux-panel转发面板 哆啦A梦转发面板
|
||||
# FLVX
|
||||
|
||||
> 📞 **联系我们**: [Telegram群组](https://t.me/flvxpanel)
|
||||
|
||||
本项目基于 [go-gost/gost](https://github.com/go-gost/gost) 和 [go-gost/x](https://github.com/go-gost/x) 两个开源库,实现了转发面板。
|
||||
---
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
+24
@@ -0,0 +1,24 @@
|
||||
# 常见问题 (FAQ)
|
||||
|
||||
### Q1: 安装脚本提示 "Docker command not found"?
|
||||
**A**: 请确保您的系统已安装 Docker 和 Docker Compose。
|
||||
- Ubuntu/Debian 安装 Docker: `curl -fsSL https://get.docker.com | bash`
|
||||
|
||||
### Q2: 面板无法访问 (Connection Refused)?
|
||||
**A**:
|
||||
1. 检查防火墙是否放行了前端端口(默认 `6366`)。
|
||||
2. 检查容器是否正常运行: `docker ps`。
|
||||
3. 查看容器日志: `docker logs flux-panel-backend` 或 `docker logs vite-frontend`。
|
||||
|
||||
### Q3: 节点显示离线?
|
||||
**A**:
|
||||
1. 检查节点服务器与面板服务器之间的网络连通性。
|
||||
2. 确认在节点端安装时输入的 **面板地址** 和 **密钥** 是否正确。
|
||||
3. 检查节点端服务状态: `systemctl status flux_agent`。
|
||||
4. 查看节点端日志: `journalctl -u flux_agent -f`。
|
||||
|
||||
### Q4: 只有 TCP 能通,UDP 不通?
|
||||
**A**: 请检查服务器防火墙和安全组(AWS/阿里云/腾讯云等)是否同时放行了对应端口的 **TCP 和 UDP** 协议。
|
||||
|
||||
### Q5: IPv6 无法使用?
|
||||
**A**: 面板安装脚本会自动尝试配置 Docker 的 IPv6。如果失败,请手动检查 `/etc/docker/daemon.json` 配置,确保 `ipv6: true` 且分配了正确的 `fixed-cidr-v6` 子网。
|
||||
@@ -0,0 +1,37 @@
|
||||
# FLVX 官方文档
|
||||
|
||||
**FLVX** 是一个基于 [go-gost/gost](https://github.com/go-gost/gost) 和 [go-gost/x](https://github.com/go-gost/x) 开发的高性能流量转发管理系统。
|
||||
|
||||
> 📞 **联系我们**: [Telegram群组](https://t.me/flvxpanel)
|
||||
|
||||
## 核心特性
|
||||
|
||||
- **多协议支持**: 完美支持 TCP 和 UDP 协议转发。
|
||||
- **灵活转发**: 支持 **端口转发** 与 **隧道转发** 两种模式。
|
||||
- **流量控制**: 支持按 **隧道账号级别** 管理流量转发数量,用于用户/隧道配额控制。
|
||||
- **限速管理**: 可针对 **指定用户的指定隧道进行限速** 设置。
|
||||
- **计费策略**: 支持配置 **单向或双向流量计费方式**,灵活适配不同计费模型。
|
||||
- **策略配置**: 提供灵活的转发策略配置,适用于多种网络场景。
|
||||
|
||||
## 快速开始
|
||||
|
||||
- [安装部署](./install.md)
|
||||
- [使用指南](./usage.md)
|
||||
- [常见问题](./faq.md)
|
||||
|
||||
## 免责声明
|
||||
|
||||
本项目仅供个人学习与研究使用,基于开源项目进行二次开发。
|
||||
使用本项目所带来的任何风险均由使用者自行承担。本项目为开源的流量转发工具,仅限合法、合规用途。
|
||||
**禁止将本项目用于任何违法或未经授权的行为,包括但不限于网络攻击、数据窃取、非法访问等。**
|
||||
|
||||
## 捐赠支持
|
||||
|
||||
如果您觉得本项目对您有帮助,欢迎请作者喝杯咖啡!
|
||||
|
||||
| 网络 | 地址 |
|
||||
|------------|----------------------------------------------------------------------|
|
||||
| BNB(BEP20) | `0xa608708fdc6279a2433fd4b82f0b72b8cbe97ed5` |
|
||||
| TRC20 | `TM8VYdU3s3gSX5PC8swjAJrAzZFCHKqG2k` |
|
||||
| Aptos | `0x49427bfcba1006a346447430689b2307ac156316bb34850d1d3029ff9d118da5` |
|
||||
| Polygon | `0xa608708fdc6279a2433fd4b82f0b72b8cbe97ed5` |
|
||||
@@ -0,0 +1,79 @@
|
||||
# 安装部署指南
|
||||
|
||||
本文档介绍如何部署 FLVX 面板端及节点端。
|
||||
|
||||
## 一、面板端部署
|
||||
|
||||
面板端负责管理用户、节点和转发规则。
|
||||
|
||||
### 1. 环境要求
|
||||
- 操作系统:Linux (推荐 Debian 10+ / Ubuntu 20.04+)
|
||||
- 必须安装 Docker 和 Docker Compose
|
||||
|
||||
### 2. 一键安装脚本
|
||||
|
||||
使用以下命令即可快速安装面板:
|
||||
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/Sagit-chu/flux-panel/main/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||
```
|
||||
|
||||
**安装过程中会提示输入以下信息:**
|
||||
- **前端端口**: 默认为 `6366`
|
||||
- **后端端口**: 默认为 `6365`
|
||||
|
||||
脚本会自动检测系统是否支持 IPv6,并自动配置 Docker 的 IPv6 支持。
|
||||
|
||||
### 3. 访问面板
|
||||
|
||||
安装完成后,访问:
|
||||
`http://<服务器IP>:<前端端口>` (默认: `http://<服务器IP>:6366`)
|
||||
|
||||
**默认管理员账号:**
|
||||
- 用户名: `admin_user`
|
||||
- 密码: `admin_user`
|
||||
|
||||
> ⚠️ **注意**: 首次登录后,请务必在“个人中心”或“设置”中修改默认密码!
|
||||
|
||||
### 4. 维护命令
|
||||
|
||||
再次运行 `./panel_install.sh` 脚本可以看到管理菜单:
|
||||
1. 安装面板
|
||||
2. 更新面板
|
||||
3. 卸载面板
|
||||
|
||||
---
|
||||
|
||||
## 二、节点端部署
|
||||
|
||||
节点端运行在实际进行流量转发的服务器上,需要连接到面板端进行管理。
|
||||
|
||||
### 1. 获取接入密钥
|
||||
1. 登录面板端。
|
||||
2. 进入 **节点管理 (Node)** 页面。
|
||||
3. 点击 **添加节点**。
|
||||
4. 获取该节点的 **接入密钥 (Secret)**。
|
||||
|
||||
### 2. 一键安装脚本
|
||||
|
||||
在节点服务器上运行:
|
||||
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/Sagit-chu/flux-panel/main/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||
```
|
||||
|
||||
**安装过程中会提示输入:**
|
||||
- **服务器地址**: 面板端的通信地址(通常是 `http://<面板IP>:<后端端口>`,例如 `http://1.2.3.4:6365`)。
|
||||
- **密钥**: 刚才在面板中获取的节点密钥。
|
||||
|
||||
或者直接使用带参数的命令(适用于自动化部署):
|
||||
|
||||
```bash
|
||||
# 替换 <面板地址> 和 <密钥>
|
||||
./install.sh -a "http://1.2.3.4:6365" -s "your_node_secret"
|
||||
```
|
||||
|
||||
### 3. 验证安装
|
||||
安装完成后,服务会自动启动。
|
||||
- 查看状态: `systemctl status flux_agent`
|
||||
- 回到面板 **节点管理** 页面,该节点状态应显示为 **在线**。
|
||||
@@ -0,0 +1,35 @@
|
||||
# 使用指南
|
||||
|
||||
## 1. 仪表盘 (Dashboard)
|
||||
登录系统后首先看到的是仪表盘,这里展示了系统的整体运行状态,包括:
|
||||
- 在线节点数量
|
||||
- 用户总数
|
||||
- 流量统计信息
|
||||
|
||||
## 2. 节点管理 (Node)
|
||||
节点是实际承载流量转发的服务器。
|
||||
- **添加节点**: 点击“添加”,获取密钥用于节点端安装。
|
||||
- **管理**: 可以查看节点在线状态、版本信息,以及对节点进行编辑或删除。
|
||||
|
||||
## 3. 用户管理 (User)
|
||||
管理员可以创建和管理普通用户。
|
||||
- **创建用户**: 设置用户名、密码、流量配额等。
|
||||
- **用户组**: 可以将用户分配到不同的组 (Group),便于统一管理权限或策略。
|
||||
|
||||
## 4. 转发管理 (Forward)
|
||||
这是核心功能区,用于设置端口转发规则。
|
||||
- **端口转发**: 将节点服务器的某个端口流量转发到目标地址。
|
||||
- **协议**: 支持 TCP / UDP。
|
||||
- **入口**: 选择入口节点和监听端口。
|
||||
- **出口**: 设置目标 IP 和端口。
|
||||
- **隧道转发**: 用于更复杂的网络穿透场景(具体配置视业务需求而定)。
|
||||
|
||||
## 5. 限制与策略 (Limit)
|
||||
- **限速**: 可以对指定用户或指定隧道进行带宽限制,防止资源滥用。
|
||||
- **计费模式**: 支持配置流量计算方式(单向或双向),适合运营场景。
|
||||
|
||||
## 6. 系统配置 (Config)
|
||||
在此页面进行系统的全局设置。
|
||||
|
||||
## 7. 个人设置 (Profile)
|
||||
- **修改密码**: 为了安全,建议定期修改管理员密码。
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
@@ -12,6 +12,7 @@ services:
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
LOG_DIR: /app/logs
|
||||
SERVER_ADDR: :6365
|
||||
TZ: Asia/Shanghai
|
||||
ports:
|
||||
- "${BACKEND_PORT}:6365"
|
||||
volumes:
|
||||
|
||||
@@ -12,6 +12,7 @@ services:
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
LOG_DIR: /app/logs
|
||||
SERVER_ADDR: :6365
|
||||
TZ: Asia/Shanghai
|
||||
ports:
|
||||
- "${BACKEND_PORT}:6365"
|
||||
volumes:
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# GO BACKEND KNOWLEDGE BASE
|
||||
|
||||
## OVERVIEW
|
||||
Go-based Admin API for FLVX (formerly Flux Panel). Replaces the legacy Spring Boot backend.
|
||||
**Stack:** Go 1.23, net/http (std lib), SQLite (modernc.org/sqlite).
|
||||
|
||||
## STRUCTURE
|
||||
```
|
||||
go-backend/
|
||||
├── cmd/paneld/main.go # Entry point; starts HTTP server + WebSocket
|
||||
├── internal/
|
||||
│ ├── http/ # HTTP layer
|
||||
│ │ ├── router.go # Routes (NewServeMux) + Middleware chain
|
||||
│ │ ├── handler/ # API Handlers (User, Tunnel, Node, etc.)
|
||||
│ │ ├── middleware/ # JWT, CORS, Logging, Recover
|
||||
│ │ └── response/ # JSON response helpers
|
||||
│ ├── store/sqlite/ # Data Access Layer (Repository pattern)
|
||||
│ │ ├── repository.go # SQL queries & Struct definitions
|
||||
│ │ └── sql/ # Embedded schema.sql & data.sql
|
||||
│ └── auth/ # Auth logic
|
||||
├── tests/ # Integration/Contract tests
|
||||
├── Dockerfile # Multi-stage build (alpine)
|
||||
└── Makefile # Build commands
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
| Task | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| **API Routes** | `go-backend/internal/http/router.go` | Registers handlers to `http.ServeMux` |
|
||||
| **DB Schema** | `go-backend/internal/store/sqlite/sql/schema.sql` | Embedded in binary |
|
||||
| **SQL Queries** | `go-backend/internal/store/sqlite/repository.go` | Raw SQL, no ORM |
|
||||
| **Auth Middleware** | `go-backend/internal/http/middleware/jwt.go` | Extracts `Authorization` header |
|
||||
| **WebSocket** | `go-backend/internal/ws/` | Real-time updates (traffic, status) |
|
||||
|
||||
## CONVENTIONS
|
||||
- **No ORM**: Uses raw SQL with `database/sql` and `modernc.org/sqlite`.
|
||||
- **Standard Lib**: Uses `net/http` for routing (Go 1.22+ patterns).
|
||||
- **Auth**: Expects raw JWT in `Authorization` header (no `Bearer` prefix).
|
||||
- **Config**: Loaded from environment variables (see `cmd/paneld/main.go`).
|
||||
|
||||
## COMMANDS
|
||||
```bash
|
||||
cd go-backend
|
||||
go run ./cmd/paneld
|
||||
go test ./...
|
||||
make build
|
||||
```
|
||||
@@ -5,7 +5,9 @@ COPY go.mod ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/paneld ./cmd/paneld
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} env ${TARGETARCH:+GOARCH=${TARGETARCH}} go build -o /out/paneld ./cmd/paneld
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
WORKDIR /app
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,121 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FederationClient struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type RemoteNodeInfo struct {
|
||||
ShareID int64 `json:"shareId"`
|
||||
ShareName string `json:"shareName"`
|
||||
NodeID int64 `json:"nodeId"`
|
||||
NodeName string `json:"nodeName"`
|
||||
ServerIP string `json:"serverIp"`
|
||||
Status int `json:"status"`
|
||||
MaxBandwidth int64 `json:"maxBandwidth"`
|
||||
ExpiryTime int64 `json:"expiryTime"`
|
||||
PortRangeStart int `json:"portRangeStart"`
|
||||
PortRangeEnd int `json:"portRangeEnd"`
|
||||
}
|
||||
|
||||
type RemoteTunnelResponse struct {
|
||||
TunnelID int64 `json:"tunnelId"`
|
||||
}
|
||||
|
||||
func NewFederationClient() *FederationClient {
|
||||
return &FederationClient{
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *FederationClient) Connect(url, token, localDomain string) (*RemoteNodeInfo, error) {
|
||||
url = strings.TrimSuffix(url, "/")
|
||||
req, err := http.NewRequest("POST", url+"/api/v1/federation/connect", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
if localDomain != "" {
|
||||
req.Header.Set("X-Panel-Domain", localDomain)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("remote error %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var res struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data RemoteNodeInfo `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.Code != 0 {
|
||||
return nil, fmt.Errorf("remote api error: %s", res.Msg)
|
||||
}
|
||||
|
||||
return &res.Data, nil
|
||||
}
|
||||
|
||||
func (c *FederationClient) CreateTunnel(url, token, localDomain, protocol string, remotePort int, target string) (*RemoteTunnelResponse, error) {
|
||||
url = strings.TrimSuffix(url, "/")
|
||||
payload := map[string]interface{}{
|
||||
"protocol": protocol,
|
||||
"remotePort": remotePort,
|
||||
"target": target,
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(payload)
|
||||
req, err := http.NewRequest("POST", url+"/api/v1/federation/tunnel/create", strings.NewReader(string(bodyBytes)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
if localDomain != "" {
|
||||
req.Header.Set("X-Panel-Domain", localDomain)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("remote error %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var res struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data RemoteTunnelResponse `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.Code != 0 {
|
||||
return nil, fmt.Errorf("remote api error: %s", res.Msg)
|
||||
}
|
||||
|
||||
return &res.Data, nil
|
||||
}
|
||||
@@ -59,6 +59,8 @@ type chainNodeRecord struct {
|
||||
NodeID int64
|
||||
Port int
|
||||
NodeName string
|
||||
Protocol string
|
||||
Strategy string
|
||||
}
|
||||
|
||||
type diagnosisTarget struct {
|
||||
@@ -232,9 +234,9 @@ func (h *Handler) getNodeRecord(nodeID int64) (*nodeRecord, error) {
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
func (h *Handler) resolveUserTunnelAndLimiter(userID, tunnelID int64) (int64, *int, error) {
|
||||
func (h *Handler) resolveUserTunnelAndLimiter(userID, tunnelID int64) (int64, *int64, *int, error) {
|
||||
row := h.repo.DB().QueryRow(`
|
||||
SELECT ut.id, sl.speed
|
||||
SELECT ut.id, sl.id, sl.speed
|
||||
FROM user_tunnel ut
|
||||
LEFT JOIN speed_limit sl ON sl.id = ut.speed_id
|
||||
WHERE ut.user_id = ? AND ut.tunnel_id = ?
|
||||
@@ -242,19 +244,21 @@ func (h *Handler) resolveUserTunnelAndLimiter(userID, tunnelID int64) (int64, *i
|
||||
LIMIT 1
|
||||
`, userID, tunnelID)
|
||||
var userTunnelID int64
|
||||
var limiterID sql.NullInt64
|
||||
var speed sql.NullInt64
|
||||
err := row.Scan(&userTunnelID, &speed)
|
||||
err := row.Scan(&userTunnelID, &limiterID, &speed)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, nil, nil
|
||||
return 0, nil, nil, nil
|
||||
}
|
||||
return 0, nil, err
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
if !speed.Valid || speed.Int64 <= 0 {
|
||||
return userTunnelID, nil, nil
|
||||
if !limiterID.Valid || limiterID.Int64 <= 0 {
|
||||
return userTunnelID, nil, nil, nil
|
||||
}
|
||||
v := int(speed.Int64)
|
||||
return userTunnelID, &v, nil
|
||||
v := limiterID.Int64
|
||||
s := int(speed.Int64)
|
||||
return userTunnelID, &v, &s, nil
|
||||
}
|
||||
|
||||
func (h *Handler) listUserTunnelIDs(userID, tunnelID int64) ([]int64, error) {
|
||||
@@ -326,18 +330,22 @@ func (h *Handler) syncForwardServices(forward *forwardRecord, method string, all
|
||||
return errors.New("转发入口端口不存在")
|
||||
}
|
||||
|
||||
userTunnelID, limiter, err := h.resolveUserTunnelAndLimiter(forward.UserID, forward.TunnelID)
|
||||
userTunnelID, limiterID, speed, err := h.resolveUserTunnelAndLimiter(forward.UserID, forward.TunnelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serviceBase := buildForwardServiceBase(forward.ID, forward.UserID, userTunnelID)
|
||||
|
||||
for _, fp := range ports {
|
||||
if limiterID != nil && speed != nil {
|
||||
h.ensureLimiterOnNode(fp.NodeID, *limiterID, *speed)
|
||||
}
|
||||
|
||||
node, err := h.getNodeRecord(fp.NodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
services := buildForwardServiceConfigs(serviceBase, forward, tunnel, node, fp.Port, limiter)
|
||||
services := buildForwardServiceConfigs(serviceBase, forward, tunnel, node, fp.Port, limiterID)
|
||||
_, err = h.sendNodeCommand(node.ID, method, services, true, false)
|
||||
if err != nil && allowFallbackAdd && method == "UpdateService" {
|
||||
_, err = h.sendNodeCommand(node.ID, "AddService", services, true, false)
|
||||
@@ -360,7 +368,7 @@ func (h *Handler) controlForwardServices(forward *forwardRecord, commandType str
|
||||
if len(ports) == 0 {
|
||||
return nil
|
||||
}
|
||||
userTunnelID, _, err := h.resolveUserTunnelAndLimiter(forward.UserID, forward.TunnelID)
|
||||
userTunnelID, _, _, err := h.resolveUserTunnelAndLimiter(forward.UserID, forward.TunnelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -862,7 +870,7 @@ func firstPortFromRange(portRange string) int {
|
||||
|
||||
func (h *Handler) listChainNodesForTunnel(tunnelID int64) ([]chainNodeRecord, error) {
|
||||
rows, err := h.repo.DB().Query(`
|
||||
SELECT ct.chain_type, COALESCE(ct.inx, 0), ct.node_id, COALESCE(ct.port, 0), n.name
|
||||
SELECT ct.chain_type, COALESCE(ct.inx, 0), ct.node_id, COALESCE(ct.port, 0), n.name, ct.protocol, ct.strategy
|
||||
FROM chain_tunnel ct
|
||||
LEFT JOIN node n ON n.id = ct.node_id
|
||||
WHERE ct.tunnel_id = ?
|
||||
@@ -877,7 +885,9 @@ func (h *Handler) listChainNodesForTunnel(tunnelID int64) ([]chainNodeRecord, er
|
||||
for rows.Next() {
|
||||
var item chainNodeRecord
|
||||
var name sql.NullString
|
||||
if err := rows.Scan(&item.ChainType, &item.Inx, &item.NodeID, &item.Port, &name); err != nil {
|
||||
var protocol sql.NullString
|
||||
var strategy sql.NullString
|
||||
if err := rows.Scan(&item.ChainType, &item.Inx, &item.NodeID, &item.Port, &name, &protocol, &strategy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(name.String) == "" {
|
||||
@@ -885,6 +895,8 @@ func (h *Handler) listChainNodesForTunnel(tunnelID int64) ([]chainNodeRecord, er
|
||||
} else {
|
||||
item.NodeName = name.String
|
||||
}
|
||||
item.Protocol = defaultString(protocol.String, "tls")
|
||||
item.Strategy = defaultString(strategy.String, "round")
|
||||
result = append(result, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -997,7 +1009,7 @@ func isNotFoundError(err error) bool {
|
||||
return strings.Contains(msg, "not found") || strings.Contains(msg, "不存在")
|
||||
}
|
||||
|
||||
func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel *tunnelRecord, node *nodeRecord, port int, limiter *int) []map[string]interface{} {
|
||||
func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel *tunnelRecord, node *nodeRecord, port int, limiterID *int64) []map[string]interface{} {
|
||||
protocols := []string{"tcp", "udp"}
|
||||
services := make([]map[string]interface{}, 0, 2)
|
||||
targets := splitRemoteTargets(forward.RemoteAddr)
|
||||
@@ -1038,8 +1050,8 @@ func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel
|
||||
if tunnel != nil && tunnel.Type == 1 && strings.TrimSpace(node.InterfaceName) != "" {
|
||||
service["metadata"] = map[string]interface{}{"interface": node.InterfaceName}
|
||||
}
|
||||
if limiter != nil && *limiter > 0 {
|
||||
service["limiter"] = strconv.Itoa(*limiter)
|
||||
if limiterID != nil && *limiterID > 0 {
|
||||
service["limiter"] = strconv.FormatInt(*limiterID, 10)
|
||||
}
|
||||
services = append(services, service)
|
||||
}
|
||||
@@ -1102,3 +1114,49 @@ func asBool(v interface{}, def bool) bool {
|
||||
return def
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) sendLimiterConfig(limiterID int64, speedMbps int, tunnelID int64) error {
|
||||
rate := float64(speedMbps) / 8.0
|
||||
limitStr := fmt.Sprintf("$ %.1fMB %.1fMB", rate, rate)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"name": strconv.FormatInt(limiterID, 10),
|
||||
"limits": []string{limitStr},
|
||||
}
|
||||
|
||||
nodes, err := h.tunnelEntryNodeIDs(tunnelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, nodeID := range nodes {
|
||||
_, _ = h.sendNodeCommand(nodeID, "AddLimiters", payload, false, false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) sendDeleteLimiterConfig(limiterID int64, tunnelID int64) error {
|
||||
payload := map[string]interface{}{
|
||||
"limiter": strconv.FormatInt(limiterID, 10),
|
||||
}
|
||||
|
||||
nodes, err := h.tunnelEntryNodeIDs(tunnelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, nodeID := range nodes {
|
||||
_, _ = h.sendNodeCommand(nodeID, "DeleteLimiters", payload, false, true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) ensureLimiterOnNode(nodeID int64, limiterID int64, speed int) {
|
||||
rate := float64(speed) / 8.0
|
||||
limitStr := fmt.Sprintf("$ %.1fMB %.1fMB", rate, rate)
|
||||
payload := map[string]interface{}{
|
||||
"name": strconv.FormatInt(limiterID, 10),
|
||||
"limits": []string{limitStr},
|
||||
}
|
||||
_, _ = h.sendNodeCommand(nodeID, "AddLimiters", payload, false, false)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go-backend/internal/http/client"
|
||||
"go-backend/internal/http/response"
|
||||
"go-backend/internal/store/sqlite"
|
||||
)
|
||||
|
||||
type federationTunnelRequest struct {
|
||||
Protocol string `json:"protocol"`
|
||||
RemotePort int `json:"remotePort"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
type createPeerShareRequest struct {
|
||||
Name string `json:"name"`
|
||||
NodeID int64 `json:"nodeId"`
|
||||
MaxBandwidth int64 `json:"maxBandwidth"`
|
||||
ExpiryTime int64 `json:"expiryTime"`
|
||||
PortRangeStart int `json:"portRangeStart"`
|
||||
PortRangeEnd int `json:"portRangeEnd"`
|
||||
AllowedDomains string `json:"allowedDomains"`
|
||||
}
|
||||
|
||||
type deletePeerShareRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
type nodeImportRequest struct {
|
||||
RemoteURL string `json:"remoteUrl"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (h *Handler) federationShareList(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("Invalid method"))
|
||||
return
|
||||
}
|
||||
|
||||
shares, err := h.repo.ListPeerShares()
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, response.OK(shares))
|
||||
}
|
||||
|
||||
func (h *Handler) federationShareCreate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("Invalid method"))
|
||||
return
|
||||
}
|
||||
|
||||
var req createPeerShareRequest
|
||||
if err := decodeJSON(r.Body, &req); err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault("Invalid JSON"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" || req.NodeID == 0 {
|
||||
response.WriteJSON(w, response.ErrDefault("Name and NodeID are required"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.MaxBandwidth < 0 {
|
||||
response.WriteJSON(w, response.ErrDefault("Max bandwidth cannot be negative"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.ExpiryTime < 0 {
|
||||
response.WriteJSON(w, response.ErrDefault("Expiry time cannot be negative"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.PortRangeStart < 0 || req.PortRangeStart > 65535 || req.PortRangeEnd < 0 || req.PortRangeEnd > 65535 {
|
||||
response.WriteJSON(w, response.ErrDefault("Invalid port range"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.PortRangeStart > req.PortRangeEnd {
|
||||
response.WriteJSON(w, response.ErrDefault("Port range start cannot be greater than end"))
|
||||
return
|
||||
}
|
||||
|
||||
node, err := h.repo.GetNodeByID(req.NodeID)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
if node == nil {
|
||||
response.WriteJSON(w, response.ErrDefault("Node not found"))
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
token := randomToken(32)
|
||||
|
||||
share := &sqlite.PeerShare{
|
||||
Name: req.Name,
|
||||
NodeID: req.NodeID,
|
||||
Token: token,
|
||||
MaxBandwidth: req.MaxBandwidth,
|
||||
ExpiryTime: req.ExpiryTime,
|
||||
PortRangeStart: req.PortRangeStart,
|
||||
PortRangeEnd: req.PortRangeEnd,
|
||||
IsActive: 1,
|
||||
CreatedTime: now,
|
||||
UpdatedTime: now,
|
||||
AllowedDomains: req.AllowedDomains,
|
||||
}
|
||||
|
||||
if err := h.repo.CreatePeerShare(share); err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteJSON(w, response.OKEmpty())
|
||||
}
|
||||
|
||||
func (h *Handler) federationShareDelete(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("Invalid method"))
|
||||
return
|
||||
}
|
||||
|
||||
var req deletePeerShareRequest
|
||||
if err := decodeJSON(r.Body, &req); err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault("Invalid JSON"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.repo.DeletePeerShare(req.ID); err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteJSON(w, response.OKEmpty())
|
||||
}
|
||||
|
||||
func (h *Handler) nodeImport(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("Invalid method"))
|
||||
return
|
||||
}
|
||||
|
||||
var req nodeImportRequest
|
||||
if err := decodeJSON(r.Body, &req); err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault("Invalid JSON"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.RemoteURL == "" || req.Token == "" {
|
||||
response.WriteJSON(w, response.ErrDefault("Remote URL and Token are required"))
|
||||
return
|
||||
}
|
||||
|
||||
domainCfg, _ := h.repo.GetConfigByName("panel_domain")
|
||||
localDomain := ""
|
||||
if domainCfg != nil {
|
||||
localDomain = domainCfg.Value
|
||||
}
|
||||
|
||||
fc := client.NewFederationClient()
|
||||
info, err := fc.Connect(req.RemoteURL, req.Token, localDomain)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, "Failed to connect: "+err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Prepare config json for local storage (metadata about limits)
|
||||
configData := map[string]interface{}{
|
||||
"shareId": info.ShareID,
|
||||
"maxBandwidth": info.MaxBandwidth,
|
||||
"expiryTime": info.ExpiryTime,
|
||||
"portRangeStart": info.PortRangeStart,
|
||||
"portRangeEnd": info.PortRangeEnd,
|
||||
}
|
||||
configBytes, _ := json.Marshal(configData)
|
||||
|
||||
db := h.repo.DB()
|
||||
inx := nextIndex(db, "node")
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx, is_remote, remote_url, remote_token, remote_config)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)
|
||||
`,
|
||||
fmt.Sprintf("%s (Remote)", info.NodeName),
|
||||
randomToken(16), // Dummy secret
|
||||
info.ServerIP,
|
||||
"", "", // v4/v6 unknown, use server_ip
|
||||
"0", // port range not applicable for remote
|
||||
"",
|
||||
"",
|
||||
now, now,
|
||||
info.Status,
|
||||
"[::]", "[::]",
|
||||
inx,
|
||||
req.RemoteURL,
|
||||
req.Token,
|
||||
string(configBytes),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, "Database error: "+err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteJSON(w, response.OKEmpty())
|
||||
}
|
||||
|
||||
func (h *Handler) authPeer(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
response.WriteJSON(w, response.Err(401, "Missing Authorization header"))
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
response.WriteJSON(w, response.Err(401, "Invalid Authorization format"))
|
||||
return
|
||||
}
|
||||
|
||||
token := parts[1]
|
||||
share, err := h.repo.GetPeerShareByToken(token)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
if share == nil {
|
||||
response.WriteJSON(w, response.Err(401, "Invalid token"))
|
||||
return
|
||||
}
|
||||
|
||||
if share.IsActive == 0 {
|
||||
response.WriteJSON(w, response.Err(403, "Share is disabled"))
|
||||
return
|
||||
}
|
||||
|
||||
if share.ExpiryTime > 0 && share.ExpiryTime < time.Now().UnixMilli() {
|
||||
response.WriteJSON(w, response.Err(403, "Share expired"))
|
||||
return
|
||||
}
|
||||
|
||||
if share.AllowedDomains != "" {
|
||||
clientDomain := r.Header.Get("X-Panel-Domain")
|
||||
if clientDomain == "" {
|
||||
response.WriteJSON(w, response.Err(403, "Domain verification required"))
|
||||
return
|
||||
}
|
||||
allowed := false
|
||||
domains := strings.Split(share.AllowedDomains, ",")
|
||||
for _, d := range domains {
|
||||
if strings.TrimSpace(d) == clientDomain {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
response.WriteJSON(w, response.Err(403, "Domain not allowed"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) federationConnect(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("Invalid method"))
|
||||
return
|
||||
}
|
||||
|
||||
token := extractBearerToken(r)
|
||||
share, err := h.repo.GetPeerShareByToken(token)
|
||||
if err != nil || share == nil {
|
||||
response.WriteJSON(w, response.Err(401, "Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
var nodeName string
|
||||
var serverIP string
|
||||
var status int
|
||||
|
||||
err = h.repo.DB().QueryRow("SELECT name, server_ip, status FROM node WHERE id = ?", share.NodeID).Scan(&nodeName, &serverIP, &status)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, "Node not found"))
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteJSON(w, response.OK(map[string]interface{}{
|
||||
"shareId": share.ID,
|
||||
"shareName": share.Name,
|
||||
"nodeId": share.NodeID,
|
||||
"nodeName": nodeName,
|
||||
"serverIp": serverIP,
|
||||
"status": status,
|
||||
"maxBandwidth": share.MaxBandwidth,
|
||||
"expiryTime": share.ExpiryTime,
|
||||
"portRangeStart": share.PortRangeStart,
|
||||
"portRangeEnd": share.PortRangeEnd,
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *Handler) federationTunnelCreate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("Invalid method"))
|
||||
return
|
||||
}
|
||||
|
||||
token := extractBearerToken(r)
|
||||
share, err := h.repo.GetPeerShareByToken(token)
|
||||
if err != nil || share == nil {
|
||||
response.WriteJSON(w, response.Err(401, "Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
var req federationTunnelRequest
|
||||
if err := decodeJSON(r.Body, &req); err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault("Invalid JSON"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.RemotePort < share.PortRangeStart || req.RemotePort > share.PortRangeEnd {
|
||||
response.WriteJSON(w, response.Err(403, "Port out of range"))
|
||||
return
|
||||
}
|
||||
|
||||
tunnelType := 1
|
||||
if strings.ToLower(req.Protocol) == "udp" {
|
||||
tunnelType = 2
|
||||
}
|
||||
|
||||
tx, err := h.repo.DB().Begin()
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
res, err := tx.Exec(`INSERT INTO tunnel (name, type, protocol, flow, created_time, updated_time, status, in_ip) VALUES (?, ?, ?, 0, ?, ?, 1, ?)`,
|
||||
fmt.Sprintf("Share-%d-Port-%d", share.ID, req.RemotePort),
|
||||
tunnelType,
|
||||
req.Protocol,
|
||||
now,
|
||||
now,
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
tunnelID, _ := res.LastInsertId()
|
||||
|
||||
_, err = tx.Exec(`INSERT INTO chain_tunnel (tunnel_id, chain_type, node_id, port, strategy, inx, protocol) VALUES (?, 1, ?, ?, 'fifo', 0, ?)`,
|
||||
tunnelID,
|
||||
share.NodeID,
|
||||
req.RemotePort,
|
||||
req.Protocol,
|
||||
)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
h.wsServer.SendCommand(share.NodeID, "reload", nil, time.Second*5)
|
||||
|
||||
response.WriteJSON(w, response.OK(map[string]interface{}{
|
||||
"tunnelId": tunnelID,
|
||||
}))
|
||||
}
|
||||
|
||||
func extractBearerToken(r *http.Request) string {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) == 2 && parts[0] == "Bearer" {
|
||||
return parts[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -143,6 +143,12 @@ func (h *Handler) Register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/group/permission/assign", h.groupPermissionAssign)
|
||||
mux.HandleFunc("/api/v1/group/permission/remove", h.groupPermissionRemove)
|
||||
mux.HandleFunc("/api/v1/open_api/sub_store", h.openAPISubStore)
|
||||
mux.HandleFunc("/api/v1/federation/share/list", h.federationShareList)
|
||||
mux.HandleFunc("/api/v1/federation/share/create", h.federationShareCreate)
|
||||
mux.HandleFunc("/api/v1/federation/share/delete", h.federationShareDelete)
|
||||
mux.HandleFunc("/api/v1/federation/connect", h.authPeer(h.federationConnect))
|
||||
mux.HandleFunc("/api/v1/federation/tunnel/create", h.authPeer(h.federationTunnelCreate))
|
||||
mux.HandleFunc("/api/v1/federation/node/import", h.nodeImport)
|
||||
|
||||
mux.HandleFunc("/flow/test", h.flowTest)
|
||||
mux.HandleFunc("/flow/config", h.flowConfig)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go-backend/internal/http/client"
|
||||
"go-backend/internal/http/response"
|
||||
"go-backend/internal/security"
|
||||
)
|
||||
@@ -273,8 +274,8 @@ func (h *Handler) nodeCreate(w http.ResponseWriter, r *http.Request) {
|
||||
now := time.Now().UnixMilli()
|
||||
inx := nextIndex(db, "node")
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx, is_remote, remote_url, remote_token, remote_config)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
name,
|
||||
randomToken(16),
|
||||
@@ -293,6 +294,10 @@ func (h *Handler) nodeCreate(w http.ResponseWriter, r *http.Request) {
|
||||
defaultString(asString(req["tcpListenAddr"]), "[::]"),
|
||||
defaultString(asString(req["udpListenAddr"]), "[::]"),
|
||||
inx,
|
||||
asInt(req["isRemote"], 0),
|
||||
nullableText(asString(req["remoteUrl"])),
|
||||
nullableText(asString(req["remoteToken"])),
|
||||
nullableText(asString(req["remoteConfig"])),
|
||||
)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
@@ -509,6 +514,50 @@ func (h *Handler) tunnelCreate(w http.ResponseWriter, r *http.Request) {
|
||||
inIP = buildTunnelInIP(runtimeState.InNodes, runtimeState.Nodes)
|
||||
}
|
||||
|
||||
if len(runtimeState.InNodes) > 0 {
|
||||
firstNodeID := runtimeState.InNodes[0].NodeID
|
||||
var isRemote int
|
||||
var rUrl, rToken sql.NullString
|
||||
if err := h.repo.DB().QueryRow("SELECT is_remote, remote_url, remote_token FROM node WHERE id = ?", firstNodeID).Scan(&isRemote, &rUrl, &rToken); err == nil && isRemote == 1 {
|
||||
fc := client.NewFederationClient()
|
||||
|
||||
targetProto := "tcp"
|
||||
targetPort := 0
|
||||
targetAddr := ""
|
||||
|
||||
if typeVal == 1 {
|
||||
if len(runtimeState.OutNodes) > 0 {
|
||||
outNode := runtimeState.OutNodes[0]
|
||||
outNodeRec := runtimeState.Nodes[outNode.NodeID]
|
||||
targetAddr = processServerAddress(outNodeRec.ServerIP)
|
||||
if outNode.Port > 0 {
|
||||
targetAddr = fmt.Sprintf("%s:%d", targetAddr, outNode.Port)
|
||||
}
|
||||
}
|
||||
if len(runtimeState.InNodes) > 0 {
|
||||
inNodesRaw := asMapSlice(req["inNodeId"])
|
||||
if len(inNodesRaw) > 0 {
|
||||
targetPort = asInt(inNodesRaw[0]["port"], 0)
|
||||
targetProto = defaultString(asString(inNodesRaw[0]["protocol"]), "tcp")
|
||||
}
|
||||
}
|
||||
|
||||
if targetPort > 0 && targetAddr != "" {
|
||||
domainCfg, _ := h.repo.GetConfigByName("panel_domain")
|
||||
localDomain := ""
|
||||
if domainCfg != nil {
|
||||
localDomain = domainCfg.Value
|
||||
}
|
||||
_, err := fc.CreateTunnel(rUrl.String, rToken.String, localDomain, targetProto, targetPort, targetAddr)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault("Remote tunnel creation failed: "+err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res, err := tx.Exec(`INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
name, trafficRatio, typeVal, "tls", flow, now, now, status, nullableText(inIP), inx)
|
||||
if err != nil {
|
||||
@@ -731,6 +780,82 @@ func (h *Handler) tunnelBatchDelete(w http.ResponseWriter, r *http.Request) {
|
||||
response.WriteJSON(w, response.OK(map[string]interface{}{"successCount": success, "failCount": fail}))
|
||||
}
|
||||
|
||||
func (h *Handler) reconstructTunnelState(tunnelID int64) (*tunnelCreateState, error) {
|
||||
tunnel, err := h.getTunnelRecord(tunnelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chainRows, err := h.listChainNodesForTunnel(tunnelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
state := &tunnelCreateState{
|
||||
TunnelID: tunnelID,
|
||||
Type: tunnel.Type,
|
||||
InNodes: make([]tunnelRuntimeNode, 0),
|
||||
ChainHops: make([][]tunnelRuntimeNode, 0),
|
||||
OutNodes: make([]tunnelRuntimeNode, 0),
|
||||
Nodes: make(map[int64]*nodeRecord),
|
||||
NodeIDList: make([]int64, 0),
|
||||
}
|
||||
|
||||
inNodes, chainHops, outNodes := splitChainNodeGroups(chainRows)
|
||||
|
||||
for _, r := range inNodes {
|
||||
state.InNodes = append(state.InNodes, tunnelRuntimeNode{
|
||||
NodeID: r.NodeID,
|
||||
Protocol: r.Protocol,
|
||||
Strategy: r.Strategy,
|
||||
ChainType: 1,
|
||||
})
|
||||
state.NodeIDList = append(state.NodeIDList, r.NodeID)
|
||||
}
|
||||
|
||||
for _, r := range outNodes {
|
||||
state.OutNodes = append(state.OutNodes, tunnelRuntimeNode{
|
||||
NodeID: r.NodeID,
|
||||
Protocol: r.Protocol,
|
||||
Strategy: r.Strategy,
|
||||
ChainType: 3,
|
||||
Port: r.Port,
|
||||
})
|
||||
state.NodeIDList = append(state.NodeIDList, r.NodeID)
|
||||
}
|
||||
|
||||
for _, hop := range chainHops {
|
||||
stateHop := make([]tunnelRuntimeNode, 0)
|
||||
for _, r := range hop {
|
||||
stateHop = append(stateHop, tunnelRuntimeNode{
|
||||
NodeID: r.NodeID,
|
||||
Protocol: r.Protocol,
|
||||
Strategy: r.Strategy,
|
||||
ChainType: 2,
|
||||
Inx: int(r.Inx),
|
||||
Port: r.Port,
|
||||
})
|
||||
state.NodeIDList = append(state.NodeIDList, r.NodeID)
|
||||
}
|
||||
state.ChainHops = append(state.ChainHops, stateHop)
|
||||
}
|
||||
|
||||
seen := make(map[int64]struct{})
|
||||
for _, id := range state.NodeIDList {
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
node, err := h.getNodeRecord(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state.Nodes[id] = node
|
||||
}
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func (h *Handler) tunnelBatchRedeploy(w http.ResponseWriter, r *http.Request) {
|
||||
ids := idsFromBody(r, w)
|
||||
if ids == nil {
|
||||
@@ -739,6 +864,26 @@ func (h *Handler) tunnelBatchRedeploy(w http.ResponseWriter, r *http.Request) {
|
||||
success := 0
|
||||
fail := 0
|
||||
for _, tunnelID := range ids {
|
||||
tunnel, err := h.getTunnelRecord(tunnelID)
|
||||
if err != nil {
|
||||
fail++
|
||||
continue
|
||||
}
|
||||
|
||||
if tunnel.Type == 2 {
|
||||
h.cleanupTunnelRuntime(tunnelID)
|
||||
state, err := h.reconstructTunnelState(tunnelID)
|
||||
if err != nil {
|
||||
fail++
|
||||
continue
|
||||
}
|
||||
_, _, applyErr := h.applyTunnelRuntime(state)
|
||||
if applyErr != nil {
|
||||
fail++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
forwards, err := h.listForwardsByTunnel(tunnelID)
|
||||
if err != nil {
|
||||
fail++
|
||||
@@ -841,6 +986,13 @@ func (h *Handler) userTunnelUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch details to sync forwards
|
||||
var userID, tunnelID int64
|
||||
if err := h.repo.DB().QueryRow("SELECT user_id, tunnel_id FROM user_tunnel WHERE id = ?", id).Scan(&userID, &tunnelID); err == nil {
|
||||
h.syncUserTunnelForwards(userID, tunnelID)
|
||||
}
|
||||
|
||||
response.WriteJSON(w, response.OKEmpty())
|
||||
}
|
||||
|
||||
@@ -1366,12 +1518,15 @@ func (h *Handler) speedLimitCreate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
now := time.Now().UnixMilli()
|
||||
_, err := h.repo.DB().Exec(`INSERT INTO speed_limit(name, speed, tunnel_id, tunnel_name, created_time, updated_time, status) VALUES(?, ?, ?, ?, ?, ?, ?)`,
|
||||
name, asInt(req["speed"], 100), tunnelID, tunnelName, now, now, asInt(req["status"], 1))
|
||||
speed := asInt(req["speed"], 100)
|
||||
res, err := h.repo.DB().Exec(`INSERT INTO speed_limit(name, speed, tunnel_id, tunnel_name, created_time, updated_time, status) VALUES(?, ?, ?, ?, ?, ?, ?)`,
|
||||
name, speed, tunnelID, tunnelName, now, now, asInt(req["status"], 1))
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
_ = h.sendLimiterConfig(id, speed, tunnelID)
|
||||
response.WriteJSON(w, response.OKEmpty())
|
||||
}
|
||||
|
||||
@@ -1393,12 +1548,14 @@ func (h *Handler) speedLimitUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
response.WriteJSON(w, response.ErrDefault("隧道不存在"))
|
||||
return
|
||||
}
|
||||
speed := asInt(req["speed"], 100)
|
||||
_, err := h.repo.DB().Exec(`UPDATE speed_limit SET name=?, speed=?, tunnel_id=?, tunnel_name=?, status=?, updated_time=? WHERE id=?`,
|
||||
asString(req["name"]), asInt(req["speed"], 100), tunnelID, tunnelName, asInt(req["status"], 1), time.Now().UnixMilli(), id)
|
||||
asString(req["name"]), speed, tunnelID, tunnelName, asInt(req["status"], 1), time.Now().UnixMilli(), id)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
_ = h.sendLimiterConfig(id, speed, tunnelID)
|
||||
response.WriteJSON(w, response.OKEmpty())
|
||||
}
|
||||
|
||||
@@ -1407,11 +1564,17 @@ func (h *Handler) speedLimitDelete(w http.ResponseWriter, r *http.Request) {
|
||||
if id <= 0 {
|
||||
return
|
||||
}
|
||||
var tunnelID int64
|
||||
_ = h.repo.DB().QueryRow(`SELECT tunnel_id FROM speed_limit WHERE id = ?`, id).Scan(&tunnelID)
|
||||
|
||||
_, err := h.repo.DB().Exec(`DELETE FROM speed_limit WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
if tunnelID > 0 {
|
||||
_ = h.sendDeleteLimiterConfig(id, tunnelID)
|
||||
}
|
||||
response.WriteJSON(w, response.OKEmpty())
|
||||
}
|
||||
|
||||
@@ -2279,8 +2442,8 @@ func replaceTunnelChainsTx(tx *sql.Tx, tunnelID int64, req map[string]interface{
|
||||
return pickErr
|
||||
}
|
||||
}
|
||||
_, err := tx.Exec(`INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol) VALUES(?, 3, ?, ?, NULL, 0, ?)`,
|
||||
tunnelID, nodeID, port, defaultString(asString(n["protocol"]), "tls"))
|
||||
_, err := tx.Exec(`INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol) VALUES(?, 3, ?, ?, ?, 0, ?)`,
|
||||
tunnelID, nodeID, port, defaultString(asString(n["strategy"]), "round"), defaultString(asString(n["protocol"]), "tls"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2683,9 +2846,26 @@ func (h *Handler) upsertUserTunnel(req map[string]interface{}) error {
|
||||
|
||||
_, err = db.Exec(`UPDATE user_tunnel SET speed_id = ?, flow = ?, num = ?, exp_time = ?, flow_reset_time = ?, status = ? WHERE id = ?`,
|
||||
newSpeedID, newFlow, newNum, newExpTime, newFlowReset, newStatus, existingID)
|
||||
|
||||
if err == nil {
|
||||
h.syncUserTunnelForwards(userID, tunnelID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Handler) syncUserTunnelForwards(userID, tunnelID int64) {
|
||||
forwards, err := h.listForwardsByTunnel(tunnelID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for i := range forwards {
|
||||
f := &forwards[i]
|
||||
if f.UserID == userID {
|
||||
_ = h.syncForwardServices(f, "UpdateService", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func asAnySlice(v interface{}) []interface{} {
|
||||
if v == nil {
|
||||
return nil
|
||||
|
||||
@@ -81,6 +81,10 @@ func shouldSkip(path string) bool {
|
||||
return true
|
||||
case path == "/api/v1/user/login":
|
||||
return true
|
||||
case path == "/api/v1/federation/connect":
|
||||
return true
|
||||
case path == "/api/v1/federation/tunnel/create":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -91,6 +95,10 @@ func requiresAdmin(path string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "/api/v1/federation/share/") {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "/api/v1/node/") {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -95,13 +95,33 @@ type StatisticsFlow struct {
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
ID int64
|
||||
Secret string
|
||||
Version sql.NullString
|
||||
HTTP int
|
||||
TLS int
|
||||
Socks int
|
||||
Status int
|
||||
ID int64
|
||||
Secret string
|
||||
Version sql.NullString
|
||||
HTTP int
|
||||
TLS int
|
||||
Socks int
|
||||
Status int
|
||||
IsRemote int
|
||||
RemoteURL sql.NullString
|
||||
RemoteToken sql.NullString
|
||||
RemoteConfig sql.NullString
|
||||
}
|
||||
|
||||
type PeerShare struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
NodeID int64 `json:"nodeId"`
|
||||
Token string `json:"token"`
|
||||
MaxBandwidth int64 `json:"maxBandwidth"`
|
||||
ExpiryTime int64 `json:"expiryTime"`
|
||||
PortRangeStart int `json:"portRangeStart"`
|
||||
PortRangeEnd int `json:"portRangeEnd"`
|
||||
CurrentFlow int64 `json:"currentFlow"`
|
||||
IsActive int `json:"isActive"`
|
||||
CreatedTime int64 `json:"createdTime"`
|
||||
UpdatedTime int64 `json:"updatedTime"`
|
||||
AllowedDomains string `json:"allowedDomains"`
|
||||
}
|
||||
|
||||
func Open(path string) (*Repository, error) {
|
||||
@@ -124,6 +144,11 @@ func Open(path string) (*Repository, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := ensurePeerSchema(db); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Repository{db: db}, nil
|
||||
}
|
||||
|
||||
@@ -389,9 +414,25 @@ func (r *Repository) GetNodeBySecret(secret string) (*Node, error) {
|
||||
return nil, errors.New("repository not initialized")
|
||||
}
|
||||
|
||||
row := r.db.QueryRow(`SELECT id, secret, version, http, tls, socks, status FROM node WHERE secret = ? LIMIT 1`, secret)
|
||||
row := r.db.QueryRow(`SELECT id, secret, version, http, tls, socks, status, is_remote, remote_url, remote_token, remote_config FROM node WHERE secret = ? LIMIT 1`, secret)
|
||||
var n Node
|
||||
if err := row.Scan(&n.ID, &n.Secret, &n.Version, &n.HTTP, &n.TLS, &n.Socks, &n.Status); err != nil {
|
||||
if err := row.Scan(&n.ID, &n.Secret, &n.Version, &n.HTTP, &n.TLS, &n.Socks, &n.Status, &n.IsRemote, &n.RemoteURL, &n.RemoteToken, &n.RemoteConfig); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetNodeByID(id int64) (*Node, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, errors.New("repository not initialized")
|
||||
}
|
||||
|
||||
row := r.db.QueryRow(`SELECT id, secret, version, http, tls, socks, status, is_remote, remote_url, remote_token, remote_config FROM node WHERE id = ? LIMIT 1`, id)
|
||||
var n Node
|
||||
if err := row.Scan(&n.ID, &n.Secret, &n.Version, &n.HTTP, &n.TLS, &n.Socks, &n.Status, &n.IsRemote, &n.RemoteURL, &n.RemoteToken, &n.RemoteConfig); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -454,7 +495,7 @@ func (r *Repository) ListNodes() ([]map[string]interface{}, error) {
|
||||
}
|
||||
|
||||
rows, err := r.db.Query(`
|
||||
SELECT id, inx, name, server_ip, server_ip_v4, server_ip_v6, port, tcp_listen_addr, udp_listen_addr, version, http, tls, socks, status
|
||||
SELECT id, inx, name, server_ip, server_ip_v4, server_ip_v6, port, tcp_listen_addr, udp_listen_addr, version, http, tls, socks, status, is_remote, remote_url, remote_token, remote_config
|
||||
FROM node
|
||||
ORDER BY inx ASC, id ASC
|
||||
`)
|
||||
@@ -467,10 +508,10 @@ func (r *Repository) ListNodes() ([]map[string]interface{}, error) {
|
||||
for rows.Next() {
|
||||
var id, inx int64
|
||||
var name, serverIP, port string
|
||||
var serverIPV4, serverIPV6, tcpListen, udpListen, version sql.NullString
|
||||
var httpVal, tlsVal, socksVal, status int
|
||||
var serverIPV4, serverIPV6, tcpListen, udpListen, version, remoteURL, remoteToken, remoteConfig sql.NullString
|
||||
var httpVal, tlsVal, socksVal, status, isRemote int
|
||||
|
||||
if err := rows.Scan(&id, &inx, &name, &serverIP, &serverIPV4, &serverIPV6, &port, &tcpListen, &udpListen, &version, &httpVal, &tlsVal, &socksVal, &status); err != nil {
|
||||
if err := rows.Scan(&id, &inx, &name, &serverIP, &serverIPV4, &serverIPV6, &port, &tcpListen, &udpListen, &version, &httpVal, &tlsVal, &socksVal, &status, &isRemote, &remoteURL, &remoteToken, &remoteConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -490,6 +531,10 @@ func (r *Repository) ListNodes() ([]map[string]interface{}, error) {
|
||||
"tls": tlsVal,
|
||||
"socks": socksVal,
|
||||
"status": status,
|
||||
"isRemote": isRemote,
|
||||
"remoteUrl": nullableString(remoteURL),
|
||||
"remoteToken": nullableString(remoteToken),
|
||||
"remoteConfig": nullableString(remoteConfig),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1167,25 +1212,155 @@ func bootstrapSchema(db *sql.DB) error {
|
||||
return errors.New("nil db")
|
||||
}
|
||||
|
||||
var exists int
|
||||
err := db.QueryRow(`SELECT COUNT(1) FROM sqlite_master WHERE type='table' AND name='user'`).Scan(&exists)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check schema: %w", err)
|
||||
}
|
||||
if exists > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("sqlite schema not found, bootstrapping embedded schema")
|
||||
if _, err := db.Exec(embeddedSchema); err != nil {
|
||||
return fmt.Errorf("apply schema.sql: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(embeddedSeedData); err != nil {
|
||||
return fmt.Errorf("apply data.sql: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensurePeerSchema(db *sql.DB) error {
|
||||
if db == nil {
|
||||
return errors.New("nil db")
|
||||
}
|
||||
|
||||
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS peer_share (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
node_id INTEGER NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
max_bandwidth INTEGER DEFAULT 0,
|
||||
expiry_time INTEGER DEFAULT 0,
|
||||
port_range_start INTEGER DEFAULT 0,
|
||||
port_range_end INTEGER DEFAULT 0,
|
||||
current_flow INTEGER DEFAULT 0,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_time INTEGER NOT NULL,
|
||||
updated_time INTEGER NOT NULL,
|
||||
allowed_domains TEXT DEFAULT ''
|
||||
)`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create peer_share: %w", err)
|
||||
}
|
||||
|
||||
// Add allowed_domains column if it doesn't exist (for existing tables)
|
||||
var dummy interface{}
|
||||
err = db.QueryRow("SELECT allowed_domains FROM peer_share LIMIT 1").Scan(&dummy)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no such column") {
|
||||
_, err = db.Exec("ALTER TABLE peer_share ADD COLUMN allowed_domains TEXT DEFAULT ''")
|
||||
if err != nil {
|
||||
log.Printf("failed to add column allowed_domains to peer_share: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
columns := map[string]string{
|
||||
"is_remote": "INTEGER DEFAULT 0",
|
||||
"remote_url": "TEXT",
|
||||
"remote_token": "TEXT",
|
||||
"remote_config": "TEXT",
|
||||
}
|
||||
|
||||
for col, typ := range columns {
|
||||
var dummy interface{}
|
||||
err := db.QueryRow(fmt.Sprintf("SELECT %s FROM node LIMIT 1", col)).Scan(&dummy)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no such column") {
|
||||
_, err = db.Exec(fmt.Sprintf("ALTER TABLE node ADD COLUMN %s %s", col, typ))
|
||||
if err != nil {
|
||||
log.Printf("failed to add column %s: %v", col, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreatePeerShare(share *PeerShare) error {
|
||||
if r == nil || r.db == nil {
|
||||
return errors.New("repository not initialized")
|
||||
}
|
||||
_, err := r.db.Exec(`
|
||||
INSERT INTO peer_share(name, node_id, token, max_bandwidth, expiry_time, port_range_start, port_range_end, current_flow, is_active, created_time, updated_time, allowed_domains)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, share.Name, share.NodeID, share.Token, share.MaxBandwidth, share.ExpiryTime, share.PortRangeStart, share.PortRangeEnd, share.CurrentFlow, share.IsActive, share.CreatedTime, share.UpdatedTime, share.AllowedDomains)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) UpdatePeerShare(share *PeerShare) error {
|
||||
if r == nil || r.db == nil {
|
||||
return errors.New("repository not initialized")
|
||||
}
|
||||
_, err := r.db.Exec(`
|
||||
UPDATE peer_share SET name=?, max_bandwidth=?, expiry_time=?, port_range_start=?, port_range_end=?, is_active=?, updated_time=?, allowed_domains=?
|
||||
WHERE id=?
|
||||
`, share.Name, share.MaxBandwidth, share.ExpiryTime, share.PortRangeStart, share.PortRangeEnd, share.IsActive, share.UpdatedTime, share.AllowedDomains, share.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) DeletePeerShare(id int64) error {
|
||||
if r == nil || r.db == nil {
|
||||
return errors.New("repository not initialized")
|
||||
}
|
||||
_, err := r.db.Exec(`DELETE FROM peer_share WHERE id=?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) GetPeerShare(id int64) (*PeerShare, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, errors.New("repository not initialized")
|
||||
}
|
||||
row := r.db.QueryRow(`SELECT id, name, node_id, token, max_bandwidth, expiry_time, port_range_start, port_range_end, current_flow, is_active, created_time, updated_time, allowed_domains FROM peer_share WHERE id = ?`, id)
|
||||
var s PeerShare
|
||||
if err := row.Scan(&s.ID, &s.Name, &s.NodeID, &s.Token, &s.MaxBandwidth, &s.ExpiryTime, &s.PortRangeStart, &s.PortRangeEnd, &s.CurrentFlow, &s.IsActive, &s.CreatedTime, &s.UpdatedTime, &s.AllowedDomains); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetPeerShareByToken(token string) (*PeerShare, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, errors.New("repository not initialized")
|
||||
}
|
||||
row := r.db.QueryRow(`SELECT id, name, node_id, token, max_bandwidth, expiry_time, port_range_start, port_range_end, current_flow, is_active, created_time, updated_time, allowed_domains FROM peer_share WHERE token = ?`, token)
|
||||
var s PeerShare
|
||||
if err := row.Scan(&s.ID, &s.Name, &s.NodeID, &s.Token, &s.MaxBandwidth, &s.ExpiryTime, &s.PortRangeStart, &s.PortRangeEnd, &s.CurrentFlow, &s.IsActive, &s.CreatedTime, &s.UpdatedTime, &s.AllowedDomains); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ListPeerShares() ([]PeerShare, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, errors.New("repository not initialized")
|
||||
}
|
||||
rows, err := r.db.Query(`SELECT id, name, node_id, token, max_bandwidth, expiry_time, port_range_start, port_range_end, current_flow, is_active, created_time, updated_time, allowed_domains FROM peer_share ORDER BY id DESC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var shares []PeerShare
|
||||
for rows.Next() {
|
||||
var s PeerShare
|
||||
if err := rows.Scan(&s.ID, &s.Name, &s.NodeID, &s.Token, &s.MaxBandwidth, &s.ExpiryTime, &s.PortRangeStart, &s.PortRangeEnd, &s.CurrentFlow, &s.IsActive, &s.CreatedTime, &s.UpdatedTime, &s.AllowedDomains); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shares = append(shares, s)
|
||||
}
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
var osMkdirAll = func(path string) error {
|
||||
return os.MkdirAll(path, 0o755)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -30,6 +31,7 @@ import (
|
||||
logger_parser "github.com/go-gost/x/config/parsing/logger"
|
||||
selector_parser "github.com/go-gost/x/config/parsing/selector"
|
||||
tls_util "github.com/go-gost/x/internal/util/tls"
|
||||
xtraffic "github.com/go-gost/x/limiter/traffic"
|
||||
cache_limiter "github.com/go-gost/x/limiter/traffic/cache"
|
||||
"github.com/go-gost/x/metadata"
|
||||
mdutil "github.com/go-gost/x/metadata/util"
|
||||
@@ -181,6 +183,32 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
||||
)
|
||||
}
|
||||
|
||||
var trafficLimiter listener.Option
|
||||
if cfg.Limiter != "" {
|
||||
lim := registry.TrafficLimiterRegistry().Get(cfg.Limiter)
|
||||
if lim == nil {
|
||||
// Try to parse as simple number (bandwidth in bytes/sec)
|
||||
if val, err := strconv.Atoi(cfg.Limiter); err == nil && val > 0 {
|
||||
lim = xtraffic.NewTrafficLimiter(
|
||||
xtraffic.LimitsOption(fmt.Sprintf("%s %dB %dB", xtraffic.ServiceLimitKey, val, val)),
|
||||
)
|
||||
}
|
||||
if lim == nil {
|
||||
lim = xtraffic.NewTrafficLimiter(
|
||||
xtraffic.LimitsOption(fmt.Sprintf("%s %s %s", xtraffic.ServiceLimitKey, cfg.Limiter, cfg.Limiter)),
|
||||
)
|
||||
}
|
||||
}
|
||||
trafficLimiter = listener.TrafficLimiterOption(
|
||||
cache_limiter.NewCachedTrafficLimiter(
|
||||
lim,
|
||||
cache_limiter.RefreshIntervalOption(limiterRefreshInterval),
|
||||
cache_limiter.CleanupIntervalOption(limiterCleanupInterval),
|
||||
cache_limiter.ScopeOption(limiterScope),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
listenOpts := []listener.Option{
|
||||
listener.AddrOption(cfg.Addr),
|
||||
listener.RouterOption(xchain.NewRouter(routerOpts...)),
|
||||
@@ -188,14 +216,6 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
||||
listener.AuthOption(auth_parser.Info(cfg.Listener.Auth)),
|
||||
listener.TLSConfigOption(tlsConfig),
|
||||
listener.AdmissionOption(xadmission.AdmissionGroup(admissions...)),
|
||||
listener.TrafficLimiterOption(
|
||||
cache_limiter.NewCachedTrafficLimiter(
|
||||
registry.TrafficLimiterRegistry().Get(cfg.Limiter),
|
||||
cache_limiter.RefreshIntervalOption(limiterRefreshInterval),
|
||||
cache_limiter.CleanupIntervalOption(limiterCleanupInterval),
|
||||
cache_limiter.ScopeOption(limiterScope),
|
||||
),
|
||||
),
|
||||
listener.ConnLimiterOption(registry.ConnLimiterRegistry().Get(cfg.CLimiter)),
|
||||
listener.ServiceOption(cfg.Name),
|
||||
listener.ProxyProtocolOption(ppv),
|
||||
@@ -203,6 +223,9 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
||||
listener.NetnsOption(netnsIn),
|
||||
listener.LoggerOption(listenerLogger),
|
||||
}
|
||||
if trafficLimiter != nil {
|
||||
listenOpts = append(listenOpts, trafficLimiter)
|
||||
}
|
||||
|
||||
if netnsIn != "" {
|
||||
runtime.LockOSThread()
|
||||
|
||||
+8
-3
@@ -4,9 +4,7 @@ go 1.22.0
|
||||
|
||||
toolchain go1.23.4
|
||||
|
||||
require (
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
)
|
||||
require github.com/shirou/gopsutil/v3 v3.24.5
|
||||
|
||||
require (
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137
|
||||
@@ -76,6 +74,7 @@ require (
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
@@ -91,6 +90,7 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/klauspost/reedsolomon v1.11.8 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
@@ -103,6 +103,7 @@ require (
|
||||
github.com/pion/udp/v2 v2.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/prometheus/client_model v0.6.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
@@ -110,6 +111,7 @@ require (
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
@@ -118,8 +120,11 @@ require (
|
||||
github.com/templexxx/cpu v0.1.0 // indirect
|
||||
github.com/templexxx/xorsimd v0.4.2 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
|
||||
@@ -65,6 +65,8 @@ github.com/go-gost/tls-dissector v0.1.1 h1:2zUOTPzCQAUQ54Rpy0UEi3JPMQSYsIFSeFeKr
|
||||
github.com/go-gost/tls-dissector v0.1.1/go.mod h1:/9QfdewqmHdaE362Hv5nDaSWLx3pCmtD870d6GaquXs=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -102,6 +104,7 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -133,6 +136,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@@ -175,6 +180,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
@@ -204,7 +211,12 @@ github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1H
|
||||
github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM=
|
||||
github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601 h1:XU9hik0exChEmY92ALW4l9WnDodxLVS9yOSNh2SizaQ=
|
||||
github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601/go.mod h1:mttDPaeLm87u74HMrP+n2tugXvIKWcwff/cqSX0lehY=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
@@ -244,6 +256,10 @@ github.com/templexxx/xorsimd v0.4.2 h1:ocZZ+Nvu65LGHmCLZ7OoCtg8Fx8jnHKK37SjvngUo
|
||||
github.com/templexxx/xorsimd v0.4.2/go.mod h1:HgwaPoDREdi6OnULpSfxhzaiiSUY4Fi3JPn1wpt28NI=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
@@ -266,6 +282,8 @@ github.com/xtaci/tcpraw v1.2.25/go.mod h1:dKyZ2V75s0cZ7cbgJYdxPvms7af0joIeOyx1Gg
|
||||
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
|
||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68=
|
||||
github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
@@ -321,15 +339,19 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
|
||||
@@ -136,6 +136,9 @@ func (l *trafficLimiter) In(ctx context.Context, key string, opts ...limiter.Opt
|
||||
return nil
|
||||
|
||||
case limiter.ScopeClient:
|
||||
if lim, ok := l.inLimits.Get(key); ok && lim != nil {
|
||||
return lim.(traffic.Limiter)
|
||||
}
|
||||
return nil
|
||||
|
||||
case limiter.ScopeConn:
|
||||
@@ -215,6 +218,9 @@ func (l *trafficLimiter) Out(ctx context.Context, key string, opts ...limiter.Op
|
||||
return nil
|
||||
|
||||
case limiter.ScopeClient:
|
||||
if lim, ok := l.outLimits.Get(key); ok && lim != nil {
|
||||
return lim.(traffic.Limiter)
|
||||
}
|
||||
return nil
|
||||
|
||||
case limiter.ScopeConn:
|
||||
|
||||
@@ -43,8 +43,8 @@ func updateChain(req updateChainRequest) error {
|
||||
|
||||
name := strings.TrimSpace(req.Chain)
|
||||
|
||||
if !registry.ChainRegistry().IsRegistered(name) {
|
||||
return errors.New("chain " + name + " not found")
|
||||
if registry.ChainRegistry().IsRegistered(name) {
|
||||
registry.ChainRegistry().Unregister(name)
|
||||
}
|
||||
|
||||
req.Data.Name = name
|
||||
@@ -54,19 +54,22 @@ func updateChain(req updateChainRequest) error {
|
||||
return errors.New("create chain " + name + " failed: " + err.Error())
|
||||
}
|
||||
|
||||
registry.ChainRegistry().Unregister(name)
|
||||
|
||||
if err := registry.ChainRegistry().Register(name, v); err != nil {
|
||||
return errors.New("chain " + name + " already exists")
|
||||
}
|
||||
|
||||
config.OnUpdate(func(c *config.Config) error {
|
||||
found := false
|
||||
for i := range c.Chains {
|
||||
if c.Chains[i].Name == name {
|
||||
c.Chains[i] = &req.Data
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
c.Chains = append(c.Chains, &req.Data)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -77,10 +80,9 @@ func deleteChain(req deleteChainRequest) error {
|
||||
|
||||
name := strings.TrimSpace(req.Chain)
|
||||
|
||||
if !registry.ChainRegistry().IsRegistered(name) {
|
||||
return errors.New("chain " + name + " not found")
|
||||
if registry.ChainRegistry().IsRegistered(name) {
|
||||
registry.ChainRegistry().Unregister(name)
|
||||
}
|
||||
registry.ChainRegistry().Unregister(name)
|
||||
|
||||
config.OnUpdate(func(c *config.Config) error {
|
||||
chains := c.Chains
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
// configMutex 保护配置文件的并发写入
|
||||
var configMutex sync.Mutex
|
||||
|
||||
func saveConfig() {
|
||||
func saveConfig() error {
|
||||
configMutex.Lock()
|
||||
defer configMutex.Unlock()
|
||||
|
||||
@@ -18,14 +18,13 @@ func saveConfig() {
|
||||
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := config.Global().Write(f, "json"); err != nil {
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -37,27 +37,30 @@ func updateLimiter(req updateLimiterRequest) error {
|
||||
|
||||
name := strings.TrimSpace(req.Limiter)
|
||||
|
||||
if !registry.TrafficLimiterRegistry().IsRegistered(name) {
|
||||
return errors.New("limiter " + name + " not found")
|
||||
if registry.TrafficLimiterRegistry().IsRegistered(name) {
|
||||
registry.TrafficLimiterRegistry().Unregister(name)
|
||||
}
|
||||
|
||||
req.Data.Name = name
|
||||
|
||||
v := parser.ParseTrafficLimiter(&req.Data)
|
||||
|
||||
registry.TrafficLimiterRegistry().Unregister(name)
|
||||
|
||||
if err := registry.TrafficLimiterRegistry().Register(name, v); err != nil {
|
||||
return errors.New("limiter " + name + " already exists")
|
||||
}
|
||||
|
||||
config.OnUpdate(func(c *config.Config) error {
|
||||
found := false
|
||||
for i := range c.Limiters {
|
||||
if c.Limiters[i].Name == name {
|
||||
c.Limiters[i] = &req.Data
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
c.Limiters = append(c.Limiters, &req.Data)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -68,10 +71,9 @@ func deleteLimiter(req deleteLimiterRequest) error {
|
||||
|
||||
name := strings.TrimSpace(req.Limiter)
|
||||
|
||||
if !registry.TrafficLimiterRegistry().IsRegistered(name) {
|
||||
return errors.New("limiter " + name + " not found")
|
||||
if registry.TrafficLimiterRegistry().IsRegistered(name) {
|
||||
registry.TrafficLimiterRegistry().Unregister(name)
|
||||
}
|
||||
registry.TrafficLimiterRegistry().Unregister(name)
|
||||
|
||||
config.OnUpdate(func(c *config.Config) error {
|
||||
limiteres := c.Limiters
|
||||
|
||||
+41
-32
@@ -87,36 +87,32 @@ func updateServices(req updateServicesRequest) error {
|
||||
return errors.New("services list cannot be empty")
|
||||
}
|
||||
|
||||
// 第一阶段:验证所有服务存在
|
||||
for _, serviceConfig := range req.Data {
|
||||
name := strings.TrimSpace(serviceConfig.Name)
|
||||
// 第一阶段:验证所有服务名称有效性
|
||||
for i := range req.Data {
|
||||
name := strings.TrimSpace(req.Data[i].Name)
|
||||
if name == "" {
|
||||
return errors.New("service name is required")
|
||||
}
|
||||
serviceConfig.Name = name
|
||||
|
||||
old := registry.ServiceRegistry().Get(name)
|
||||
if old == nil {
|
||||
return errors.New("service " + name + " not found")
|
||||
}
|
||||
req.Data[i].Name = name
|
||||
}
|
||||
|
||||
// 第二阶段:按照原来的updateService逻辑,逐个更新服务
|
||||
for _, serviceConfig := range req.Data {
|
||||
name := strings.TrimSpace(serviceConfig.Name)
|
||||
serviceConfig.Name = name
|
||||
// 第二阶段:逐个更新服务(Upsert模式:存在则更新,不存在则创建)
|
||||
for i := range req.Data {
|
||||
serviceConfig := &req.Data[i]
|
||||
name := serviceConfig.Name
|
||||
|
||||
// 1. 获取旧服务
|
||||
old := registry.ServiceRegistry().Get(name)
|
||||
|
||||
// 2. 关闭旧服务
|
||||
old.Close()
|
||||
|
||||
// 3. 从注册表移除旧服务
|
||||
registry.ServiceRegistry().Unregister(name)
|
||||
// 2. 关闭旧服务 (如果存在)
|
||||
if old != nil {
|
||||
old.Close()
|
||||
// 3. 从注册表移除旧服务
|
||||
registry.ServiceRegistry().Unregister(name)
|
||||
}
|
||||
|
||||
// 4. 解析新服务配置
|
||||
svc, err := parser.ParseService(&serviceConfig)
|
||||
svc, err := parser.ParseService(serviceConfig)
|
||||
if err != nil {
|
||||
return errors.New("create service " + name + " failed: " + err.Error())
|
||||
}
|
||||
@@ -133,13 +129,20 @@ func updateServices(req updateServicesRequest) error {
|
||||
|
||||
// 第三阶段:更新配置
|
||||
config.OnUpdate(func(c *config.Config) error {
|
||||
for _, serviceConfig := range req.Data {
|
||||
for i := range c.Services {
|
||||
if c.Services[i].Name == serviceConfig.Name {
|
||||
c.Services[i] = &serviceConfig
|
||||
for i := range req.Data {
|
||||
// 创建副本以确保指针安全
|
||||
cfgCopy := req.Data[i]
|
||||
found := false
|
||||
for j := range c.Services {
|
||||
if c.Services[j].Name == cfgCopy.Name {
|
||||
c.Services[j] = &cfgCopy
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
c.Services = append(c.Services, &cfgCopy)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -158,22 +161,22 @@ func deleteServices(req deleteServicesRequest) error {
|
||||
name string
|
||||
service service.Service
|
||||
}
|
||||
var namesToRemove []string
|
||||
|
||||
for _, serviceName := range req.Services {
|
||||
name := strings.TrimSpace(serviceName)
|
||||
if name == "" {
|
||||
return errors.New("service name is required")
|
||||
}
|
||||
namesToRemove = append(namesToRemove, name)
|
||||
|
||||
svc := registry.ServiceRegistry().Get(name)
|
||||
if svc == nil {
|
||||
return errors.New("service " + name + " not found")
|
||||
if svc != nil {
|
||||
servicesToDelete = append(servicesToDelete, struct {
|
||||
name string
|
||||
service service.Service
|
||||
}{name, svc})
|
||||
}
|
||||
|
||||
servicesToDelete = append(servicesToDelete, struct {
|
||||
name string
|
||||
service service.Service
|
||||
}{name, svc})
|
||||
}
|
||||
|
||||
// 第二阶段:删除所有服务
|
||||
@@ -181,6 +184,12 @@ func deleteServices(req deleteServicesRequest) error {
|
||||
registry.ServiceRegistry().Unregister(std.name)
|
||||
std.service.Close()
|
||||
}
|
||||
// 确保所有请求删除的服务都从注册表中移除(即使之前未找到实例)
|
||||
for _, name := range namesToRemove {
|
||||
if registry.ServiceRegistry().IsRegistered(name) {
|
||||
registry.ServiceRegistry().Unregister(name)
|
||||
}
|
||||
}
|
||||
|
||||
// 第三阶段:更新配置
|
||||
config.OnUpdate(func(c *config.Config) error {
|
||||
@@ -188,8 +197,8 @@ func deleteServices(req deleteServicesRequest) error {
|
||||
c.Services = nil
|
||||
for _, s := range services {
|
||||
shouldDelete := false
|
||||
for _, std := range servicesToDelete {
|
||||
if s.Name == std.name {
|
||||
for _, name := range namesToRemove {
|
||||
if s.Name == name {
|
||||
shouldDelete = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -586,7 +586,16 @@ func (w *WebSocketReporter) routeCommand(cmd CommandMessage) {
|
||||
|
||||
// 只有状态变更命令才保存配置
|
||||
if needSaveConfig {
|
||||
saveConfig()
|
||||
if saveErr := saveConfig(); saveErr != nil {
|
||||
fmt.Printf("❌ 保存配置失败: %v\n", saveErr)
|
||||
if err == nil {
|
||||
err = fmt.Errorf("保存配置失败: %v", saveErr)
|
||||
} else {
|
||||
err = fmt.Errorf("%v; 保存配置失败: %v", err, saveErr)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("✅ 配置已保存到 gost.json")
|
||||
}
|
||||
}
|
||||
|
||||
// 发送响应
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ COUNTRY=$(curl -s https://ipinfo.io/country)
|
||||
maybe_proxy_url() {
|
||||
local url="$1"
|
||||
if [ "$COUNTRY" = "CN" ]; then
|
||||
echo "https://ghfast.top/${url}"
|
||||
echo "https://gcode.hostcentral.cc/${url}"
|
||||
else
|
||||
echo "$url"
|
||||
fi
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
site_name: FLVX 文档
|
||||
site_url: https://sagit-chu.github.io/flux-panel/
|
||||
site_description: FLVX官方文档
|
||||
site_author: Sagit-chu
|
||||
|
||||
theme:
|
||||
name: material
|
||||
language: zh
|
||||
palette:
|
||||
- scheme: default
|
||||
primary: indigo
|
||||
accent: indigo
|
||||
- scheme: slate
|
||||
primary: indigo
|
||||
accent: indigo
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: Switch to dark mode
|
||||
features:
|
||||
- navigation.tabs
|
||||
- navigation.sections
|
||||
- navigation.top
|
||||
- search.suggest
|
||||
- search.highlight
|
||||
- content.code.copy
|
||||
|
||||
docs_dir: doc
|
||||
|
||||
nav:
|
||||
- 首页: index.md
|
||||
- 安装部署: install.md
|
||||
- 使用指南: usage.md
|
||||
- 常见问题: faq.md
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- pymdownx.details
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
+1
-1
@@ -15,7 +15,7 @@ COUNTRY=$(curl -s https://ipinfo.io/country)
|
||||
maybe_proxy_url() {
|
||||
local url="$1"
|
||||
if [ "$COUNTRY" = "CN" ]; then
|
||||
echo "https://ghfast.top/${url}"
|
||||
echo "https://gcode.hostcentral.cc/${url}"
|
||||
else
|
||||
echo "$url"
|
||||
fi
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<factorypath>
|
||||
<factorypathentry kind="VARJAR" id="M2_REPO/org/projectlombok/lombok/1.18.30/lombok-1.18.30.jar" enabled="true" runInBatchMode="false"/>
|
||||
</factorypath>
|
||||
@@ -1,39 +0,0 @@
|
||||
# SPRINGBOOT BACKEND KNOWLEDGE BASE
|
||||
|
||||
**Generated:** Mon Feb 02 2026
|
||||
|
||||
## OVERVIEW
|
||||
Admin API for Flux Panel. Manages users, tunnels, nodes, forwards, quotas, and speed limits.
|
||||
**Stack:** Java 21, Spring Boot 2.7.18, SQLite, MyBatis Plus (+ join), FastJSON2.
|
||||
|
||||
## STRUCTURE
|
||||
```
|
||||
springboot-backend/
|
||||
├── src/main/java/com/admin/
|
||||
│ ├── controller/ # /api/v1/* endpoints
|
||||
│ ├── entity/ # DB models
|
||||
│ ├── mapper/ # MyBatis Plus mappers
|
||||
│ ├── service/ # Business logic
|
||||
│ ├── config/ # WebMvc/JWT/CORS/WebSocket config
|
||||
│ └── common/ # DTOs, auth, exception handling, utilities
|
||||
└── src/main/resources/
|
||||
├── application.yml # Config (DB_PATH/JWT_SECRET/LOG_DIR)
|
||||
├── mapper/ # XML mappers
|
||||
├── schema.sql # Schema
|
||||
└── data.sql # Seed data
|
||||
```
|
||||
|
||||
## CONVENTIONS
|
||||
- **DB**: SQLite URL is `jdbc:sqlite:${DB_PATH:/app/data/gost.db}` (`springboot-backend/src/main/resources/application.yml`).
|
||||
- **Auth**: JWT in `Authorization` header; enforced by `com.admin.common.interceptor.JwtInterceptor` for `/api/**` (with explicit excludes in `com.admin.config.WebMvcConfig`).
|
||||
- **Roles**: `@RequireRole` means admin-only (`role_id == 0`) via `com.admin.common.aop.RoleAspect`.
|
||||
- **Responses**: Controllers return `com.admin.common.lang.R` (`code == 0` success).
|
||||
- **CORS**: Allow-all origins; `Authorization` is exposed (`com.admin.config.WebMvcConfig`).
|
||||
|
||||
## COMMANDS
|
||||
```bash
|
||||
cd springboot-backend
|
||||
mvn clean package
|
||||
mvn test
|
||||
java -jar target/admin-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
@@ -1,27 +0,0 @@
|
||||
# 构建阶段
|
||||
FROM maven:3.9.6-eclipse-temurin-21 AS build
|
||||
WORKDIR /app
|
||||
COPY pom.xml .
|
||||
COPY src ./src
|
||||
RUN mvn clean package -DskipTests
|
||||
|
||||
# 运行阶段
|
||||
FROM eclipse-temurin:21-jre
|
||||
WORKDIR /app
|
||||
ENV JAVA_OPTS="-Xmx512m -Xms256m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai"
|
||||
ENV LANG=en_US.UTF-8
|
||||
ENV LANGUAGE=en_US:en
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
|
||||
RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
|
||||
&& sed -i 's/security.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends fontconfig fonts-dejavu sqlite3 \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=build /app/target/*.jar app.jar
|
||||
|
||||
EXPOSE 6365
|
||||
|
||||
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]
|
||||
@@ -1,180 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.admin</groupId>
|
||||
<artifactId>admin</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>admin</name>
|
||||
<description>admin</description>
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<spring-boot.version>2.7.18</spring-boot.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
</properties>
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.43</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.yulichang</groupId>
|
||||
<artifactId>mybatis-plus-join</artifactId>
|
||||
<version>1.3.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>1.2.70</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>3.4.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-generator</artifactId>
|
||||
<version>3.4.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.45.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.3.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.11</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.5.14</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.freemarker</groupId>
|
||||
<artifactId>freemarker</artifactId>
|
||||
<version>2.3.31</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- tianai-captcha Spring Boot Starter -->
|
||||
<dependency>
|
||||
<groupId>cloud.tianai.captcha</groupId>
|
||||
<artifactId>tianai-captcha-springboot-starter</artifactId>
|
||||
<version>1.5.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<configuration>
|
||||
<mainClass>com.admin.AdminApplication</mainClass>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>repackage</id>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -1,32 +0,0 @@
|
||||
# SPRINGBOOT BACKEND (com.admin) KNOWLEDGE BASE
|
||||
|
||||
## OVERVIEW
|
||||
Primary Java code for the admin API. Controllers expose `/api/v1/*` endpoints and return `R` response envelopes.
|
||||
|
||||
## STRUCTURE
|
||||
```
|
||||
springboot-backend/src/main/java/com/admin/
|
||||
├── controller/ # REST controllers (e.g., /api/v1/user)
|
||||
├── service/ # Business logic interfaces + impl/
|
||||
├── mapper/ # MyBatis Plus mappers
|
||||
├── entity/ # DB entities
|
||||
├── config/ # WebMvc/JWT/CORS/WebSocket config
|
||||
└── common/ # DTOs, auth, exception handling, utilities
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
| Task | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| User/login endpoints | `springboot-backend/src/main/java/com/admin/controller/UserController.java` | `/api/v1/user/*` |
|
||||
| Auth enforcement | `springboot-backend/src/main/java/com/admin/config/WebMvcConfig.java` | Intercepts `/api/**`, excludes login/config/captcha |
|
||||
| JWT validation | `springboot-backend/src/main/java/com/admin/common/interceptor/JwtInterceptor.java` | Requires `Authorization` header |
|
||||
| Admin-only ops | `springboot-backend/src/main/java/com/admin/common/annotation/RequireRole.java` | Enforced by `RoleAspect` |
|
||||
| Response envelope | `springboot-backend/src/main/java/com/admin/common/lang/R.java` | `code == 0` success |
|
||||
| Global error handling | `springboot-backend/src/main/java/com/admin/common/exception/GlobalExceptionHandler.java` | Maps exceptions -> `R.err(...)` |
|
||||
|
||||
## CONVENTIONS
|
||||
- Controllers are mostly `@PostMapping` (even for list/get/delete) and use `/api/v1/*` prefixes.
|
||||
- JWT is custom (no 3p lib) and includes `role_id` in payload (`springboot-backend/src/main/java/com/admin/common/utils/JwtUtil.java`).
|
||||
|
||||
## ANTI-PATTERNS
|
||||
- Do not change auth header format lightly: frontend expects `Authorization: <token>` (no `Bearer`).
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.admin;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
@MapperScan("com.admin.mapper")
|
||||
public class AdminApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AdminApplication.class, args);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.admin.common.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 权限控制注解
|
||||
* 用于标记需要管理员权限的方法(role_id = 0)
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface RequireRole {
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.admin.common.aop;
|
||||
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface LogAnnotation {}
|
||||
@@ -1,193 +0,0 @@
|
||||
package com.admin.common.aop;
|
||||
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.admin.common.utils.JwtUtil;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.admin.common.utils.HttpContextUtils;
|
||||
import com.admin.common.utils.IpUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.*;
|
||||
import org.aspectj.lang.reflect.CodeSignature;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Aspect
|
||||
@Slf4j
|
||||
public class LogAspect {
|
||||
|
||||
@Pointcut("@annotation(com.admin.common.aop.LogAnnotation)")
|
||||
public void pt() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回后通知(@AfterReturning):在某连接点(joinpoint)
|
||||
* 正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回
|
||||
* 方法执行完毕之后
|
||||
* 注意在这里不能使用ProceedingJoinPoint
|
||||
* 不然会报错ProceedingJoinPoint is only supported for around advice
|
||||
* crmAspect()指向需要控制的方法
|
||||
* returning 注解返回值
|
||||
*
|
||||
* @param joinPoint
|
||||
* @param returnValue 返回值
|
||||
* @throws Exception
|
||||
*/
|
||||
@AfterReturning(value = "pt()", returning = "returnValue")
|
||||
public void log(JoinPoint joinPoint, Object returnValue) throws Throwable {
|
||||
// 获取请求信息
|
||||
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
|
||||
|
||||
// 获取请求方法类型(POST/GET等)
|
||||
String requestMethod = request.getMethod();
|
||||
|
||||
// 获取用户ID
|
||||
String authorization = request.getHeader("Authorization") + "";
|
||||
Object user_id = "未登录"; // 请求用户的id
|
||||
if (!authorization.equals("null")) {
|
||||
user_id = JwtUtil.getUserIdFromToken(authorization);
|
||||
}
|
||||
|
||||
// 获取请求IP
|
||||
String ipAddr = IpUtils.getIpAddr(request);
|
||||
|
||||
// 获取方法签名信息
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
|
||||
// 获取控制器方法名
|
||||
String className = joinPoint.getTarget().getClass().getName();
|
||||
String methodName = signature.getName();
|
||||
String controllerMethod = className + "." + methodName;
|
||||
|
||||
|
||||
// 获取请求参数
|
||||
String requestParams = getRequestParams(joinPoint);
|
||||
|
||||
// 获取返回参数
|
||||
String responseParams = returnValue != null ? JSON.toJSONString(returnValue) : "无返回值";
|
||||
|
||||
// 合并为一条完整的日志信息
|
||||
String logMessage = String.format(
|
||||
"【请求日志】用户ID:[%s], IP地址:[%s], 请求方式:[%s], 控制器方法:[%s], 请求参数:[%s], 返回参数:[%s]", user_id, ipAddr, requestMethod, controllerMethod, requestParams, responseParams
|
||||
);
|
||||
|
||||
// 打印单条完整日志
|
||||
log.info(logMessage);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 抛出异常后通知(@AfterThrowing):方法抛出异常退出时执行的通知
|
||||
* 注意在这里不能使用ProceedingJoinPoint
|
||||
* 不然会报错ProceedingJoinPoint is only supported for around advice
|
||||
* throwing注解为错误信息
|
||||
*
|
||||
* @param joinPoint
|
||||
* @param ex
|
||||
*/
|
||||
@AfterThrowing(value = "pt()", throwing = "ex")
|
||||
public void recordLog(JoinPoint joinPoint, Exception ex) {
|
||||
try {
|
||||
// 获取请求信息
|
||||
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
|
||||
|
||||
// 获取请求方法类型(POST/GET等)
|
||||
String requestMethod = request.getMethod();
|
||||
|
||||
// 获取用户ID
|
||||
String authorization = request.getHeader("Authorization") + "";
|
||||
Object user_id = "未登录"; // 请求用户的id
|
||||
if (!authorization.equals("null")) {
|
||||
user_id = JwtUtil.getUserIdFromToken(authorization);
|
||||
}
|
||||
|
||||
// 获取请求IP
|
||||
String ipAddr = IpUtils.getIpAddr(request);
|
||||
|
||||
// 获取方法签名信息
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
|
||||
// 获取控制器方法名
|
||||
String className = joinPoint.getTarget().getClass().getName();
|
||||
String methodName = signature.getName();
|
||||
String controllerMethod = className + "." + methodName;
|
||||
|
||||
|
||||
|
||||
// 获取请求参数
|
||||
String requestParams = getRequestParams(joinPoint);
|
||||
|
||||
// 获取异常信息
|
||||
String exceptionMsg = ex != null ? ex.getMessage() : "未知异常";
|
||||
|
||||
// 合并为一条完整的异常日志信息
|
||||
String errorMessage = String.format(
|
||||
"【异常日志】用户ID:[%s], IP地址:[%s], 请求方式:[%s], 控制器方法:[%s], 请求参数:[%s], 异常信息:[%s]", user_id, ipAddr, requestMethod, controllerMethod, requestParams, exceptionMsg
|
||||
);
|
||||
|
||||
// 打印单条完整异常日志
|
||||
log.info(errorMessage, ex);
|
||||
} catch (Exception e) {
|
||||
log.info("记录异常日志时出错: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求参数
|
||||
*/
|
||||
private String getRequestParams(JoinPoint joinPoint) {
|
||||
try {
|
||||
Object[] args = joinPoint.getArgs();
|
||||
if (args.length == 0) {
|
||||
return "无参数";
|
||||
} else if (args[0] != null && args[0].toString().contains("SecurityContextHolderAwareRequestWrapper")) {
|
||||
return JSON.toJSONString(Arrays.toString(ArrayUtil.remove(args, 0)));
|
||||
} else {
|
||||
// 检查是否只有一个参数且已经是JSON字符串格式
|
||||
if (args.length == 1 && args[0] != null) {
|
||||
// 如果参数本身就是字符串且是JSON格式,直接返回
|
||||
if (args[0] instanceof String && ((String) args[0]).startsWith("{") && ((String) args[0]).endsWith("}")) {
|
||||
return (String) args[0];
|
||||
}
|
||||
|
||||
// 如果参数是普通对象,直接序列化
|
||||
try {
|
||||
return JSON.toJSONString(args[0]);
|
||||
} catch (Exception e) {
|
||||
// 如果序列化失败,再尝试使用参数名映射
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
String[] names = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
|
||||
if (names != null) {
|
||||
map.put(names[0], args[0]);
|
||||
return JSON.toJSONString(map);
|
||||
}
|
||||
return JSON.toJSONString(args[0]);
|
||||
}
|
||||
} else {
|
||||
// 多个参数时,使用参数名映射
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
String[] names = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
|
||||
if (names != null) {
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
map.put(names[i], args[i]);
|
||||
}
|
||||
}
|
||||
return JSON.toJSONString(map);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return "获取参数失败: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.admin.common.aop;
|
||||
|
||||
import com.admin.common.annotation.RequireRole;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.common.utils.JwtUtil;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 权限控制切面
|
||||
* 处理 @RequireRole 注解,检查管理员权限(role_id = 0)
|
||||
* 注意:JWT拦截器已经验证了token的有效性,这里只需要检查权限
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class RoleAspect {
|
||||
|
||||
@Around("@annotation(requireRole)")
|
||||
public Object checkRole(ProceedingJoinPoint joinPoint, RequireRole requireRole) throws Throwable {
|
||||
// 获取当前请求
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return R.err(500, "无法获取请求信息");
|
||||
}
|
||||
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
String token = request.getHeader("Authorization");
|
||||
|
||||
// JWT拦截器已经验证过token存在且有效,这里直接获取role_id
|
||||
Integer roleId = JwtUtil.getRoleIdFromToken(token);
|
||||
if (roleId == null) {
|
||||
return R.err(401, "无法获取用户权限信息");
|
||||
}
|
||||
|
||||
// 检查是否为管理员(role_id = 0)
|
||||
if (roleId != 0) {
|
||||
return R.err(403, "权限不足,仅管理员可操作");
|
||||
}
|
||||
|
||||
// 权限检查通过,执行原方法
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class BatchChangeTunnelDto {
|
||||
|
||||
@NotEmpty(message = "转发ID列表不能为空")
|
||||
private List<Long> forwardIds;
|
||||
|
||||
@NotNull(message = "目标隧道ID不能为空")
|
||||
private Long targetTunnelId;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class BatchDeleteDto {
|
||||
|
||||
@NotEmpty(message = "ID列表不能为空")
|
||||
private List<Long> ids;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@Data
|
||||
public class BatchOperationResultDto {
|
||||
|
||||
private int successCount;
|
||||
private int failCount;
|
||||
private List<FailedItem> failedItems = new ArrayList<>();
|
||||
|
||||
@Data
|
||||
public static class FailedItem {
|
||||
private Long id;
|
||||
private String reason;
|
||||
|
||||
public FailedItem() {}
|
||||
|
||||
public FailedItem(Long id, String reason) {
|
||||
this.id = id;
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
public void addFailedItem(Long id, String reason) {
|
||||
this.failedItems.add(new FailedItem(id, reason));
|
||||
this.failCount++;
|
||||
}
|
||||
|
||||
public void incrementSuccess() {
|
||||
this.successCount++;
|
||||
}
|
||||
|
||||
public boolean isAllSuccess() {
|
||||
return failCount == 0;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class BatchRedeployDto {
|
||||
|
||||
@NotEmpty(message = "ID列表不能为空")
|
||||
private List<Long> ids;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 验证码验证请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class CaptchaVerifyDto {
|
||||
|
||||
private String id;
|
||||
|
||||
private ImageCaptchaTrack data;
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ChainNodesItems {
|
||||
|
||||
private String mode;
|
||||
|
||||
private List<Integer> nodeIds;
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
public class ChangePasswordDto {
|
||||
|
||||
@NotBlank(message = "新用户名不能为空")
|
||||
private String newUsername;
|
||||
|
||||
@NotBlank(message = "当前密码不能为空")
|
||||
private String currentPassword;
|
||||
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
private String newPassword;
|
||||
|
||||
@NotBlank(message = "确认密码不能为空")
|
||||
private String confirmPassword;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ConfigItem {
|
||||
private String name;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DiagnosisResult {
|
||||
private Long nodeId;
|
||||
private String nodeName;
|
||||
private String targetIp;
|
||||
private Integer targetPort;
|
||||
private String description;
|
||||
private boolean success;
|
||||
private String message;
|
||||
private double averageTime;
|
||||
private double packetLoss;
|
||||
private long timestamp;
|
||||
private Integer fromChainType;
|
||||
private Integer fromInx;
|
||||
private Integer toChainType;
|
||||
private Integer toInx;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class FlowDto {
|
||||
// 转发id_类型
|
||||
private String n;
|
||||
|
||||
// 上传流量
|
||||
private Long u;
|
||||
|
||||
// 下载流量
|
||||
private Long d;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.Max;
|
||||
|
||||
@Data
|
||||
public class ForwardDto {
|
||||
|
||||
@NotBlank(message = "转发名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Integer tunnelId;
|
||||
|
||||
@NotBlank(message = "远程地址不能为空")
|
||||
private String remoteAddr;
|
||||
|
||||
private String strategy;
|
||||
|
||||
private Integer inPort;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ForwardPortDto {
|
||||
private Integer node_id;
|
||||
private Integer port;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.Max;
|
||||
|
||||
@Data
|
||||
public class ForwardUpdateDto {
|
||||
|
||||
@NotNull(message = "ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
private Integer userId;
|
||||
|
||||
@NotBlank(message = "转发名称不能为空")
|
||||
private String name;
|
||||
|
||||
private Integer tunnelId;
|
||||
|
||||
@NotBlank(message = "远程地址不能为空")
|
||||
private String remoteAddr;
|
||||
|
||||
private String strategy;
|
||||
|
||||
private Integer inPort;
|
||||
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 转发信息及关联隧道信息DTO
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-06-03
|
||||
*/
|
||||
@Data
|
||||
public class ForwardWithTunnelDto {
|
||||
|
||||
/**
|
||||
* 转发记录ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
|
||||
/**
|
||||
* 转发名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
|
||||
/**
|
||||
* 入口IP
|
||||
*/
|
||||
private String inIp;
|
||||
|
||||
/**
|
||||
* 入口端口
|
||||
*/
|
||||
private Integer inPort;
|
||||
|
||||
/**
|
||||
* 远程地址
|
||||
*/
|
||||
private String remoteAddr;
|
||||
|
||||
/**
|
||||
* 转发状态
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Long createdTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Long updatedTime;
|
||||
|
||||
// 以下为隧道相关字段
|
||||
|
||||
/**
|
||||
* 隧道名称
|
||||
*/
|
||||
private String tunnelName;
|
||||
|
||||
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Integer userId;
|
||||
/**
|
||||
* 隧道ID
|
||||
*/
|
||||
private Integer tunnelId;
|
||||
|
||||
/**
|
||||
* 入站流量(字节)
|
||||
*/
|
||||
private Long inFlow;
|
||||
|
||||
/**
|
||||
* 出站流量(字节)
|
||||
*/
|
||||
private Long outFlow;
|
||||
|
||||
private String strategy;
|
||||
|
||||
private Integer inx;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class GostConfigDto {
|
||||
|
||||
private List<ConfigItem> limiters;
|
||||
|
||||
private List<ConfigItem> chains;
|
||||
|
||||
private List<ConfigItem> services;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class GostDto {
|
||||
private Integer code;
|
||||
|
||||
private String msg;
|
||||
|
||||
private Object data; // 添加数据字段,用于存储响应的详细数据
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
public class GroupCreateDto {
|
||||
|
||||
@NotBlank(message = "分组名称不能为空")
|
||||
private String name;
|
||||
|
||||
private Integer status;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
public class GroupPermissionAssignDto {
|
||||
|
||||
@NotNull(message = "用户分组ID不能为空")
|
||||
private Long userGroupId;
|
||||
|
||||
@NotNull(message = "隧道分组ID不能为空")
|
||||
private Long tunnelGroupId;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class GroupPermissionDetailDto {
|
||||
private Long id;
|
||||
private Long userGroupId;
|
||||
private String userGroupName;
|
||||
private Long tunnelGroupId;
|
||||
private String tunnelGroupName;
|
||||
private Long createdTime;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
public class GroupUpdateDto {
|
||||
|
||||
@NotNull(message = "分组ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "分组名称不能为空")
|
||||
private String name;
|
||||
|
||||
private Integer status;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
public class LoginDto {
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
private String captchaId;
|
||||
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
@Data
|
||||
public class NodeDto {
|
||||
|
||||
@NotBlank(message = "节点名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "服务器ip不能为空")
|
||||
private String serverIp;
|
||||
|
||||
private String serverIpV4;
|
||||
|
||||
private String serverIpV6;
|
||||
|
||||
@NotBlank(message = "可用端口不能为空")
|
||||
private String port;
|
||||
|
||||
private String interfaceName;
|
||||
|
||||
private String tcpListenAddr = "0.0.0.0";
|
||||
|
||||
private String udpListenAddr = "0.0.0.0";
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
public class NodeUpdateDto {
|
||||
|
||||
@NotNull(message = "节点ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "节点名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "服务器ip不能为空")
|
||||
private String serverIp;
|
||||
|
||||
private String serverIpV4;
|
||||
|
||||
private String serverIpV6;
|
||||
|
||||
@NotBlank(message = "可用port不能为空")
|
||||
private String port;
|
||||
|
||||
private String interfaceName;
|
||||
private Integer http;
|
||||
private Integer tls;
|
||||
private Integer socks;
|
||||
|
||||
private String tcpListenAddr = "0.0.0.0";
|
||||
|
||||
private String udpListenAddr = "0.0.0.0";
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
public class ResetFlowDto {
|
||||
|
||||
@NotNull(message = "重置账号id不能为空")
|
||||
private Integer id;
|
||||
|
||||
@NotNull(message = "重置类型不能为空")
|
||||
private Integer type;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
@Data
|
||||
public class SpeedLimitDto {
|
||||
|
||||
@NotBlank(message = "限速规则名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "速度限制不能为空")
|
||||
@Min(value = 1, message = "速度限制必须大于0")
|
||||
private Integer speed;
|
||||
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Long tunnelId;
|
||||
|
||||
@NotBlank(message = "隧道名称不能为空")
|
||||
private String tunnelName;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
@Data
|
||||
public class SpeedLimitUpdateDto {
|
||||
|
||||
@NotNull(message = "ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "限速规则名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "速度限制不能为空")
|
||||
@Min(value = 1, message = "速度限制必须大于0")
|
||||
private Integer speed;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 系统信息DTO
|
||||
* 对应Go客户端上报的系统信息结构
|
||||
*/
|
||||
@Data
|
||||
public class SystemInfoDto {
|
||||
|
||||
/**
|
||||
* 主机IP地址
|
||||
*/
|
||||
@JsonProperty("host_ip")
|
||||
private String hostIp;
|
||||
|
||||
/**
|
||||
* 开机时间(秒)
|
||||
*/
|
||||
@JsonProperty("uptime")
|
||||
private Long uptime;
|
||||
|
||||
/**
|
||||
* 接收字节数
|
||||
*/
|
||||
@JsonProperty("bytes_received")
|
||||
private Long bytesReceived;
|
||||
|
||||
/**
|
||||
* 发送字节数
|
||||
*/
|
||||
@JsonProperty("bytes_transmitted")
|
||||
private Long bytesTransmitted;
|
||||
|
||||
/**
|
||||
* CPU使用率(百分比)
|
||||
*/
|
||||
@JsonProperty("cpu_usage")
|
||||
private Double cpuUsage;
|
||||
|
||||
/**
|
||||
* 内存使用率(百分比)
|
||||
*/
|
||||
@JsonProperty("memory_usage")
|
||||
private Double memoryUsage;
|
||||
|
||||
/**
|
||||
* 上报时间戳
|
||||
*/
|
||||
private Long timestamp;
|
||||
|
||||
public SystemInfoDto() {
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.admin.entity.ChainTunnel;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 隧道详情DTO - 包含节点配置信息
|
||||
*/
|
||||
@Data
|
||||
public class TunnelDetailDto {
|
||||
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* Display/order index for drag-sorting in admin UI.
|
||||
* Lower value appears first.
|
||||
*/
|
||||
private Integer inx;
|
||||
|
||||
private String name;
|
||||
|
||||
private Integer type; // 1: 端口转发, 2: 隧道转发
|
||||
|
||||
private Integer flow; // 1: 单向, 2: 双向
|
||||
|
||||
private BigDecimal trafficRatio;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private Long createdTime;
|
||||
|
||||
private Long updatedTime;
|
||||
|
||||
private String inIp;
|
||||
|
||||
// 入口节点列表
|
||||
private List<ChainTunnel> inNodeId = new ArrayList<>();
|
||||
|
||||
// 转发链节点列表(二维数组结构)
|
||||
private List<List<ChainTunnel>> chainNodes = new ArrayList<>();
|
||||
|
||||
// 出口节点列表
|
||||
private List<ChainTunnel> outNodeId = new ArrayList<>();
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.admin.entity.ChainTunnel;
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.DecimalMax;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class TunnelDto {
|
||||
|
||||
@NotBlank(message = "隧道名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "入口节点不能为空")
|
||||
private List<ChainTunnel> inNodeId;
|
||||
|
||||
private List<List<ChainTunnel>> chainNodes;
|
||||
|
||||
private List<ChainTunnel> outNodeId;
|
||||
|
||||
private String inIp;
|
||||
|
||||
@NotNull(message = "隧道类型不能为空")
|
||||
private Integer type;
|
||||
|
||||
@NotNull(message = "流量计算类型不能为空")
|
||||
private Integer flow;
|
||||
|
||||
@DecimalMin(value = "0.0", inclusive = false, message = "流量倍率必须大于0.0")
|
||||
@DecimalMax(value = "100.0", message = "流量倍率不能大于100.0")
|
||||
private BigDecimal trafficRatio;
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class TunnelGroupAssignTunnelsDto {
|
||||
|
||||
@NotNull(message = "隧道分组ID不能为空")
|
||||
private Long groupId;
|
||||
|
||||
@NotNull(message = "隧道列表不能为空")
|
||||
private List<Long> tunnelIds;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class TunnelGroupDetailDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private Integer status;
|
||||
private Long createdTime;
|
||||
private Long updatedTime;
|
||||
private List<Long> tunnelIds = new ArrayList<>();
|
||||
private List<String> tunnelNames = new ArrayList<>();
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.admin.entity.ChainTunnel;
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.DecimalMax;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class TunnelUpdateDto {
|
||||
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "隧道名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "流量计算类型不能为空")
|
||||
private Integer flow;
|
||||
|
||||
private String inIp;
|
||||
|
||||
@DecimalMin(value = "0.0", inclusive = false, message = "流量倍率必须大于0.0")
|
||||
@DecimalMax(value = "100.0", message = "流量倍率不能大于100.0")
|
||||
private BigDecimal trafficRatio;
|
||||
|
||||
// 入口节点配置(可选,为空时不更新节点配置)
|
||||
private List<ChainTunnel> inNodeId;
|
||||
|
||||
// 转发链节点配置(二维数组,每一跳可有多个节点)
|
||||
private List<List<ChainTunnel>> chainNodes;
|
||||
|
||||
// 出口节点配置
|
||||
private List<ChainTunnel> outNodeId;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
@Data
|
||||
public class UserDto {
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String user;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String pwd;
|
||||
|
||||
@NotNull(message = "流量不能为空")
|
||||
@Min(value = 0, message = "流量不能小于0")
|
||||
private Long flow;
|
||||
|
||||
@NotNull(message = "转发数量不能为空")
|
||||
@Min(value = 0, message = "转发数量不能小于0")
|
||||
private Integer num;
|
||||
|
||||
@NotNull(message = "过期时间不能为空")
|
||||
private Long expTime;
|
||||
|
||||
@NotNull(message = "流量重置时间不能为空")
|
||||
private Long flowResetTime;
|
||||
|
||||
private Integer status;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class UserGroupAssignUsersDto {
|
||||
|
||||
@NotNull(message = "用户分组ID不能为空")
|
||||
private Long groupId;
|
||||
|
||||
@NotNull(message = "用户列表不能为空")
|
||||
private List<Long> userIds;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class UserGroupDetailDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private Integer status;
|
||||
private Long createdTime;
|
||||
private Long updatedTime;
|
||||
private List<Long> userIds = new ArrayList<>();
|
||||
private List<String> userNames = new ArrayList<>();
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UserInfo {
|
||||
private final Integer userId;
|
||||
private final Integer roleId;
|
||||
private final String userName;
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.admin.entity.StatisticsFlow;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户套餐信息DTO
|
||||
*/
|
||||
@Data
|
||||
public class UserPackageDto {
|
||||
|
||||
/**
|
||||
* 用户基本信息
|
||||
*/
|
||||
private UserInfoDto userInfo;
|
||||
|
||||
/**
|
||||
* 用户隧道权限列表
|
||||
*/
|
||||
private List<UserTunnelDetailDto> tunnelPermissions;
|
||||
|
||||
/**
|
||||
* 用户转发列表
|
||||
*/
|
||||
private List<UserForwardDetailDto> forwards;
|
||||
|
||||
/**
|
||||
* 用户转发列表
|
||||
*/
|
||||
private List<StatisticsFlow> statisticsFlows;
|
||||
|
||||
/**
|
||||
* 用户基本信息
|
||||
*/
|
||||
@Data
|
||||
public static class UserInfoDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String user;
|
||||
private Integer status;
|
||||
private Long flow; // 总流量配额(GB)
|
||||
private Long inFlow; // 已用入站流量(字节)
|
||||
private Long outFlow; // 已用出站流量(字节)
|
||||
private Integer num; // 转发数量配额
|
||||
private Long expTime; // 过期时间
|
||||
private Long flowResetTime; // 流量重置时间
|
||||
private Long createdTime;
|
||||
private Long updatedTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户隧道权限详情
|
||||
*/
|
||||
@Data
|
||||
public static class UserTunnelDetailDto {
|
||||
private Integer id;
|
||||
private Integer userId;
|
||||
private Integer tunnelId;
|
||||
private String tunnelName;
|
||||
private Integer tunnelFlow; // 隧道流量计算类型(1-单向,2-双向)
|
||||
private Long flow; // 隧道流量配额(GB)
|
||||
private Long inFlow; // 隧道已用入站流量(字节)
|
||||
private Long outFlow; // 隧道已用出站流量(字节)
|
||||
private Integer num; // 隧道转发数量配额
|
||||
private Long flowResetTime; // 流量重置时间
|
||||
private Long expTime; // 隧道权限过期时间
|
||||
private Integer speedId;
|
||||
private String speedLimitName;
|
||||
private Integer speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户转发详情
|
||||
*/
|
||||
@Data
|
||||
public static class UserForwardDetailDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private Integer tunnelId;
|
||||
private String tunnelName;
|
||||
private String inIp;
|
||||
private Integer inPort;
|
||||
private String remoteAddr;
|
||||
private Long inFlow; // 转发入站流量(字节)
|
||||
private Long outFlow; // 转发出站流量(字节)
|
||||
private Integer status;
|
||||
private Long createdTime;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.admin.entity.UserTunnel;
|
||||
import com.admin.service.impl.ForwardServiceImpl;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UserPermissionResult {
|
||||
public boolean hasError;
|
||||
private String errorMessage;
|
||||
private Integer limiter;
|
||||
private UserTunnel userTunnel;
|
||||
|
||||
public static UserPermissionResult success(Integer limiter, UserTunnel userTunnel) {
|
||||
UserPermissionResult result = new UserPermissionResult();
|
||||
result.setLimiter(limiter);
|
||||
result.setUserTunnel(userTunnel);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static UserPermissionResult error(String errorMessage) {
|
||||
UserPermissionResult result = new UserPermissionResult();
|
||||
result.setErrorMessage(errorMessage);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class UserTunnelBatchAssignDto {
|
||||
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
private Integer userId;
|
||||
|
||||
@Valid
|
||||
@NotEmpty(message = "隧道列表不能为空")
|
||||
private List<TunnelAssignItem> tunnels;
|
||||
|
||||
@Data
|
||||
public static class TunnelAssignItem {
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Integer tunnelId;
|
||||
|
||||
private Integer speedId;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
@Data
|
||||
public class UserTunnelDto {
|
||||
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
private Integer userId;
|
||||
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Integer tunnelId;
|
||||
|
||||
@Min(value = 0, message = "流量限制不能小于0")
|
||||
private Long flow;
|
||||
|
||||
@Min(value = 0, message = "转发数量不能小于0")
|
||||
private Integer num;
|
||||
|
||||
private Long flowResetTime;
|
||||
|
||||
private Long expTime;
|
||||
|
||||
private Integer speedId;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
public class UserTunnelQueryDto {
|
||||
|
||||
@NotNull
|
||||
private Integer userId;
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
@Data
|
||||
public class UserTunnelUpdateDto {
|
||||
|
||||
@NotNull(message = "用户隧道权限ID不能为空")
|
||||
private Integer id;
|
||||
|
||||
@NotNull(message = "流量限制不能为空")
|
||||
@Min(value = 0, message = "流量限制不能小于0")
|
||||
private Long flow;
|
||||
|
||||
@NotNull(message = "转发数量不能为空")
|
||||
@Min(value = 0, message = "转发数量不能小于0")
|
||||
private Integer num;
|
||||
|
||||
/**
|
||||
* 流量重置时间(时间戳)
|
||||
*/
|
||||
@NotNull(message = "流量重置时间不能为空")
|
||||
private Long flowResetTime;
|
||||
|
||||
/**
|
||||
* 到期时间(时间戳)
|
||||
*/
|
||||
@NotNull(message = "到期时间不能为空")
|
||||
private Long expTime;
|
||||
|
||||
@NotNull(message = "状态必选")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 限速规则ID(可选,null表示不限速)
|
||||
*/
|
||||
private Integer speedId;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 用户隧道权限及隧道详细信息DTO
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-06-03
|
||||
*/
|
||||
@Data
|
||||
public class UserTunnelWithDetailDto {
|
||||
|
||||
/**
|
||||
* 用户隧道权限记录ID
|
||||
*/
|
||||
private Integer id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Integer userId;
|
||||
|
||||
/**
|
||||
* 隧道ID
|
||||
*/
|
||||
private Integer tunnelId;
|
||||
|
||||
/**
|
||||
* 流量限制
|
||||
*/
|
||||
private Integer flow;
|
||||
|
||||
/**
|
||||
* 转发数量限制
|
||||
*/
|
||||
private Integer num;
|
||||
|
||||
/**
|
||||
* 流量重置时间(时间戳)
|
||||
*/
|
||||
private Long flowResetTime;
|
||||
|
||||
/**
|
||||
* 到期时间(时间戳)
|
||||
*/
|
||||
private Long expTime;
|
||||
|
||||
/**
|
||||
* 限速规则ID
|
||||
*/
|
||||
private Integer speedId;
|
||||
|
||||
/**
|
||||
* 限速规则名称
|
||||
*/
|
||||
private String speedLimitName;
|
||||
|
||||
/**
|
||||
* 限速值
|
||||
*/
|
||||
private Integer speed;
|
||||
|
||||
/**
|
||||
* 隧道名称
|
||||
*/
|
||||
private String tunnelName;
|
||||
|
||||
/**
|
||||
* 隧道流量计算类型(1-单向,2-双向)
|
||||
*/
|
||||
private Integer tunnelFlow;
|
||||
|
||||
/**
|
||||
* 入站流量(字节)
|
||||
*/
|
||||
private Long inFlow;
|
||||
|
||||
/**
|
||||
* 出站流量(字节)
|
||||
*/
|
||||
private Long outFlow;
|
||||
|
||||
private Integer status;
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
@Data
|
||||
public class UserUpdateDto {
|
||||
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String user;
|
||||
|
||||
private String pwd; // 更新时密码可选
|
||||
|
||||
@NotNull(message = "流量不能为空")
|
||||
@Min(value = 0, message = "流量不能小于0")
|
||||
private Long flow;
|
||||
|
||||
@NotNull(message = "转发数量不能为空")
|
||||
@Min(value = 0, message = "转发数量不能小于0")
|
||||
private Integer num;
|
||||
|
||||
@NotNull(message = "过期时间不能为空")
|
||||
private Long expTime;
|
||||
|
||||
@NotNull(message = "流量重置时间不能为空")
|
||||
private Long flowResetTime;
|
||||
|
||||
private Integer status;
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
package com.admin.common.exception;
|
||||
|
||||
import com.admin.common.lang.R;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.catalina.connector.ClientAbortException;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.ObjectError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
//
|
||||
|
||||
// 实体校验异常捕获
|
||||
//@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
@ExceptionHandler(value = MethodArgumentNotValidException.class)
|
||||
public R MethodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||
BindingResult result = e.getBindingResult();
|
||||
ObjectError objectError = result.getAllErrors().stream().findFirst().get();
|
||||
log.info("实体校验异常:----------------{}", objectError.getDefaultMessage());
|
||||
return R.err(500, objectError.getDefaultMessage());
|
||||
}
|
||||
|
||||
// 未授权异常捕获
|
||||
@ExceptionHandler(value = UnauthorizedException.class)
|
||||
public R handleUnauthorizedException(UnauthorizedException e) {
|
||||
log.info("未授权异常:----------------{}", e.getMessage());
|
||||
return R.err(401, e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = Exception.class)
|
||||
public R Exception(Exception e){
|
||||
log.info("异常:----------------{}", e.getMessage());
|
||||
return R.err(-2, e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.admin.common.exception;
|
||||
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.web.client.ResponseErrorHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class HttpErrorHandler implements ResponseErrorHandler {
|
||||
@Override
|
||||
public boolean hasError(ClientHttpResponse response) throws IOException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(ClientHttpResponse clientHttpResponse) throws IOException {
|
||||
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package com.admin.common.exception;
|
||||
|
||||
/**
|
||||
* 未授权异常类
|
||||
*/
|
||||
public class UnauthorizedException extends RuntimeException {
|
||||
|
||||
public UnauthorizedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UnauthorizedException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.admin.common.interceptor;
|
||||
|
||||
|
||||
import com.admin.common.exception.UnauthorizedException;
|
||||
import com.admin.common.utils.JwtUtil;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
|
||||
/**
|
||||
* JWT拦截器,验证用户是否登录
|
||||
*/
|
||||
public class JwtInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
String token = request.getHeader("Authorization");
|
||||
|
||||
if (!StringUtils.hasText(token)) {
|
||||
throw new UnauthorizedException("未登录或token已过期");
|
||||
}
|
||||
|
||||
|
||||
if (!JwtUtil.validateToken(token)) {
|
||||
throw new UnauthorizedException("无效的token或token已过期");
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.admin.common.lang;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class R {
|
||||
|
||||
private int code = 0;
|
||||
private String msg = "操作成功";
|
||||
private long ts = System.currentTimeMillis();
|
||||
private Object data;
|
||||
|
||||
|
||||
|
||||
public static R ok(Object data){
|
||||
R m = new R();
|
||||
m.setData(data);
|
||||
return m;
|
||||
}
|
||||
|
||||
public static R ok(){
|
||||
return new R();
|
||||
}
|
||||
|
||||
public static R err(int code, String msg){
|
||||
R m = new R();
|
||||
m.setCode(code);
|
||||
m.setMsg(msg);
|
||||
return m;
|
||||
}
|
||||
|
||||
public static R err(String msg){
|
||||
R m = new R();
|
||||
m.setCode(-1);
|
||||
m.setMsg(msg);
|
||||
return m;
|
||||
}
|
||||
|
||||
public static R err(){
|
||||
R m = new R();
|
||||
m.setCode(-1);
|
||||
m.setMsg("请求失败");
|
||||
return m;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
package com.admin.common.migration;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Lightweight SQLite schema migration.
|
||||
*
|
||||
* Spring Boot SQL init uses CREATE TABLE IF NOT EXISTS, so existing installations
|
||||
* won't automatically receive new columns. This runner adds missing columns in-place.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class SqliteSchemaMigration implements ApplicationRunner {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public SqliteSchemaMigration(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
ensureColumn("node", "inx", "INTEGER NOT NULL DEFAULT 0");
|
||||
ensureColumn("tunnel", "inx", "INTEGER NOT NULL DEFAULT 0");
|
||||
ensureTable("CREATE TABLE IF NOT EXISTS tunnel_group (id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(100) NOT NULL, created_time INTEGER NOT NULL, updated_time INTEGER NOT NULL, status INTEGER NOT NULL)");
|
||||
ensureTable("CREATE TABLE IF NOT EXISTS user_group (id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(100) NOT NULL, created_time INTEGER NOT NULL, updated_time INTEGER NOT NULL, status INTEGER NOT NULL)");
|
||||
ensureTable("CREATE TABLE IF NOT EXISTS tunnel_group_tunnel (id INTEGER PRIMARY KEY AUTOINCREMENT, tunnel_group_id INTEGER NOT NULL, tunnel_id INTEGER NOT NULL, created_time INTEGER NOT NULL)");
|
||||
ensureTable("CREATE TABLE IF NOT EXISTS user_group_user (id INTEGER PRIMARY KEY AUTOINCREMENT, user_group_id INTEGER NOT NULL, user_id INTEGER NOT NULL, created_time INTEGER NOT NULL)");
|
||||
ensureTable("CREATE TABLE IF NOT EXISTS group_permission (id INTEGER PRIMARY KEY AUTOINCREMENT, user_group_id INTEGER NOT NULL, tunnel_group_id INTEGER NOT NULL, created_time INTEGER NOT NULL)");
|
||||
ensureTable("CREATE TABLE IF NOT EXISTS group_permission_grant (id INTEGER PRIMARY KEY AUTOINCREMENT, user_group_id INTEGER NOT NULL, tunnel_group_id INTEGER NOT NULL, user_tunnel_id INTEGER NOT NULL, created_by_group INTEGER NOT NULL DEFAULT 0, created_time INTEGER NOT NULL)");
|
||||
ensureColumn("group_permission_grant", "created_by_group", "INTEGER NOT NULL DEFAULT 0");
|
||||
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_name ON tunnel_group(name)");
|
||||
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_name ON user_group(name)");
|
||||
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_tunnel_unique ON tunnel_group_tunnel(tunnel_group_id, tunnel_id)");
|
||||
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_user_unique ON user_group_user(user_group_id, user_id)");
|
||||
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_unique ON group_permission(user_group_id, tunnel_group_id)");
|
||||
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_grant_unique ON group_permission_grant(user_group_id, tunnel_group_id, user_tunnel_id)");
|
||||
}
|
||||
|
||||
private void ensureColumn(String table, String column, String columnDefinition) {
|
||||
Set<String> columns = new HashSet<>(
|
||||
jdbcTemplate.query(
|
||||
"PRAGMA table_info(" + table + ")",
|
||||
(rs, rowNum) -> rs.getString("name")
|
||||
)
|
||||
);
|
||||
|
||||
if (columns.contains(column)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Adding missing column {}.{}", table, column);
|
||||
jdbcTemplate.execute(
|
||||
"ALTER TABLE " + table + " ADD COLUMN " + column + " " + columnDefinition
|
||||
);
|
||||
}
|
||||
|
||||
private void ensureTable(String ddl) {
|
||||
jdbcTemplate.execute(ddl);
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package com.admin.common.task;
|
||||
|
||||
import com.admin.common.dto.*;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
import com.admin.entity.*;
|
||||
import com.admin.service.*;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CheckGostConfigAsync {
|
||||
|
||||
@Resource
|
||||
private NodeService nodeService;
|
||||
|
||||
@Resource
|
||||
@Lazy
|
||||
private ForwardService forwardService;
|
||||
|
||||
@Resource
|
||||
@Lazy
|
||||
private SpeedLimitService speedLimitService;
|
||||
|
||||
@Resource
|
||||
TunnelService tunnelService;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 清理孤立的Gost配置项
|
||||
*/
|
||||
@Async
|
||||
public void cleanNodeConfigs(String node_id, GostConfigDto gostConfig) {
|
||||
Node node = nodeService.getById(node_id);
|
||||
if (node != null) {
|
||||
cleanOrphanedServices(gostConfig.getServices(), node);
|
||||
cleanOrphanedChains(gostConfig.getChains(), node);
|
||||
cleanOrphanedLimiters(gostConfig.getLimiters(), node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理孤立的服务
|
||||
*/
|
||||
private void cleanOrphanedServices(List<ConfigItem> configItems, Node node) {
|
||||
if (configItems == null) return;
|
||||
for (ConfigItem service : configItems) {
|
||||
safeExecute(() -> {
|
||||
|
||||
if (!Objects.equals(service.getName(), "web_api")){
|
||||
List<String> serviceIds = parseServiceName(service.getName());
|
||||
|
||||
JSONArray services = new JSONArray();
|
||||
if (Objects.equals(serviceIds.getLast(), "tls")){
|
||||
String forward_id = serviceIds.getFirst();
|
||||
services.add(forward_id + "_tls");
|
||||
|
||||
Tunnel tunnel = tunnelService.getById(forward_id);
|
||||
if (tunnel == null) {
|
||||
GostUtil.DeleteService(node.getId(), services);
|
||||
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (Objects.equals(serviceIds.getLast(), "tcp")){
|
||||
String forward_id = serviceIds.getFirst();
|
||||
services.add(forward_id + "_" + serviceIds.get(1) + "_" + serviceIds.get(2) + "_tcp");
|
||||
services.add(forward_id + "_" + serviceIds.get(1) + "_" + serviceIds.get(2) + "_udp");
|
||||
|
||||
Forward forward = forwardService.getById(forward_id);
|
||||
if (forward == null) {
|
||||
GostUtil.DeleteService(node.getId(), services);
|
||||
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}, "清理服务 " + service.getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理孤立的链
|
||||
*/
|
||||
private void cleanOrphanedChains(List<ConfigItem> configItems, Node node) {
|
||||
if (configItems == null) return;
|
||||
for (ConfigItem chain : configItems) {
|
||||
safeExecute(() -> {
|
||||
List<String> serviceIds = parseServiceName(chain.getName());
|
||||
Tunnel tunnel = tunnelService.getById(serviceIds.getLast());
|
||||
if (tunnel == null) {
|
||||
GostUtil.DeleteChains(node.getId(), chain.getName());
|
||||
log.info("删除孤立的链: {} (节点: {})", chain.getName(), node.getId());
|
||||
}
|
||||
}, "清理链 " + chain.getName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理孤立的限流器
|
||||
*/
|
||||
private void cleanOrphanedLimiters(List<ConfigItem> configItems, Node node) {
|
||||
if (configItems == null) return;
|
||||
|
||||
|
||||
for (ConfigItem limiter : configItems) {
|
||||
safeExecute(() -> {
|
||||
SpeedLimit speedLimit = speedLimitService.getById(limiter.getName());
|
||||
if (speedLimit == null) {
|
||||
GostUtil.DeleteLimiters(node.getId(), Long.parseLong(limiter.getName()));
|
||||
log.info("删除孤立的限流器: {} (节点: {})", limiter.getName(), node.getId());
|
||||
}
|
||||
}, "清理限流器 " + limiter.getName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 安全执行操作,捕获异常
|
||||
*/
|
||||
private void safeExecute(Runnable operation, String operationDesc) {
|
||||
try {
|
||||
operation.run();
|
||||
} catch (Exception e) {
|
||||
log.info("执行操作失败: {}", operationDesc, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析服务名称
|
||||
*/
|
||||
private List<String> parseServiceName(String serviceName) {
|
||||
String[] split = serviceName.split("_");
|
||||
return new ArrayList<>(Arrays.asList(split));
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
package com.admin.common.task;
|
||||
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
import com.admin.entity.*;
|
||||
import com.admin.service.*;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class ResetFlowAsync {
|
||||
|
||||
@Resource
|
||||
UserService userService;
|
||||
|
||||
@Resource
|
||||
UserTunnelService userTunnelService;
|
||||
|
||||
@Resource
|
||||
ForwardService forwardService;
|
||||
|
||||
@Resource
|
||||
TunnelService tunnelService;
|
||||
|
||||
@Resource
|
||||
ChainTunnelService chainTunnelService;
|
||||
|
||||
/**
|
||||
* 每天0点执行流量重置任务
|
||||
* 查询出用户和隧道的重置流量日期是今天的数据,将上下流量重置为0
|
||||
* 考虑当月是29、30天,但是选择是31的这种边界情况
|
||||
*
|
||||
* 并发安全说明:
|
||||
* - 使用setSql()进行原子SQL更新,只更新流量字段(in_flow, out_flow)
|
||||
* - 不会影响DelayQueueManager的到期任务对status等其他字段的更新
|
||||
* - 避免了并发修改导致的数据覆盖问题
|
||||
*/
|
||||
@Scheduled(cron = "5 0 0 * * ?")
|
||||
public void reset_flow(){
|
||||
log.info("开始执行流量重置任务");
|
||||
|
||||
try {
|
||||
// 获取当前日期信息
|
||||
LocalDate today = LocalDate.now();
|
||||
int currentDay = today.getDayOfMonth(); // 当前是几号
|
||||
int lastDayOfMonth = today.lengthOfMonth(); // 当月最后一天
|
||||
|
||||
log.info("当前日期: {}, 当月第{}天, 当月最后一天: {}", today, currentDay, lastDayOfMonth);
|
||||
|
||||
// 重置用户流量
|
||||
resetUserFlow(currentDay, lastDayOfMonth);
|
||||
|
||||
// 重置用户隧道流量
|
||||
resetUserTunnelFlow(currentDay, lastDayOfMonth);
|
||||
|
||||
log.info("流量重置任务执行完成");
|
||||
|
||||
|
||||
// 处理过期账号
|
||||
user();
|
||||
|
||||
// 处理过期隧道
|
||||
userTunnel();
|
||||
|
||||
log.info("到期任务执行完成");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.info("定时任务执行失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置用户流量
|
||||
* @param currentDay 当前日期(几号)
|
||||
* @param lastDayOfMonth 当月最后一天
|
||||
*/
|
||||
private void resetUserFlow(int currentDay, int lastDayOfMonth) {
|
||||
try {
|
||||
// flowResetTime字段存储的是0-31的数字,0表示不重置,1-31表示每月第几号重置
|
||||
// 构建查询条件:重置日期等于今天,或者重置日期大于当月最大天数且今天是月末
|
||||
// 排除flowResetTime为0的记录(不重置)
|
||||
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.ne("flow_reset_time", 0); // 排除不重置的用户
|
||||
|
||||
if (currentDay == lastDayOfMonth) {
|
||||
// 如果今天是月末,查询重置日期等于今天或者大于当月最大天数的记录
|
||||
// 例如:当月30天,但用户设置31号重置,则在30号执行重置
|
||||
queryWrapper.and(wrapper -> wrapper.eq("flow_reset_time", currentDay)
|
||||
.or().gt("flow_reset_time", lastDayOfMonth));
|
||||
} else {
|
||||
// 否则只查询重置日期等于今天的记录
|
||||
queryWrapper.eq("flow_reset_time", currentDay);
|
||||
}
|
||||
|
||||
// 查询需要重置的用户
|
||||
List<User> usersToReset = userService.list(queryWrapper);
|
||||
|
||||
if (usersToReset.isEmpty()) {
|
||||
log.info("没有需要重置流量的用户");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("找到{}个需要重置流量的用户", usersToReset.size());
|
||||
|
||||
// 批量重置用户流量 - 使用SQL原子操作避免与到期任务的并发冲突
|
||||
for (User user : usersToReset) {
|
||||
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.eq("id", user.getId())
|
||||
.setSql("in_flow = 0, out_flow = 0"); // 使用SQL原子操作,只更新流量字段
|
||||
|
||||
boolean success = userService.update(null, updateWrapper);
|
||||
if (success) {
|
||||
log.info("用户[ID: {}, 用户名: {}]流量重置成功,重置日期: 每月{}号",
|
||||
user.getId(), user.getUser(), user.getFlowResetTime());
|
||||
} else {
|
||||
log.info("用户[ID: {}, 用户名: {}]流量重置失败", user.getId(), user.getUser());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.info("重置用户流量失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置用户隧道流量
|
||||
* @param currentDay 当前日期(几号)
|
||||
* @param lastDayOfMonth 当月最后一天
|
||||
*/
|
||||
private void resetUserTunnelFlow(int currentDay, int lastDayOfMonth) {
|
||||
try {
|
||||
// flowResetTime字段存储的是0-31的数字,0表示不重置,1-31表示每月第几号重置
|
||||
// 构建查询条件:重置日期等于今天,或者重置日期大于当月最大天数且今天是月末
|
||||
// 排除flowResetTime为0的记录(不重置)
|
||||
QueryWrapper<UserTunnel> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.ne("flow_reset_time", 0); // 排除不重置的用户隧道
|
||||
|
||||
if (currentDay == lastDayOfMonth) {
|
||||
// 如果今天是月末,查询重置日期等于今天或者大于当月最大天数的记录
|
||||
// 例如:当月30天,但用户设置31号重置,则在30号执行重置
|
||||
queryWrapper.and(wrapper -> wrapper.eq("flow_reset_time", currentDay)
|
||||
.or().gt("flow_reset_time", lastDayOfMonth));
|
||||
} else {
|
||||
// 否则只查询重置日期等于今天的记录
|
||||
queryWrapper.eq("flow_reset_time", currentDay);
|
||||
}
|
||||
|
||||
// 查询需要重置的用户隧道
|
||||
List<UserTunnel> userTunnelsToReset = userTunnelService.list(queryWrapper);
|
||||
|
||||
if (userTunnelsToReset.isEmpty()) {
|
||||
log.info("没有需要重置流量的用户隧道");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("找到{}个需要重置流量的用户隧道", userTunnelsToReset.size());
|
||||
|
||||
// 批量重置用户隧道流量 - 使用SQL原子操作避免与到期任务的并发冲突
|
||||
for (UserTunnel userTunnel : userTunnelsToReset) {
|
||||
UpdateWrapper<UserTunnel> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.eq("id", userTunnel.getId())
|
||||
.setSql("in_flow = 0, out_flow = 0"); // 使用SQL原子操作,只更新流量字段
|
||||
|
||||
boolean success = userTunnelService.update(null, updateWrapper);
|
||||
if (success) {
|
||||
log.info("用户隧道[ID: {}, 用户ID: {}, 隧道ID: {}]流量重置成功,重置日期: 每月{}号",
|
||||
userTunnel.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(), userTunnel.getFlowResetTime());
|
||||
} else {
|
||||
log.info("用户隧道[ID: {}, 用户ID: {}, 隧道ID: {}]流量重置失败",
|
||||
userTunnel.getId(), userTunnel.getUserId(), userTunnel.getTunnelId());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.info("重置用户隧道流量失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void user(){
|
||||
// 查询过期用户
|
||||
List<User> user_list = userService.list(new QueryWrapper<User>().ne("role_id", 0).eq("status", 1).isNotNull("exp_time").lt("exp_time", new Date().getTime()));
|
||||
for (User user : user_list) {
|
||||
// 查询对应转发
|
||||
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("user_id", user.getId()).eq("status", 1));
|
||||
for (Forward forward : forwardList) {
|
||||
|
||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", forward.getTunnelId()).eq("chain_type", 1));
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
UserTunnel userTunnel = userTunnelService.getOne(new QueryWrapper<UserTunnel>().eq("user_id", forward.getUserId()).eq("tunnel_id", forward.getTunnelId()));
|
||||
if (userTunnel != null) {
|
||||
String name = buildServiceName(forward.getId(), forward.getUserId(), userTunnel.getId());
|
||||
GostUtil.PauseAndResumeService(chainTunnel.getNodeId(), name, "PauseService");
|
||||
}
|
||||
}
|
||||
|
||||
forward.setStatus(0);
|
||||
forwardService.updateById(forward);
|
||||
}
|
||||
user.setStatus(0);
|
||||
userService.updateById(user);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void userTunnel(){
|
||||
// 查询过期隧道
|
||||
List<UserTunnel> user_tunnel_list = userTunnelService.list(new QueryWrapper<UserTunnel>().eq("status", 1).isNotNull("exp_time").lt("exp_time", new Date().getTime()));
|
||||
// 查询对应转发
|
||||
for (UserTunnel userTunnel : user_tunnel_list) {
|
||||
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("tunnel_id", userTunnel.getTunnelId()).eq("user_id", userTunnel.getUserId()).eq("status", 1));
|
||||
for (Forward forward : forwardList) {
|
||||
|
||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", forward.getTunnelId()).eq("chain_type", 1));
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
String name = buildServiceName(forward.getId(), forward.getUserId(), userTunnel.getId());
|
||||
GostUtil.PauseAndResumeService(chainTunnel.getNodeId(), name, "PauseService");
|
||||
}
|
||||
forward.setStatus(0);
|
||||
forwardService.updateById(forward);
|
||||
}
|
||||
|
||||
|
||||
userTunnel.setStatus(0);
|
||||
userTunnelService.updateById(userTunnel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private String buildServiceName(Long forwardId, Integer userId, Integer userTunnelId) {
|
||||
return forwardId + "_" + userId + "_" + userTunnelId;
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package com.admin.common.task;
|
||||
|
||||
|
||||
import com.admin.entity.StatisticsFlow;
|
||||
import com.admin.entity.User;
|
||||
import com.admin.service.StatisticsFlowService;
|
||||
import com.admin.service.UserService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class StatisticsFlowAsync {
|
||||
|
||||
@Resource
|
||||
UserService userService;
|
||||
|
||||
@Resource
|
||||
StatisticsFlowService statisticsFlowService;
|
||||
|
||||
@Scheduled(cron = "0 0 * * * ?")
|
||||
public void statistics_flow() {
|
||||
LocalDateTime currentHour = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0);
|
||||
String hourString = currentHour.format(DateTimeFormatter.ofPattern("HH:mm"));
|
||||
long time = new Date().getTime();
|
||||
|
||||
// 删除48小时前的数据
|
||||
long nowMs = new Date().getTime();
|
||||
long cutoffMs = nowMs - 48L * 60 * 60 * 1000;
|
||||
statisticsFlowService.remove(
|
||||
new LambdaQueryWrapper<StatisticsFlow>()
|
||||
.lt(StatisticsFlow::getCreatedTime, cutoffMs)
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
List<User> list = userService.list();
|
||||
List<StatisticsFlow> statisticsFlowList = new ArrayList<>();
|
||||
|
||||
for (User user : list) {
|
||||
long currentFlow = user.getInFlow() + user.getOutFlow();
|
||||
|
||||
// 从数据库获取上一次记录
|
||||
StatisticsFlow lastFlowRecord = statisticsFlowService.getOne(
|
||||
new LambdaQueryWrapper<StatisticsFlow>()
|
||||
.eq(StatisticsFlow::getUserId, user.getId())
|
||||
.orderByDesc(StatisticsFlow::getId)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
|
||||
long currentTotalFlow = currentFlow;
|
||||
long incrementFlow = currentTotalFlow;
|
||||
|
||||
if (lastFlowRecord != null) {
|
||||
long lastTotalFlow = lastFlowRecord.getTotalFlow();
|
||||
incrementFlow = currentTotalFlow - lastTotalFlow;
|
||||
|
||||
if (incrementFlow < 0) {
|
||||
incrementFlow = currentTotalFlow;
|
||||
}
|
||||
}
|
||||
|
||||
StatisticsFlow statisticsFlow = new StatisticsFlow();
|
||||
statisticsFlow.setUserId(user.getId());
|
||||
statisticsFlow.setFlow(incrementFlow);
|
||||
statisticsFlow.setTotalFlow(currentTotalFlow);
|
||||
statisticsFlow.setTime(hourString);
|
||||
statisticsFlow.setCreatedTime(time);
|
||||
|
||||
statisticsFlowList.add(statisticsFlow);
|
||||
}
|
||||
|
||||
statisticsFlowService.saveBatch(statisticsFlowList);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* AES加密工具类
|
||||
* 使用AES-256-GCM模式,与Go端保持兼容
|
||||
*/
|
||||
@Slf4j
|
||||
public class AESCrypto {
|
||||
|
||||
private static final String ALGORITHM = "AES";
|
||||
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
|
||||
private static final int GCM_IV_LENGTH = 12; // GCM推荐的IV长度
|
||||
private static final int GCM_TAG_LENGTH = 16; // GCM认证标签长度
|
||||
|
||||
private final SecretKeySpec secretKey;
|
||||
private final SecureRandom secureRandom;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param secret 密钥字符串,将使用SHA-256转换为32字节密钥
|
||||
*/
|
||||
public AESCrypto(String secret) {
|
||||
if (secret == null || secret.isEmpty()) {
|
||||
throw new IllegalArgumentException("密钥不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用SHA-256将密码转换为32字节密钥,与Go端保持一致
|
||||
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||
byte[] keyBytes = sha256.digest(secret.getBytes(StandardCharsets.UTF_8));
|
||||
this.secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
|
||||
this.secureRandom = new SecureRandom();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.info("AES加密器初始化失败", e);
|
||||
throw new RuntimeException("AES加密器初始化失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密数据
|
||||
* @param data 要加密的原始数据
|
||||
* @return Base64编码的加密数据,格式为: nonce + ciphertext
|
||||
*/
|
||||
public String encrypt(byte[] data) {
|
||||
if (data == null || data.length == 0) {
|
||||
throw new IllegalArgumentException("待加密数据不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 生成随机IV(nonce)
|
||||
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||
secureRandom.nextBytes(iv);
|
||||
|
||||
// 创建GCM参数规范
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
|
||||
|
||||
// 初始化Cipher
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec);
|
||||
|
||||
// 加密数据
|
||||
byte[] ciphertext = cipher.doFinal(data);
|
||||
|
||||
// 组合IV + ciphertext,与Go端格式保持一致
|
||||
ByteBuffer buffer = ByteBuffer.allocate(iv.length + ciphertext.length);
|
||||
buffer.put(iv);
|
||||
buffer.put(ciphertext);
|
||||
|
||||
// 返回Base64编码结果
|
||||
return Base64.getEncoder().encodeToString(buffer.array());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.info("数据加密失败", e);
|
||||
throw new RuntimeException("数据加密失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密字符串
|
||||
* @param data 要加密的字符串
|
||||
* @return Base64编码的加密数据
|
||||
*/
|
||||
public String encrypt(String data) {
|
||||
if (data == null) {
|
||||
throw new IllegalArgumentException("待加密字符串不能为空");
|
||||
}
|
||||
return encrypt(data.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密数据
|
||||
* @param encryptedData Base64编码的加密数据
|
||||
* @return 解密后的原始数据
|
||||
*/
|
||||
public byte[] decrypt(String encryptedData) {
|
||||
if (encryptedData == null || encryptedData.isEmpty()) {
|
||||
throw new IllegalArgumentException("加密数据不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// Base64解码
|
||||
byte[] encrypted = Base64.getDecoder().decode(encryptedData);
|
||||
|
||||
if (encrypted.length < GCM_IV_LENGTH) {
|
||||
throw new IllegalArgumentException("加密数据长度不足");
|
||||
}
|
||||
|
||||
// 分离IV和密文
|
||||
ByteBuffer buffer = ByteBuffer.wrap(encrypted);
|
||||
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||
buffer.get(iv);
|
||||
|
||||
byte[] ciphertext = new byte[buffer.remaining()];
|
||||
buffer.get(ciphertext);
|
||||
|
||||
// 创建GCM参数规范
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
|
||||
|
||||
// 初始化Cipher
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec);
|
||||
|
||||
// 解密数据
|
||||
return cipher.doFinal(ciphertext);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.info("数据解密失败", e);
|
||||
throw new RuntimeException("数据解密失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密字符串
|
||||
* @param encryptedData Base64编码的加密数据
|
||||
* @return 解密后的字符串
|
||||
*/
|
||||
public String decryptString(String encryptedData) {
|
||||
byte[] decrypted = decrypt(encryptedData);
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建AES加密器实例
|
||||
* @param secret 密钥字符串
|
||||
* @return AES加密器实例,如果创建失败返回null
|
||||
*/
|
||||
public static AESCrypto create(String secret) {
|
||||
try {
|
||||
return new AESCrypto(secret);
|
||||
} catch (Exception e) {
|
||||
log.info("创建AES加密器失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,411 +0,0 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.admin.entity.*;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class GostUtil {
|
||||
|
||||
|
||||
public static GostDto AddLimiters(Long node_id, Long name, String speed) {
|
||||
JSONObject data = createLimiterData(name, speed);
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "AddLimiters");
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto UpdateLimiters(Long node_id, Long name, String speed) {
|
||||
JSONObject data = createLimiterData(name, speed);
|
||||
JSONObject req = new JSONObject();
|
||||
req.put("limiter", name + "");
|
||||
req.put("data", data);
|
||||
return WebSocketServer.send_msg(node_id, req, "UpdateLimiters");
|
||||
}
|
||||
|
||||
public static GostDto DeleteLimiters(Long node_id, Long name) {
|
||||
JSONObject req = new JSONObject();
|
||||
req.put("limiter", name + "");
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, req, "DeleteLimiters");
|
||||
if (gostDto.getMsg().contains("not found")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto AddChains(Long node_id, List<ChainTunnel> chainTunnels, Map<Long, Node> node_s) {
|
||||
JSONArray nodes = new JSONArray();
|
||||
Node fromNode = node_s.get(node_id);
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
JSONObject dialer = new JSONObject();
|
||||
dialer.put("type", chainTunnel.getProtocol());
|
||||
|
||||
JSONObject connector = new JSONObject();
|
||||
connector.put("type", "relay");
|
||||
|
||||
Node node_info = node_s.get(chainTunnel.getNodeId());
|
||||
JSONObject node = new JSONObject();
|
||||
node.put("name", "node_" + chainTunnel.getInx());
|
||||
|
||||
String dialHost = (fromNode != null && node_info != null)
|
||||
? selectDialHost(fromNode, node_info)
|
||||
: (node_info != null ? node_info.getServerIp() : null);
|
||||
node.put("addr", processServerAddress(dialHost + ":" + chainTunnel.getPort()));
|
||||
node.put("connector", connector);
|
||||
node.put("dialer", dialer);
|
||||
|
||||
|
||||
|
||||
nodes.add(node);
|
||||
}
|
||||
JSONObject hop = new JSONObject();
|
||||
hop.put("name", "hop_" + chainTunnels.getFirst().getTunnelId());
|
||||
|
||||
// interface设置在转发链
|
||||
if (StringUtils.isNotBlank(node_s.get(node_id).getInterfaceName())) {
|
||||
hop.put("interface", node_s.get(node_id).getInterfaceName());
|
||||
}
|
||||
|
||||
|
||||
JSONObject selector = new JSONObject();
|
||||
selector.put("strategy", chainTunnels.getFirst().getStrategy());
|
||||
selector.put("maxFails", 1);
|
||||
selector.put("failTimeout", 600000000000L); // 600 秒(纳秒单位)
|
||||
|
||||
|
||||
hop.put("selector", selector);
|
||||
hop.put("nodes", nodes);
|
||||
|
||||
JSONArray hops = new JSONArray();
|
||||
hops.add(hop);
|
||||
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("name", "chains_" + chainTunnels.getFirst().getTunnelId());
|
||||
data.put("hops", hops);
|
||||
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "AddChains");
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto DeleteChains(Long node_id, String name) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("chain", name);
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "DeleteChains");
|
||||
if (gostDto.getMsg().contains("not found")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto AddChainService(Long node_id, ChainTunnel chainTunnel, Map<Long, Node> node_s) {
|
||||
JSONArray services = new JSONArray();
|
||||
Node node_info = node_s.get(chainTunnel.getNodeId());
|
||||
JSONObject service_item = new JSONObject();
|
||||
service_item.put("name", chainTunnel.getTunnelId() + "_tls");
|
||||
service_item.put("addr", node_info.getTcpListenAddr() + ":" + chainTunnel.getPort());
|
||||
|
||||
// 只为出口节点(chainType=3)设置 interface
|
||||
if (chainTunnel.getChainType() == 3 && StringUtils.isNotBlank(node_s.get(node_id).getInterfaceName())) {
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("interface", node_s.get(node_id).getInterfaceName());
|
||||
service_item.put("metadata", metadata);
|
||||
}
|
||||
|
||||
JSONObject handler = new JSONObject();
|
||||
handler.put("type", "relay");
|
||||
if (chainTunnel.getChainType() == 2){
|
||||
handler.put("chain","chains_" + chainTunnel.getTunnelId());
|
||||
}
|
||||
service_item.put("handler", handler);
|
||||
|
||||
JSONObject listener = new JSONObject();
|
||||
listener.put("type", chainTunnel.getProtocol());
|
||||
service_item.put("listener", listener);
|
||||
|
||||
services.add(service_item);
|
||||
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, services, "AddService");
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto AddAndUpdateService(String name, Integer limiter, Node node, Forward forward, ForwardPort forwardPort, Tunnel tunnel, String meth) {
|
||||
JSONArray services = new JSONArray();
|
||||
String[] protocols = {"tcp", "udp"};
|
||||
for (String protocol : protocols) {
|
||||
JSONObject service = new JSONObject();
|
||||
service.put("name", name + "_" + protocol);
|
||||
if (Objects.equals(protocol, "tcp")){
|
||||
service.put("addr", node.getTcpListenAddr() + ":" + forwardPort.getPort());
|
||||
}else {
|
||||
service.put("addr", node.getUdpListenAddr() + ":" + forwardPort.getPort());
|
||||
}
|
||||
|
||||
// 只在端口转发时设置 interface(隧道转发时 interface 在转发链的节点上设置)
|
||||
if (tunnel.getType() == 1 && StringUtils.isNotBlank(node.getInterfaceName())) {
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("interface", node.getInterfaceName());
|
||||
service.put("metadata", metadata);
|
||||
}
|
||||
|
||||
// 添加限流器配置
|
||||
if (limiter != null) {
|
||||
service.put("limiter", limiter.toString());
|
||||
}
|
||||
|
||||
// 配置处理器
|
||||
JSONObject handler = new JSONObject();
|
||||
handler.put("type", protocol);
|
||||
if (tunnel.getType() == 2){
|
||||
handler.put("chain", "chains_" + forward.getTunnelId());
|
||||
}
|
||||
service.put("handler", handler);
|
||||
|
||||
// 配置监听器
|
||||
JSONObject listener = createListener(protocol);
|
||||
service.put("listener", listener);
|
||||
|
||||
JSONObject forwarder = createForwarder(forward.getRemoteAddr(), forward.getStrategy());
|
||||
service.put("forwarder", forwarder);
|
||||
|
||||
services.add(service);
|
||||
}
|
||||
GostDto gostDto = WebSocketServer.send_msg(node.getId(), services, meth);
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto DeleteService(Long node_id, JSONArray services) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("services", services);
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "DeleteService");
|
||||
if (gostDto.getMsg().contains("not found")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto PauseAndResumeService(Long node_id, String name, String meth) {
|
||||
JSONObject data = new JSONObject();
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(name + "_tcp");
|
||||
services.add(name + "_udp");
|
||||
data.put("services", services);
|
||||
return WebSocketServer.send_msg(node_id, data, meth);
|
||||
}
|
||||
|
||||
|
||||
private static JSONObject createLimiterData(Long name, String speed) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("name", name.toString());
|
||||
JSONArray limits = new JSONArray();
|
||||
limits.add("$ " + speed + "MB " + speed + "MB");
|
||||
data.put("limits", limits);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static JSONObject createListener(String protocol) {
|
||||
JSONObject listener = new JSONObject();
|
||||
listener.put("type", protocol);
|
||||
if (Objects.equals(protocol, "udp")) {
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("keepAlive", true);
|
||||
listener.put("metadata", metadata);
|
||||
}
|
||||
return listener;
|
||||
}
|
||||
|
||||
private static JSONObject createForwarder(String remoteAddr, String strategy) {
|
||||
JSONObject forwarder = new JSONObject();
|
||||
JSONArray nodes = new JSONArray();
|
||||
|
||||
String[] split = remoteAddr.split(",");
|
||||
int num = 1;
|
||||
for (String addr : split) {
|
||||
JSONObject node = new JSONObject();
|
||||
node.put("name", "node_" + num);
|
||||
node.put("addr", addr);
|
||||
nodes.add(node);
|
||||
num++;
|
||||
}
|
||||
|
||||
if (strategy == null || strategy.isEmpty()) {
|
||||
strategy = "fifo";
|
||||
}
|
||||
|
||||
forwarder.put("nodes", nodes);
|
||||
|
||||
JSONObject selector = new JSONObject();
|
||||
selector.put("strategy", strategy);
|
||||
selector.put("maxFails", 1);
|
||||
selector.put("failTimeout", "600s");
|
||||
forwarder.put("selector", selector);
|
||||
return forwarder;
|
||||
}
|
||||
|
||||
public static String processServerAddress(String serverAddr) {
|
||||
if (StrUtil.isBlank(serverAddr)) {
|
||||
return serverAddr;
|
||||
}
|
||||
|
||||
// 如果已经被方括号包裹,直接返回
|
||||
if (serverAddr.startsWith("[")) {
|
||||
return serverAddr;
|
||||
}
|
||||
|
||||
// 查找最后一个冒号,分离主机和端口
|
||||
int lastColonIndex = serverAddr.lastIndexOf(':');
|
||||
if (lastColonIndex == -1) {
|
||||
// 没有端口号,直接检查是否需要包裹
|
||||
return isIPv6Address(serverAddr) ? "[" + serverAddr + "]" : serverAddr;
|
||||
}
|
||||
|
||||
String host = serverAddr.substring(0, lastColonIndex);
|
||||
String port = serverAddr.substring(lastColonIndex);
|
||||
|
||||
// 检查主机部分是否为IPv6地址
|
||||
if (isIPv6Address(host)) {
|
||||
return "[" + host + "]" + port;
|
||||
}
|
||||
|
||||
return serverAddr;
|
||||
}
|
||||
|
||||
private static boolean isIPv6Address(String address) {
|
||||
// IPv6地址包含多个冒号,至少2个
|
||||
if (!address.contains(":")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算冒号数量,IPv6地址至少有2个冒号
|
||||
long colonCount = address.chars().filter(ch -> ch == ':').count();
|
||||
return colonCount >= 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* v4 优先:当两端都有 v4 时选择 v4,否则尝试 v6。
|
||||
* 用于节点之间建立链路(A -> B 需要选择 B 的地址族,且 A 需要支持该地址族)。
|
||||
*/
|
||||
public static String selectDialHost(Node fromNode, Node toNode) {
|
||||
if (fromNode == null || toNode == null) {
|
||||
throw new IllegalArgumentException("node is null");
|
||||
}
|
||||
|
||||
boolean fromV4 = supportsV4(fromNode);
|
||||
boolean fromV6 = supportsV6(fromNode);
|
||||
boolean toV4 = supportsV4(toNode);
|
||||
boolean toV6 = supportsV6(toNode);
|
||||
|
||||
if (fromV4 && toV4) {
|
||||
return pickToAddressV4(toNode);
|
||||
}
|
||||
if (fromV6 && toV6) {
|
||||
return pickToAddressV6(toNode);
|
||||
}
|
||||
|
||||
throw new RuntimeException(
|
||||
"节点链路不兼容:" + safeName(fromNode) + "(v4=" + fromV4 + ",v6=" + fromV6 + ") -> "
|
||||
+ safeName(toNode) + "(v4=" + toV4 + ",v6=" + toV6 + ")"
|
||||
);
|
||||
}
|
||||
|
||||
private static String safeName(Node node) {
|
||||
if (node.getName() == null || node.getName().isBlank()) {
|
||||
return "node_" + node.getId();
|
||||
}
|
||||
return node.getName();
|
||||
}
|
||||
|
||||
private static boolean supportsV4(Node node) {
|
||||
// New dual-stack fields take precedence over legacy serverIp.
|
||||
// If user explicitly provided only v6, treat as v6-only.
|
||||
if (StrUtil.isNotBlank(node.getServerIpV4())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (StrUtil.isNotBlank(node.getServerIpV6())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String legacy = node.getServerIp();
|
||||
if (StrUtil.isBlank(legacy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
legacy = legacy.trim();
|
||||
if (looksLikeIpv4(legacy)) {
|
||||
return true;
|
||||
}
|
||||
if (isIPv6Address(legacy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 域名/其它:无法判断,按双栈处理以保持兼容
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean supportsV6(Node node) {
|
||||
// New dual-stack fields take precedence over legacy serverIp.
|
||||
// If user explicitly provided only v4, treat as v4-only.
|
||||
if (StrUtil.isNotBlank(node.getServerIpV6())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (StrUtil.isNotBlank(node.getServerIpV4())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String legacy = node.getServerIp();
|
||||
if (StrUtil.isBlank(legacy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
legacy = legacy.trim();
|
||||
if (isIPv6Address(legacy)) {
|
||||
return true;
|
||||
}
|
||||
if (looksLikeIpv4(legacy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 域名/其它:无法判断,按双栈处理以保持兼容
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String pickToAddressV4(Node toNode) {
|
||||
if (StrUtil.isNotBlank(toNode.getServerIpV4())) {
|
||||
return toNode.getServerIpV4().trim();
|
||||
}
|
||||
String legacy = toNode.getServerIp();
|
||||
return legacy != null ? legacy.trim() : null;
|
||||
}
|
||||
|
||||
private static String pickToAddressV6(Node toNode) {
|
||||
if (StrUtil.isNotBlank(toNode.getServerIpV6())) {
|
||||
return toNode.getServerIpV6().trim();
|
||||
}
|
||||
String legacy = toNode.getServerIp();
|
||||
return legacy != null ? legacy.trim() : null;
|
||||
}
|
||||
|
||||
private static boolean looksLikeIpv4(String value) {
|
||||
Pattern ipv4 = Pattern.compile("^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
|
||||
return ipv4.matcher(value).matches();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
public class HttpContextUtils {
|
||||
|
||||
public static HttpServletRequest getHttpServletRequest(){
|
||||
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import com.admin.common.dto.GostConfigDto;
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.admin.config.RestTemplateConfig;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.SneakyThrows;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.ResponseErrorHandler;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* HTTP请求工具类
|
||||
* 支持GET和POST请求,支持表单和JSON格式的请求体
|
||||
*/
|
||||
@Component
|
||||
public class HttpUtils{
|
||||
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user