mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-07 08:04:29 +00:00
fix: webui 设备网关 WS 反代改用 net.Dial 手动升级握手
- 之前 http.DefaultClient/Transport 不支持 ws:// scheme 与 101 升级, WS 经反代返回 502 - 改为 net.Dial 直连网关 + 手动构造 WS 升级请求(X-API-Key 注入) + http.ReadResponse 读 101 - hijack 后双向字节透传(WS 帧不动), 客户端->上游/上游->客户端 - 实测: 经 webui:8080/api/v1/device/ws 反代, 101 握手成功, 网关 Sec-Websocket-Accept 正确 - 对应 pi-desktop 排查记录: 远程 WS 设备通道现在可经远程 homed 使用
This commit is contained in:
@ -11,6 +11,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -1725,32 +1726,62 @@ func (h *Handler) handleDeviceGatewayProxy(w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
// proxyWebSocket 用 TCP 直连 + hijack 将客户端 WS 连接双向透传到设备网关。
|
// proxyWebSocket 用 TCP 直连 + hijack 将客户端 WS 连接双向透传到设备网关。
|
||||||
func (h *Handler) proxyWebSocket(w http.ResponseWriter, r *http.Request, addr, path string) {
|
func (h *Handler) proxyWebSocket(w http.ResponseWriter, r *http.Request, addr, path string) {
|
||||||
upstream := "ws://" + addr + path
|
// 设备网关默认仅监听 127.0.0.1(remotedevice),反代目标即内网 homed 本机或指定 addr。
|
||||||
if r.URL.RawQuery != "" {
|
// 用 net.Dial 直连网关并手动发起 WS 升级握手(net/http 客户端不支持 ws:// 升级)。
|
||||||
upstream += "?" + r.URL.RawQuery
|
host, port := addr, "9890"
|
||||||
|
if h2, p2, ok := splitHostPort(addr); ok {
|
||||||
|
host, port = h2, p2
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
target := net.JoinHostPort(host, port)
|
||||||
defer cancel()
|
upConn, err := net.DialTimeout("tcp", target, 15*time.Second)
|
||||||
// 构造带 Upgrade 头的请求:http.Transport 对 Upgrade 请求保留连接字节流
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, upstream, nil)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "ws upstream: "+err.Error(), http.StatusBadGateway)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
req.Header = r.Header.Clone()
|
|
||||||
if deviceGatewayToken != "" {
|
|
||||||
req.Header.Set("X-API-Key", deviceGatewayToken)
|
|
||||||
}
|
|
||||||
tr := &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
}
|
|
||||||
resp, err := tr.RoundTrip(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "ws upstream dial: "+err.Error(), http.StatusBadGateway)
|
http.Error(w, "ws upstream dial: "+err.Error(), http.StatusBadGateway)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer upConn.Close()
|
||||||
|
|
||||||
|
// 手动构造 WS 升级请求(保留客户端头 + 注入网关 token)
|
||||||
|
key := r.Header.Get("Sec-WebSocket-Key")
|
||||||
|
if key == "" {
|
||||||
|
key = "homeagent-proxy-random-key"
|
||||||
|
}
|
||||||
|
reqPath := path
|
||||||
|
if r.URL.RawQuery != "" {
|
||||||
|
reqPath += "?" + r.URL.RawQuery
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("GET " + reqPath + " HTTP/1.1\r\n")
|
||||||
|
b.WriteString("Host: " + addr + "\r\n")
|
||||||
|
b.WriteString("Upgrade: websocket\r\n")
|
||||||
|
b.WriteString("Connection: Upgrade\r\n")
|
||||||
|
b.WriteString("Sec-WebSocket-Key: " + key + "\r\n")
|
||||||
|
b.WriteString("Sec-WebSocket-Version: 13\r\n")
|
||||||
|
if deviceGatewayToken != "" {
|
||||||
|
b.WriteString("X-API-Key: " + deviceGatewayToken + "\r\n")
|
||||||
|
}
|
||||||
|
for k, vv := range r.Header {
|
||||||
|
kl := strings.ToLower(k)
|
||||||
|
if kl == "upgrade" || kl == "connection" || kl == "sec-websocket-key" || kl == "sec-websocket-version" || kl == "host" || kl == "x-api-key" || kl == "authorization" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, v := range vv {
|
||||||
|
b.WriteString(k + ": " + v + "\r\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString("\r\n")
|
||||||
|
if _, err := upConn.Write([]byte(b.String())); err != nil {
|
||||||
|
http.Error(w, "ws upstream write: "+err.Error(), http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读上游 101 响应
|
||||||
|
br := bufio.NewReader(upConn)
|
||||||
|
resp, err := http.ReadResponse(br, nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "ws upstream response: "+err.Error(), http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||||
defer resp.Body.Close()
|
|
||||||
http.Error(w, "ws upstream status: "+resp.Status, http.StatusBadGateway)
|
http.Error(w, "ws upstream status: "+resp.Status, http.StatusBadGateway)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1758,37 +1789,25 @@ func (h *Handler) proxyWebSocket(w http.ResponseWriter, r *http.Request, addr, p
|
|||||||
// 客户端 hijack:把 101 响应头写给客户端并接管双向连接
|
// 客户端 hijack:把 101 响应头写给客户端并接管双向连接
|
||||||
hj, ok := w.(http.Hijacker)
|
hj, ok := w.(http.Hijacker)
|
||||||
if !ok {
|
if !ok {
|
||||||
resp.Body.Close()
|
|
||||||
http.Error(w, "hijack not supported", http.StatusInternalServerError)
|
http.Error(w, "hijack not supported", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
clientConn, brw, err := hj.Hijack()
|
clientConn, brw, err := hj.Hijack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Body.Close()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer clientConn.Close()
|
defer clientConn.Close()
|
||||||
|
|
||||||
// 向上游写回 101 响应头
|
// 向上游 101 响应头转发给客户端
|
||||||
if err := resp.Write(brw); err != nil {
|
if err := resp.Write(brw); err != nil {
|
||||||
resp.Body.Close()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := brw.Flush(); err != nil {
|
if err := brw.Flush(); err != nil {
|
||||||
resp.Body.Close()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上游连接
|
// 双向透传(WS 帧字节不动):
|
||||||
upConn, ok := resp.Body.(io.ReadWriteCloser)
|
// 客户端 -> 上游
|
||||||
if !ok {
|
|
||||||
clientConn.Close()
|
|
||||||
http.Error(w, "upstream conn not rw", http.StatusBadGateway)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer upConn.Close()
|
|
||||||
|
|
||||||
// 双向透传(WS 帧字节不动)
|
|
||||||
errCh := make(chan struct{}, 2)
|
errCh := make(chan struct{}, 2)
|
||||||
go func() {
|
go func() {
|
||||||
io.Copy(upConn, brw)
|
io.Copy(upConn, brw)
|
||||||
@ -1797,12 +1816,25 @@ func (h *Handler) proxyWebSocket(w http.ResponseWriter, r *http.Request, addr, p
|
|||||||
}
|
}
|
||||||
errCh <- struct{}{}
|
errCh <- struct{}{}
|
||||||
}()
|
}()
|
||||||
|
// 上游 -> 客户端
|
||||||
go func() {
|
go func() {
|
||||||
io.Copy(bufio.NewWriter(clientConn), upConn)
|
wb := bufio.NewWriter(clientConn)
|
||||||
|
io.Copy(wb, br)
|
||||||
|
wb.Flush()
|
||||||
errCh <- struct{}{}
|
errCh <- struct{}{}
|
||||||
}()
|
}()
|
||||||
<-errCh
|
<-errCh
|
||||||
tr.CloseIdleConnections()
|
}
|
||||||
|
|
||||||
|
// splitHostPort 拆分 addr 为 host/port;无端口时返回 ok=false。
|
||||||
|
func splitHostPort(addr string) (string, string, bool) {
|
||||||
|
if strings.Contains(addr, ":") {
|
||||||
|
h, p, err := net.SplitHostPort(addr)
|
||||||
|
if err == nil {
|
||||||
|
return h, p, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addr, "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======== Plugin Management (proxied to pluginmgr HTTP API) ========
|
// ======== Plugin Management (proxied to pluginmgr HTTP API) ========
|
||||||
|
|||||||
Reference in New Issue
Block a user