package api import ( "net/http" "net/url" "strings" "github.com/gin-gonic/gin" ) // allowedHosts 定義 CORS 白名單的 hostname。 // 任何 port 都允許,scheme 只允許 http(本機不可能是 https)。 // // M8-8(TDD v2/cors-security.md §3.1): // v2 模式下 UI 改在使用者瀏覽器中跑,server 同時暴露給其他瀏覽器分頁, // 必須限定 cross-origin 來源在本機 loopback,避免惡意網站透過 CORS 攻擊。 var allowedHosts = map[string]bool{ "127.0.0.1": true, "localhost": true, "[::1]": true, "::1": true, } // isAllowedOrigin 判斷 Origin header 是否屬於白名單。 // // 合法例:http://127.0.0.1:3721 / http://localhost:3721 / http://[::1]:3721 // 不合法例:https://127.0.0.1:3721 / http://evil.com / null / http://192.168.1.5:3721 // // 注意: // - 空字串視為非白名單(呼叫端會自行決定 same-origin 路徑)。 // - "null"(local file、某些 sandboxed iframe)一律拒絕。 // - 只允許 http scheme,本機不會有 https。 func isAllowedOrigin(origin string) bool { if origin == "" || origin == "null" { return false } u, err := url.Parse(origin) if err != nil { return false } if u.Scheme != "http" { return false } host := strings.ToLower(u.Hostname()) return allowedHosts[host] } // CORSMiddleware 僅允許 127.0.0.1/localhost/::1 任意 port 的跨來源請求。 // // 行為(M8-8 / TDD v2/cors-security.md §4.1): // // 1. Origin header 為空 → same-origin(瀏覽器 same-origin 不送 Origin)→ 直接放行; // 若是 OPTIONS 預檢則回 204 即停(避免帶 ACA* 給沒人看的請求)。 // 2. Origin 在白名單 → 回完整 ACA* headers;OPTIONS → 204;其他方法 → 繼續執行 handler。 // 3. Origin 不在白名單: // - state-changing 方法(POST/PUT/DELETE/PATCH/OPTIONS)→ 403 Forbidden,不回 ACA*。 // - 簡單讀取(GET/HEAD)→ 執行 handler 但不回 ACA*,瀏覽器 JS 讀不到 body。 // // 為什麼 GET/HEAD 不直接擋:CORS 的設計就是讓 GET 可以執行(畢竟 ``、`