mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-03 08:40:36 +00:00
164 lines
5.8 KiB
Go
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
|
|
}
|