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