refactor(api): streamline WebSocket and Pico URL construction to prefer forwarded host and omit port when unspecified

This commit is contained in:
zeed-w-beez 2026-03-25 13:17:41 +08:00
parent 01705249af
commit 051a737eeb
No known key found for this signature in database
GPG key ID: 80D9A061738877DD
2 changed files with 70 additions and 39 deletions

View file

@ -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"
}

View file

@ -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")
}
}