package gateway import ( "net/http" "strings" ) // originChecker allows same-origin, same-host, and whitelisted WebSocket origins, // mirroring the CORS allow list. A "*" allows all. // // Same-host relaxation: when cyrene serves its own embedded frontend (production // mode), the page origin is exactly the gateway's Host (e.g. http://localhost:10942), // which is not in the dev-phase ALLOWED_ORIGINS list. We must let that through so a // user who changes PORT does not need to edit .env for the WS to connect. Only truly // cross-origin clients (external IM / dashboards on another origin) hit the allow-list. func originChecker(allowed map[string]bool, allowAll bool) func(*http.Request) bool { return func(r *http.Request) bool { origin := r.Header.Get("Origin") if origin == "" { return true } if allowAll { return true } if allowed[origin] { return true } // Same-host: the Origin matches this request's Host (scheme://host:port == host[:port]). return sameOriginAsHost(origin, r) } } // sameOriginAsHost reports whether origin (a full URL origin string such as // "http://localhost:10942") corresponds to this request's Host header. It compares // the host[:port] portion with loopback equivalence: "localhost" and "127.0.0.1" // (and the IPv6 form) are treated as the same host, so serving the embedded frontend // on 127.0.0.1 while a client connects via localhost (or vice versa) still passes. func sameOriginAsHost(origin string, r *http.Request) bool { if origin == "" || r == nil { return false } after := origin if i := strings.Index(after, "://"); i >= 0 { after = after[i+3:] } if i := strings.IndexByte(after, '/'); i >= 0 { after = after[:i] } return hostEquivalent(after, r.Host) } // hostEquivalent compares two host[:port] strings, treating empty-port variants, // case, and loopback aliases as equal. It handles "localhost" vs "127.0.0.1" and // bracketed IPv6 loopback "[::1]". func hostEquivalent(a, b string) bool { na := normalizeHost(a) nb := normalizeHost(b) if na == nb { return true } // Loopback aliases: localhost, 127.0.0.1, [::1] loop := map[string]bool{"localhost": true, "127.0.0.1": true, "[::1]": true, "::1": true, "127.0.0.2": true} return loop[na] && loop[nb] } // normalizeHost strips the port (if present), lowercases, and trims brackets. func normalizeHost(h string) string { h = strings.ToLower(strings.TrimSpace(h)) if i := strings.LastIndex(h, ":"); i >= 0 && !strings.Contains(h[i:], "]") { h = h[:i] } return strings.TrimPrefix(strings.TrimSuffix(h, "]"), "[") }