- ws_hub.go 182(hub核心/常量/wsEvent/newHub/broadcast) + ws_hub_conn.go 165(连接层handleWS/read·writePump) + ws_hub_turn.go 168(relay/streamChat/runTurn/interrupt/broadcastStream) + ws_hub_origin.go 73(Origin校验) + ws_hub_message_event.go 30(message_edited/deleted广播) - 代码行多重集 identical + 顶层声明diff空; 每文件≤500/函数≤80(最大handleWS 28/streamChat 25) - gateway测试绿 + build/vet + win/darwin交叉编译绿
74 lines
2.5 KiB
Go
74 lines
2.5 KiB
Go
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, "]"), "[")
|
|
}
|