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, `
与子节点连接失败,正在自动返回主节点...
`) 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, `与子节点连接失败,正在自动返回主节点...
`) 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() } }