From f8a27b805848da4076f41f640451a2079544de50 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 13 Jun 2026 07:47:03 +0800 Subject: [PATCH] feat: proxy local service embeds --- api/proxy.go | 57 +++++++++++++++++++++++++++++++++++++++ api/server.go | 3 +++ dashboard/src/lib/data.ts | 4 +-- 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 api/proxy.go diff --git a/api/proxy.go b/api/proxy.go new file mode 100644 index 0000000..17877b4 --- /dev/null +++ b/api/proxy.go @@ -0,0 +1,57 @@ +package main + +import ( + "net/http" + "net/http/httputil" + "net/url" + "strings" +) + +func NewLocalProxy(target string, prefix string) http.Handler { + upstream, err := url.Parse(target) + if err != nil { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, err.Error(), http.StatusInternalServerError) + }) + } + + proxy := httputil.NewSingleHostReverseProxy(upstream) + originalDirector := proxy.Director + proxy.Director = func(r *http.Request) { + originalDirector(r) + r.URL.Scheme = upstream.Scheme + r.URL.Host = upstream.Host + r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix) + if r.URL.Path == "" { + r.URL.Path = "/" + } + r.Host = upstream.Host + } + proxy.ModifyResponse = func(resp *http.Response) error { + stripFrameBlockingHeaders(resp.Header) + return nil + } + return proxy +} + +func stripFrameBlockingHeaders(header http.Header) { + header.Del("X-Frame-Options") + csp := header.Get("Content-Security-Policy") + if csp == "" { + return + } + header.Set("Content-Security-Policy", removeCSPDirective(csp, "frame-ancestors")) +} + +func removeCSPDirective(csp string, directive string) string { + parts := strings.Split(csp, ";") + kept := parts[:0] + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed == "" || strings.HasPrefix(strings.ToLower(trimmed), directive) { + continue + } + kept = append(kept, trimmed) + } + return strings.Join(kept, "; ") +} diff --git a/api/server.go b/api/server.go index 1ec3cb4..153d7e8 100644 --- a/api/server.go +++ b/api/server.go @@ -29,6 +29,9 @@ func (s *Server) routes() http.Handler { mux.HandleFunc("/health", s.healthHandler) mux.HandleFunc("/services", s.servicesHandler) mux.HandleFunc("/metrics/simple", s.metricsHandler) + mux.Handle("/proxy/openclaw/", NewLocalProxy("http://127.0.0.1:18789", "/proxy/openclaw")) + mux.Handle("/proxy/vault/", NewLocalProxy("http://127.0.0.1:8200", "/proxy/vault")) + mux.Handle("/ui/", NewLocalProxy("http://127.0.0.1:8200", "")) return mux } diff --git a/dashboard/src/lib/data.ts b/dashboard/src/lib/data.ts index 03a3110..3ae12b9 100644 --- a/dashboard/src/lib/data.ts +++ b/dashboard/src/lib/data.ts @@ -48,8 +48,8 @@ export type ServiceDef = NavItem & { export const serviceRegistry: ServiceDef[] = [ { id: 'workspace', label: 'Overview', icon: 'home', href: '#workspace', kind: 'internal', group: 0 }, - { id: 'openclaw', label: 'OpenClaw', icon: 'claw', href: 'http://127.0.0.1:18789/channels', kind: 'embed', group: 1, port: 18789, match: ['openclaw', 'gateway'], frameMode: 'external' }, - { id: 'vault', label: 'Vault Server', icon: 'shield', href: 'http://127.0.0.1:8200/ui/', kind: 'embed', group: 1, port: 8200, match: ['vault'], frameMode: 'external' }, + { id: 'openclaw', label: 'OpenClaw', icon: 'claw', href: 'http://127.0.0.1:8788/proxy/openclaw/channels', kind: 'embed', group: 1, port: 18789, match: ['openclaw', 'gateway'] }, + { id: 'vault', label: 'Vault Server', icon: 'shield', href: 'http://127.0.0.1:8788/proxy/vault/ui/', kind: 'embed', group: 1, port: 8200, match: ['vault'] }, { id: 'litellm', label: 'LiteLLM Admin UI', icon: 'chart', href: 'http://localhost:4000/ui', kind: 'embed', group: 1, port: 4000, match: ['litellm', 'lite'] }, { id: 'bridge', label: 'Bridge', icon: 'bridge', href: '#bridge', kind: 'internal', group: 2, match: ['bridge'] }, { id: 'runtime', label: 'Runtime', icon: 'cube', href: '#runtime', kind: 'internal', group: 2 },