jim800121chen 22f0837ba8 feat(visionA-backend): Phase 0 → 0.7 雲端後端(雙 binary + OIDC BFF + stage 部署)
從 edge-ai-platform POC 轉為正式產品的雲端後端,含以下整合階段:

- Phase 0:雛形骨架 — `cmd/api-server` (REST :3721) + `cmd/remote-proxy`
  (tunnel :3800 / internal :3801) 雙 binary 共用 internal/,沿用 POC 的
  WebSocket+yamux tunnel 協定但解耦 relay 與 API
- Phase 0.6:OIDC BFF 接 Innovedus Member Center
  - internal/oidc package(coreos/go-oidc + PKCE S256 + state + nonce)
  - internal/usersession package(HMAC-SHA256 cookie + RotateSessionID
    防 session fixation, OWASP ASVS V3.2.1)
  - 4 個 OIDC handler(/api/auth/login|callback|me|logout)+ AuthMiddleware
  - 完全拔除 StaticAuthProvider,OIDC 是唯一認證路徑
  - 9 個 ADR(含 ADR-010 BFF / ADR-011 取代 static auth /
    ADR-012 pending session shared cookie / ADR-013 PKCE-only public client)
- Phase 0.7:A1 改造 + security audit 修復
  - OIDC ClientSecret 變選填,支援 stage MC 的 public PKCE-only client
    (AuthStyleInParams 強制 token endpoint 不送 client_secret)
  - 預留 ServiceClient* 欄位給未來 client_credentials grant
  - 移除 13+ 處 resolveUserID(uc, StaticUserID) fallback 改 strict mode
    (Audit C1:multi-tenant 隔離破口)
  - Pairing exchange MarkUsed 失敗 abort + revoke session token(Audit M3)
  - 新增 all_endpoints_require_auth_test 整合測試(51 endpoint × 401)

驗證:go test -race -count=3 ./... 17 packages 全綠 / go vet 0 warning

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:21:20 +08:00

147 lines
6.4 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 session 定義 tunnel session 管理介面與記憶體實作,對齊 tunnel.md §5。
//
// 在雛形雙 binary 架構下:
// - remote-proxy 端持有唯一的 InMemoryStore真正 own *yamux.Session
// - api-server 端用 ProxyClientStore透過 internal HTTP 查詢 remote-proxy— 留給 B4 實作。
//
// 此 package 僅定義 interface 與 in-memory 實作HTTP client 實作留給 B4。
package session
import (
"context"
"errors"
"net"
"time"
)
// ==========================================================================
// Errors
// ==========================================================================
var (
// ErrSessionNotFound 表示指定 token 對應的 session 不存在(從未註冊或已移除)。
ErrSessionNotFound = errors.New("session: not found")
// ErrSessionExpired 表示 session 雖存在但 LastHeartbeat 已超過 IdleTimeout。
// 大多情境下 CleanupExpired 會先行移除Lookup 會直接回 ErrSessionNotFound
// 少數時序下 caller 可能碰到,保留明確語意便於除錯。
ErrSessionExpired = errors.New("session: expired")
// ErrNotSupported 表示此 Store 實作不支援該操作ProxyClientStore.Register
ErrNotSupported = errors.New("session: operation not supported by this store")
// ErrSessionClosed 表示 session 底層連線已關閉OpenStream 無法再開。
ErrSessionClosed = errors.New("session: underlying connection closed")
)
// ==========================================================================
// Summary & Handle
// ==========================================================================
// Summary 是 session 的可序列化描述,對 List / internal HTTP API 回傳。
type Summary struct {
Token string `json:"token"`
UserID string `json:"userId"`
DeviceID string `json:"deviceId,omitempty"`
ConnectedAt time.Time `json:"connectedAt"`
LastHeartbeat time.Time `json:"lastHeartbeat"`
RemoteAddr string `json:"remoteAddr,omitempty"`
ProxyNodeID string `json:"proxyNodeId,omitempty"` // Phase 1 多節點使用
ProxyInternalURL string `json:"proxyInternalUrl,omitempty"` // Phase 1 多節點使用
}
// Handle 是實際可操作的 session綁在某個 proxy 節點的記憶體)。
//
// 雛形單節點LocalHandle wrap *yamux.SessionB3 實作)。
// api-server 端RemoteHandle wrap internal HTTP clientB4 實作)。
//
// 並發安全:
// - 實作必須自行保護 Summary().LastHeartbeat 的讀寫,
// 因為 Store.Heartbeat 會呼叫 RecordHeartbeat而 Store.List / CleanupExpired
// 會透過 Summary() 讀取 LastHeartbeat。這是為了修 B2 Review M1 race。
type Handle interface {
// OpenStream 在此 session 上開一條新的雙向 stream。
// 若底層連線已關閉,回 ErrSessionClosed。
OpenStream(ctx context.Context) (net.Conn, error)
// Close 主動關閉此 session通常由 remote-proxy 在 CleanupExpired 呼叫)。
Close() error
// IsClosed 回報底層連線是否已斷。
IsClosed() bool
// Summary 回傳 session 的可讀資訊log / List 用)。
//
// 實作應回傳「內部 Summary 的副本」或「lock 保護下 snapshot」
// 以避免 caller 觀察到中間態(例如 LastHeartbeat 正在被更新)。
Summary() *Summary
// RecordHeartbeat 更新此 session 的 LastHeartbeat 時間。
// 實作應以 mutex / atomic 保護,確保與 Summary() 的並發讀取安全(修 B2 M1
RecordHeartbeat(t time.Time)
}
// ==========================================================================
// Store interface
// ==========================================================================
// Store 管理所有 active tunnel session。
//
// 對齊 tunnel.md §5.1 + Minor-4CleanupExpired+ interface-contracts.md §8.3。
// 實作必須是並發安全的。
type Store interface {
// Register 註冊一個 session handle若 token 已存在,實作應**關閉舊 handle 並覆蓋**Q5 裁決)。
Register(ctx context.Context, token string, h Handle) error
// Unregister 移除指定 token 的 session通常 tunnel 斷線時呼叫)。
// 若不存在為 no-op不回 error。
Unregister(ctx context.Context, token string) error
// Lookup 查詢 token 對應的 session handle不存在回 ErrSessionNotFound。
Lookup(ctx context.Context, token string) (Handle, error)
// Exists 判斷指定 token 是否有 active session不存在回 (false, nil),非 error。
Exists(ctx context.Context, token string) (bool, error)
// List 回傳所有 active session 的 summary。
List(ctx context.Context) ([]*Summary, error)
// Heartbeat 更新 session 的 LastHeartbeat 時間。
// 若 session 不存在回 ErrSessionNotFound。
Heartbeat(ctx context.Context, token string) error
// CleanupExpired 移除所有 LastHeartbeat 超過 expireAfter 的 sessionMinor-4
//
// 實作須 Close() 對應 Handle 以釋放 yamux.Session / WS conn。
// 回傳被清理的 session 數量,供觀測。
//
// 由 remote-proxy 的 background goroutine 每 30s 呼叫一次(對齊 tunnel.md §4.2)。
CleanupExpired(ctx context.Context, expireAfter time.Duration) (removed int, err error)
}
// ==========================================================================
// ProxyClient interfaceapi-server 端 → remote-proxy 內部 HTTP
// ==========================================================================
// ProxyClient 是 api-server 端呼叫 remote-proxy internal HTTP API 的抽象。
//
// 雛形只定義 interface實際 HTTP 呼叫留給 B4 的 proxy_client.go。
// Store 的 ProxyClientStore 實作會 delegate 到此 client。
//
// 相關 internal HTTP 端點(見 tunnel.md §7.1
// - GET /internal/session/:token → GetSession / Exists
// - POST /internal/forward/http?token=… → ForwardHTTP
// - GET /internal/forward/ws?token=… → ForwardWebSocket
// - POST /internal/session/:token/close → CloseSession
type ProxyClient interface {
// GetSession 從 remote-proxy 查詢指定 token 的 session summary
// 不存在回 ErrSessionNotFound。
GetSession(ctx context.Context, token string) (*Summary, error)
// ListSessions 列出 remote-proxy 當前所有 active session。
ListSessions(ctx context.Context) ([]*Summary, error)
// CloseSession 主動要求 remote-proxy 關閉指定 session管理動作
CloseSession(ctx context.Context, token string) error
}