從 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>
147 lines
6.4 KiB
Go
147 lines
6.4 KiB
Go
// 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.Session(B3 實作)。
|
||
// api-server 端:RemoteHandle wrap internal HTTP client(B4 實作)。
|
||
//
|
||
// 並發安全:
|
||
// - 實作必須自行保護 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-4(CleanupExpired)+ 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 的 session(Minor-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 interface(api-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
|
||
}
|