diff --git a/web/backend/api/gateway_host.go b/web/backend/api/gateway_host.go index e48f4b6a8..a4899e19e 100644 --- a/web/backend/api/gateway_host.go +++ b/web/backend/api/gateway_host.go @@ -75,6 +75,38 @@ func requestHostName(r *http.Request) string { return "127.0.0.1" } +// clientFacingAuthorityFromRequest returns host[:port] for URLs returned to the +// browser. If Host / X-Forwarded-Host has no explicit port, the listen port is +// omitted so default ports for http/https/ws/wss apply (reverse proxy, FRP, etc.). +func clientFacingAuthorityFromRequest(r *http.Request, listenPort int) string { + raw := strings.TrimSpace(r.Header.Get("X-Forwarded-Host")) + if raw != "" { + raw = strings.TrimSpace(strings.Split(raw, ",")[0]) + } else { + raw = strings.TrimSpace(r.Host) + } + if raw == "" { + return net.JoinHostPort("127.0.0.1", strconv.Itoa(listenPort)) + } + host, port, err := net.SplitHostPort(raw) + if err != nil { + return raw + } + return net.JoinHostPort(host, port) +} + +func (h *Handler) picoURLAuthority(r *http.Request, cfg *config.Config) string { + webPort := h.serverPort + if webPort == 0 { + webPort = 18800 + } + bind := h.effectiveGatewayBindHost(cfg) + if bind != "" && bind != "0.0.0.0" { + return net.JoinHostPort(bind, strconv.Itoa(webPort)) + } + return clientFacingAuthorityFromRequest(r, webPort) +} + // forwardedRFC7239Proto returns the proto value from the first Forwarded element (RFC 7239), or "". func forwardedRFC7239Proto(r *http.Request) string { raw := strings.TrimSpace(r.Header.Get("Forwarded")) @@ -149,39 +181,16 @@ func requestHTTPScheme(r *http.Request) string { } func (h *Handler) buildWsURL(r *http.Request, cfg *config.Config) string { - host := h.effectiveGatewayBindHost(cfg) - if host == "" || host == "0.0.0.0" { - host = requestHostName(r) - } - // Use web server port instead of gateway port to avoid exposing extra ports - // The WebSocket connection will be proxied by the backend to the gateway - wsPort := h.serverPort - if wsPort == 0 { - wsPort = 18800 // default web server port - } - return requestWSScheme(r) + "://" + net.JoinHostPort(host, strconv.Itoa(wsPort)) + "/pico/ws" + auth := h.picoURLAuthority(r, cfg) + return requestWSScheme(r) + "://" + auth + "/pico/ws" } func (h *Handler) buildPicoEventsURL(r *http.Request, cfg *config.Config) string { - host := h.effectiveGatewayBindHost(cfg) - if host == "" || host == "0.0.0.0" { - host = requestHostName(r) - } - webPort := h.serverPort - if webPort == 0 { - webPort = 18800 - } - return requestHTTPScheme(r) + "://" + net.JoinHostPort(host, strconv.Itoa(webPort)) + "/pico/events" + auth := h.picoURLAuthority(r, cfg) + return requestHTTPScheme(r) + "://" + auth + "/pico/events" } func (h *Handler) buildPicoSendURL(r *http.Request, cfg *config.Config) string { - host := h.effectiveGatewayBindHost(cfg) - if host == "" || host == "0.0.0.0" { - host = requestHostName(r) - } - webPort := h.serverPort - if webPort == 0 { - webPort = 18800 - } - return requestHTTPScheme(r) + "://" + net.JoinHostPort(host, strconv.Itoa(webPort)) + "/pico/send" + auth := h.picoURLAuthority(r, cfg) + return requestHTTPScheme(r) + "://" + auth + "/pico/send" } diff --git a/web/backend/api/gateway_host_test.go b/web/backend/api/gateway_host_test.go index 30a8921dd..86da0c7c9 100644 --- a/web/backend/api/gateway_host_test.go +++ b/web/backend/api/gateway_host_test.go @@ -147,8 +147,8 @@ func TestBuildWsURLUsesWSSWhenForwardedProtoIsHTTPS(t *testing.T) { req.Host = "chat.example.com" req.Header.Set("X-Forwarded-Proto", "https") - if got := h.buildWsURL(req, cfg); got != "wss://chat.example.com:18800/pico/ws" { - t.Fatalf("buildWsURL() = %q, want %q", got, "wss://chat.example.com:18800/pico/ws") + if got := h.buildWsURL(req, cfg); got != "wss://chat.example.com/pico/ws" { + t.Fatalf("buildWsURL() = %q, want %q", got, "wss://chat.example.com/pico/ws") } } @@ -164,8 +164,8 @@ func TestBuildWsURLUsesWSSWhenRequestIsTLS(t *testing.T) { req.Host = "secure.example.com" req.TLS = &tls.ConnectionState{} - if got := h.buildWsURL(req, cfg); got != "wss://secure.example.com:18800/pico/ws" { - t.Fatalf("buildWsURL() = %q, want %q", got, "wss://secure.example.com:18800/pico/ws") + if got := h.buildWsURL(req, cfg); got != "wss://secure.example.com/pico/ws" { + t.Fatalf("buildWsURL() = %q, want %q", got, "wss://secure.example.com/pico/ws") } } @@ -182,8 +182,8 @@ func TestBuildWsURLPrefersForwardedHTTPOverTLS(t *testing.T) { req.TLS = &tls.ConnectionState{} req.Header.Set("X-Forwarded-Proto", "http") - if got := h.buildWsURL(req, cfg); got != "ws://chat.example.com:18800/pico/ws" { - t.Fatalf("buildWsURL() = %q, want %q", got, "ws://chat.example.com:18800/pico/ws") + if got := h.buildWsURL(req, cfg); got != "ws://chat.example.com/pico/ws" { + t.Fatalf("buildWsURL() = %q, want %q", got, "ws://chat.example.com/pico/ws") } } @@ -199,8 +199,8 @@ func TestBuildWsURLUsesWSSWhenForwardedHeaderProtoIsHTTPS(t *testing.T) { req.Host = "chat.example.com" req.Header.Set("Forwarded", `for=203.0.113.1;proto=https;host=chat.example.com`) - if got := h.buildWsURL(req, cfg); got != "wss://chat.example.com:18800/pico/ws" { - t.Fatalf("buildWsURL() = %q, want %q", got, "wss://chat.example.com:18800/pico/ws") + if got := h.buildWsURL(req, cfg); got != "wss://chat.example.com/pico/ws" { + t.Fatalf("buildWsURL() = %q, want %q", got, "wss://chat.example.com/pico/ws") } } @@ -216,7 +216,29 @@ func TestBuildWsURLUsesWSSWhenXForwardedSslOn(t *testing.T) { req.Host = "chat.example.com" req.Header.Set("X-Forwarded-Ssl", "on") - if got := h.buildWsURL(req, cfg); got != "wss://chat.example.com:18800/pico/ws" { - t.Fatalf("buildWsURL() = %q, want %q", got, "wss://chat.example.com:18800/pico/ws") + if got := h.buildWsURL(req, cfg); got != "wss://chat.example.com/pico/ws" { + t.Fatalf("buildWsURL() = %q, want %q", got, "wss://chat.example.com/pico/ws") + } +} + +func TestPicoURLsPreferForwardedHostAndOmitPortWhenUnspecified(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "config.json") + h := NewHandler(configPath) + h.SetServerOptions(8080, false, false, nil) + + cfg := config.DefaultConfig() + cfg.Gateway.Host = "0.0.0.0" + cfg.Gateway.Port = 18790 + + req := httptest.NewRequest("GET", "http://127.0.0.1:8080/api/pico/token", nil) + req.Host = "127.0.0.1:8080" + req.Header.Set("X-Forwarded-Host", "piclaw02-3rho55-8080.svc-usw2.nicegpu.com") + req.Header.Set("X-Forwarded-Proto", "http") + + if got := h.buildPicoEventsURL(req, cfg); got != "http://piclaw02-3rho55-8080.svc-usw2.nicegpu.com/pico/events" { + t.Fatalf("buildPicoEventsURL() = %q, want %q", got, "http://piclaw02-3rho55-8080.svc-usw2.nicegpu.com/pico/events") + } + if got := h.buildPicoSendURL(req, cfg); got != "http://piclaw02-3rho55-8080.svc-usw2.nicegpu.com/pico/send" { + t.Fatalf("buildPicoSendURL() = %q, want %q", got, "http://piclaw02-3rho55-8080.svc-usw2.nicegpu.com/pico/send") } }