// 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 }