Enhance Mihomo WebSocket functionality and testing for connections
Publish telemt-api gateway Docker image / test (push) Successful in 25s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 2m18s

- Added support for WebSocket connections to the `/connections` endpoint, allowing for improved handling of WebSocket requests.
- Updated the `useMihomoWebSocketTunnel` function to accept `Sec-WebSocket-Key` for `/connections`, enhancing compatibility with various proxies.
- Introduced new test cases in `mihomo_test.go` to validate WebSocket tunnel behavior for connections, ensuring comprehensive coverage.
- Enhanced the `applyMihomoWSDialAuth` function to manage token handling for WebSocket connections, improving security and functionality.
- Updated Svelte components to implement native WebSocket loops for connections, providing real-time data updates and improved user experience.
This commit is contained in:
Denozordec
2026-03-31 11:54:18 +07:00
parent 49f36d2b8b
commit c3a0a771a3
5 changed files with 160 additions and 22 deletions
+26 -6
View File
@@ -42,7 +42,7 @@ func NewMihomoForward(
}
// useMihomoWebSocketTunnel is true when the client is doing a WS handshake.
// For /traffic and /memory we also accept Sec-WebSocket-Key alone: some hops strip
// For /traffic, /memory, /connections we also accept Sec-WebSocket-Key alone: some hops strip
// Connection/Upgrade but leave Sec-WebSocket-Key; plain streaming GET has no key.
func useMihomoWebSocketTunnel(rest string, r *http.Request) bool {
if r == nil {
@@ -55,13 +55,36 @@ func useMihomoWebSocketTunnel(rest string, r *http.Request) bool {
return false
}
switch rest {
case "traffic", "memory":
case "traffic", "memory", "connections":
return strings.TrimSpace(r.Header.Get("Sec-WebSocket-Key")) != ""
default:
return false
}
}
// applyMihomoWSDialAuth prepares upstream WS URL and Dial headers.
// Mihomo: /connections WebSocket expects ?token=<secret> (raw secret, no "Bearer " prefix);
// /traffic, /memory use Authorization: Bearer … like REST.
func applyMihomoWSDialAuth(rest string, dest *url.URL, auth string) http.Header {
hdr := make(http.Header)
a := strings.TrimSpace(auth)
if a == "" || dest == nil {
return hdr
}
if rest == "connections" {
token := a
if len(token) > 7 && strings.EqualFold(token[:7], "Bearer ") {
token = strings.TrimSpace(token[7:])
}
q := dest.Query()
q.Set("token", token)
dest.RawQuery = q.Encode()
return hdr
}
hdr.Set("Authorization", a)
return hdr
}
func mihomoWebSocketURL(dest *url.URL) (string, error) {
if dest == nil {
return "", fmt.Errorf("nil destination URL")
@@ -111,16 +134,13 @@ func newMihomoWSTunnel(
du.RawQuery = r.URL.RawQuery
dest = &du
dialHdr := applyMihomoWSDialAuth(rest, dest, auth)
wsURL, err := mihomoWebSocketURL(dest)
if err != nil {
errHandler(w, r, err)
return
}
dialHdr := make(http.Header)
if auth != "" {
dialHdr.Set("Authorization", auth)
}
d := websocket.Dialer{
HandshakeTimeout: 15 * time.Second,
Proxy: func(*http.Request) (*url.URL, error) { return nil, nil },
+44
View File
@@ -82,6 +82,50 @@ func TestUseMihomoWebSocketTunnel(t *testing.T) {
t.Fatal("proxies must not use WS tunnel")
}
})
t.Run("connections with key only", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/api/x/mihomo/connections", nil)
r.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
if !useMihomoWebSocketTunnel("connections", r) {
t.Fatal("expected tunnel for connections+Sec-WebSocket-Key")
}
})
t.Run("connections REST GET without key", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/api/x/mihomo/connections", nil)
if useMihomoWebSocketTunnel("connections", r) {
t.Fatal("plain GET /connections must use HTTP forwarder")
}
})
}
func TestApplyMihomoWSDialAuth(t *testing.T) {
t.Run("connections strips Bearer for token query", func(t *testing.T) {
u, _ := url.Parse("http://127.0.0.1:9090/connections")
hdr := applyMihomoWSDialAuth("connections", u, "Bearer mysecret")
if hdr.Get("Authorization") != "" {
t.Fatal("connections WS must not set Authorization")
}
if u.RawQuery != "token=mysecret" {
t.Fatalf("query: %q", u.RawQuery)
}
})
t.Run("connections raw token", func(t *testing.T) {
u, _ := url.Parse("http://127.0.0.1:9090/connections")
_ = applyMihomoWSDialAuth("connections", u, "rawonly")
if u.RawQuery != "token=rawonly" {
t.Fatalf("query: %q", u.RawQuery)
}
})
t.Run("traffic uses Authorization", func(t *testing.T) {
u, _ := url.Parse("http://127.0.0.1:9090/traffic")
hdr := applyMihomoWSDialAuth("traffic", u, "Bearer t")
if hdr.Get("Authorization") != "Bearer t" {
t.Fatalf("Authorization: %q", hdr.Get("Authorization"))
}
})
}
func TestMihomoWebSocketURL(t *testing.T) {