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 可以執行(畢竟 `
`、`