mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-29 07:56:37 +08:00
去除代理头支持, 增加节点面板通信加密 增加导出sql
This commit is contained in:
@@ -76,6 +76,7 @@ volumes:
|
||||
|
||||
networks:
|
||||
gost-network:
|
||||
name: gost-network
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
|
||||
@@ -69,13 +69,16 @@ services:
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
name: mysql_data
|
||||
driver: local
|
||||
backend_logs:
|
||||
name: backend_logs
|
||||
driver: local
|
||||
|
||||
|
||||
networks:
|
||||
gost-network:
|
||||
name: gost-network
|
||||
driver: bridge
|
||||
enable_ipv6: true
|
||||
ipam:
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// AESCrypto AES 加密器结构
|
||||
type AESCrypto struct {
|
||||
key []byte
|
||||
}
|
||||
|
||||
// NewAESCrypto 创建新的 AES 加密器
|
||||
// secret: 用于生成密钥的密码字符串
|
||||
func NewAESCrypto(secret string) (*AESCrypto, error) {
|
||||
if secret == "" {
|
||||
return nil, fmt.Errorf("密钥不能为空")
|
||||
}
|
||||
|
||||
// 使用 SHA256 将密码转换为 32 字节密钥
|
||||
hash := sha256.Sum256([]byte(secret))
|
||||
|
||||
return &AESCrypto{
|
||||
key: hash[:],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Encrypt 加密数据
|
||||
// data: 要加密的原始数据
|
||||
// 返回: base64编码的加密数据
|
||||
func (a *AESCrypto) Encrypt(data []byte) (string, error) {
|
||||
if len(data) == 0 {
|
||||
return "", fmt.Errorf("待加密数据不能为空")
|
||||
}
|
||||
|
||||
// 创建 AES cipher
|
||||
block, err := aes.NewCipher(a.key)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建 AES cipher 失败: %v", err)
|
||||
}
|
||||
|
||||
// 使用 GCM 模式
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建 GCM 失败: %v", err)
|
||||
}
|
||||
|
||||
// 生成随机 nonce
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return "", fmt.Errorf("生成 nonce 失败: %v", err)
|
||||
}
|
||||
|
||||
// 加密数据
|
||||
ciphertext := gcm.Seal(nil, nonce, data, nil)
|
||||
|
||||
// 组合 nonce + ciphertext
|
||||
encrypted := append(nonce, ciphertext...)
|
||||
|
||||
// 返回 base64 编码结果
|
||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||
}
|
||||
|
||||
// Decrypt 解密数据
|
||||
// encryptedData: base64编码的加密数据
|
||||
// 返回: 解密后的原始数据
|
||||
func (a *AESCrypto) Decrypt(encryptedData string) ([]byte, error) {
|
||||
if encryptedData == "" {
|
||||
return nil, fmt.Errorf("加密数据不能为空")
|
||||
}
|
||||
|
||||
// base64 解码
|
||||
encrypted, err := base64.StdEncoding.DecodeString(encryptedData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("base64 解码失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建 AES cipher
|
||||
block, err := aes.NewCipher(a.key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建 AES cipher 失败: %v", err)
|
||||
}
|
||||
|
||||
// 使用 GCM 模式
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建 GCM 失败: %v", err)
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(encrypted) < nonceSize {
|
||||
return nil, fmt.Errorf("加密数据长度不足")
|
||||
}
|
||||
|
||||
// 分离 nonce 和 ciphertext
|
||||
nonce := encrypted[:nonceSize]
|
||||
ciphertext := encrypted[nonceSize:]
|
||||
|
||||
// 解密数据
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解密失败: %v", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// EncryptString 加密字符串并返回加密后的字符串
|
||||
func (a *AESCrypto) EncryptString(data string) (string, error) {
|
||||
return a.Encrypt([]byte(data))
|
||||
}
|
||||
|
||||
// DecryptString 解密字符串并返回解密后的字符串
|
||||
func (a *AESCrypto) DecryptString(encryptedData string) (string, error) {
|
||||
plaintext, err := a.Decrypt(encryptedData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(plaintext), nil
|
||||
}
|
||||
@@ -11,11 +11,13 @@ import (
|
||||
|
||||
"github.com/go-gost/core/observer/stats"
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/internal/util/crypto"
|
||||
"github.com/go-gost/x/registry"
|
||||
)
|
||||
|
||||
var httpReportURL string
|
||||
var configReportURL string
|
||||
var httpAESCrypto *crypto.AESCrypto // 新增:HTTP上报加密器
|
||||
|
||||
// TrafficReportItem 流量报告项(压缩格式)
|
||||
type TrafficReportItem struct {
|
||||
@@ -28,6 +30,16 @@ type TrafficReportItem struct {
|
||||
func SetHTTPReportURL(addr string, secret string) {
|
||||
httpReportURL = "http://" + addr + "/flow/upload?secret=" + secret
|
||||
configReportURL = "http://" + addr + "/flow/config?secret=" + secret
|
||||
|
||||
// 创建 AES 加密器
|
||||
var err error
|
||||
httpAESCrypto, err = crypto.NewAESCrypto(secret)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 创建 HTTP AES 加密器失败: %v\n", err)
|
||||
httpAESCrypto = nil
|
||||
} else {
|
||||
fmt.Printf("🔐 HTTP AES 加密器创建成功\n")
|
||||
}
|
||||
}
|
||||
|
||||
// sendTrafficReport 发送流量报告到HTTP接口
|
||||
@@ -37,7 +49,32 @@ func sendTrafficReport(ctx context.Context, reportItems []TrafficReportItem) (bo
|
||||
return false, fmt.Errorf("序列化报告数据失败: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", httpReportURL, bytes.NewBuffer(jsonData))
|
||||
var requestBody []byte
|
||||
|
||||
// 如果有加密器,则加密数据
|
||||
if httpAESCrypto != nil {
|
||||
encryptedData, err := httpAESCrypto.Encrypt(jsonData)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 加密流量报告失败,发送原始数据: %v\n", err)
|
||||
requestBody = jsonData
|
||||
} else {
|
||||
// 创建加密消息包装器
|
||||
encryptedMessage := map[string]interface{}{
|
||||
"encrypted": true,
|
||||
"data": encryptedData,
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
requestBody, err = json.Marshal(encryptedMessage)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 序列化加密流量报告失败,发送原始数据: %v\n", err)
|
||||
requestBody = jsonData
|
||||
}
|
||||
}
|
||||
} else {
|
||||
requestBody = jsonData
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", httpReportURL, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("创建HTTP请求失败: %v", err)
|
||||
}
|
||||
@@ -88,13 +125,38 @@ func sendConfigReport(ctx context.Context) (bool, error) {
|
||||
return false, fmt.Errorf("获取配置数据失败: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", configReportURL, bytes.NewBuffer(configData))
|
||||
var requestBody []byte
|
||||
|
||||
// 如果有加密器,则加密数据
|
||||
if httpAESCrypto != nil {
|
||||
encryptedData, err := httpAESCrypto.Encrypt(configData)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 加密配置报告失败,发送原始数据: %v\n", err)
|
||||
requestBody = configData
|
||||
} else {
|
||||
// 创建加密消息包装器
|
||||
encryptedMessage := map[string]interface{}{
|
||||
"encrypted": true,
|
||||
"data": encryptedData,
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
requestBody, err = json.Marshal(encryptedMessage)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 序列化加密配置报告失败,发送原始数据: %v\n", err)
|
||||
requestBody = configData
|
||||
}
|
||||
}
|
||||
} else {
|
||||
requestBody = configData
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", configReportURL, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("创建HTTP请求失败: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "GOST-Config-Reporter/1.0")
|
||||
req.Header.Set("User-Agent", "Config-Reporter/1.0")
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second, // 配置上报可以稍长一些
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/internal/util/crypto"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
"github.com/shirou/gopsutil/v3/host"
|
||||
@@ -90,13 +91,24 @@ type WebSocketReporter struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
connected bool
|
||||
connecting bool // 新增:正在连接状态
|
||||
connMutex sync.Mutex // 新增:连接状态锁
|
||||
connecting bool // 新增:正在连接状态
|
||||
connMutex sync.Mutex // 新增:连接状态锁
|
||||
aesCrypto *crypto.AESCrypto // 新增:AES加密器
|
||||
}
|
||||
|
||||
// NewWebSocketReporter 创建一个新的WebSocket报告器
|
||||
func NewWebSocketReporter(serverURL string) *WebSocketReporter {
|
||||
func NewWebSocketReporter(serverURL string, secret string) *WebSocketReporter {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// 创建 AES 加密器
|
||||
aesCrypto, err := crypto.NewAESCrypto(secret)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 创建 AES 加密器失败: %v\n", err)
|
||||
aesCrypto = nil
|
||||
} else {
|
||||
fmt.Printf("🔐 AES 加密器创建成功\n")
|
||||
}
|
||||
|
||||
return &WebSocketReporter{
|
||||
url: serverURL,
|
||||
reconnectTime: 5 * time.Second, // 重连间隔
|
||||
@@ -106,6 +118,7 @@ func NewWebSocketReporter(serverURL string) *WebSocketReporter {
|
||||
cancel: cancel,
|
||||
connected: false,
|
||||
connecting: false,
|
||||
aesCrypto: aesCrypto,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +147,7 @@ func (w *WebSocketReporter) run() {
|
||||
w.connMutex.Lock()
|
||||
needConnect := !w.connected && !w.connecting
|
||||
w.connMutex.Unlock()
|
||||
|
||||
|
||||
if needConnect {
|
||||
if err := w.connect(); err != nil {
|
||||
fmt.Printf("❌ WebSocket连接失败: %v,%v后重试\n", err, w.reconnectTime)
|
||||
@@ -167,12 +180,12 @@ func (w *WebSocketReporter) run() {
|
||||
func (w *WebSocketReporter) connect() error {
|
||||
w.connMutex.Lock()
|
||||
defer w.connMutex.Unlock()
|
||||
|
||||
|
||||
// 如果已经在连接中或已连接,直接返回
|
||||
if w.connecting || w.connected {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// 设置连接中状态
|
||||
w.connecting = true
|
||||
defer func() {
|
||||
@@ -242,7 +255,7 @@ func (w *WebSocketReporter) handleConnection() {
|
||||
w.connMutex.Lock()
|
||||
isConnected := w.connected
|
||||
w.connMutex.Unlock()
|
||||
|
||||
|
||||
if !isConnected {
|
||||
return
|
||||
}
|
||||
@@ -276,7 +289,7 @@ func (w *WebSocketReporter) collectSystemInfo() SystemInfo {
|
||||
func (w *WebSocketReporter) sendSystemInfo(sysInfo SystemInfo) error {
|
||||
w.connMutex.Lock()
|
||||
defer w.connMutex.Unlock()
|
||||
|
||||
|
||||
if w.conn == nil || !w.connected {
|
||||
return fmt.Errorf("连接未建立")
|
||||
}
|
||||
@@ -287,10 +300,35 @@ func (w *WebSocketReporter) sendSystemInfo(sysInfo SystemInfo) error {
|
||||
return fmt.Errorf("序列化系统信息失败: %v", err)
|
||||
}
|
||||
|
||||
var messageData []byte
|
||||
|
||||
// 如果有加密器,则加密数据
|
||||
if w.aesCrypto != nil {
|
||||
encryptedData, err := w.aesCrypto.Encrypt(jsonData)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 加密失败,发送原始数据: %v\n", err)
|
||||
messageData = jsonData
|
||||
} else {
|
||||
// 创建加密消息包装器
|
||||
encryptedMessage := map[string]interface{}{
|
||||
"encrypted": true,
|
||||
"data": encryptedData,
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
messageData, err = json.Marshal(encryptedMessage)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 序列化加密消息失败,发送原始数据: %v\n", err)
|
||||
messageData = jsonData
|
||||
}
|
||||
}
|
||||
} else {
|
||||
messageData = jsonData
|
||||
}
|
||||
|
||||
// 设置写入超时
|
||||
w.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
|
||||
if err := w.conn.WriteMessage(websocket.TextMessage, jsonData); err != nil {
|
||||
if err := w.conn.WriteMessage(websocket.TextMessage, messageData); err != nil {
|
||||
w.connected = false // 标记连接已断开
|
||||
return fmt.Errorf("写入消息失败: %v", err)
|
||||
}
|
||||
@@ -309,7 +347,7 @@ func (w *WebSocketReporter) receiveMessages() {
|
||||
conn := w.conn
|
||||
connected := w.connected
|
||||
w.connMutex.Unlock()
|
||||
|
||||
|
||||
if conn == nil || !connected {
|
||||
return
|
||||
}
|
||||
@@ -338,6 +376,30 @@ func (w *WebSocketReporter) receiveMessages() {
|
||||
func (w *WebSocketReporter) handleReceivedMessage(messageType int, message []byte) {
|
||||
switch messageType {
|
||||
case websocket.TextMessage:
|
||||
// 先检查是否是加密消息
|
||||
var encryptedWrapper struct {
|
||||
Encrypted bool `json:"encrypted"`
|
||||
Data string `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// 尝试解析为加密消息格式
|
||||
if err := json.Unmarshal(message, &encryptedWrapper); err == nil && encryptedWrapper.Encrypted {
|
||||
if w.aesCrypto != nil {
|
||||
// 解密数据
|
||||
decryptedData, err := w.aesCrypto.Decrypt(encryptedWrapper.Data)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 解密失败: %v\n", err)
|
||||
w.sendErrorResponse("DecryptError", fmt.Sprintf("解密失败: %v", err))
|
||||
return
|
||||
}
|
||||
message = decryptedData
|
||||
} else {
|
||||
fmt.Printf("❌ 收到加密消息但没有加密器\n")
|
||||
w.sendErrorResponse("NoDecryptor", "没有可用的解密器")
|
||||
return
|
||||
}
|
||||
}
|
||||
// 先尝试解析是否是压缩消息
|
||||
var compressedMsg struct {
|
||||
Type string `json:"type"`
|
||||
@@ -744,7 +806,7 @@ func (w *WebSocketReporter) handleCall(data interface{}) error {
|
||||
func (w *WebSocketReporter) sendResponse(response CommandResponse) {
|
||||
w.connMutex.Lock()
|
||||
defer w.connMutex.Unlock()
|
||||
|
||||
|
||||
if w.conn == nil || !w.connected {
|
||||
fmt.Printf("❌ 无法发送响应:连接未建立\n")
|
||||
return
|
||||
@@ -756,19 +818,44 @@ func (w *WebSocketReporter) sendResponse(response CommandResponse) {
|
||||
return
|
||||
}
|
||||
|
||||
var messageData []byte
|
||||
|
||||
// 如果有加密器,则加密数据
|
||||
if w.aesCrypto != nil {
|
||||
encryptedData, err := w.aesCrypto.Encrypt(jsonData)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 加密响应失败,发送原始数据: %v\n", err)
|
||||
messageData = jsonData
|
||||
} else {
|
||||
// 创建加密消息包装器
|
||||
encryptedMessage := map[string]interface{}{
|
||||
"encrypted": true,
|
||||
"data": encryptedData,
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
messageData, err = json.Marshal(encryptedMessage)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 序列化加密响应失败,发送原始数据: %v\n", err)
|
||||
messageData = jsonData
|
||||
}
|
||||
}
|
||||
} else {
|
||||
messageData = jsonData
|
||||
}
|
||||
|
||||
// 检查消息大小,如果超过10MB则记录警告
|
||||
if len(jsonData) > 10*1024*1024 {
|
||||
fmt.Printf("⚠️ 响应消息过大 (%.2f MB),可能会被拒绝\n", float64(len(jsonData))/(1024*1024))
|
||||
if len(messageData) > 10*1024*1024 {
|
||||
fmt.Printf("⚠️ 响应消息过大 (%.2f MB),可能会被拒绝\n", float64(len(messageData))/(1024*1024))
|
||||
}
|
||||
|
||||
// 设置较长的写入超时,以应对大消息
|
||||
timeout := 5 * time.Second
|
||||
if len(jsonData) > 1024*1024 {
|
||||
if len(messageData) > 1024*1024 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
w.conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if err := w.conn.WriteMessage(websocket.TextMessage, jsonData); err != nil {
|
||||
if err := w.conn.WriteMessage(websocket.TextMessage, messageData); err != nil {
|
||||
fmt.Printf("❌ 发送响应失败: %v\n", err)
|
||||
w.connected = false
|
||||
}
|
||||
@@ -852,7 +939,7 @@ func StartWebSocketReporterWithConfig(Addr string, Secret string, Version string
|
||||
|
||||
fmt.Printf("🔗 WebSocket连接URL: %s\n", fullURL)
|
||||
|
||||
reporter := NewWebSocketReporter(fullURL)
|
||||
reporter := NewWebSocketReporter(fullURL, Secret) // Pass Secret here
|
||||
reporter.Start()
|
||||
return reporter
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ CREATE TABLE `forward` (
|
||||
`out_port` int(10) DEFAULT NULL,
|
||||
`remote_addr` varchar(100) NOT NULL,
|
||||
`strategy` varchar(100) NOT NULL DEFAULT 'fifo',
|
||||
`proxy_protocol` int(10) NOT NULL DEFAULT 0,
|
||||
`in_flow` bigint(20) NOT NULL DEFAULT '0',
|
||||
`out_flow` bigint(20) NOT NULL DEFAULT '0',
|
||||
`created_time` bigint(20) NOT NULL,
|
||||
|
||||
+3
-7
@@ -144,19 +144,15 @@ install_gost() {
|
||||
# 打印版本
|
||||
echo "🔎 gost 版本:$($INSTALL_DIR/gost -V)"
|
||||
|
||||
# 写入 config.json
|
||||
# 写入 config.json (安装时总是创建新的)
|
||||
CONFIG_FILE="$INSTALL_DIR/config.json"
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
echo "⏭️ 跳过配置文件: config.json (已存在)"
|
||||
else
|
||||
echo "📄 创建新配置: config.json"
|
||||
cat > "$CONFIG_FILE" <<EOF
|
||||
echo "📄 创建新配置: config.json"
|
||||
cat > "$CONFIG_FILE" <<EOF
|
||||
{
|
||||
"addr": "$SERVER_ADDR",
|
||||
"secret": "$SECRET"
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# 写入 gost.json
|
||||
GOST_CONFIG="$INSTALL_DIR/gost.json"
|
||||
|
||||
+142
-26
@@ -137,7 +137,8 @@ show_menu() {
|
||||
echo "1. 安装面板"
|
||||
echo "2. 更新面板"
|
||||
echo "3. 卸载面板"
|
||||
echo "4. 退出"
|
||||
echo "4. 导出数据库备份"
|
||||
echo "5. 退出"
|
||||
echo "==============================================="
|
||||
}
|
||||
|
||||
@@ -184,8 +185,15 @@ install_panel() {
|
||||
DOCKER_COMPOSE_URL=$(get_docker_compose_url)
|
||||
echo "📡 选择配置文件:$(basename "$DOCKER_COMPOSE_URL")"
|
||||
curl -L -o docker-compose.yml "$DOCKER_COMPOSE_URL"
|
||||
curl -L -o gost.sql "$GOST_SQL_URL"
|
||||
echo "✅ 下载完成"
|
||||
|
||||
# 检查 gost.sql 是否已存在
|
||||
if [[ -f "gost.sql" ]]; then
|
||||
echo "⏭️ 跳过下载: gost.sql (使用当前位置的文件)"
|
||||
else
|
||||
echo "📡 下载数据库初始化文件..."
|
||||
curl -L -o gost.sql "$GOST_SQL_URL"
|
||||
fi
|
||||
echo "✅ 文件准备完成"
|
||||
|
||||
# 自动检测并配置 IPv6 支持
|
||||
if check_ipv6_support; then
|
||||
@@ -667,6 +675,24 @@ UPDATE \`tunnel\`
|
||||
SET \`traffic_ratio\` = 1.0
|
||||
WHERE \`traffic_ratio\` IS NULL;
|
||||
|
||||
-- forward 表:删除 proxy_protocol 字段(如果存在)
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'forward'
|
||||
AND column_name = 'proxy_protocol'
|
||||
),
|
||||
'ALTER TABLE \`forward\` DROP COLUMN \`proxy_protocol\`;',
|
||||
'SELECT "Column \`proxy_protocol\` not exists in \`forward\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- forward 表:修改 remote_addr 字段类型为 longtext
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
@@ -709,28 +735,7 @@ UPDATE \`forward\`
|
||||
SET \`strategy\` = 'fifo'
|
||||
WHERE \`strategy\` IS NULL;
|
||||
|
||||
-- forward 表:添加 proxy_protocol 字段
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'forward'
|
||||
AND column_name = 'proxy_protocol'
|
||||
),
|
||||
'ALTER TABLE \`forward\` ADD COLUMN \`proxy_protocol\` INT(10) NOT NULL DEFAULT 0 COMMENT "Proxy Protocol 支持";',
|
||||
'SELECT "Column \`proxy_protocol\` already exists in \`forward\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 为现有数据设置默认 proxy_protocol 值
|
||||
UPDATE \`forward\`
|
||||
SET \`proxy_protocol\` = 0
|
||||
WHERE \`proxy_protocol\` IS NULL;
|
||||
EOF
|
||||
|
||||
# 检查数据库容器
|
||||
@@ -765,6 +770,113 @@ EOF
|
||||
echo "✅ 更新完成"
|
||||
}
|
||||
|
||||
# 导出数据库备份
|
||||
export_migration_sql() {
|
||||
echo "📄 开始导出数据库备份..."
|
||||
|
||||
# 获取数据库配置信息
|
||||
echo "🔍 获取数据库配置信息..."
|
||||
|
||||
# 先检查后端容器是否在运行
|
||||
if ! docker ps --format "{{.Names}}" | grep -q "^springboot-backend$"; then
|
||||
echo "❌ 后端容器未运行,尝试从 .env 文件读取配置..."
|
||||
|
||||
# 从 .env 文件读取配置
|
||||
if [[ -f ".env" ]]; then
|
||||
DB_NAME=$(grep "^DB_NAME=" .env | cut -d'=' -f2 2>/dev/null)
|
||||
DB_PASSWORD=$(grep "^DB_PASSWORD=" .env | cut -d'=' -f2 2>/dev/null)
|
||||
DB_USER=$(grep "^DB_USER=" .env | cut -d'=' -f2 2>/dev/null)
|
||||
|
||||
if [[ -n "$DB_NAME" && -n "$DB_PASSWORD" && -n "$DB_USER" ]]; then
|
||||
echo "✅ 从 .env 文件读取数据库配置成功"
|
||||
else
|
||||
echo "❌ .env 文件中的数据库配置不完整"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo "❌ 未找到 .env 文件"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
# 从容器环境变量获取数据库信息
|
||||
DB_INFO=$(docker exec springboot-backend env | grep "^DB_" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$DB_INFO" ]]; then
|
||||
DB_NAME=$(echo "$DB_INFO" | grep "^DB_NAME=" | cut -d'=' -f2)
|
||||
DB_PASSWORD=$(echo "$DB_INFO" | grep "^DB_PASSWORD=" | cut -d'=' -f2)
|
||||
DB_USER=$(echo "$DB_INFO" | grep "^DB_USER=" | cut -d'=' -f2)
|
||||
|
||||
echo "✅ 从容器环境变量读取数据库配置成功"
|
||||
else
|
||||
echo "❌ 无法从容器获取数据库配置,尝试从 .env 文件读取..."
|
||||
|
||||
if [[ -f ".env" ]]; then
|
||||
DB_NAME=$(grep "^DB_NAME=" .env | cut -d'=' -f2 2>/dev/null)
|
||||
DB_PASSWORD=$(grep "^DB_PASSWORD=" .env | cut -d'=' -f2 2>/dev/null)
|
||||
DB_USER=$(grep "^DB_USER=" .env | cut -d'=' -f2 2>/dev/null)
|
||||
|
||||
if [[ -n "$DB_NAME" && -n "$DB_PASSWORD" && -n "$DB_USER" ]]; then
|
||||
echo "✅ 从 .env 文件读取数据库配置成功"
|
||||
else
|
||||
echo "❌ .env 文件中的数据库配置不完整"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo "❌ 未找到 .env 文件"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# 检查必要的数据库配置
|
||||
if [[ -z "$DB_PASSWORD" || -z "$DB_USER" || -z "$DB_NAME" ]]; then
|
||||
echo "❌ 数据库配置不完整(缺少必要参数)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "📋 数据库配置:"
|
||||
echo " 数据库名: $DB_NAME"
|
||||
echo " 用户名: $DB_USER"
|
||||
|
||||
# 检查数据库容器是否运行
|
||||
if ! docker ps --format "{{.Names}}" | grep -q "^gost-mysql$"; then
|
||||
echo "❌ 数据库容器未运行,无法导出数据"
|
||||
echo "🔍 当前运行的容器:"
|
||||
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 生成数据库备份文件
|
||||
SQL_FILE="database_backup_$(date +%Y%m%d_%H%M%S).sql"
|
||||
echo "📝 导出数据库备份: $SQL_FILE"
|
||||
|
||||
# 使用 mysqldump 导出数据库
|
||||
echo "⏳ 正在导出数据库..."
|
||||
if docker exec gost-mysql mysqldump -u "$DB_USER" -p"$DB_PASSWORD" --single-transaction --routines --triggers "$DB_NAME" > "$SQL_FILE" 2>/dev/null; then
|
||||
echo "✅ 数据库导出成功"
|
||||
else
|
||||
echo "⚠️ 使用用户密码失败,尝试root密码..."
|
||||
if docker exec gost-mysql mysqldump -u root -p"$DB_PASSWORD" --single-transaction --routines --triggers "$DB_NAME" > "$SQL_FILE" 2>/dev/null; then
|
||||
echo "✅ 数据库导出成功"
|
||||
else
|
||||
echo "❌ 数据库导出失败"
|
||||
rm -f "$SQL_FILE"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 检查文件大小
|
||||
if [[ -f "$SQL_FILE" ]] && [[ -s "$SQL_FILE" ]]; then
|
||||
FILE_SIZE=$(du -h "$SQL_FILE" | cut -f1)
|
||||
echo "📁 文件位置: $(pwd)/$SQL_FILE"
|
||||
echo "📊 文件大小: $FILE_SIZE"
|
||||
else
|
||||
echo "❌ 导出的文件为空或不存在"
|
||||
rm -f "$SQL_FILE"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 卸载功能
|
||||
uninstall_panel() {
|
||||
echo "🗑️ 开始卸载面板..."
|
||||
@@ -796,7 +908,7 @@ main() {
|
||||
# 显示交互式菜单
|
||||
while true; do
|
||||
show_menu
|
||||
read -p "请输入选项 (1-4): " choice
|
||||
read -p "请输入选项 (1-5): " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
@@ -812,11 +924,15 @@ main() {
|
||||
break
|
||||
;;
|
||||
4)
|
||||
export_migration_sql
|
||||
break
|
||||
;;
|
||||
5)
|
||||
echo "👋 退出脚本"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "❌ 无效选项,请输入 1-4"
|
||||
echo "❌ 无效选项,请输入 1-5"
|
||||
echo ""
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -26,9 +26,5 @@ public class ForwardDto {
|
||||
@Min(value = 1, message = "端口号不能小于1")
|
||||
@Max(value = 65535, message = "端口号不能大于65535")
|
||||
private Integer inPort;
|
||||
|
||||
/**
|
||||
* 是否启用代理协议(0: 禁用, 1: 启用)
|
||||
*/
|
||||
private Integer proxyProtocol = 0; // 设置默认值为0(禁用)
|
||||
|
||||
}
|
||||
@@ -32,9 +32,4 @@ public class ForwardUpdateDto {
|
||||
@Min(value = 1, message = "端口号不能小于1")
|
||||
@Max(value = 65535, message = "端口号不能大于65535")
|
||||
private Integer inPort;
|
||||
|
||||
/**
|
||||
* 是否启用代理协议(0: 禁用, 1: 启用)
|
||||
*/
|
||||
private Integer proxyProtocol = 0; // 设置默认值为0(禁用)
|
||||
}
|
||||
@@ -85,9 +85,4 @@ public class ForwardWithTunnelDto {
|
||||
private Long outFlow;
|
||||
|
||||
private String strategy;
|
||||
|
||||
/**
|
||||
* 是否启用代理协议(0: 禁用, 1: 启用)
|
||||
*/
|
||||
private Integer proxyProtocol;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
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();
|
||||
|
||||
log.debug("AES加密器初始化成功");
|
||||
} catch (Exception e) {
|
||||
log.error("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.error("数据加密失败", 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.error("数据解密失败", 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.error("创建AES加密器失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,21 +31,21 @@ public class GostUtil {
|
||||
return WebSocketServer.send_msg(node_id, req, "DeleteLimiters");
|
||||
}
|
||||
|
||||
public static GostDto AddService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy, Integer proxy_protocol) {
|
||||
public static GostDto AddService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy) {
|
||||
JSONArray services = new JSONArray();
|
||||
String[] protocols = {"tcp", "udp"};
|
||||
for (String protocol : protocols) {
|
||||
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy, proxy_protocol);
|
||||
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy);
|
||||
services.add(service);
|
||||
}
|
||||
return WebSocketServer.send_msg(node_id, services, "AddService");
|
||||
}
|
||||
|
||||
public static GostDto UpdateService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy, Integer proxy_protocol) {
|
||||
public static GostDto UpdateService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy) {
|
||||
JSONArray services = new JSONArray();
|
||||
String[] protocols = {"tcp", "udp"};
|
||||
for (String protocol : protocols) {
|
||||
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy, proxy_protocol);
|
||||
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy);
|
||||
services.add(service);
|
||||
}
|
||||
return WebSocketServer.send_msg(node_id, services, "UpdateService");
|
||||
@@ -255,7 +255,7 @@ public class GostUtil {
|
||||
return data;
|
||||
}
|
||||
|
||||
private static JSONObject createServiceConfig(String name, Integer in_port, Integer limiter, String remoteAddr, String protocol, Integer fow_type, Tunnel tunnel, String strategy, Integer proxy_protocol) {
|
||||
private static JSONObject createServiceConfig(String name, Integer in_port, Integer limiter, String remoteAddr, String protocol, Integer fow_type, Tunnel tunnel, String strategy) {
|
||||
JSONObject service = new JSONObject();
|
||||
service.put("name", name + "_" + protocol);
|
||||
if (Objects.equals(protocol, "tcp")){
|
||||
@@ -279,13 +279,9 @@ public class GostUtil {
|
||||
|
||||
// 端口转发需要配置转发器
|
||||
if (isPortForwarding(fow_type)) {
|
||||
JSONObject forwarder = createForwarder(protocol, remoteAddr, strategy);
|
||||
JSONObject forwarder = createForwarder(remoteAddr, strategy);
|
||||
service.put("forwarder", forwarder);
|
||||
}
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("proxyProtocol", proxy_protocol);
|
||||
service.put("metadata", metadata);
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
@@ -312,7 +308,7 @@ public class GostUtil {
|
||||
return listener;
|
||||
}
|
||||
|
||||
private static JSONObject createForwarder(String protocol, String remoteAddr, String strategy) {
|
||||
private static JSONObject createForwarder(String remoteAddr, String strategy) {
|
||||
JSONObject forwarder = new JSONObject();
|
||||
JSONArray nodes = new JSONArray();
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.admin.common.dto.GostDto;
|
||||
import com.admin.common.task.CheckGostConfigAsync;
|
||||
import com.admin.entity.Node;
|
||||
import com.admin.service.NodeService;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -42,6 +43,25 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
// 存储等待响应的请求,key为requestId,value为CompletableFuture
|
||||
private static final ConcurrentHashMap<String, CompletableFuture<GostDto>> pendingRequests = new ConcurrentHashMap<>();
|
||||
|
||||
// 缓存加密器实例,避免重复创建
|
||||
private static final ConcurrentHashMap<String, AESCrypto> cryptoCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 加密消息包装器
|
||||
*/
|
||||
public static class EncryptedMessage {
|
||||
private boolean encrypted;
|
||||
private String data;
|
||||
private Long timestamp;
|
||||
|
||||
// getters and setters
|
||||
public boolean isEncrypted() { return encrypted; }
|
||||
public void setEncrypted(boolean encrypted) { this.encrypted = encrypted; }
|
||||
public String getData() { return data; }
|
||||
public void setData(String data) { this.data = data; }
|
||||
public Long getTimestamp() { return timestamp; }
|
||||
public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }
|
||||
}
|
||||
|
||||
//接受客户端消息
|
||||
@Override
|
||||
@@ -51,15 +71,19 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
|
||||
String id = session.getAttributes().get("id").toString();
|
||||
String type = session.getAttributes().get("type").toString();
|
||||
String nodeSecret = (String) session.getAttributes().get("nodeSecret");
|
||||
|
||||
// 尝试解密消息
|
||||
String decryptedPayload = decryptMessageIfNeeded(message.getPayload(), nodeSecret);
|
||||
|
||||
if (message.getPayload().contains("memory_usage")){
|
||||
if (decryptedPayload.contains("memory_usage")){
|
||||
// 先发送确认消息
|
||||
sendToUser(session, "{\"type\":\"call\"}");
|
||||
}else if (message.getPayload().contains("requestId")) {
|
||||
log.info("收到消息: {}", message.getPayload());
|
||||
sendToUser(session, "{\"type\":\"call\"}", nodeSecret);
|
||||
}else if (decryptedPayload.contains("requestId")) {
|
||||
log.info("收到消息: {}", decryptedPayload);
|
||||
// 处理命令响应消息
|
||||
try {
|
||||
JSONObject responseJson = JSONObject.parseObject(message.getPayload());
|
||||
JSONObject responseJson = JSONObject.parseObject(decryptedPayload);
|
||||
String requestId = responseJson.getString("requestId");
|
||||
String responseMessage = responseJson.getString("message");
|
||||
String responseType = responseJson.getString("type");
|
||||
@@ -91,7 +115,7 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
log.error("处理响应消息失败: {}", e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
log.info("收到消息: {}", message.getPayload());
|
||||
log.info("收到消息: {}", decryptedPayload);
|
||||
}
|
||||
|
||||
// 如果是节点类型,转发消息给其他会话
|
||||
@@ -99,13 +123,13 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("id", id);
|
||||
jsonObject.put("type", "info");
|
||||
jsonObject.put("data", message.getPayload());
|
||||
jsonObject.put("data", decryptedPayload);
|
||||
String broadcastMessage = jsonObject.toJSONString();
|
||||
|
||||
// 异步处理广播消息,避免阻塞当前线程
|
||||
for (WebSocketSession targetSession : activeSessions) {
|
||||
if (targetSession != null && targetSession.isOpen() && !targetSession.equals(session)) {
|
||||
sendToUser(targetSession, broadcastMessage);
|
||||
sendToUser(targetSession, broadcastMessage, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +139,78 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试解密消息(如果需要)
|
||||
*/
|
||||
private String decryptMessageIfNeeded(String payload, String nodeSecret) {
|
||||
if (payload == null || payload.trim().isEmpty()) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试解析为加密消息格式
|
||||
EncryptedMessage encryptedMessage = JSON.parseObject(payload, EncryptedMessage.class);
|
||||
|
||||
if (encryptedMessage.isEncrypted() && encryptedMessage.getData() != null) {
|
||||
// 获取或创建加密器
|
||||
AESCrypto crypto = getOrCreateCrypto(nodeSecret);
|
||||
if (crypto == null) {
|
||||
log.warn("⚠️ 收到加密消息但无法创建解密器,使用原始数据");
|
||||
return payload;
|
||||
}
|
||||
|
||||
// 解密数据
|
||||
String decryptedData = crypto.decryptString(encryptedMessage.getData());
|
||||
log.debug("🔓 WebSocket消息解密成功");
|
||||
return decryptedData;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 解析失败,可能是非加密格式,直接返回原始数据
|
||||
log.debug("WebSocket消息未加密或解密失败,使用原始数据: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密消息(如果可能)
|
||||
*/
|
||||
private static String encryptMessageIfPossible(String message, String nodeSecret) {
|
||||
if (message == null || nodeSecret == null) {
|
||||
return message;
|
||||
}
|
||||
|
||||
try {
|
||||
AESCrypto crypto = getOrCreateCrypto(nodeSecret);
|
||||
if (crypto != null) {
|
||||
String encryptedData = crypto.encrypt(message);
|
||||
|
||||
// 创建加密消息包装器
|
||||
JSONObject encryptedMessage = new JSONObject();
|
||||
encryptedMessage.put("encrypted", true);
|
||||
encryptedMessage.put("data", encryptedData);
|
||||
encryptedMessage.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
log.debug("🔐 WebSocket消息加密成功");
|
||||
return encryptedMessage.toJSONString();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("⚠️ WebSocket消息加密失败,发送原始数据: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建加密器实例
|
||||
*/
|
||||
private static AESCrypto getOrCreateCrypto(String secret) {
|
||||
if (secret == null || secret.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return cryptoCache.computeIfAbsent(secret, AESCrypto::create);
|
||||
}
|
||||
|
||||
// 建立连接
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
@@ -232,7 +328,7 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
boolean shouldUpdateOffline = true;
|
||||
try {
|
||||
// 尝试发送验证消息,如果发送成功说明连接可能还活跃
|
||||
sendToUser(session, "{\"type\":\"call\"}");
|
||||
sendToUser(session, "{\"type\":\"call\"}", null);
|
||||
log.warn("节点 {} 连接关闭但仍能发送消息,可能是假断开", nodeId);
|
||||
shouldUpdateOffline = false;
|
||||
} catch (Exception e) {
|
||||
@@ -277,6 +373,12 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
// 点对点发送消息
|
||||
@SneakyThrows
|
||||
public static void sendToUser(WebSocketSession socketSession, String message) {
|
||||
sendToUser(socketSession, message, null);
|
||||
}
|
||||
|
||||
// 点对点发送消息(支持加密)
|
||||
@SneakyThrows
|
||||
public static void sendToUser(WebSocketSession socketSession, String message, String nodeSecret) {
|
||||
if (socketSession != null && socketSession.isOpen()) {
|
||||
String sessionId = socketSession.getId();
|
||||
Object lock = sessionLocks.computeIfAbsent(sessionId, k -> new Object());
|
||||
@@ -284,7 +386,15 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
synchronized (lock) {
|
||||
try {
|
||||
if (socketSession.isOpen()) {
|
||||
socketSession.sendMessage(new TextMessage(message));
|
||||
// 如果是节点连接且有密钥,尝试加密消息
|
||||
String finalMessage = message;
|
||||
if (nodeSecret != null && !nodeSecret.isEmpty()) {
|
||||
String type = (String) socketSession.getAttributes().get("type");
|
||||
if ("1".equals(type)) { // 节点连接
|
||||
finalMessage = encryptMessageIfPossible(message, nodeSecret);
|
||||
}
|
||||
}
|
||||
socketSession.sendMessage(new TextMessage(finalMessage));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("发送WebSocket消息失败 [sessionId={}]: {}", sessionId, e.getMessage());
|
||||
@@ -354,13 +464,15 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
CompletableFuture<GostDto> future = new CompletableFuture<>();
|
||||
pendingRequests.put(requestId, future);
|
||||
|
||||
// 获取节点密钥用于加密
|
||||
String nodeSecret = (String) nodeSession.getAttributes().get("nodeSecret");
|
||||
|
||||
try {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("type", type);
|
||||
data.put("data", msg);
|
||||
data.put("requestId", requestId);
|
||||
sendToUser(nodeSession, data.toJSONString());
|
||||
sendToUser(nodeSession, data.toJSONString(), nodeSecret);
|
||||
GostDto result = future.get(10, TimeUnit.SECONDS);
|
||||
|
||||
log.debug("成功发送消息到节点 {} 并收到响应: {}", node_id, result.getMsg());
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.admin.config;
|
||||
|
||||
import com.admin.common.utils.AESCrypto;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 加密配置管理类
|
||||
* 统一管理AES加密功能,为HTTP上报和WebSocket通信提供加密支持
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class EncryptionConfig {
|
||||
|
||||
// 缓存加密器实例,避免重复创建
|
||||
private static final ConcurrentHashMap<String, AESCrypto> CRYPTO_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 获取或创建AES加密器实例
|
||||
*
|
||||
* @param secret 密钥字符串
|
||||
* @return AES加密器实例,如果创建失败返回null
|
||||
*/
|
||||
public static AESCrypto getOrCreateCrypto(String secret) {
|
||||
if (secret == null || secret.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return CRYPTO_CACHE.computeIfAbsent(secret, AESCrypto::create);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测消息是否为加密格式
|
||||
*
|
||||
* @param message 消息内容
|
||||
* @return 如果是加密格式返回true,否则返回false
|
||||
*/
|
||||
public static boolean isEncryptedMessage(String message) {
|
||||
if (message == null || message.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 简单检查是否包含加密标识
|
||||
return message.contains("\"encrypted\":true") || message.contains("\"encrypted\": true");
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存的加密器实例
|
||||
*
|
||||
* @param secret 要清理的密钥
|
||||
*/
|
||||
public static void clearCrypto(String secret) {
|
||||
if (secret != null) {
|
||||
CRYPTO_CACHE.remove(secret);
|
||||
log.debug("已清理密钥对应的加密器实例");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有缓存的加密器实例
|
||||
*/
|
||||
public static void clearAllCrypto() {
|
||||
int size = CRYPTO_CACHE.size();
|
||||
CRYPTO_CACHE.clear();
|
||||
log.info("已清理所有加密器实例缓存,共清理 {} 个实例", size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前缓存的加密器数量
|
||||
*
|
||||
* @return 缓存的加密器数量
|
||||
*/
|
||||
public static int getCacheSize() {
|
||||
return CRYPTO_CACHE.size();
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,15 @@ import com.admin.common.dto.FlowDto;
|
||||
import com.admin.common.dto.GostConfigDto;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.common.task.CheckGostConfigAsync;
|
||||
import com.admin.common.utils.AESCrypto;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
import com.admin.entity.*;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
@@ -41,6 +45,7 @@ import java.util.stream.Collectors;
|
||||
@RestController
|
||||
@RequestMapping("/flow")
|
||||
@CrossOrigin
|
||||
@Slf4j
|
||||
public class FlowController extends BaseController {
|
||||
|
||||
// 常量定义
|
||||
@@ -55,19 +60,51 @@ public class FlowController extends BaseController {
|
||||
private static final ConcurrentHashMap<String, Object> TUNNEL_LOCKS = new ConcurrentHashMap<>();
|
||||
private static final ConcurrentHashMap<String, Object> FORWARD_LOCKS = new ConcurrentHashMap<>();
|
||||
|
||||
// 缓存加密器实例,避免重复创建
|
||||
private static final ConcurrentHashMap<String, AESCrypto> CRYPTO_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
@Resource
|
||||
CheckGostConfigAsync checkGostConfigAsync;
|
||||
|
||||
@PostMapping("/config")
|
||||
@LogAnnotation
|
||||
public String config(@RequestBody GostConfigDto gostConfigDto, String secret) {
|
||||
Node node = nodeService.getOne(new QueryWrapper<Node>().eq("secret", secret));
|
||||
if (node == null) return SUCCESS_RESPONSE;
|
||||
checkGostConfigAsync.cleanNodeConfigs(node.getId().toString(), gostConfigDto);
|
||||
return SUCCESS_RESPONSE;
|
||||
/**
|
||||
* 加密消息包装器
|
||||
*/
|
||||
public static class EncryptedMessage {
|
||||
private boolean encrypted;
|
||||
private String data;
|
||||
private Long timestamp;
|
||||
|
||||
// getters and setters
|
||||
public boolean isEncrypted() { return encrypted; }
|
||||
public void setEncrypted(boolean encrypted) { this.encrypted = encrypted; }
|
||||
public String getData() { return data; }
|
||||
public void setData(String data) { this.data = data; }
|
||||
public Long getTimestamp() { return timestamp; }
|
||||
public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }
|
||||
}
|
||||
|
||||
@PostMapping("/config")
|
||||
@LogAnnotation
|
||||
public String config(@RequestBody String rawData, String secret) {
|
||||
Node node = nodeService.getOne(new QueryWrapper<Node>().eq("secret", secret));
|
||||
if (node == null) return SUCCESS_RESPONSE;
|
||||
|
||||
try {
|
||||
// 尝试解密数据
|
||||
String decryptedData = decryptIfNeeded(rawData, secret);
|
||||
|
||||
// 解析为GostConfigDto
|
||||
GostConfigDto gostConfigDto = JSON.parseObject(decryptedData, GostConfigDto.class);
|
||||
checkGostConfigAsync.cleanNodeConfigs(node.getId().toString(), gostConfigDto);
|
||||
|
||||
log.info("🔓 节点 {} 配置数据接收成功{}", node.getId(), isEncryptedMessage(rawData) ? "(已解密)" : "");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理节点 {} 配置数据失败: {}", node.getId(), e.getMessage());
|
||||
}
|
||||
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
|
||||
@RequestMapping("/test")
|
||||
@LogAnnotation
|
||||
@@ -78,32 +115,107 @@ public class FlowController extends BaseController {
|
||||
/**
|
||||
* 处理流量数据上报
|
||||
*
|
||||
* @param flowDataList 流量数据列表
|
||||
* @param secret 节点密钥
|
||||
* @param rawData 原始数据(可能是加密的)
|
||||
* @param secret 节点密钥
|
||||
* @return 处理结果
|
||||
*/
|
||||
@RequestMapping("/upload")
|
||||
@LogAnnotation
|
||||
public String uploadFlowData(@RequestBody List<FlowDto> flowDataList, String secret) {
|
||||
public String uploadFlowData(@RequestBody String rawData, String secret) {
|
||||
// 1. 验证节点权限
|
||||
if (!isValidNode(secret)) {
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
if (flowDataList.isEmpty()) {
|
||||
|
||||
try {
|
||||
// 2. 尝试解密数据
|
||||
String decryptedData = decryptIfNeeded(rawData, secret);
|
||||
|
||||
// 3. 解析为FlowDto列表
|
||||
List<FlowDto> flowDataList = JSON.parseArray(decryptedData, FlowDto.class);
|
||||
|
||||
if (flowDataList.isEmpty()) {
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
if (Objects.equals(flowDataList.get(0).getN(), "web_api")) {
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
log.debug("🔓 节点流量数据接收成功{}",
|
||||
isEncryptedMessage(rawData) ? "(已解密)" : "");
|
||||
|
||||
// 4. 处理流量数据
|
||||
return processFlowData(flowDataList);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理流量数据失败: {}", e.getMessage(), e);
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
if(Objects.equals(flowDataList.get(0).getN(), "web_api")){
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测消息是否为加密格式
|
||||
*/
|
||||
private boolean isEncryptedMessage(String data) {
|
||||
try {
|
||||
JSONObject json = JSON.parseObject(data);
|
||||
return json.getBooleanValue("encrypted");
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据需要解密数据
|
||||
*/
|
||||
private String decryptIfNeeded(String rawData, String secret) {
|
||||
if (rawData == null || rawData.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("数据不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试解析为加密消息格式
|
||||
EncryptedMessage encryptedMessage = JSON.parseObject(rawData, EncryptedMessage.class);
|
||||
|
||||
if (encryptedMessage.isEncrypted() && encryptedMessage.getData() != null) {
|
||||
// 获取或创建加密器
|
||||
AESCrypto crypto = getOrCreateCrypto(secret);
|
||||
if (crypto == null) {
|
||||
log.warn("⚠️ 收到加密消息但无法创建解密器,使用原始数据");
|
||||
return rawData;
|
||||
}
|
||||
|
||||
// 解密数据
|
||||
String decryptedData = crypto.decryptString(encryptedMessage.getData());
|
||||
log.debug("🔓 数据解密成功");
|
||||
return decryptedData;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 解析失败,可能是非加密格式,直接返回原始数据
|
||||
log.debug("数据未加密或解密失败,使用原始数据: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return rawData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建加密器实例
|
||||
*/
|
||||
private AESCrypto getOrCreateCrypto(String secret) {
|
||||
return CRYPTO_CACHE.computeIfAbsent(secret, AESCrypto::create);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理流量数据的核心逻辑
|
||||
*/
|
||||
private String processFlowData(List<FlowDto> flowDataList) {
|
||||
// 2. 解析服务名称获取ID信息
|
||||
String[] serviceIds = parseServiceName(flowDataList.get(0).getN());
|
||||
String forwardId = serviceIds[0];
|
||||
String userId = serviceIds[1];
|
||||
String userTunnelId = serviceIds[2];
|
||||
|
||||
|
||||
// 3. 一次性查询相关实体,避免后续重复查询
|
||||
Forward forward = forwardService.getById(forwardId);
|
||||
User user = userService.getById(userId);
|
||||
@@ -111,16 +223,13 @@ public class FlowController extends BaseController {
|
||||
if (!Objects.equals(userTunnelId, DEFAULT_USER_TUNNEL_ID)) {
|
||||
userTunnel = userTunnelService.getById(userTunnelId);
|
||||
}
|
||||
|
||||
// 4. 处理流量倍率
|
||||
List<FlowDto> validFlowData = filterFlowData(flowDataList, forward);
|
||||
|
||||
|
||||
|
||||
// 5. 计算总流量
|
||||
FlowStatistics flowStats = calculateTotalFlow(validFlowData);
|
||||
|
||||
|
||||
|
||||
// 6. 获取流量计费类型
|
||||
int flowType = getFlowType(forward);
|
||||
|
||||
|
||||
@@ -38,7 +38,4 @@ public class Forward extends BaseEntity{
|
||||
|
||||
private Long outFlow;
|
||||
|
||||
private Integer proxyProtocol;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -835,7 +835,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
|
||||
|
||||
// 创建主服务
|
||||
R serviceResult = createMainService(nodeInfo.getInNode(), serviceName, forward, limiter, tunnel.getType(), tunnel, forward.getStrategy(), forward.getProxyProtocol());
|
||||
R serviceResult = createMainService(nodeInfo.getInNode(), serviceName, forward, limiter, tunnel.getType(), tunnel, forward.getStrategy());
|
||||
if (serviceResult.getCode() != 0) {
|
||||
GostUtil.DeleteChains(nodeInfo.getInNode().getId(), serviceName);
|
||||
if (nodeInfo.getOutNode() != null) {
|
||||
@@ -1008,8 +1008,8 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
/**
|
||||
* 创建主服务
|
||||
*/
|
||||
private R createMainService(Node inNode, String serviceName, Forward forward, Integer limiter, Integer tunnelType, Tunnel tunnel, String strategy, Integer proxy_protocol) {
|
||||
GostDto result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy, proxy_protocol);
|
||||
private R createMainService(Node inNode, String serviceName, Forward forward, Integer limiter, Integer tunnelType, Tunnel tunnel, String strategy) {
|
||||
GostDto result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy);
|
||||
return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg());
|
||||
}
|
||||
|
||||
@@ -1049,10 +1049,10 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
* 更新主服务
|
||||
*/
|
||||
private R updateMainService(Node inNode, String serviceName, Forward forward, Integer limiter, Integer tunnelType, Tunnel tunnel, String strategy) {
|
||||
GostDto result = GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy, forward.getProxyProtocol());
|
||||
GostDto result = GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy);
|
||||
|
||||
if (result.getMsg().contains(GOST_NOT_FOUND_MSG)) {
|
||||
result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy, forward.getProxyProtocol());
|
||||
result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy);
|
||||
}
|
||||
|
||||
return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg());
|
||||
|
||||
@@ -499,7 +499,7 @@ public class UserTunnelServiceImpl extends ServiceImpl<UserTunnelMapper, UserTun
|
||||
String serviceName = buildServiceName(forward.getId(), Long.valueOf(userId), userTunnel.getId());
|
||||
|
||||
// 6. 更新入口节点的主服务限速配置(使用批量UpdateService接口)
|
||||
GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), speedId, forward.getRemoteAddr(), tunnel.getType(), tunnel, forward.getStrategy(), forward.getProxyProtocol());
|
||||
GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), speedId, forward.getRemoteAddr(), tunnel.getType(), tunnel, forward.getStrategy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
f.remote_addr AS remoteAddr,
|
||||
f.status,
|
||||
f.created_time AS createdTime,
|
||||
f.proxy_protocol AS proxyProtocol,
|
||||
f.updated_time AS updatedTime,
|
||||
f.user_name AS userName,
|
||||
f.in_flow AS inFlow,
|
||||
@@ -48,7 +47,6 @@
|
||||
f.updated_time AS updatedTime,
|
||||
f.user_name AS userName,
|
||||
f.in_flow AS inFlow,
|
||||
f.proxy_protocol AS proxyProtocol,
|
||||
f.out_flow AS outFlow,
|
||||
f.strategy AS strategy,
|
||||
t.name AS tunnelName,
|
||||
|
||||
@@ -33,7 +33,6 @@ interface Forward {
|
||||
inPort: number;
|
||||
remoteAddr: string;
|
||||
strategy: string;
|
||||
proxyProtocol: number; // 添加 proxy_protocol 字段
|
||||
status: number;
|
||||
inFlow: number;
|
||||
outFlow: number;
|
||||
@@ -58,7 +57,6 @@ interface ForwardForm {
|
||||
inPort: number | null;
|
||||
remoteAddr: string;
|
||||
strategy: string;
|
||||
proxyProtocol: boolean; // 在表单中使用 boolean 类型更方便
|
||||
}
|
||||
|
||||
interface AddressItem {
|
||||
@@ -108,8 +106,7 @@ export default function ForwardPage() {
|
||||
tunnelId: null,
|
||||
inPort: null,
|
||||
remoteAddr: '',
|
||||
strategy: 'fifo',
|
||||
proxyProtocol: false // 新增 proxyProtocol 字段
|
||||
strategy: 'fifo'
|
||||
});
|
||||
|
||||
// 表单验证错误
|
||||
@@ -205,8 +202,7 @@ export default function ForwardPage() {
|
||||
tunnelId: null,
|
||||
inPort: null,
|
||||
remoteAddr: '',
|
||||
strategy: 'fifo',
|
||||
proxyProtocol: false
|
||||
strategy: 'fifo'
|
||||
});
|
||||
setSelectedTunnel(null);
|
||||
setErrors({});
|
||||
@@ -223,8 +219,7 @@ export default function ForwardPage() {
|
||||
tunnelId: forward.tunnelId,
|
||||
inPort: forward.inPort,
|
||||
remoteAddr: forward.remoteAddr.split(',').join('\n'),
|
||||
strategy: forward.strategy || 'fifo',
|
||||
proxyProtocol: forward.proxyProtocol === 1 // 编辑时需要将数字转换为 boolean
|
||||
strategy: forward.strategy || 'fifo'
|
||||
});
|
||||
const tunnel = tunnels.find(t => t.id === forward.tunnelId);
|
||||
setSelectedTunnel(tunnel || null);
|
||||
@@ -302,8 +297,7 @@ export default function ForwardPage() {
|
||||
tunnelId: form.tunnelId,
|
||||
inPort: form.inPort,
|
||||
remoteAddr: processedRemoteAddr,
|
||||
strategy: addressCount > 1 ? form.strategy : 'fifo',
|
||||
proxyProtocol: form.proxyProtocol ? 1 : 0 // 提交时需要将 boolean 转换为数字
|
||||
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
||||
};
|
||||
res = await updateForward(updateData);
|
||||
} else {
|
||||
@@ -313,8 +307,7 @@ export default function ForwardPage() {
|
||||
tunnelId: form.tunnelId,
|
||||
inPort: form.inPort,
|
||||
remoteAddr: processedRemoteAddr,
|
||||
strategy: addressCount > 1 ? form.strategy : 'fifo',
|
||||
proxyProtocol: form.proxyProtocol ? 1 : 0 // 创建时需要将 boolean 转换为数字
|
||||
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
||||
};
|
||||
res = await createForward(createData);
|
||||
}
|
||||
@@ -909,18 +902,7 @@ export default function ForwardPage() {
|
||||
</Select>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-foreground">启用PROXY Protocol</span>
|
||||
<span className="text-xs text-default-500">不知道干嘛的就别动</span>
|
||||
</div>
|
||||
<Switch
|
||||
isSelected={form.proxyProtocol}
|
||||
onValueChange={(checked) => setForm(prev => ({ ...prev, proxyProtocol: checked }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
Reference in New Issue
Block a user