From 0ff9245a0e5ca6b89f5dbfcfacc8c9a1a0ffc342 Mon Sep 17 00:00:00 2001 From: lpf Date: Mon, 16 Mar 2026 13:46:50 +0800 Subject: [PATCH] Allow direct IP webui sessions --- cmd/workspace/embedkeep.txt | 1 + pkg/api/server.go | 31 ++++++++++++++++++++++++++- pkg/api/server_node_artifacts_test.go | 5 +++-- pkg/api/server_security_test.go | 28 ++++++++++++++++++++++++ workspace/embedkeep.txt | 1 + 5 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 cmd/workspace/embedkeep.txt create mode 100644 workspace/embedkeep.txt diff --git a/cmd/workspace/embedkeep.txt b/cmd/workspace/embedkeep.txt new file mode 100644 index 0000000..f1345e7 --- /dev/null +++ b/cmd/workspace/embedkeep.txt @@ -0,0 +1 @@ +embedded workspace placeholder diff --git a/pkg/api/server.go b/pkg/api/server.go index eb90fac..a4bcc21 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -343,6 +343,25 @@ func requestOrigin(r *http.Request) string { return scheme + "://" + canonicalOriginHost(r.Host, scheme == "https") } +func sameSiteOrigin(originA, originB string) bool { + parsedA, err := url.Parse(strings.TrimSpace(originA)) + if err != nil || parsedA == nil { + return false + } + parsedB, err := url.Parse(strings.TrimSpace(originB)) + if err != nil || parsedB == nil { + return false + } + schemeA := strings.ToLower(strings.TrimSpace(parsedA.Scheme)) + schemeB := strings.ToLower(strings.TrimSpace(parsedB.Scheme)) + if schemeA == "" || schemeB == "" || schemeA != schemeB { + return false + } + hostA := strings.ToLower(strings.TrimSpace(parsedA.Hostname())) + hostB := strings.ToLower(strings.TrimSpace(parsedB.Hostname())) + return hostA != "" && hostA == hostB +} + func (s *Server) isTrustedOrigin(r *http.Request) bool { if r == nil { return false @@ -360,7 +379,17 @@ func (s *Server) isTrustedOrigin(r *http.Request) bool { func (s *Server) shouldUseCrossSiteCookie(r *http.Request) bool { origin := normalizeOrigin(r.Header.Get("Origin")) - return origin != "" && origin != requestOrigin(r) && s.isTrustedOrigin(r) + if origin == "" || !s.isTrustedOrigin(r) { + return false + } + reqOrigin := requestOrigin(r) + if origin == reqOrigin { + return false + } + if sameSiteOrigin(origin, reqOrigin) { + return false + } + return true } func (s *Server) websocketUpgrader() *websocket.Upgrader { diff --git a/pkg/api/server_node_artifacts_test.go b/pkg/api/server_node_artifacts_test.go index 7ea1691..eae09b3 100644 --- a/pkg/api/server_node_artifacts_test.go +++ b/pkg/api/server_node_artifacts_test.go @@ -209,9 +209,10 @@ func TestHandleWebUINodeArtifactsAppliesRetentionConfig(t *testing.T) { t.Fatalf("save config: %v", err) } srv.SetConfigPath(cfgPath) + base := time.Now().UTC().Add(-2 * time.Hour) auditLines := strings.Join([]string{ - "{\"time\":\"2026-03-09T00:00:00Z\",\"node\":\"edge-a\",\"action\":\"screen_snapshot\",\"ok\":true,\"artifacts\":[{\"name\":\"one.txt\",\"kind\":\"text\",\"mime_type\":\"text/plain\",\"content_base64\":\"b25l\"}]}", - "{\"time\":\"2026-03-09T00:01:00Z\",\"node\":\"edge-a\",\"action\":\"screen_snapshot\",\"ok\":true,\"artifacts\":[{\"name\":\"two.txt\",\"kind\":\"text\",\"mime_type\":\"text/plain\",\"content_base64\":\"dHdv\"}]}", + fmt.Sprintf("{\"time\":%q,\"node\":\"edge-a\",\"action\":\"screen_snapshot\",\"ok\":true,\"artifacts\":[{\"name\":\"one.txt\",\"kind\":\"text\",\"mime_type\":\"text/plain\",\"content_base64\":\"b25l\"}]}", base.Format(time.RFC3339)), + fmt.Sprintf("{\"time\":%q,\"node\":\"edge-a\",\"action\":\"screen_snapshot\",\"ok\":true,\"artifacts\":[{\"name\":\"two.txt\",\"kind\":\"text\",\"mime_type\":\"text/plain\",\"content_base64\":\"dHdv\"}]}", base.Add(time.Minute).Format(time.RFC3339)), }, "\n") + "\n" if err := os.WriteFile(filepath.Join(workspace, "memory", "nodes-dispatch-audit.jsonl"), []byte(auditLines), 0o644); err != nil { t.Fatalf("write audit: %v", err) diff --git a/pkg/api/server_security_test.go b/pkg/api/server_security_test.go index 1027c24..dbd1ba0 100644 --- a/pkg/api/server_security_test.go +++ b/pkg/api/server_security_test.go @@ -197,6 +197,34 @@ func TestHandleWebUIAuthSessionSetsCrossSiteCookieForAllowedOrigin(t *testing.T) } } +func TestHandleWebUIAuthSessionKeepsLaxCookieForSameIPDifferentPort(t *testing.T) { + t.Parallel() + + srv := NewServer("0.0.0.0", 0, "secret-token", nil) + + req := httptest.NewRequest(http.MethodPost, "http://134.195.210.114:18790/api/auth/session", nil) + req.Host = "134.195.210.114:18790" + req.Header.Set("Origin", "http://134.195.210.114:3000") + req.Header.Set("Authorization", "Bearer secret-token") + rec := httptest.NewRecorder() + + srv.withCORS(http.HandlerFunc(srv.handleWebUIAuthSession)).ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) + } + cookies := rec.Result().Cookies() + if len(cookies) != 1 { + t.Fatalf("expected one cookie, got %d", len(cookies)) + } + if cookies[0].SameSite != http.SameSiteLaxMode { + t.Fatalf("expected SameSite=Lax for same-IP direct session, got %v", cookies[0].SameSite) + } + if cookies[0].Secure { + t.Fatalf("expected non-secure cookie for plain HTTP direct IP session") + } +} + func TestHandleWebUIUploadDoesNotExposeAbsolutePath(t *testing.T) { t.Parallel() diff --git a/workspace/embedkeep.txt b/workspace/embedkeep.txt new file mode 100644 index 0000000..4f56dce --- /dev/null +++ b/workspace/embedkeep.txt @@ -0,0 +1 @@ +workspace assets placeholder