從 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>
91 lines
2.4 KiB
Go
91 lines
2.4 KiB
Go
package relay
|
||
|
||
import (
|
||
"context"
|
||
"net"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/hashicorp/yamux"
|
||
|
||
"visiona-backend/internal/session"
|
||
)
|
||
|
||
// LocalHandle 是 remote-proxy 端的 session.Handle 實作,
|
||
// 直接包住一個 yamux.Session(真實持有 tunnel 連線的地方)。
|
||
//
|
||
// 為修 B2 Review M1(Heartbeat vs CleanupExpired race),
|
||
// LocalHandle 以 mu 保護 summary 的 LastHeartbeat 讀寫;
|
||
// Summary() 回傳 snapshot(副本)、RecordHeartbeat 在 lock 下寫入。
|
||
type LocalHandle struct {
|
||
yamuxSession *yamux.Session
|
||
|
||
mu sync.Mutex
|
||
summary session.Summary
|
||
}
|
||
|
||
// NewLocalHandle 建立一個 LocalHandle。
|
||
//
|
||
// token / remoteAddr 由 relay server 在 handleTunnelConnect 時傳入。
|
||
// ConnectedAt 與 LastHeartbeat 初始為 time.Now().UTC()。
|
||
func NewLocalHandle(yamuxSession *yamux.Session, token, remoteAddr string) *LocalHandle {
|
||
now := time.Now().UTC()
|
||
return &LocalHandle{
|
||
yamuxSession: yamuxSession,
|
||
summary: session.Summary{
|
||
Token: token,
|
||
ConnectedAt: now,
|
||
LastHeartbeat: now,
|
||
RemoteAddr: remoteAddr,
|
||
},
|
||
}
|
||
}
|
||
|
||
// OpenStream 在 yamux session 上開一條新 stream。
|
||
// 若 session 已關閉回 ErrSessionClosed。
|
||
func (h *LocalHandle) OpenStream(ctx context.Context) (net.Conn, error) {
|
||
if h.yamuxSession.IsClosed() {
|
||
return nil, session.ErrSessionClosed
|
||
}
|
||
// yamux.Session.Open() 不接受 context;若 ctx 已取消應盡量早退。
|
||
if err := ctx.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
stream, err := h.yamuxSession.Open()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return stream, nil
|
||
}
|
||
|
||
// Close 關閉底層 yamux session(會同時關閉底下的 WebSocket)。
|
||
func (h *LocalHandle) Close() error {
|
||
return h.yamuxSession.Close()
|
||
}
|
||
|
||
// IsClosed 回報 yamux session 是否已關閉。
|
||
func (h *LocalHandle) IsClosed() bool {
|
||
return h.yamuxSession.IsClosed()
|
||
}
|
||
|
||
// Summary 回傳 session 的資訊快照。
|
||
//
|
||
// 回傳的是 summary 的複本,防止 caller 觀察到中間態,
|
||
// 也避免 Store.List 與 RecordHeartbeat 對同一 pointer 的並發寫入。
|
||
func (h *LocalHandle) Summary() *session.Summary {
|
||
h.mu.Lock()
|
||
defer h.mu.Unlock()
|
||
cp := h.summary
|
||
return &cp
|
||
}
|
||
|
||
// RecordHeartbeat 更新 LastHeartbeat;由 Store.Heartbeat 呼叫。
|
||
func (h *LocalHandle) RecordHeartbeat(t time.Time) {
|
||
h.mu.Lock()
|
||
defer h.mu.Unlock()
|
||
h.summary.LastHeartbeat = t
|
||
}
|
||
|
||
// 編譯時檢查:LocalHandle 必須實作 session.Handle。
|
||
var _ session.Handle = (*LocalHandle)(nil)
|