openai_cyber_session_block.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. package service
  2. import (
  3. "context"
  4. "crypto/sha256"
  5. "encoding/hex"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  10. "github.com/gin-gonic/gin"
  11. )
  12. // CyberSessionBlockStore 是 cyber 会话屏蔽表的存取接口。
  13. // repository 层 gatewayCache 附带实现(类型断言探测接入,不改 GatewayCache
  14. // 共享接口);测试 stub 不实现时屏蔽能力自动降级关闭。
  15. type CyberSessionBlockStore interface {
  16. SetCyberSessionBlocked(ctx context.Context, scopeKey string, keys []string, ttl time.Duration) error
  17. IsCyberSessionScopeActive(ctx context.Context, scopeKey string) (bool, error)
  18. FindCyberSessionBlocked(ctx context.Context, keys []string) (string, error)
  19. }
  20. const cyberSessionTranscriptLookupOverflowBlockKey = "transcript_lookup_limit_exceeded"
  21. // CyberSessionExplicitBlockKey returns an inexpensive exact key when the
  22. // client supplies a stable session signal.
  23. func CyberSessionExplicitBlockKey(apiKeyID int64, c *gin.Context, body []byte) string {
  24. return hashCyberSessionBlockKey(apiKeyID, explicitOpenAISessionID(c, body))
  25. }
  26. // CyberSessionTranscriptBlockKeys returns the exact full-request key followed
  27. // by an optional rewrite-tolerant context key. The latter is emitted only after
  28. // model-generated history has been observed.
  29. func CyberSessionTranscriptBlockKeys(apiKeyID int64, body []byte) []string {
  30. derived := deriveOpenAICyberTranscriptBlockKeys(apiKeyID, body)
  31. if len(derived.lookupKeys) == 0 {
  32. return nil
  33. }
  34. keys := []string{derived.lookupKeys[len(derived.lookupKeys)-1]}
  35. if derived.preLatestUserKey != "" && derived.preLatestUserKey != keys[0] {
  36. keys = append(keys, derived.preLatestUserKey)
  37. }
  38. return keys
  39. }
  40. func CyberSessionTranscriptLookupKeys(apiKeyID int64, body []byte) []string {
  41. return deriveOpenAICyberTranscriptBlockKeys(apiKeyID, body).lookupKeys
  42. }
  43. // CyberSessionScopeKey is a coarse, non-blocking fingerprint used only to
  44. // avoid transcript parsing and MGET for sources that never produced a hit.
  45. func CyberSessionScopeKey(apiKeyID int64, clientIP, userAgent string) string {
  46. if apiKeyID <= 0 {
  47. return ""
  48. }
  49. raw := "cyber-scope:v1|api_key=" + strconv.FormatInt(apiKeyID, 10) +
  50. "|ip=" + strings.TrimSpace(clientIP) +
  51. "|ua=" + NormalizeSessionUserAgent(userAgent)
  52. sum := sha256.Sum256([]byte(raw))
  53. return hex.EncodeToString(sum[:])
  54. }
  55. func hashCyberSessionBlockKey(apiKeyID int64, raw string) string {
  56. if raw == "" {
  57. return ""
  58. }
  59. isolated := isolateOpenAISessionID(apiKeyID, raw)
  60. sum := sha256.Sum256([]byte(isolated))
  61. return hex.EncodeToString(sum[:])
  62. }
  63. // cyberSessionBlockStore 探测 cache 是否具备屏蔽存储能力。
  64. // 注意:若未来以装饰器包装 GatewayCache(如日志/指标装饰器),该装饰器必须同时实现
  65. // CyberSessionBlockStore,否则会话屏蔽能力将静默降级关闭
  66. // (编译断言 var _ service.CyberSessionBlockStore = (*gatewayCache)(nil) 只覆盖
  67. // *gatewayCache 本体,无法覆盖其外层包装)。
  68. func (s *OpenAIGatewayService) cyberSessionBlockStore() CyberSessionBlockStore {
  69. if s == nil || s.cache == nil {
  70. return nil
  71. }
  72. store, ok := s.cache.(CyberSessionBlockStore)
  73. if !ok {
  74. return nil
  75. }
  76. return store
  77. }
  78. // CyberSessionBlockRuntime 返回 (开关, TTL)。开关默认关。
  79. // 委托给 SettingService.GetCyberSessionBlockRuntime,进程内缓存避免热路径 DB 往返。
  80. func (s *OpenAIGatewayService) CyberSessionBlockRuntime(ctx context.Context) (bool, time.Duration) {
  81. if s == nil || s.settingService == nil {
  82. return false, time.Hour
  83. }
  84. return s.settingService.GetCyberSessionBlockRuntime(ctx)
  85. }
  86. // MarkCyberSessionBlocked 把会话写入屏蔽表(写入点:cyber 命中后)。
  87. // 开关关闭、key 为空或存储不可用时静默跳过。
  88. func (s *OpenAIGatewayService) MarkCyberSessionBlocked(ctx context.Context, scopeKey string, keys []string) {
  89. if s == nil || len(keys) == 0 {
  90. return
  91. }
  92. enabled, ttl := s.CyberSessionBlockRuntime(ctx)
  93. if !enabled {
  94. return
  95. }
  96. store := s.cyberSessionBlockStore()
  97. if store == nil {
  98. return
  99. }
  100. if err := store.SetCyberSessionBlocked(ctx, scopeKey, keys, ttl); err != nil {
  101. logger.LegacyPrintf("service.openai_gateway", "cyber session block write failed: err=%v", err)
  102. }
  103. }
  104. // FindCyberSessionBlockedForRequest applies explicit-first lookup followed by
  105. // scope-gated transcript matching. All failures remain fail-open.
  106. func (s *OpenAIGatewayService) FindCyberSessionBlockedForRequest(ctx context.Context, apiKeyID int64, c *gin.Context, body []byte, clientIP, userAgent string) string {
  107. enabled, _ := s.CyberSessionBlockRuntime(ctx)
  108. if !enabled {
  109. return ""
  110. }
  111. store := s.cyberSessionBlockStore()
  112. if store == nil {
  113. return ""
  114. }
  115. if explicitKey := CyberSessionExplicitBlockKey(apiKeyID, c, body); explicitKey != "" {
  116. key, err := store.FindCyberSessionBlocked(ctx, []string{explicitKey})
  117. if err != nil {
  118. logger.LegacyPrintf("service.openai_gateway", "cyber explicit session read failed: err=%v", err)
  119. return ""
  120. }
  121. if key != "" {
  122. return key
  123. }
  124. }
  125. scopeKey := CyberSessionScopeKey(apiKeyID, clientIP, userAgent)
  126. active, err := store.IsCyberSessionScopeActive(ctx, scopeKey)
  127. if err != nil {
  128. logger.LegacyPrintf("service.openai_gateway", "cyber session scope read failed: err=%v", err)
  129. return ""
  130. }
  131. if !active {
  132. return ""
  133. }
  134. transcript := deriveOpenAICyberTranscriptBlockKeys(apiKeyID, body)
  135. if transcript.lookupKeysTruncated {
  136. // Once the coarse scope is active, silently dropping old candidates would
  137. // let a blocked client evade prefix matching by appending dummy items.
  138. return cyberSessionTranscriptLookupOverflowBlockKey
  139. }
  140. keys := transcript.lookupKeys
  141. if len(keys) == 0 {
  142. return ""
  143. }
  144. key, err := store.FindCyberSessionBlocked(ctx, keys)
  145. if err != nil {
  146. logger.LegacyPrintf("service.openai_gateway", "cyber session block batch read failed: err=%v", err)
  147. return ""
  148. }
  149. return key
  150. }