Files
sub2api/backend/internal/service/openai_cyber_session_block.go
T

164 lines
5.8 KiB
Go

package service
import (
"context"
"crypto/sha256"
"encoding/hex"
"strconv"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/gin-gonic/gin"
)
// CyberSessionBlockStore 是 cyber 会话屏蔽表的存取接口。
// repository 层 gatewayCache 附带实现(类型断言探测接入,不改 GatewayCache
// 共享接口);测试 stub 不实现时屏蔽能力自动降级关闭。
type CyberSessionBlockStore interface {
SetCyberSessionBlocked(ctx context.Context, scopeKey string, keys []string, ttl time.Duration) error
IsCyberSessionScopeActive(ctx context.Context, scopeKey string) (bool, error)
FindCyberSessionBlocked(ctx context.Context, keys []string) (string, error)
}
const cyberSessionTranscriptLookupOverflowBlockKey = "transcript_lookup_limit_exceeded"
// CyberSessionExplicitBlockKey returns an inexpensive exact key when the
// client supplies a stable session signal.
func CyberSessionExplicitBlockKey(apiKeyID int64, c *gin.Context, body []byte) string {
return hashCyberSessionBlockKey(apiKeyID, explicitOpenAISessionID(c, body))
}
// CyberSessionTranscriptBlockKeys returns the exact full-request key followed
// by an optional rewrite-tolerant context key. The latter is emitted only after
// model-generated history has been observed.
func CyberSessionTranscriptBlockKeys(apiKeyID int64, body []byte) []string {
derived := deriveOpenAICyberTranscriptBlockKeys(apiKeyID, body)
if len(derived.lookupKeys) == 0 {
return nil
}
keys := []string{derived.lookupKeys[len(derived.lookupKeys)-1]}
if derived.preLatestUserKey != "" && derived.preLatestUserKey != keys[0] {
keys = append(keys, derived.preLatestUserKey)
}
return keys
}
func CyberSessionTranscriptLookupKeys(apiKeyID int64, body []byte) []string {
return deriveOpenAICyberTranscriptBlockKeys(apiKeyID, body).lookupKeys
}
// CyberSessionScopeKey is a coarse, non-blocking fingerprint used only to
// avoid transcript parsing and MGET for sources that never produced a hit.
func CyberSessionScopeKey(apiKeyID int64, clientIP, userAgent string) string {
if apiKeyID <= 0 {
return ""
}
raw := "cyber-scope:v1|api_key=" + strconv.FormatInt(apiKeyID, 10) +
"|ip=" + strings.TrimSpace(clientIP) +
"|ua=" + NormalizeSessionUserAgent(userAgent)
sum := sha256.Sum256([]byte(raw))
return hex.EncodeToString(sum[:])
}
func hashCyberSessionBlockKey(apiKeyID int64, raw string) string {
if raw == "" {
return ""
}
isolated := isolateOpenAISessionID(apiKeyID, raw)
sum := sha256.Sum256([]byte(isolated))
return hex.EncodeToString(sum[:])
}
// cyberSessionBlockStore 探测 cache 是否具备屏蔽存储能力。
// 注意:若未来以装饰器包装 GatewayCache(如日志/指标装饰器),该装饰器必须同时实现
// CyberSessionBlockStore,否则会话屏蔽能力将静默降级关闭
// (编译断言 var _ service.CyberSessionBlockStore = (*gatewayCache)(nil) 只覆盖
// *gatewayCache 本体,无法覆盖其外层包装)。
func (s *OpenAIGatewayService) cyberSessionBlockStore() CyberSessionBlockStore {
if s == nil || s.cache == nil {
return nil
}
store, ok := s.cache.(CyberSessionBlockStore)
if !ok {
return nil
}
return store
}
// CyberSessionBlockRuntime 返回 (开关, TTL)。开关默认关。
// 委托给 SettingService.GetCyberSessionBlockRuntime,进程内缓存避免热路径 DB 往返。
func (s *OpenAIGatewayService) CyberSessionBlockRuntime(ctx context.Context) (bool, time.Duration) {
if s == nil || s.settingService == nil {
return false, time.Hour
}
return s.settingService.GetCyberSessionBlockRuntime(ctx)
}
// MarkCyberSessionBlocked 把会话写入屏蔽表(写入点:cyber 命中后)。
// 开关关闭、key 为空或存储不可用时静默跳过。
func (s *OpenAIGatewayService) MarkCyberSessionBlocked(ctx context.Context, scopeKey string, keys []string) {
if s == nil || len(keys) == 0 {
return
}
enabled, ttl := s.CyberSessionBlockRuntime(ctx)
if !enabled {
return
}
store := s.cyberSessionBlockStore()
if store == nil {
return
}
if err := store.SetCyberSessionBlocked(ctx, scopeKey, keys, ttl); err != nil {
logger.LegacyPrintf("service.openai_gateway", "cyber session block write failed: err=%v", err)
}
}
// FindCyberSessionBlockedForRequest applies explicit-first lookup followed by
// scope-gated transcript matching. All failures remain fail-open.
func (s *OpenAIGatewayService) FindCyberSessionBlockedForRequest(ctx context.Context, apiKeyID int64, c *gin.Context, body []byte, clientIP, userAgent string) string {
enabled, _ := s.CyberSessionBlockRuntime(ctx)
if !enabled {
return ""
}
store := s.cyberSessionBlockStore()
if store == nil {
return ""
}
if explicitKey := CyberSessionExplicitBlockKey(apiKeyID, c, body); explicitKey != "" {
key, err := store.FindCyberSessionBlocked(ctx, []string{explicitKey})
if err != nil {
logger.LegacyPrintf("service.openai_gateway", "cyber explicit session read failed: err=%v", err)
return ""
}
if key != "" {
return key
}
}
scopeKey := CyberSessionScopeKey(apiKeyID, clientIP, userAgent)
active, err := store.IsCyberSessionScopeActive(ctx, scopeKey)
if err != nil {
logger.LegacyPrintf("service.openai_gateway", "cyber session scope read failed: err=%v", err)
return ""
}
if !active {
return ""
}
transcript := deriveOpenAICyberTranscriptBlockKeys(apiKeyID, body)
if transcript.lookupKeysTruncated {
// Once the coarse scope is active, silently dropping old candidates would
// let a blocked client evade prefix matching by appending dummy items.
return cyberSessionTranscriptLookupOverflowBlockKey
}
keys := transcript.lookupKeys
if len(keys) == 0 {
return ""
}
key, err := store.FindCyberSessionBlocked(ctx, keys)
if err != nil {
logger.LegacyPrintf("service.openai_gateway", "cyber session block batch read failed: err=%v", err)
return ""
}
return key
}