From db3e2ed5edc853821a180e7fb02e451ef18974bc Mon Sep 17 00:00:00 2001
From: q107580018 <44695873+q107580018@users.noreply.github.com>
Date: Tue, 31 Mar 2026 19:42:59 +0800
Subject: [PATCH] fix web admin bridge websocket access (#385)
---
core/management.go | 50 +++++++++-------
core/management_test.go | 79 ++++++++++++++++++++++++++
web/src/pages/Chat/ChatView.tsx | 7 ++-
web/src/pages/Sessions/SessionChat.tsx | 7 ++-
4 files changed, 120 insertions(+), 23 deletions(-)
diff --git a/core/management.go b/core/management.go
index 1d39e1a..1667a82 100644
--- a/core/management.go
+++ b/core/management.go
@@ -109,9 +109,23 @@ func (m *ManagementServer) SetSaveGlobalSettings(fn func(map[string]any) error)
m.saveGlobalSettings = fn
}
-
func (m *ManagementServer) Start() {
mux := http.NewServeMux()
+ handler := m.buildHandler(mux)
+
+ m.server = &http.Server{
+ Addr: fmt.Sprintf(":%d", m.port),
+ Handler: handler,
+ }
+ go func() {
+ if err := m.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ slog.Error("management api server error", "error", err)
+ }
+ }()
+ slog.Info("management api started", "port", m.port)
+}
+
+func (m *ManagementServer) buildHandler(mux *http.ServeMux) http.Handler {
prefix := "/api/v1"
// System
@@ -141,18 +155,7 @@ func (m *ManagementServer) Start() {
mux.HandleFunc(prefix+"/bridge/adapters", m.wrap(m.handleBridgeAdapters))
// Static file serving for cc-connect-web (SPA)
- handler := m.withStaticFallback(mux)
-
- m.server = &http.Server{
- Addr: fmt.Sprintf(":%d", m.port),
- Handler: handler,
- }
- go func() {
- if err := m.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
- slog.Error("management api server error", "error", err)
- }
- }()
- slog.Info("management api started", "port", m.port)
+ return m.withStaticFallback(mux)
}
func (m *ManagementServer) Stop() {
@@ -170,6 +173,10 @@ func (m *ManagementServer) withStaticFallback(apiMux *http.ServeMux) http.Handle
apiMux.ServeHTTP(w, r)
return
}
+ if m.bridgeServer != nil && r.URL.Path == m.bridgeServer.path {
+ m.bridgeServer.handleWS(w, r)
+ return
+ }
assets := GetWebAssets()
if assets == nil {
apiMux.ServeHTTP(w, r)
@@ -314,6 +321,7 @@ func (m *ManagementServer) handleStatus(w http.ResponseWriter, r *http.Request)
"enabled": true,
"port": m.bridgeServer.port,
"path": m.bridgeServer.path,
+ "token": m.bridgeServer.token,
"token_set": m.bridgeServer.token != "",
}
}
@@ -592,14 +600,14 @@ func (m *ManagementServer) handleProjectDetail(w http.ResponseWriter, r *http.Re
if r.Method == http.MethodPatch {
var body struct {
- Quiet *bool `json:"quiet"`
- Language *string `json:"language"`
- AdminFrom *string `json:"admin_from"`
- DisabledCommands []string `json:"disabled_commands"`
- WorkDir *string `json:"work_dir"`
- Mode *string `json:"mode"`
- ShowContextIndicator *bool `json:"show_context_indicator"`
- PlatformAllowFrom map[string]string `json:"platform_allow_from"`
+ Quiet *bool `json:"quiet"`
+ Language *string `json:"language"`
+ AdminFrom *string `json:"admin_from"`
+ DisabledCommands []string `json:"disabled_commands"`
+ WorkDir *string `json:"work_dir"`
+ Mode *string `json:"mode"`
+ ShowContextIndicator *bool `json:"show_context_indicator"`
+ PlatformAllowFrom map[string]string `json:"platform_allow_from"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
mgmtError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
diff --git a/core/management_test.go b/core/management_test.go
index 4fe12e2..6dd52fe 100644
--- a/core/management_test.go
+++ b/core/management_test.go
@@ -191,6 +191,34 @@ func TestMgmt_Status(t *testing.T) {
}
}
+func TestMgmt_StatusIncludesBridgeToken(t *testing.T) {
+ mgmt, ts, _ := testManagementServer(t, "tok")
+ mgmt.SetBridgeServer(NewBridgeServer(9810, "bridge-secret", "/bridge/ws", nil))
+
+ r := mgmtGet(t, ts.URL+"/api/v1/status", "tok")
+ if !r.OK {
+ t.Fatalf("status failed: %s", r.Error)
+ }
+
+ var data struct {
+ Bridge struct {
+ Enabled bool `json:"enabled"`
+ Port int `json:"port"`
+ Path string `json:"path"`
+ Token string `json:"token"`
+ } `json:"bridge"`
+ }
+ if err := json.Unmarshal(r.Data, &data); err != nil {
+ t.Fatalf("unmarshal status data: %v", err)
+ }
+ if !data.Bridge.Enabled {
+ t.Fatal("expected bridge to be enabled")
+ }
+ if data.Bridge.Token != "bridge-secret" {
+ t.Fatalf("expected bridge token, got %q", data.Bridge.Token)
+ }
+}
+
func TestMgmt_Projects(t *testing.T) {
_, ts, _ := testManagementServer(t, "tok")
@@ -482,6 +510,57 @@ func TestMgmt_CORS(t *testing.T) {
}
}
+func TestMgmt_BridgeWebSocketPathProxiesToBridgeServer(t *testing.T) {
+ mgmt := NewManagementServer(0, "", []string{"*"})
+ mgmt.RegisterEngine("p", NewEngine("p", &stubAgent{}, nil, "", LangEnglish))
+ mgmt.SetBridgeServer(NewBridgeServer(9810, "bridge-secret", "/bridge/ws", []string{"*"}))
+
+ mux := http.NewServeMux()
+ ts := httptest.NewServer(mgmt.buildHandler(mux))
+ defer ts.Close()
+
+ req, _ := http.NewRequest("GET", ts.URL+"/bridge/ws?token=bridge-secret", nil)
+ req.Header.Set("Connection", "Upgrade")
+ req.Header.Set("Upgrade", "websocket")
+ req.Header.Set("Sec-WebSocket-Version", "13")
+ req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusSwitchingProtocols {
+ t.Fatalf("expected websocket upgrade, got %d", resp.StatusCode)
+ }
+}
+
+func TestMgmt_BridgeWebSocketPathWorksWhenBridgeServerSetAfterHandlerBuild(t *testing.T) {
+ mgmt := NewManagementServer(0, "", []string{"*"})
+ mgmt.RegisterEngine("p", NewEngine("p", &stubAgent{}, nil, "", LangEnglish))
+
+ mux := http.NewServeMux()
+ ts := httptest.NewServer(mgmt.buildHandler(mux))
+ defer ts.Close()
+
+ mgmt.SetBridgeServer(NewBridgeServer(9810, "bridge-secret", "/bridge/ws", []string{"*"}))
+
+ req, _ := http.NewRequest("GET", ts.URL+"/bridge/ws?token=bridge-secret", nil)
+ req.Header.Set("Connection", "Upgrade")
+ req.Header.Set("Upgrade", "websocket")
+ req.Header.Set("Sec-WebSocket-Version", "13")
+ req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusSwitchingProtocols {
+ t.Fatalf("expected websocket upgrade after late bridge setup, got %d", resp.StatusCode)
+ }
+}
+
func TestMgmt_MethodNotAllowed(t *testing.T) {
_, ts, _ := testManagementServer(t, "tok")
diff --git a/web/src/pages/Chat/ChatView.tsx b/web/src/pages/Chat/ChatView.tsx
index 4ab443c..e815a88 100644
--- a/web/src/pages/Chat/ChatView.tsx
+++ b/web/src/pages/Chat/ChatView.tsx
@@ -699,11 +699,16 @@ export default function ChatView() {
{sending ?