依 R5 五輪決策把 visionA-local 從「Wails 內嵌 Next.js」重構為「Wails
本機伺服器控制台 + 瀏覽器 Web UI」模式(類比 Docker Desktop / Ollama)。
程式碼變動
- M8-1 砍 yt-dlp 全套(後端 resolver / URL handler / 前端 URL tab /
Makefile vendor / installer / bootstrap / CI workflow,-555 行)
- M8-2 砍 Mock 模式全套(driver/mock、mock_camera、Settings runtimeMode、
VISIONA_MOCK 環境變數,-528 行)
- M8-3 ffmpeg 從 GPL 切換到 LGPL 混合方案:Windows/Linux 用 BtbN 現成
LGPL binary,macOS 自 build minimal decoder-only 進 git
(vendor/ffmpeg/macos/ffmpeg 5.7MB + ffprobe 5.6MB,比 GPL 版省 85% 空間)
- M8-4 Wails Server Controller:state machine、log ring buffer 2000 行、
preferences.json atomic write、boot-id、Gin SkipPaths、shutdown 7+1 秒、
notify_*.go 三平台 OS 通知、watchServer 改 Error state 不 os.Exit
- M8-4b 啟動階段管線 R5-E:6 階段進度 event、20s soft / 60s hard timeout、
stage 5/6 skip 規則、sentinel file、RestartStartupSequence 5 步驟
- M8-5 Wails 控制台 vanilla HTML/JS/CSS(9 檔 ~2012 行)取代 M7-B splash:
state 視覺、log panel、startup progress panel、Stage 6 manual CTA
pulse、shutdown modal、Settings、Dark Mode、i18n 中英雙語
- M8-6 上傳影片副檔名擴充(mp4/avi/mov/mpeg/mpg)
- M8-7 Web UI Server Offline Overlay(role=alertdialog + focus trap +
wsEverConnected 容錯 + Page Visibility)
- M8-8 CORS middleware(127.0.0.1/localhost only + suffix attack 防護)+
ws/origin.go 獨立 WebSocket CheckOrigin 避 package cycle
- MAJ-4 server:shutdown-imminent WebSocket broadcast 機制
(/ws/system endpoint + notifyShutdownImminent helper)
- M8-9 Boot-ID + 瀏覽器 tab 自動重連(sessionStorage loop guard)
品質
- ~105+ 新 unit test + race detector (-count=2) 全綠
- 10 個 milestone 全部通過 Reviewer 審查
- 三方 v2 + v2.1 文件(PRD / Design Spec / TDD)+ 交叉互審紀錄
收錄在 .autoflow/
交付前待處理(M8-10)
- 重跑 make payload-macos 把舊 GPL 77MB binary 換成新 LGPL
- 三平台 end-to-end build 驗證
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
11 KiB
Reviewer 審查 M8-8 CORS middleware(2026-04-15)
摘要
- 總結論:✅ 通過。CORSMiddleware + WebSocket CheckOrigin 實作完全符合
v2/cors-security.md§3–§5,build / vet / test / race 全部 PASS,6 個 smoke test 親跑行為正確。 - 阻擋 M8-10 嗎:不阻擋。所有 Critical / Major 皆零,僅 3 個 Minor / Suggestion,不影響後續任務。
A. Origin 判斷邏輯
server/internal/api/middleware.go:33-46
- 使用
url.Parse+strings.ToLower(u.Hostname())+ map 查表(非 regex / prefix match) - 白名單 map
allowedHosts(第 17-22 行)包含127.0.0.1/localhost/::1 - Scheme check 強制
http(第 41 行),自動擋掉https:///ws:///wss:// url.Hostname()自動處理 IPv6 方括號:http://[::1]:3721→ hostname::1- 大小寫不敏感(
strings.ToLower)— 測試http://LOCALHOST:9999PASS - 抗 suffix attack:
http://127.0.0.1.evil.com→ hostname 完整等於127.0.0.1.evil.com,map 查表 miss → false(測試 case 驗證) - 空字串 /
"null"明確拒絕(第 34 行)
符合 TDD §3.1 / §4.1 全部要求。
B. CORSMiddleware 行為
server/internal/api/middleware.go:62-106
| 情境 | 實作 | TDD 要求 | 結果 |
|---|---|---|---|
| Same-origin(無 Origin)非 OPTIONS | 直接 c.Next(),不回 ACA |
§4.1 注釋 | ✅ |
| Same-origin OPTIONS | 回 204 即停 | §4.1 | ✅ |
| 白名單 Origin 非 OPTIONS | 回 ACA-Origin/Methods/Headers/Credentials + Vary: Origin → next |
§4.1 | ✅ |
| 白名單 OPTIONS 預檢 | 回完整 ACA + 204 | §3.2 | ✅(smoke 3 驗證) |
| 非白名單 POST/PUT/DELETE/PATCH/OPTIONS | 403,不回 ACA | §3.2 / §4.1 | ✅ |
| 非白名單 GET/HEAD | 執行 handler 但無 ACA | §3.3 | ✅(smoke 5 驗證) |
- 砍掉 v1 的
X-Relay-Token(relay 功能 M1 已砍) Vary: Origin已加(第 98 行)Access-Control-Allow-Credentials: true(第 97 行)
C. Test 覆蓋度
middleware_test.go
- TestIsAllowedOrigin(19 case):
- 白名單 7 case(含 IPv6、大小寫、無 port)
- scheme 3 case(https × 2、ws × 1)
- hostname 5 case(192.168.x / example.com / malicious.local / suffix 攻擊 × 2)
- 特殊 4 case(空字串、"null"、"http://"、"not-a-url")
- TestCORSMiddleware_ × 7*:
- AllowedOriginGET / LocalhostAllowed / DisallowedOriginPOST / DisallowedOriginGET / PreflightAllowed / PreflightDisallowed / SameOrigin
- 涵蓋
Vary: Origin、ACA-Credentials、非白名單不得回 ACA 三項關鍵斷言。
ws/origin_test.go
- TestCheckOrigin(9 sub-test):empty / 127.0.0.1 / localhost /
[::1]/ https / 192.168 / evil / null / suffix 攻擊
測試覆蓋度完整,符合 TDD §4.2 要求(TDD 只列了 10 case,實作更完整,算加分)。
D. WebSocket Origin 獨立 package
ws/origin.go不 importapipackage(只 importnet/http/net/url/strings),避免api ↔ ws循環(origin.go:1-7+ 注釋 14-17 有明確說明)- 邏輯與
api/middleware.go的isAllowedOrigin一致(scheme、ToLower、hostname 白名單) ws/device_events_ws.go:13-15宣告 package 層級共用var upgrader,CheckOrigin: CheckOrigin;其他 WS handler 全部共用同一個 upgrader(flash_ws.go:11、inference_ws.go:13、server_logs_ws.go:14、system_ws.go:29皆upgrader.Upgrade(...),grep 驗證)device_events_ws.go已移除net/httpimport(依任務描述),建立 upgrader 不再使用 inline anonymousCheckOrigin: func(r *http.Request) bool { return true }
唯一與 HTTP middleware 的行為差異:CheckOrigin 對空 Origin 回 true(same-origin 或 websocat / Postman 非瀏覽器 client),而 HTTP middleware 對空 Origin 走「same-origin 快路徑 → next」。兩者在語意上一致:瀏覽器 same-origin 不送 Origin,非瀏覽器 client(如 websocat)也不會被誤擋。此處符合 TDD §5 注釋。
E. Router 整合
server/internal/api/router.go:44-45
CORSMiddleware()掛在broadcasterLogger後面、api := r.Group("/api")之前,位置合理- M8-4 的
broadcasterLoggerSkipPaths(/api/system/boot-id+/api/system/health)沒被動到(行 123-128、144-147) - M8-4b 的 Hub sentinel 機制沒被動到(
hub_sentinel_test.go跑過 race 仍 PASS,ws/*.go未修改 Hub 相關 code) - WS route 註冊(行 97-102)沒動,所有 WS handler 繼續透過共用
upgrader自動走新CheckOrigin
順序微注意:CORSMiddleware 掛在 broadcasterLogger 之後,代表 403 的 preflight 會被寫 access log。這對排障有幫助,不是問題,符合預期。
F. Build / Test / Race 結果
$ cd server && go build ./... → PASS(無輸出)
$ cd server && go vet ./... → PASS(無輸出)
$ cd server && go test ./... → ALL PASS
ok visiona-local/server/internal/api 0.921s
ok visiona-local/server/internal/api/ws 0.585s
ok visiona-local/server/internal/device
ok visiona-local/server/internal/model
$ cd server && go test -race ./... → ALL PASS(api 2.069s / ws 1.629s)
go test -v -run 'TestIsAllowedOrigin|TestCORSMiddleware|TestCheckOrigin' 所有 case 逐一 PASS,無任何 FAIL / SKIP。
G. Smoke test 結果
於 127.0.0.1:3721(本機已啟動 server,binary 為今日 go-build 產物)親跑:
| # | curl | 預期 | 實測 |
|---|---|---|---|
| 1 | GET /api/system/health -H "Origin: http://127.0.0.1:9999" |
200 + ACA | ✅ 200 + ACA-Origin: http://127.0.0.1:9999 + Vary: Origin + ACA-Credentials: true |
| 2 | POST /api/devices/scan -H "Origin: http://evil.com" |
403 | ✅ 403,無 ACA header |
| 3 | OPTIONS /api/camera/start -H "Origin: http://127.0.0.1:9999" -H "Access-Control-Request-Method: POST" |
204 + 完整 ACA | ✅ 204 + ACA-Methods: GET,POST,PUT,DELETE,OPTIONS + ACA-Origin: http://127.0.0.1:9999 |
| 4 | POST /api/devices/scan -H "Origin: null" |
403 | ✅ 403 |
| 5 | GET /api/system/health -H "Origin: http://evil.com" |
200 無 ACA | ✅ 200,response header 只有 content-type / date / length |
| 6 | POST /api/devices/scan -H "Origin: https://127.0.0.1:3721" |
403 | ✅ 403(scheme mismatch) |
全部 6 條行為與 TDD §9 驗收表對應欄位完全吻合。
H. 安全檢查
- IPv6
http://[::1]:3721parse 正確(hostname 回::1,map 命中) - Origin 大小寫不敏感(
strings.ToLower處理HTTP://127.0.0.1的 hostname 部分;但注意u.Scheme是小寫,因為url.Parse會 normalize scheme;若原字串為HTTP://...,u.Scheme仍回http,測試 casehttp://LOCALHOST:9999PASS) - Port 任意放行(map 只查 hostname)
ws:///wss://不支援:測試 casews://127.0.0.1:3721明確斷言 false ✅。WebSocket 本身不用 Origin 比對 scheme(HTTP upgrade 的 Origin header 本來就是 http/https),所以這個行為正確。- hostname 完整 match:
127.0.0.1≠127.0.0.10(map 查表天然安全),亦擋127.0.0.1.evil.com - IPv4 loopback 只允許
127.0.0.1(127.0.0.2等非標準 loopback 不放行,符合 TDD) u.Scheme != "http"擋掉https(smoke 6 驗證)
無發現攻擊面。
I. 遺漏項
TDD §4.3 的「二道防線」requireSameOriginOrNoOrigin 未實作
TDD v2/cors-security.md:210-244 建議在 /api group 再掛一層 origin check 作為 defense-in-depth,但實作只有 CORSMiddleware(單層)。
判讀:TDD 本身在 §4.3 的開頭寫的是「額外掛一層 origin check,確保即使 CORSMiddleware 有 bug 也不會出事」,屬於 defensive bonus,而不是主線必須。§9 的驗收表格沒有列對應 case。因此:
- 不算 Critical / Major
- 歸為 Minor 建議(見下方清單)
其他可優化但不阻擋
allowedHostsmap 同時有"[::1]"與"::1"兩個 key。url.Parse("http://[::1]:3721").Hostname()只會回::1(不帶方括號),所以"[::1]"是 dead entry。ws/origin.go:36的 switch 同樣寫case "127.0.0.1", "localhost", "::1", "[::1]",[::1]永不命中。
不影響正確性,但有誤導閱讀的風險。
J. 問題清單
Critical
無。
Major
無。
Minor
| # | 檔案 | 行數 | 問題描述 | 建議修改方式 |
|---|---|---|---|---|
| 1 | server/internal/api/middleware.go |
20 | allowedHosts["[::1]"] = true 為 dead entry,url.Hostname() 不會回傳帶方括號版本 |
移除 "[::1]": true, 這一行,並在 ::1 同列加注釋「IPv6 loopback,url.Hostname() 會拆掉方括號」 |
| 2 | server/internal/api/ws/origin.go |
36 | 同上,switch 的 "[::1]" case 永不命中 |
移除 "[::1]" case |
| 3 | server/internal/api/router.go |
52 | 未實作 TDD §4.3 的 requireSameOriginOrNoOrigin 二道防線 |
若使用者 / Architect 要求 defense-in-depth,補上 api.Use(requireSameOriginOrNoOrigin());否則在 TDD §4.3 加 note 說明「已評估,單層 CORSMiddleware 足夠,不做」 |
Suggestion
| # | 檔案 | 行數 | 建議內容 |
|---|---|---|---|
| 1 | server/internal/api/middleware.go |
94 | ACA-Methods 固定 GET, POST, PUT, DELETE, OPTIONS,沒有 HEAD / PATCH。若未來有 PATCH handler 可能要補上。目前 router 無 PATCH 路由,不緊急 |
| 2 | server/internal/api/ws/origin.go |
18-40 | 可考慮讓 api.isAllowedOrigin export 成 api.IsAllowedOrigin,再讓 ws/origin.go import;不過這會引入 ws → api 依賴,需評估層級。目前重複實作 + 注釋交代清楚也可接受 |
K. 結論
✅ 通過 Review,M8-8 可標記為完成,不阻擋 M8-10。
實作品質摘要:
- 邏輯嚴謹:URL parse + map 查表,天然抗 suffix attack;scheme 強制
http;大小寫不敏感 - 測試完整:19 + 7 + 9 = 35 個 assertion,含正向 / 反向 / 邊界
- 架構乾淨:
ws/origin.go獨立實作避免 package cycle,注釋清楚交代理由;所有 WS handler 共用一個 package 層級upgrader變數,單點維護 - 驗證紮實:build / vet / test / race 全部 PASS,6 條 smoke test 對齊 TDD §9 驗收表
建議下一輪(或與後續任務一併)處理的 Minor:移除 [::1] dead entry × 2、決定是否實作 §4.3 二道防線。皆非阻擋項。
Reviewer:Autoflow Reviewer Agent 日期:2026-04-15 審查輪次:第 1 輪 下游任務:M8-10(不阻擋)