diff --git a/backend/plugins/domain/admin/handler/logs.go b/backend/plugins/domain/admin/handler/logs.go index aac63a14..3bb2083c 100644 --- a/backend/plugins/domain/admin/handler/logs.go +++ b/backend/plugins/domain/admin/handler/logs.go @@ -14,6 +14,7 @@ import ( "errors" "net/http" "strconv" + "strings" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" @@ -179,11 +180,31 @@ func GetLogsAnalytics(c *gin.Context) { func getUpgrader() *websocket.Upgrader { return &websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { - return service.IsAllowedLogOrigin(r.Context(), r.Header.Get("Origin"), r.Host) + return service.IsAllowedLogOrigin( + r.Context(), + r.Header.Get("Origin"), + r.Host, + forwardedHosts(r)..., + ) }, } } +func forwardedHosts(r *http.Request) []string { + raw := r.Header.Get("X-Forwarded-Host") + if raw == "" { + return nil + } + parts := strings.Split(raw, ",") + hosts := make([]string, 0, len(parts)) + for _, part := range parts { + if h := strings.TrimSpace(part); h != "" { + hosts = append(hosts, h) + } + } + return hosts +} + // errNegativeParam 表示查询参数解析出了负数。 var errNegativeParam = errors.New("parameter must not be negative") diff --git a/backend/plugins/domain/admin/service/log.go b/backend/plugins/domain/admin/service/log.go index e0cca4a6..f138613f 100644 --- a/backend/plugins/domain/admin/service/log.go +++ b/backend/plugins/domain/admin/service/log.go @@ -11,6 +11,7 @@ import ( "Wavelet/plugins/domain/admin/repository" "context" "fmt" + "net" "net/url" "strings" "time" @@ -47,18 +48,20 @@ func RobotsTxtBody(ctx context.Context) string { } // IsAllowedLogOrigin reports whether a WebSocket handshake origin may subscribe to logs. -func IsAllowedLogOrigin(ctx context.Context, origin, host string) bool { +// extraHosts are reverse-proxy hosts such as X-Forwarded-Host (the browser origin +// when Next.js rewrites /api to the backend). +func IsAllowedLogOrigin(ctx context.Context, origin, host string, extraHosts ...string) bool { if origin == "" { return true } - // 1. 同源检查 (Same-origin check) u, err := url.Parse(origin) - if err == nil && strings.EqualFold(u.Host, host) { - return true + if err == nil { + if originMatchesHost(u.Host, host, extraHosts...) { + return true + } } - // 2. 检查配置的允许跨域 Origin (Check allowed origins in system config) sc, cfgErr := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress) if cfgErr != nil || sc.Value == "" { return false @@ -73,6 +76,41 @@ func IsAllowedLogOrigin(ctx context.Context, origin, host string) bool { return false } +func originMatchesHost(originHost, host string, extraHosts ...string) bool { + if originHost == "" { + return false + } + if hostMatches(originHost, host) { + return true + } + for _, extra := range extraHosts { + if hostMatches(originHost, extra) { + return true + } + } + return false +} + +func hostMatches(originHost, candidate string) bool { + candidate = strings.TrimSpace(candidate) + if candidate == "" { + return false + } + if strings.EqualFold(originHost, candidate) { + return true + } + // Reverse-proxy / local Next rewrite: Origin is :3000, backend Host is :8000. + return strings.EqualFold(hostName(originHost), hostName(candidate)) +} + +func hostName(hostport string) string { + h, _, err := net.SplitHostPort(hostport) + if err != nil { + return hostport + } + return h +} + // AccessLogs queries the analytical access log store and decorates rows with user names. func AccessLogs(ctx context.Context, q model.AccessLogQuery) (model.AccessLogsResponse, error) { rc := GetRiskControlService(ctx) diff --git a/backend/plugins/domain/admin/service/log_origin_test.go b/backend/plugins/domain/admin/service/log_origin_test.go new file mode 100644 index 00000000..092310c2 --- /dev/null +++ b/backend/plugins/domain/admin/service/log_origin_test.go @@ -0,0 +1,55 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package service_test + +import ( + "Wavelet/plugins/domain/admin/service" + "context" + "testing" +) + +func TestIsAllowedLogOrigin(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + origin string + host string + extraHosts []string + want bool + }{ + {name: "empty origin", origin: "", host: "localhost:8000", want: true}, + {name: "same host", origin: "http://localhost:8000", host: "localhost:8000", want: true}, + {name: "same host different case", origin: "http://LocalHost:8000", host: "localhost:8000", want: true}, + { + name: "next rewrite different port same hostname", + origin: "http://localhost:3000", + host: "localhost:8000", + want: true, + }, + { + name: "x-forwarded-host matches origin", + origin: "http://localhost:3000", + host: "backend:8080", + extraHosts: []string{"localhost:3000"}, + want: true, + }, + { + name: "unrelated origin", + origin: "https://evil.example", + host: "localhost:8000", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := service.IsAllowedLogOrigin(ctx, tt.origin, tt.host, tt.extraHosts...) + if got != tt.want { + t.Errorf("IsAllowedLogOrigin(%q, %q, %v) = %v, want %v", + tt.origin, tt.host, tt.extraHosts, got, tt.want) + } + }) + } +} diff --git a/frontend/app/(main)/admin/logs/components/app-logs.tsx b/frontend/app/(main)/admin/logs/components/app-logs.tsx index 2429166d..8c9fc99f 100644 --- a/frontend/app/(main)/admin/logs/components/app-logs.tsx +++ b/frontend/app/(main)/admin/logs/components/app-logs.tsx @@ -252,10 +252,16 @@ export function AppLogs() { // ---- Initialize ------------------------------------------------------ useEffect(() => { - loadHistory(0).then(() => connectWs()); + let cancelled = false; + loadHistory(0).then(() => { + if (cancelled) return; + connectWs(); + }); return () => { - wsRef.current?.close(); + cancelled = true; + const ws = wsRef.current; wsRef.current = null; + ws?.close(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/frontend/app/(main)/admin/push/components/events-tab.tsx b/frontend/app/(main)/admin/push/components/events-tab.tsx index aabcfdc2..b45e9f17 100644 --- a/frontend/app/(main)/admin/push/components/events-tab.tsx +++ b/frontend/app/(main)/admin/push/components/events-tab.tsx @@ -713,8 +713,8 @@ export function EventsTab() { {newEventType === 'task' - ? t('taskTemplateVars') - : t('eventTemplateVars')} + ? t.raw('taskTemplateVars') + : t.raw('eventTemplateVars')}