package gateway import ( "net/http" "strings" ) // originChecker 允许同源、同 host 以及白名单内的 WebSocket 来源, // 镜像 CORS 允许列表。"*" 允许全部。 // // 同 host 放宽:cyrene 提供自己的内嵌前端时(生产 // 模式),页面来源恰好是网关的 Host(例如 http://localhost:10942), // 它不在开发期的 ALLOWED_ORIGINS 列表中。我们必须放行它,使 // 改过 PORT 的用户不必为了 WS 能连上而编辑 .env。只有真正的 // 跨源客户端(外部 IM / 另一来源上的看板)才会走允许列表。 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 } // 同 host:Origin 与该请求的 Host 匹配(scheme://host:port == host[:port])。 return sameOriginAsHost(origin, r) } } // sameOriginAsHost 报告 origin(完整的 URL 来源串,例如 // "http://localhost:10942")是否对应本请求的 Host 头。它比较 // host[:port] 部分并做回环等价:"localhost" 与 "127.0.0.1" // (以及 IPv6 形式)被视为同一 host,因此在 127.0.0.1 上提供内嵌 // 前端而客户端经 localhost 连接(或反之)仍然通过。 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 比较两个 host[:port] 串,把空端口变体、 // 大小写与回环别名都视为相等。它处理 "localhost" 与 "127.0.0.1" // 以及带括号的 IPv6 回环 "[::1]"。 func hostEquivalent(a, b string) bool { na := normalizeHost(a) nb := normalizeHost(b) if na == nb { return true } // 回环别名: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 剥掉端口(若有)、转小写并去掉括号。 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, "]"), "[") }