feat(ws): flash-progress WS tunnel proxy(後端,載入模型進度回顯)
flash「載入模型到裝置」後端缺口:flash-progress WS 從 501 stub 換真
tunnel proxy,複用 inference WS 的 path-agnostic handler。
- camera.go registerWebSocketRoutes:加 /ws/devices/:id/flash-progress
(複用 newWebSocketProxyHandler、掛 wsAuthGroup same-origin cookie)
- stubs.go:刪 flash-progress 501 stub(成對防 radix panic)+ 更新註解
- api.go:更新 wsAuthGroup 註解(現含 inference + flash-progress 兩條裝置級 WS)
- camera_ws_test.go:+3 test(NoForwarder/TunnelDisconnected/CoexistsWithInference)
- all_endpoints_require_auth_test.go:收窄白名單,flash-progress 從 skipped→
covered 納入「無 cookie 應 401」回歸檢查(守得住證明:移除 auth 即 FAIL)
契約:WS /ws/devices/:id/flash-progress、payload raw {percent,stage,message?,error?}
透明轉發。Reviewer 0C/0M/1Mi 通過(Mi 註解已修)。build/vet/全回歸綠。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
051994ed54
commit
4f50ad7350
@ -45,8 +45,9 @@ import (
|
||||
//
|
||||
// /ws/* stub endpoints(仍是 501、尚未實作 WebSocket proxy、註冊在 r 而非 wsAuthGroup,
|
||||
// 故不走 AuthMiddleware)也逐條列在這裡。**逐條精確列出**、而非用 /ws/ prefix 一次放行,
|
||||
// 是為了讓「已升級成 authed 的 WS route」(如 GET /ws/devices/:id/inference,掛在
|
||||
// wsAuthGroup、走 same-origin cookie AuthMiddleware)自動落入主測試的「必須 401」檢查。
|
||||
// 是為了讓「已升級成 authed 的 WS route」(如 GET /ws/devices/:id/inference 與
|
||||
// GET /ws/devices/:id/flash-progress,皆掛在 wsAuthGroup、走 same-origin cookie
|
||||
// AuthMiddleware)自動落入主測試的「必須 401」檢查。
|
||||
// 未來任一條 stub 補實作並套 auth 後,把它從這份清單移除即可納入回歸檢查。
|
||||
//
|
||||
// 任何往這份清單裡新加 endpoint 的 PR 都該特別 review — 你正在繞過 OIDC 保護。
|
||||
@ -57,10 +58,10 @@ var publicPaths = map[string]bool{
|
||||
"POST /api/pairing/exchange": true,
|
||||
|
||||
// /ws/* 仍為 501 stub 的 endpoint(見 internal/api/stubs.go registerWebSocketStubs)。
|
||||
// 注意:GET /ws/devices/:id/inference 已升級為 authed(wsAuthGroup),**刻意不在此清單**,
|
||||
// 注意:GET /ws/devices/:id/inference 與 GET /ws/devices/:id/flash-progress 已升級為
|
||||
// authed(掛 wsAuthGroup,走 same-origin cookie AuthMiddleware),**刻意不在此清單**,
|
||||
// 因此會被主測試納入「無 cookie 應回 401」的檢查。
|
||||
"GET /ws/devices/events": true,
|
||||
"GET /ws/devices/:id/flash-progress": true,
|
||||
"GET /ws/server-logs": true,
|
||||
"GET /ws/system": true,
|
||||
"GET /ws/clusters/:id/inference": true,
|
||||
|
||||
@ -233,12 +233,13 @@ func NewRouter(deps Deps) *gin.Engine {
|
||||
// Pairing Token 本身就是這個 endpoint 的憑證。詳見 security.md §1.2。
|
||||
registerPairingPublicRoutes(r, deps)
|
||||
|
||||
// /ws/* 雛形大多仍 501;已實作的 WS tunnel proxy(/ws/devices/:id/inference)
|
||||
// 改掛在下方 wsAuthGroup(AuthMiddleware group)。
|
||||
// /ws/* 雛形大多仍 501;已實作的 WS tunnel proxy(/ws/devices/:id/inference
|
||||
// 與 /ws/devices/:id/flash-progress)改掛在下方 wsAuthGroup(AuthMiddleware group)。
|
||||
registerWebSocketStubs(r)
|
||||
|
||||
// WS tunnel proxy group:走 same-origin cookie AuthMiddleware(security 定案,
|
||||
// 不放 token 到 URL)。目前只有推論結果 WS(/ws/devices/:id/inference)。
|
||||
// 不放 token 到 URL)。目前有兩條裝置級 WS:推論結果(/ws/devices/:id/inference)
|
||||
// 與 flash 進度回顯(/ws/devices/:id/flash-progress),兩者都走透明 tunnel proxy。
|
||||
// 刻意獨立成 group 而非掛 /api:WS endpoint 對外路徑就是 /ws/*(對齊前端與
|
||||
// api-spec);但認證邏輯與 /api 共用 AuthMiddleware。
|
||||
wsAuthGroup := r.Group("/ws")
|
||||
|
||||
@ -66,14 +66,25 @@ func registerCameraRoutes(g *gin.RouterGroup, deps Deps) {
|
||||
|
||||
// registerWebSocketRoutes 註冊需要 WS tunnel proxy 的 /ws/* endpoint。
|
||||
//
|
||||
// 目前只有 /ws/devices/:id/inference(推論結果即時推播 — camera overlay + media 結果
|
||||
// 顯示的共用資料通道)。掛在 wsGroup(已套 AuthMiddleware)下,走 same-origin cookie
|
||||
// 認證,不放 token 到 URL(security 定案)。
|
||||
// 目前有兩條:
|
||||
// - /ws/devices/:id/inference — 推論結果即時推播(camera overlay + media 結果
|
||||
// 顯示的共用資料通道)。
|
||||
// - /ws/devices/:id/flash-progress — flash(載入模型到裝置)進度回顯(percent /
|
||||
// stage / message / error,raw JSON 透明轉發)。
|
||||
//
|
||||
// 其餘 /ws/* 仍是 registerWebSocketStubs 的 501(events / flash-progress / server-logs /
|
||||
// system / clusters / pairing),非本次範圍。
|
||||
// 兩條共用同一個 newWebSocketProxyHandler:該 handler 是 path-agnostic 的
|
||||
// (outPath 取自 c.Request.URL.Path,原樣轉發到 local agent,不綁定任何 path),
|
||||
// 所以 inference 與 flash-progress 只差路由掛載,handler 零改動。local agent 端
|
||||
// 兩條 path 都已實作(router.go /ws/devices/:id/flash-progress → flash_ws.go)。
|
||||
//
|
||||
// 掛在 wsGroup(已套 AuthMiddleware)下,走 same-origin cookie 認證,不放 token 到
|
||||
// URL(security 定案)。
|
||||
//
|
||||
// 其餘 /ws/* 仍是 registerWebSocketStubs 的 501(events / server-logs / system /
|
||||
// clusters / pairing),非本次範圍。
|
||||
//
|
||||
// wsGroup 必須是「path 前綴為 /ws 且套了 AuthMiddleware」的 group(見 api.go NewRouter)。
|
||||
func registerWebSocketRoutes(wsGroup *gin.RouterGroup, deps Deps) {
|
||||
wsGroup.GET("/devices/:id/inference", newWebSocketProxyHandler(deps))
|
||||
wsGroup.GET("/devices/:id/flash-progress", newWebSocketProxyHandler(deps))
|
||||
}
|
||||
|
||||
@ -46,6 +46,51 @@ func TestWSInference_TunnelDisconnected(t *testing.T) {
|
||||
assert.Contains(t, w.Body.String(), ErrCodeTunnelDisconnect)
|
||||
}
|
||||
|
||||
// TestWSFlashProgress_NoForwarder 驗證 flash-progress route 已掛上(非 404/501-stub),
|
||||
// 且與 inference 共用同一個 path-agnostic handler:缺 Forwarder 時回 501(handler 內部
|
||||
// 的「forwarder/session store not configured」,非舊的 registerWebSocketStubs 501)。
|
||||
//
|
||||
// 這條與 TestWSInference_NoForwarder 對照,證明 flash-progress 走的是
|
||||
// registerWebSocketRoutes(wsAuthGroup)而非 registerWebSocketStubs。
|
||||
func TestWSFlashProgress_NoForwarder(t *testing.T) {
|
||||
r := newWSFixture(Deps{})
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/ws/devices/dev1/flash-progress", nil))
|
||||
assert.Equal(t, http.StatusNotImplemented, w.Code)
|
||||
}
|
||||
|
||||
// TestWSFlashProgress_TunnelDisconnected 驗證 flash-progress 無 active session 時回
|
||||
// 502 TUNNEL_DISCONNECTED(與 inference 同一套 pickActiveSessionToken 路徑)。
|
||||
func TestWSFlashProgress_TunnelDisconnected(t *testing.T) {
|
||||
r := newWSFixture(Deps{
|
||||
SessionStore: &fakeSessionStore{}, // List 回空
|
||||
Forwarder: session.NewForwarder("http://localhost:0", nil),
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/ws/devices/dev1/flash-progress", nil))
|
||||
assert.Equal(t, http.StatusBadGateway, w.Code)
|
||||
assert.Contains(t, w.Body.String(), ErrCodeTunnelDisconnect)
|
||||
}
|
||||
|
||||
// TestWSFlashProgress_CoexistsWithInference 驗證 radix tree 共存:
|
||||
// /ws/devices/events(靜態) vs /ws/devices/:id/inference(param) vs
|
||||
// /ws/devices/:id/flash-progress(param)三者掛在同一 group 不 panic,且 flash-progress
|
||||
// 與 inference 都能各自路由到 handler(而非彼此蓋掉)。newWSFixture 建構本身若 panic
|
||||
// 就會 fail;這裡再各打一發確認兩條 param route 都 match 得到(回 501 = 命中 handler)。
|
||||
func TestWSFlashProgress_CoexistsWithInference(t *testing.T) {
|
||||
r := newWSFixture(Deps{}) // 建構不 panic 即代表 radix tree 共存 OK
|
||||
|
||||
for _, path := range []string{
|
||||
"/ws/devices/dev1/inference",
|
||||
"/ws/devices/dev1/flash-progress",
|
||||
} {
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
// 命中 handler(缺 Forwarder → 501)而非 404(route 沒掛上)。
|
||||
assert.Equal(t, http.StatusNotImplemented, w.Code, "path %s 應命中 WS proxy handler", path)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWSInference_HappyPath_BidirectionalPipe 是端到端的 WS forward 測試:
|
||||
//
|
||||
// browser(test client) ──► api-server handler(hijack) ──► Forwarder.OpenStream
|
||||
|
||||
@ -57,9 +57,9 @@ func registerStubRoutes(g *gin.RouterGroup, _ Deps) {
|
||||
//
|
||||
// WS tunnel proxy 的基礎設施(Forwarder.ForwardWebSocket、api-server 端 Hijack + 雙向
|
||||
// io.Copy)已實作(見 forwarder.go §ForwardWebSocket、proxy.go newWebSocketProxyHandler),
|
||||
// 且 /ws/devices/:id/inference 已由 registerWebSocketRoutes 換成真正的 WS tunnel proxy。
|
||||
// 這裡只剩尚未接上 proxy 的其餘 /ws/* 端點(events / flash-progress / server-logs /
|
||||
// system / clusters / pairing)。
|
||||
// 且 /ws/devices/:id/inference 與 /ws/devices/:id/flash-progress 已由
|
||||
// registerWebSocketRoutes 換成真正的 WS tunnel proxy。這裡只剩尚未接上 proxy 的其餘
|
||||
// /ws/* 端點(events / server-logs / system / clusters / pairing)。
|
||||
//
|
||||
// 注意:ws endpoint 在 /ws 而非 /api/ws,所以由 NewRouter 直接註冊而非 apiGroup。
|
||||
func registerWebSocketStubs(r *gin.Engine) {
|
||||
@ -70,10 +70,9 @@ func registerWebSocketStubs(r *gin.Engine) {
|
||||
}
|
||||
// 用 GET(WebSocket upgrade 的初始 HTTP request)
|
||||
r.GET("/ws/devices/events", stub("ws.devices.events — pending B7"))
|
||||
r.GET("/ws/devices/:id/flash-progress", stub("ws.flash-progress — pending B7"))
|
||||
// /ws/devices/:id/inference 已由 registerWebSocketRoutes 換成真正的 WS tunnel proxy
|
||||
// (掛在 AuthMiddleware group 內,走 same-origin cookie 認證)。不在此註冊 stub,
|
||||
// 避免 gin radix tree 同路徑重複註冊 panic。
|
||||
// /ws/devices/:id/inference 與 /ws/devices/:id/flash-progress 已由
|
||||
// registerWebSocketRoutes 換成真正的 WS tunnel proxy(掛在 AuthMiddleware group 內,
|
||||
// 走 same-origin cookie 認證)。不在此註冊 stub,避免 gin radix tree 同路徑重複註冊 panic。
|
||||
r.GET("/ws/server-logs", stub("ws.server-logs — pending B7"))
|
||||
r.GET("/ws/system", stub("ws.system — pending B7"))
|
||||
r.GET("/ws/clusters/:id/inference", stub("ws.clusters.inference — pending B7"))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user