The shared WebSocket upgrader rejected handshakes unless the browser's Origin host exactly matched r.Host. Behind a TLS-terminating CDN/proxy that rewrites Host to the internal origin (carrying the public host in X-Forwarded-Host), this comparison always failed and every realtime socket 403'd at the handshake — playback control, events, watch-together rooms, and admin log streaming all share the upgrader. checkWebSocketOrigin now also accepts an Origin matching X-Forwarded-Host, keeping the same-origin CSRF guard intact while supporting proxied deployments. Extract a shared forwardedHost helper (first hop of a multi-proxy list) and reuse it from requestBaseURL, replacing the duplicated inline parse. Also reject opaque (empty-host) origins explicitly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
30 lines
930 B
Go
30 lines
930 B
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// forwardedHost returns the public host a reverse proxy or CDN forwarded the
|
|
// request on behalf of, taken from the first X-Forwarded-Host value, or "" when
|
|
// the header is absent.
|
|
//
|
|
// Behind a TLS-terminating CDN/proxy, r.Host is the internal origin host the
|
|
// proxy dialed, while the public host the browser actually used arrives here.
|
|
// Callers that need to reason about the client-facing host (origin checks,
|
|
// absolute URL construction) must prefer this over r.Host.
|
|
func forwardedHost(r *http.Request) string {
|
|
forwarded := strings.TrimSpace(r.Header.Get("X-Forwarded-Host"))
|
|
if forwarded == "" {
|
|
return ""
|
|
}
|
|
|
|
// Multiple proxy hops produce a comma-separated list; the first entry is
|
|
// the original client-facing host.
|
|
if comma := strings.IndexByte(forwarded, ','); comma >= 0 {
|
|
forwarded = forwarded[:comma]
|
|
}
|
|
|
|
return strings.TrimSpace(forwarded)
|
|
}
|