From 7de6d611ba563bd277d4abed1518e2e279196122 Mon Sep 17 00:00:00 2001 From: Kyue Date: Wed, 22 Jul 2026 22:56:31 +0100 Subject: [PATCH] gui: respect explicit --rc-allow-origin instead of always deriving it from the bind address Fixes #9640 The auto-derived CORS origin logic (added in #9603 for the wildcard-bind case) ran unconditionally, so it clobbered an explicitly-passed --rc-allow-origin too - the flag was silently ignored. That breaks the documented Docker/remote-browser setup where the bind address and the address the browser actually reaches the GUI at are different. Now it only kicks in when the user hasn't set --rc-allow-origin themselves. --- cmd/gui/gui.go | 38 +++++++++++++++++----------- cmd/gui/gui_test.go | 61 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/cmd/gui/gui.go b/cmd/gui/gui.go index bf34d8e74..a458983f9 100644 --- a/cmd/gui/gui.go +++ b/cmd/gui/gui.go @@ -175,20 +175,8 @@ For more help see [the GUI docs](/gui/). } } - // When the GUI is bound to a wildcard address the bound origin - // (e.g. http://[::]:5522) is never what a browser sends in its - // Origin header, and the GUI may be reached via any number of - // hosts (localhost, a LAN IP, a Docker host), so no single - // origin can match them all. - switch addr, _ := guiServer.Addr().(*net.TCPAddr); { - case addr == nil || !addr.IP.IsUnspecified(): - opt.HTTP.AllowOrigin = guiOrigin - case !opt.NoAuth: - opt.HTTP.AllowOrigin = "*" - default: - opt.HTTP.AllowOrigin = guiOrigin - fs.Logf(nil, "GUI bound to a wildcard address with --no-auth: browsers can only use the API from %s. Enable auth or bind --addr to a specific host.", guiOrigin) - } + addr, _ := guiServer.Addr().(*net.TCPAddr) + opt.HTTP.AllowOrigin = resolveAllowOrigin(command.Flags().Changed("rc-allow-origin"), opt.HTTP.AllowOrigin, guiOrigin, addr, opt.NoAuth) // Start the RC server (unchanged rcserver.Start) rcServer, err := rcserver.Start(ctx, &opt) @@ -253,6 +241,28 @@ func originFromURL(rawURL string) string { return u.Scheme + "://" + u.Host } +// resolveAllowOrigin picks the Access-Control-Allow-Origin value for the RC +// API server. An explicit --rc-allow-origin always wins. Otherwise a value is +// derived from how the GUI is bound: the bound origin (e.g. http://[::]:5522) +// is never what a browser sends in its Origin header when the GUI is bound to +// a wildcard address, and the GUI may be reached via any number of hosts +// (localhost, a LAN IP, a Docker host), so no single origin can match them +// all. +func resolveAllowOrigin(explicitAllowOrigin bool, currentAllowOrigin, guiOrigin string, addr *net.TCPAddr, noAuth bool) string { + if explicitAllowOrigin { + return currentAllowOrigin + } + switch { + case addr == nil || !addr.IP.IsUnspecified(): + return guiOrigin + case !noAuth: + return "*" + default: + fs.Logf(nil, "GUI bound to a wildcard address with --no-auth: browsers can only use the API from %s. Enable auth or bind --addr to a specific host.", guiOrigin) + return guiOrigin + } +} + // guiSourceFS opens the GUI bundle at the given path. An empty path // returns the embedded bundle (read from the zip embedded in the binary). // The returned cleanup func must be called on shutdown (no-op for the diff --git a/cmd/gui/gui_test.go b/cmd/gui/gui_test.go index 942554994..ade4c756b 100644 --- a/cmd/gui/gui_test.go +++ b/cmd/gui/gui_test.go @@ -5,6 +5,7 @@ import ( "compress/gzip" "io" iofs "io/fs" + "net" "net/http" "net/http/httptest" "os" @@ -99,6 +100,66 @@ func TestOriginFromURL(t *testing.T) { } } +func TestResolveAllowOrigin(t *testing.T) { + wildcard := &net.TCPAddr{IP: net.IPv4zero, Port: 5533} + specific := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5533} + + tests := []struct { + name string + explicitAllowOrigin bool + currentAllowOrigin string + guiOrigin string + addr *net.TCPAddr + noAuth bool + want string + }{ + { + name: "explicit flag wins even on a wildcard bind", + explicitAllowOrigin: true, + currentAllowOrigin: "http://192.0.2.10:5522", + guiOrigin: "http://[::]:5580", + addr: wildcard, + noAuth: false, + want: "http://192.0.2.10:5522", + }, + { + name: "explicit flag wins on a specific bind too", + explicitAllowOrigin: true, + currentAllowOrigin: "http://192.0.2.10:5522", + guiOrigin: "http://127.0.0.1:5580", + addr: specific, + noAuth: false, + want: "http://192.0.2.10:5522", + }, + { + name: "specific bind falls back to the gui origin", + guiOrigin: "http://127.0.0.1:5580", + addr: specific, + want: "http://127.0.0.1:5580", + }, + { + name: "wildcard bind with auth falls back to *", + guiOrigin: "http://[::]:5580", + addr: wildcard, + noAuth: false, + want: "*", + }, + { + name: "wildcard bind without auth falls back to the gui origin", + guiOrigin: "http://[::]:5580", + addr: wildcard, + noAuth: true, + want: "http://[::]:5580", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := resolveAllowOrigin(tt.explicitAllowOrigin, tt.currentAllowOrigin, tt.guiOrigin, tt.addr, tt.noAuth) + assert.Equal(t, tt.want, got) + }) + } +} + // newTestHandler returns a guiHandler backed by the embedded GUI // bundle, or skips the test if it is not present (i.e. `make fetch-gui` // has not been run).