jim800121chen dc6ca211ae feat(ws): tunnel WS forward — 推論結果經 tunnel 推回雲端(後端塊2)
推論工作區後端塊2:實作 WS forward,讓 local agent 的推論結果 WS
(inference:<deviceId>)經 tunnel 推回雲端瀏覽器 canvas overlay。照
edge-ai-platform POC relay/server.go proxyWebSocket 移植(唯讀參考)。

- forwarder.go ForwardWebSocket:OpenStream→寫 upgrade→讀 101→WebSocketConn
  (新增 WebSocketConn/wsUpgradeError/AsWSUpgradeError)
- proxy.go newWebSocketProxyHandler:hijack browser→回寫 101→雙向 io.Copy
  cross-close(無 goroutine leak)+ copyWebSocketUpgradeHeaders(保留
  Sec-WebSocket-*、剝 Authorization/Origin)
- camera.go registerWebSocketRoutes:GET /ws/devices/:id/inference
- api.go wsAuthGroup(/ws + AuthMiddleware):same-origin cookie 認證,
  無 token-in-URL(security 定案)
- stubs.go:移除 WS inference 501 stub、更新過時 doc comment(Mi-1/2)

架構差異:POC 單 binary,visionA api-server(auth)+remote-proxy 雙 binary,
remote-proxy raw byte pipe 透明穿過 WS upgrade bytes。local agent 端未動。

認證/授權(security 定案 + S1/S2):same-origin cookie、剝 Auth/Origin、
WS 不套 300s timeout、走 pickActiveSessionToken(帶別人 deviceId 也只打到
自己 tunnel;多租戶嚴格綁定屬 Phase 1 M2 debt)。

Reviewer 0C/1M/3Mi 通過(修後)。Major-1 已修:收窄 all_endpoints_require_auth_test
的 /ws/ 白名單,讓 authed inference WS 納入「無 cookie 應 401」回歸檢查
(附守得住證明:移除 auth 測試即 FAIL)。+forwarder 5 test + camera_ws
端到端四層真連線雙向 pipe。build/vet/全回歸綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 04:56:54 +08:00

83 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"github.com/gin-gonic/gin"
)
// registerStubRoutes 註冊尚未實作 / Phase 1 才處理的 HTTP endpoint一律回 501 NOT_IMPLEMENTED。
//
// **為什麼只留這些**Auth / Pairing 補齊 / Devices / Models / GET /clusters /
// system/deps / /storage 都在 B5 補實作(見 auth.go / devices.go / models.go /
// clusters.go / storage.go 各檔Camera / Media 已在 camera.goregisterCameraRoutes
// 換成真正的 tunnel proxy 宣告。這裡只剩:
// - Cloud 裝置記錄(非 tunnel 的 CRUDPhase 1
// - Clusters 寫入類Phase 1
// - ConverterPhase 1
//
// WebSocket 的 501 stub 在 registerWebSocketStubs下方不在此函式。
//
// 讓前端對錯誤路徑能拿到 501 而非 404減少除錯成本。
func registerStubRoutes(g *gin.RouterGroup, _ Deps) {
stub := func(hint string) gin.HandlerFunc {
return func(c *gin.Context) {
WriteNotImplemented(c, hint)
}
}
// --- Cloud-side device records非 tunnel ---
g.GET("/cloud/devices", stub("cloud.devices.list — pending Phase 1"))
g.POST("/cloud/devices/:id/rename", stub("cloud.devices.rename — pending Phase 1"))
// --- Models 其餘 ---
// load-to-device 已在 models.go 註冊為 501 stub為了讓 /api/models/:id/load-to-device 路徑註冊完整)。
// --- Clusters 寫入類 ---
g.POST("/clusters", stub("clusters.create — pending Phase 1"))
g.GET("/clusters/:id", stub("clusters.get — pending Phase 1"))
g.DELETE("/clusters/:id", stub("clusters.delete — pending Phase 1"))
g.POST("/clusters/:id/devices", stub("clusters.add-device — pending Phase 1"))
g.DELETE("/clusters/:id/devices/:deviceId", stub("clusters.remove-device — pending Phase 1"))
g.PUT("/clusters/:id/devices/:deviceId/weight", stub("clusters.set-weight — pending Phase 1"))
g.POST("/clusters/:id/flash", stub("clusters.flash — pending Phase 1"))
g.POST("/clusters/:id/inference/start", stub("clusters.inference.start — pending Phase 1"))
g.POST("/clusters/:id/inference/stop", stub("clusters.inference.stop — pending Phase 1"))
// --- Camera / Media走 tunnel proxy ---
// 已由 registerCameraRoutescamera.go換成真正的 proxy 宣告,不再是 501 stub。
// --- ConverterPhase 1 ---
g.POST("/converter/jobs", stub("converter.submit — pending Phase 1"))
g.GET("/converter/jobs", stub("converter.list — pending Phase 1"))
g.GET("/converter/jobs/:id", stub("converter.get — pending Phase 1"))
g.GET("/converter/jobs/:id/download", stub("converter.download — pending Phase 1"))
}
// registerWebSocketStubs 註冊「尚未實作」的 /ws/* endpoint一律回 501讓前端能收到
// 明確錯誤,由 B7 補齊。
//
// 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 endpoint 在 /ws 而非 /api/ws所以由 NewRouter 直接註冊而非 apiGroup。
func registerWebSocketStubs(r *gin.Engine) {
stub := func(hint string) gin.HandlerFunc {
return func(c *gin.Context) {
WriteNotImplemented(c, hint)
}
}
// 用 GETWebSocket 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。
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"))
r.GET("/ws/clusters/:id/flash-progress", stub("ws.clusters.flash — pending B7"))
r.GET("/ws/pairing/status", stub("ws.pairing.status — pending B7"))
}