jim800121chen 8cd5751ce3 feat(local-tool): M8 重構 — Wails 控制台 + 瀏覽器 Web UI(R5 決策)
依 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>
2026-04-15 17:57:54 +08:00

11 KiB
Raw Blame History

Reviewer 審查 M8-8 CORS middleware2026-04-15

摘要

  • 總結論 通過。CORSMiddleware + WebSocket CheckOrigin 實作完全符合 v2/cors-security.md §3§5build / vet / test / race 全部 PASS6 個 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:9999 PASS
  • 抗 suffix attackhttp://127.0.0.1.evil.com → hostname 完整等於 127.0.0.1.evil.commap 查表 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-Tokenrelay 功能 M1 已砍)
  • Vary: Origin 已加(第 98 行)
  • Access-Control-Allow-Credentials: true(第 97 行)

C. Test 覆蓋度

middleware_test.go

  • TestIsAllowedOrigin19 case
    • 白名單 7 case含 IPv6、大小寫、無 port
    • scheme 3 casehttps × 2、ws × 1
    • hostname 5 case192.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: OriginACA-Credentials、非白名單不得回 ACA 三項關鍵斷言。

ws/origin_test.go

  • TestCheckOrigin9 sub-testempty / 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 不 import api package只 import net/http / net/url / strings),避免 api ↔ ws 循環(origin.go:1-7 + 注釋 14-17 有明確說明)
  • 邏輯與 api/middleware.goisAllowedOrigin 一致scheme、ToLower、hostname 白名單)
  • ws/device_events_ws.go:13-15 宣告 package 層級共用 var upgraderCheckOrigin: CheckOrigin;其他 WS handler 全部共用同一個 upgraderflash_ws.go:11inference_ws.go:13server_logs_ws.go:14system_ws.go:29upgrader.Upgrade(...)grep 驗證)
  • device_events_ws.go 已移除 net/http import依任務描述建立 upgrader 不再使用 inline anonymous CheckOrigin: func(r *http.Request) bool { return true }

唯一與 HTTP middleware 的行為差異CheckOrigin 對空 Origin 回 truesame-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 仍 PASSws/*.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 PASSapi 2.069s / ws 1.629s

go test -v -run 'TestIsAllowedOrigin|TestCORSMiddleware|TestCheckOrigin' 所有 case 逐一 PASS無任何 FAIL / SKIP。


G. Smoke test 結果

127.0.0.1:3721(本機已啟動 serverbinary 為今日 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 200response header 只有 content-type / date / length
6 POST /api/devices/scan -H "Origin: https://127.0.0.1:3721" 403 403scheme mismatch

全部 6 條行為與 TDD §9 驗收表對應欄位完全吻合。


H. 安全檢查

  • IPv6 http://[::1]:3721 parse 正確hostname 回 ::1map 命中)
  • Origin 大小寫不敏感(strings.ToLower 處理 HTTP://127.0.0.1 的 hostname 部分;但注意 u.Scheme 是小寫,因為 url.Parse 會 normalize scheme若原字串為 HTTP://...u.Scheme 仍回 http,測試 case http://LOCALHOST:9999 PASS
  • Port 任意放行map 只查 hostname
  • ws:// / wss:// 不支援:測試 case ws://127.0.0.1:3721 明確斷言 false 。WebSocket 本身不用 Origin 比對 schemeHTTP upgrade 的 Origin header 本來就是 http/https所以這個行為正確。
  • hostname 完整 match127.0.0.1127.0.0.10map 查表天然安全),亦擋 127.0.0.1.evil.com
  • IPv4 loopback 只允許 127.0.0.1127.0.0.2 等非標準 loopback 不放行,符合 TDD
  • u.Scheme != "http" 擋掉 httpssmoke 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 建議(見下方清單)

其他可優化但不阻擋

  • allowedHosts map 同時有 "[::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 entryurl.Hostname() 不會回傳帶方括號版本 移除 "[::1]": true, 這一行,並在 ::1 同列加注釋「IPv6 loopbackurl.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. 結論

通過 ReviewM8-8 可標記為完成,不阻擋 M8-10。

實作品質摘要:

  • 邏輯嚴謹URL parse + map 查表,天然抗 suffix attackscheme 強制 http;大小寫不敏感
  • 測試完整19 + 7 + 9 = 35 個 assertion含正向 / 反向 / 邊界
  • 架構乾淨:ws/origin.go 獨立實作避免 package cycle注釋清楚交代理由所有 WS handler 共用一個 package 層級 upgrader 變數,單點維護
  • 驗證紮實build / vet / test / race 全部 PASS6 條 smoke test 對齊 TDD §9 驗收表

建議下一輪(或與後續任務一併)處理的 Minor移除 [::1] dead entry × 2、決定是否實作 §4.3 二道防線。皆非阻擋項。


ReviewerAutoflow Reviewer Agent 日期2026-04-15 審查輪次:第 1 輪 下游任務M8-10不阻擋