mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 02:55:51 +00:00
fix(api): let live request origin outrank auto-detected public URL
Agent install and update commands advertised http://<LAN-IP>:7655 on installs where the operator browses Pulse over an https FQDN, because boot-time detection pre-fills config.PublicURL with an IP guess and resolvePublicURL treated that guess as configuration. Track auto-detected values (boot probe or first-request capture) separately: explicit config from the env var or the settings UI still wins, but an auto-detected guess now yields to the origin of the admin request that is asking, so copied commands match how the instance is actually reached. Also stop boot detection from clobbering a settings-persisted public URL on restart, which silently reverted the operator's saved value whenever PULSE_PUBLIC_URL was unset. Refs #1692 Contract-Neutral: behavioral fix to advertised-URL derivation: no API shape, field, or endpoint changes; PublicURLAutoDetected is internal (json:-)
This commit is contained in:
+19
-7
@@ -4765,6 +4765,7 @@ func (r *Router) capturePublicURLFromRequest(req *http.Request) {
|
||||
}
|
||||
|
||||
r.config.PublicURL = normalizedCandidate
|
||||
r.config.PublicURLAutoDetected = true
|
||||
r.publicURLDetected = true
|
||||
r.publicURLMu.Unlock()
|
||||
|
||||
@@ -10923,7 +10924,12 @@ func (r *Router) resolvePublicURL(req *http.Request) string {
|
||||
return strings.TrimRight(agentConnectURL, "/")
|
||||
}
|
||||
|
||||
if publicURL := strings.TrimSpace(r.config.PublicURL); publicURL != "" {
|
||||
// An operator-configured public URL is authoritative. An auto-detected one
|
||||
// is only a guess (boot-time LAN IP probe or first-request capture), so the
|
||||
// live request's own origin outranks it: the URL the admin is browsing
|
||||
// right now is fresher evidence of how this instance is reached (#1692).
|
||||
publicURL := strings.TrimSpace(r.config.PublicURL)
|
||||
if publicURL != "" && !r.config.PublicURLAutoDetected {
|
||||
return strings.TrimRight(publicURL, "/")
|
||||
}
|
||||
|
||||
@@ -10940,12 +10946,18 @@ func (r *Router) resolvePublicURL(req *http.Request) string {
|
||||
if req != nil {
|
||||
host = strings.TrimSpace(req.Host)
|
||||
}
|
||||
if host == "" {
|
||||
if r.config.FrontendPort > 0 {
|
||||
host = fmt.Sprintf("localhost:%d", r.config.FrontendPort)
|
||||
} else {
|
||||
host = "localhost:7655"
|
||||
}
|
||||
if host != "" {
|
||||
return fmt.Sprintf("%s://%s", scheme, host)
|
||||
}
|
||||
|
||||
if publicURL != "" {
|
||||
return strings.TrimRight(publicURL, "/")
|
||||
}
|
||||
|
||||
if r.config.FrontendPort > 0 {
|
||||
host = fmt.Sprintf("localhost:%d", r.config.FrontendPort)
|
||||
} else {
|
||||
host = "localhost:7655"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s://%s", scheme, host)
|
||||
|
||||
@@ -847,6 +847,72 @@ func TestResolvePublicURL_ConfiguredPublicURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePublicURL_AutoDetectedYieldsToRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
publicURL string
|
||||
autoDetected bool
|
||||
reqHost string
|
||||
forwardProto string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "auto-detected loses to request origin",
|
||||
publicURL: "http://192.168.1.50:7655",
|
||||
autoDetected: true,
|
||||
reqHost: "pulse.example.com",
|
||||
forwardProto: "https",
|
||||
want: "https://pulse.example.com",
|
||||
},
|
||||
{
|
||||
name: "auto-detected loses to plain http request origin",
|
||||
publicURL: "http://192.168.1.50:7655",
|
||||
autoDetected: true,
|
||||
reqHost: "pulse.lan:7655",
|
||||
want: "http://pulse.lan:7655",
|
||||
},
|
||||
{
|
||||
name: "explicit config still beats request origin",
|
||||
publicURL: "http://192.168.1.50:7655",
|
||||
autoDetected: false,
|
||||
reqHost: "pulse.example.com",
|
||||
forwardProto: "https",
|
||||
want: "http://192.168.1.50:7655",
|
||||
},
|
||||
{
|
||||
name: "auto-detected survives when request has no host",
|
||||
publicURL: "http://192.168.1.50:7655",
|
||||
autoDetected: true,
|
||||
want: "http://192.168.1.50:7655",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Router{
|
||||
config: &config.Config{
|
||||
PublicURL: tt.publicURL,
|
||||
PublicURLAutoDetected: tt.autoDetected,
|
||||
},
|
||||
}
|
||||
|
||||
var req *http.Request
|
||||
if tt.reqHost != "" {
|
||||
req = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Host = tt.reqHost
|
||||
if tt.forwardProto != "" {
|
||||
req.Header.Set("X-Forwarded-Proto", tt.forwardProto)
|
||||
}
|
||||
}
|
||||
|
||||
got := r.resolvePublicURL(req)
|
||||
if got != tt.want {
|
||||
t.Errorf("resolvePublicURL() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePublicURL_FromRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -1042,6 +1042,8 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
||||
}
|
||||
if _, ok := rawRequest["publicURL"]; ok {
|
||||
h.config.PublicURL = settings.PublicURL
|
||||
// Operator input, even empty, supersedes any auto-detected guess.
|
||||
h.config.PublicURLAutoDetected = false
|
||||
}
|
||||
|
||||
// ---- Side effects (discovery, temperature, notifications) ----
|
||||
|
||||
+17
-10
@@ -137,14 +137,18 @@ func normalizeEnvAuthPassword(raw string) (string, error) {
|
||||
// NOTE: The envconfig tags are legacy and not used - configuration is loaded from encrypted JSON files
|
||||
type Config struct {
|
||||
// Server settings
|
||||
BindAddress string
|
||||
FrontendPort int `envconfig:"FRONTEND_PORT" default:"7655"`
|
||||
ConfigPath string
|
||||
DataPath string
|
||||
AppRoot string `json:"-"` // Root directory of the application (where binary lives)
|
||||
PublicURL string `envconfig:"PULSE_PUBLIC_URL" default:""` // Full URL to access Pulse (e.g., http://198.51.100.100:7655)
|
||||
AgentConnectURL string `envconfig:"PULSE_AGENT_CONNECT_URL" default:""` // Dedicated direct connect URL for agents (e.g. http://192.0.2.5:7655)
|
||||
AgentIngestPort int `envconfig:"PULSE_AGENT_INGEST_PORT" default:"0"` // When >0, additionally serve agent ingest (/api/agents/*) on this dedicated port so it can be network-isolated from the UI/API. 0 = disabled (single-port, default).
|
||||
BindAddress string
|
||||
FrontendPort int `envconfig:"FRONTEND_PORT" default:"7655"`
|
||||
ConfigPath string
|
||||
DataPath string
|
||||
AppRoot string `json:"-"` // Root directory of the application (where binary lives)
|
||||
PublicURL string `envconfig:"PULSE_PUBLIC_URL" default:""` // Full URL to access Pulse (e.g., http://198.51.100.100:7655)
|
||||
// PublicURLAutoDetected marks PublicURL as a boot-time or request-time
|
||||
// guess rather than operator configuration. Auto-detected values yield to
|
||||
// the live request origin when advertising URLs (refs #1692).
|
||||
PublicURLAutoDetected bool `json:"-"`
|
||||
AgentConnectURL string `envconfig:"PULSE_AGENT_CONNECT_URL" default:""` // Dedicated direct connect URL for agents (e.g. http://192.0.2.5:7655)
|
||||
AgentIngestPort int `envconfig:"PULSE_AGENT_INGEST_PORT" default:"0"` // When >0, additionally serve agent ingest (/api/agents/*) on this dedicated port so it can be network-isolated from the UI/API. 0 = disabled (single-port, default).
|
||||
|
||||
// Proxmox VE connections
|
||||
PVEInstances []PVEInstance
|
||||
@@ -1675,10 +1679,13 @@ func load(initLogging bool) (*Config, error) {
|
||||
// In hosted mode, fail closed unless explicitly configured.
|
||||
if os.Getenv("PULSE_HOSTED_MODE") == "true" {
|
||||
log.Warn().Msg("Hosted mode enabled: public URL not configured; external links (e.g., magic links) will be disabled. Set PULSE_PUBLIC_URL.")
|
||||
} else {
|
||||
// Try to auto-detect public URL if not explicitly configured
|
||||
} else if cfg.PublicURL == "" {
|
||||
// Try to auto-detect public URL if not explicitly configured.
|
||||
// A value loaded from system settings is operator configuration
|
||||
// and must not be clobbered by detection on restart.
|
||||
if detectedURL := detectPublicURL(cfg.FrontendPort); detectedURL != "" {
|
||||
cfg.PublicURL = detectedURL
|
||||
cfg.PublicURLAutoDetected = true
|
||||
log.Info().Str("url", detectedURL).Msg("Auto-detected public URL for webhook notifications")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user