feat: add openconnect
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/engigu/baihu-panel/internal/constant"
|
||||
@@ -34,6 +35,47 @@ func AuthRequired() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否携带互联 Token(支持跨面板远程全接口调用)
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader != "" {
|
||||
tokenStr := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
|
||||
if tokenStr != "" {
|
||||
settingsSvc := services.NewSettingsService()
|
||||
interconnectToken := settingsSvc.Get(constant.SectionSite, constant.KeyInterconnectToken)
|
||||
parentToken := settingsSvc.Get(constant.SectionInterconnect, constant.KeyInterconnectParentToken)
|
||||
|
||||
isMatched := false
|
||||
h1 := sha256.Sum256([]byte(tokenStr))
|
||||
|
||||
if interconnectToken != "" {
|
||||
h2 := sha256.Sum256([]byte(interconnectToken))
|
||||
if subtle.ConstantTimeCompare(h1[:], h2[:]) == 1 {
|
||||
isMatched = true
|
||||
}
|
||||
}
|
||||
|
||||
if !isMatched && parentToken != "" {
|
||||
h2 := sha256.Sum256([]byte(parentToken))
|
||||
if subtle.ConstantTimeCompare(h1[:], h2[:]) == 1 {
|
||||
isMatched = true
|
||||
}
|
||||
}
|
||||
|
||||
if isMatched {
|
||||
// 模拟 Admin 角色
|
||||
var adminUser models.User
|
||||
res := database.DB.Where("role = ?", constant.AdminRole).Limit(1).Find(&adminUser)
|
||||
if res.Error == nil && res.RowsAffected > 0 {
|
||||
c.Set("userID", adminUser.ID)
|
||||
c.Set("username", adminUser.Username)
|
||||
c.Set("role", adminUser.Role)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
token, err := c.Cookie(constant.CookieName)
|
||||
if err != nil || token == "" {
|
||||
utils.Unauthorized(c, "请先登录")
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/engigu/baihu-panel/internal/constant"
|
||||
"github.com/engigu/baihu-panel/internal/database"
|
||||
"github.com/engigu/baihu-panel/internal/models"
|
||||
"github.com/engigu/baihu-panel/internal/services"
|
||||
"github.com/engigu/baihu-panel/internal/tunnel"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var travelProxyClient = &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
func TravelProxyMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 1. 检查是否存在 active_interconnect_node_id Cookie
|
||||
nodeID, err := c.Cookie(constant.CookieActiveInterconnectNodeID)
|
||||
if err != nil || nodeID == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 白名单放行:反向隧道建立连接端点必须直达本机,不能二次代理
|
||||
path := c.Request.URL.Path
|
||||
if strings.HasPrefix(path, "/api/v1/interconnect/tunnel") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 查询数据库中节点信息
|
||||
var node models.InterconnectNode
|
||||
if err := database.DB.Where("id = ?", nodeID).First(&node).Error; err != nil {
|
||||
// 节点不存在,说明 Cookie 无效,清除并放行
|
||||
c.SetCookie(constant.CookieActiveInterconnectNodeID, "", -1, "/", "", false, false)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 准备代理的路径(若主节点配置了 URLPrefix,需要剥离)
|
||||
cfg := services.GetConfig()
|
||||
urlPrefix := strings.TrimSuffix(cfg.Server.URLPrefix, "/")
|
||||
targetPath := path
|
||||
if urlPrefix != "" {
|
||||
targetPath = strings.TrimPrefix(targetPath, urlPrefix)
|
||||
}
|
||||
if !strings.HasPrefix(targetPath, "/") {
|
||||
targetPath = "/" + targetPath
|
||||
}
|
||||
|
||||
// 5. 执行代理转发
|
||||
if strings.HasPrefix(node.URL, "tunnel://") {
|
||||
// 走 WebSocket 逆向 Yamux 隧道
|
||||
err := tunnel.ProxyHTTP(nodeID, c, targetPath)
|
||||
if err != nil {
|
||||
// 如果是网页 HTML 导航请求,提供友好降级返回主节点
|
||||
if strings.Contains(c.GetHeader("Accept"), "text/html") {
|
||||
c.SetCookie(constant.CookieActiveInterconnectNodeID, "", -1, "/", "", false, false)
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.String(200, `<p>与子节点连接失败,正在自动返回主节点...</p><script>document.cookie="` + constant.CookieActiveInterconnectNodeID + `=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; window.location.href="/";</script>`)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.JSON(502, gin.H{"code": 502, "msg": "与子节点逆向隧道通信异常: " + err.Error()})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 走普通 HTTP 直连代理
|
||||
targetURL := strings.TrimRight(node.URL, "/") + targetPath
|
||||
if c.Request.URL.RawQuery != "" {
|
||||
targetURL += "?" + c.Request.URL.RawQuery
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(c.Request.Method, targetURL, c.Request.Body)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"code": 500, "msg": "Failed to create proxy request"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 复制请求头
|
||||
req.Header = c.Request.Header.Clone()
|
||||
|
||||
// 覆盖认证授权 Header 确保子节点鉴权通过
|
||||
if node.Token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+node.Token)
|
||||
}
|
||||
// 移除 Cookie 头部防干扰
|
||||
req.Header.Del("Cookie")
|
||||
req.Header.Set("X-Tunnel-Proxy", "true")
|
||||
|
||||
resp, err := travelProxyClient.Do(req)
|
||||
if err != nil {
|
||||
// 如果是客户端自己主动取消了请求(例如连续刷新、关闭网页等),直接退出,不应视为子节点离线而执行退回主节点的操作
|
||||
if c.Request.Context().Err() == context.Canceled || strings.Contains(err.Error(), "context canceled") {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if strings.Contains(c.GetHeader("Accept"), "text/html") {
|
||||
c.SetCookie(constant.CookieActiveInterconnectNodeID, "", -1, "/", "", false, false)
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.String(200, `<p>与子节点连接失败,正在自动返回主节点...</p><script>document.cookie="` + constant.CookieActiveInterconnectNodeID + `=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; window.location.href="/";</script>`)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.JSON(502, gin.H{"code": 502, "msg": "无法连接至目标子节点: " + err.Error()})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 复制响应头
|
||||
for k, v := range resp.Header {
|
||||
for _, vv := range v {
|
||||
c.Writer.Header().Add(k, vv)
|
||||
}
|
||||
}
|
||||
c.Status(resp.StatusCode)
|
||||
io.Copy(c.Writer, resp.Body)
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user