feat(tls-fingerprint): 新增 TLS 指纹 Profile 数据库管理及代码质量优化
新增功能: - 新增 TLS 指纹 Profile CRUD 管理(Ent schema + 迁移 + Admin API + 前端管理界面) - 支持账号绑定数据库中的自定义 TLS Profile,或随机选择(profile_id=-1) - HTTPUpstream.DoWithTLS 接口从 bool 改为 *tlsfingerprint.Profile,支持按账号指定 Profile - AccountUsageService 注入 TLSFingerprintProfileService,统一 usage 场景与网关的 Profile 解析逻辑 代码优化: - 删除已被 TLSFingerprintProfileService 完全取代的 registry.go 死代码(418 行) - 提取 3 个 dialer 的重复 TLS 握手逻辑为 performTLSHandshake() 共用函数 - 修复 GetTLSFingerprintProfileID 缺少 json.Number 处理的 bug - gateway_service.Forward 中 ResolveTLSProfile 从重试循环内重复调用改为预解析局部变量 - 删除冗余的 buildClientHelloSpec() 单行 wrapper 和 int64(e.ID) 无效转换 - tls_fingerprint_profile_cache.go 日志从 log.Printf 改为 slog 结构化日志 - dialer_capture_test.go 添加 //go:build integration 标签,防止 CI 失败 - 去重 TestProfileExpectation 类型至共享 test_types_test.go - 修复 9 个测试文件缺少 tlsfingerprint import 的编译错误 - 修复 error_policy_integration_test.go 中 handleError 回调签名被错误替换的问题
This commit is contained in:
@@ -1165,6 +1165,31 @@ func (a *Account) IsTLSFingerprintEnabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetTLSFingerprintProfileID 获取账号绑定的 TLS 指纹模板 ID
|
||||
// 返回 0 表示未绑定(使用内置默认 profile)
|
||||
func (a *Account) GetTLSFingerprintProfileID() int64 {
|
||||
if a.Extra == nil {
|
||||
return 0
|
||||
}
|
||||
v, ok := a.Extra["tls_fingerprint_profile_id"]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
switch id := v.(type) {
|
||||
case float64:
|
||||
return int64(id)
|
||||
case int64:
|
||||
return id
|
||||
case int:
|
||||
return int64(id)
|
||||
case json.Number:
|
||||
if i, err := id.Int64(); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetUserMsgQueueMode 获取用户消息队列模式
|
||||
// "serialize" = 串行队列, "throttle" = 软性限速, "" = 未设置(使用全局配置)
|
||||
func (a *Account) GetUserMsgQueueMode() string {
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/Wei-Shaw/sub2api/internal/util/soraerror"
|
||||
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -69,6 +70,7 @@ type AccountTestService struct {
|
||||
antigravityGatewayService *AntigravityGatewayService
|
||||
httpUpstream HTTPUpstream
|
||||
cfg *config.Config
|
||||
tlsFPProfileService *TLSFingerprintProfileService
|
||||
soraTestGuardMu sync.Mutex
|
||||
soraTestLastRun map[int64]time.Time
|
||||
soraTestCooldown time.Duration
|
||||
@@ -83,6 +85,7 @@ func NewAccountTestService(
|
||||
antigravityGatewayService *AntigravityGatewayService,
|
||||
httpUpstream HTTPUpstream,
|
||||
cfg *config.Config,
|
||||
tlsFPProfileService *TLSFingerprintProfileService,
|
||||
) *AccountTestService {
|
||||
return &AccountTestService{
|
||||
accountRepo: accountRepo,
|
||||
@@ -90,6 +93,7 @@ func NewAccountTestService(
|
||||
antigravityGatewayService: antigravityGatewayService,
|
||||
httpUpstream: httpUpstream,
|
||||
cfg: cfg,
|
||||
tlsFPProfileService: tlsFPProfileService,
|
||||
soraTestLastRun: make(map[int64]time.Time),
|
||||
soraTestCooldown: defaultSoraTestCooldown,
|
||||
}
|
||||
@@ -300,7 +304,7 @@ func (s *AccountTestService) testClaudeAccountConnection(c *gin.Context, account
|
||||
proxyURL = account.Proxy.URL()
|
||||
}
|
||||
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if err != nil {
|
||||
return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error()))
|
||||
}
|
||||
@@ -390,7 +394,7 @@ func (s *AccountTestService) testBedrockAccountConnection(c *gin.Context, ctx co
|
||||
proxyURL = account.Proxy.URL()
|
||||
}
|
||||
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, false)
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, nil)
|
||||
if err != nil {
|
||||
return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error()))
|
||||
}
|
||||
@@ -520,7 +524,7 @@ func (s *AccountTestService) testOpenAIAccountConnection(c *gin.Context, account
|
||||
proxyURL = account.Proxy.URL()
|
||||
}
|
||||
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if err != nil {
|
||||
return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error()))
|
||||
}
|
||||
@@ -610,7 +614,7 @@ func (s *AccountTestService) testGeminiAccountConnection(c *gin.Context, account
|
||||
proxyURL = account.Proxy.URL()
|
||||
}
|
||||
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if err != nil {
|
||||
return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error()))
|
||||
}
|
||||
@@ -881,9 +885,9 @@ func (s *AccountTestService) testSoraAccountConnection(c *gin.Context, account *
|
||||
if account.ProxyID != nil && account.Proxy != nil {
|
||||
proxyURL = account.Proxy.URL()
|
||||
}
|
||||
enableSoraTLSFingerprint := s.shouldEnableSoraTLSFingerprint()
|
||||
soraTLSProfile := s.resolveSoraTLSProfile()
|
||||
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, enableSoraTLSFingerprint)
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, soraTLSProfile)
|
||||
if err != nil {
|
||||
recorder.addStep("me", "failed", 0, "network_error", err.Error())
|
||||
s.emitSoraProbeSummary(c, recorder)
|
||||
@@ -948,7 +952,7 @@ func (s *AccountTestService) testSoraAccountConnection(c *gin.Context, account *
|
||||
subReq.Header.Set("Origin", "https://sora.chatgpt.com")
|
||||
subReq.Header.Set("Referer", "https://sora.chatgpt.com/")
|
||||
|
||||
subResp, subErr := s.httpUpstream.DoWithTLS(subReq, proxyURL, account.ID, account.Concurrency, enableSoraTLSFingerprint)
|
||||
subResp, subErr := s.httpUpstream.DoWithTLS(subReq, proxyURL, account.ID, account.Concurrency, soraTLSProfile)
|
||||
if subErr != nil {
|
||||
recorder.addStep("subscription", "failed", 0, "network_error", subErr.Error())
|
||||
s.sendEvent(c, TestEvent{Type: "content", Text: fmt.Sprintf("Subscription check skipped: %s", subErr.Error())})
|
||||
@@ -977,7 +981,7 @@ func (s *AccountTestService) testSoraAccountConnection(c *gin.Context, account *
|
||||
}
|
||||
|
||||
// 追加 Sora2 能力探测(对齐 sora2api 的测试思路):邀请码 + 剩余额度。
|
||||
s.testSora2Capabilities(c, ctx, account, authToken, proxyURL, enableSoraTLSFingerprint, recorder)
|
||||
s.testSora2Capabilities(c, ctx, account, authToken, proxyURL, soraTLSProfile, recorder)
|
||||
|
||||
s.emitSoraProbeSummary(c, recorder)
|
||||
s.sendEvent(c, TestEvent{Type: "test_complete", Success: true})
|
||||
@@ -990,7 +994,7 @@ func (s *AccountTestService) testSora2Capabilities(
|
||||
account *Account,
|
||||
authToken string,
|
||||
proxyURL string,
|
||||
enableTLSFingerprint bool,
|
||||
tlsProfile *tlsfingerprint.Profile,
|
||||
recorder *soraProbeRecorder,
|
||||
) {
|
||||
inviteStatus, inviteHeader, inviteBody, err := s.fetchSoraTestEndpoint(
|
||||
@@ -999,7 +1003,7 @@ func (s *AccountTestService) testSora2Capabilities(
|
||||
authToken,
|
||||
soraInviteMineURL,
|
||||
proxyURL,
|
||||
enableTLSFingerprint,
|
||||
tlsProfile,
|
||||
)
|
||||
if err != nil {
|
||||
if recorder != nil {
|
||||
@@ -1016,7 +1020,7 @@ func (s *AccountTestService) testSora2Capabilities(
|
||||
authToken,
|
||||
soraBootstrapURL,
|
||||
proxyURL,
|
||||
enableTLSFingerprint,
|
||||
tlsProfile,
|
||||
)
|
||||
if bootstrapErr == nil && bootstrapStatus == http.StatusOK {
|
||||
if recorder != nil {
|
||||
@@ -1029,7 +1033,7 @@ func (s *AccountTestService) testSora2Capabilities(
|
||||
authToken,
|
||||
soraInviteMineURL,
|
||||
proxyURL,
|
||||
enableTLSFingerprint,
|
||||
tlsProfile,
|
||||
)
|
||||
if err != nil {
|
||||
if recorder != nil {
|
||||
@@ -1081,7 +1085,7 @@ func (s *AccountTestService) testSora2Capabilities(
|
||||
authToken,
|
||||
soraRemainingURL,
|
||||
proxyURL,
|
||||
enableTLSFingerprint,
|
||||
tlsProfile,
|
||||
)
|
||||
if remainingErr != nil {
|
||||
if recorder != nil {
|
||||
@@ -1122,7 +1126,7 @@ func (s *AccountTestService) fetchSoraTestEndpoint(
|
||||
authToken string,
|
||||
url string,
|
||||
proxyURL string,
|
||||
enableTLSFingerprint bool,
|
||||
tlsProfile *tlsfingerprint.Profile,
|
||||
) (int, http.Header, []byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
@@ -1135,7 +1139,7 @@ func (s *AccountTestService) fetchSoraTestEndpoint(
|
||||
req.Header.Set("Origin", "https://sora.chatgpt.com")
|
||||
req.Header.Set("Referer", "https://sora.chatgpt.com/")
|
||||
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, enableTLSFingerprint)
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, tlsProfile)
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
@@ -1224,11 +1228,12 @@ func parseSoraRemainingSummary(body []byte) string {
|
||||
return strings.Join(parts, " | ")
|
||||
}
|
||||
|
||||
func (s *AccountTestService) shouldEnableSoraTLSFingerprint() bool {
|
||||
if s == nil || s.cfg == nil {
|
||||
return true
|
||||
func (s *AccountTestService) resolveSoraTLSProfile() *tlsfingerprint.Profile {
|
||||
if s == nil || s.cfg == nil || !s.cfg.Sora.Client.DisableTLSFingerprint {
|
||||
// Sora TLS fingerprint enabled — use built-in default profile
|
||||
return &tlsfingerprint.Profile{Name: "Built-in Default (Sora)"}
|
||||
}
|
||||
return !s.cfg.Sora.Client.DisableTLSFingerprint
|
||||
return nil // disabled
|
||||
}
|
||||
|
||||
func isCloudflareChallengeResponse(statusCode int, headers http.Header, body []byte) bool {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -24,9 +25,9 @@ func (u *queuedHTTPUpstream) Do(_ *http.Request, _ string, _ int64, _ int) (*htt
|
||||
return nil, fmt.Errorf("unexpected Do call")
|
||||
}
|
||||
|
||||
func (u *queuedHTTPUpstream) DoWithTLS(req *http.Request, _ string, _ int64, _ int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (u *queuedHTTPUpstream) DoWithTLS(req *http.Request, _ string, _ int64, _ int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
u.requests = append(u.requests, req)
|
||||
u.tlsFlags = append(u.tlsFlags, enableTLSFingerprint)
|
||||
u.tlsFlags = append(u.tlsFlags, profile != nil)
|
||||
if len(u.responses) == 0 {
|
||||
return nil, fmt.Errorf("no mocked response")
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
openaipkg "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/sync/singleflight"
|
||||
@@ -241,11 +242,11 @@ type ClaudeUsageResponse struct {
|
||||
|
||||
// ClaudeUsageFetchOptions 包含获取 Claude 用量数据所需的所有选项
|
||||
type ClaudeUsageFetchOptions struct {
|
||||
AccessToken string // OAuth access token
|
||||
ProxyURL string // 代理 URL(可选)
|
||||
AccountID int64 // 账号 ID(用于 TLS 指纹选择)
|
||||
EnableTLSFingerprint bool // 是否启用 TLS 指纹伪装
|
||||
Fingerprint *Fingerprint // 缓存的指纹信息(User-Agent 等)
|
||||
AccessToken string // OAuth access token
|
||||
ProxyURL string // 代理 URL(可选)
|
||||
AccountID int64 // 账号 ID(用于连接池隔离)
|
||||
TLSProfile *tlsfingerprint.Profile // TLS 指纹 Profile(nil 表示不启用)
|
||||
Fingerprint *Fingerprint // 缓存的指纹信息(User-Agent 等)
|
||||
}
|
||||
|
||||
// ClaudeUsageFetcher fetches usage data from Anthropic OAuth API
|
||||
@@ -264,6 +265,7 @@ type AccountUsageService struct {
|
||||
antigravityQuotaFetcher *AntigravityQuotaFetcher
|
||||
cache *UsageCache
|
||||
identityCache IdentityCache
|
||||
tlsFPProfileService *TLSFingerprintProfileService
|
||||
}
|
||||
|
||||
// NewAccountUsageService 创建AccountUsageService实例
|
||||
@@ -275,6 +277,7 @@ func NewAccountUsageService(
|
||||
antigravityQuotaFetcher *AntigravityQuotaFetcher,
|
||||
cache *UsageCache,
|
||||
identityCache IdentityCache,
|
||||
tlsFPProfileService *TLSFingerprintProfileService,
|
||||
) *AccountUsageService {
|
||||
return &AccountUsageService{
|
||||
accountRepo: accountRepo,
|
||||
@@ -284,6 +287,7 @@ func NewAccountUsageService(
|
||||
antigravityQuotaFetcher: antigravityQuotaFetcher,
|
||||
cache: cache,
|
||||
identityCache: identityCache,
|
||||
tlsFPProfileService: tlsFPProfileService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1155,10 +1159,10 @@ func (s *AccountUsageService) fetchOAuthUsageRaw(ctx context.Context, account *A
|
||||
|
||||
// 构建完整的选项
|
||||
opts := &ClaudeUsageFetchOptions{
|
||||
AccessToken: accessToken,
|
||||
ProxyURL: proxyURL,
|
||||
AccountID: account.ID,
|
||||
EnableTLSFingerprint: account.IsTLSFingerprintEnabled(),
|
||||
AccessToken: accessToken,
|
||||
ProxyURL: proxyURL,
|
||||
AccountID: account.ID,
|
||||
TLSProfile: s.tlsFPProfileService.ResolveTLSProfile(account),
|
||||
}
|
||||
|
||||
// 尝试获取缓存的 Fingerprint(包含 User-Agent 等信息)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -130,7 +131,7 @@ func (s *httpUpstreamStub) Do(_ *http.Request, _ string, _ int64, _ int) (*http.
|
||||
return s.resp, s.err
|
||||
}
|
||||
|
||||
func (s *httpUpstreamStub) DoWithTLS(_ *http.Request, _ string, _ int64, _ int, _ bool) (*http.Response, error) {
|
||||
func (s *httpUpstreamStub) DoWithTLS(_ *http.Request, _ string, _ int64, _ int, _ *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return s.resp, s.err
|
||||
}
|
||||
|
||||
@@ -171,7 +172,7 @@ func (s *queuedHTTPUpstreamStub) Do(req *http.Request, _ string, _ int64, _ int)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (s *queuedHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, accountID int64, concurrency int, _ bool) (*http.Response, error) {
|
||||
func (s *queuedHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, accountID int64, concurrency int, _ *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return s.Do(req, proxyURL, accountID, concurrency)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -40,7 +41,7 @@ func (r *recordingOKUpstream) Do(req *http.Request, proxyURL string, accountID i
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *recordingOKUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (r *recordingOKUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return r.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
@@ -61,7 +62,7 @@ func (s *stubAntigravityUpstream) Do(req *http.Request, proxyURL string, account
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *stubAntigravityUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (s *stubAntigravityUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return s.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -93,7 +94,7 @@ func (m *mockSmartRetryUpstream) Do(req *http.Request, proxyURL string, accountI
|
||||
}, respErr
|
||||
}
|
||||
|
||||
func (m *mockSmartRetryUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (m *mockSmartRetryUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return m.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -35,7 +36,7 @@ func (u *epFixedUpstream) Do(req *http.Request, proxyURL string, accountID int64
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *epFixedUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (u *epFixedUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return u.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -60,7 +61,7 @@ func (u *anthropicHTTPUpstreamRecorder) Do(req *http.Request, proxyURL string, a
|
||||
return u.resp, nil
|
||||
}
|
||||
|
||||
func (u *anthropicHTTPUpstreamRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (u *anthropicHTTPUpstreamRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return u.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ func (s *GatewayService) ForwardAsChatCompletions(
|
||||
}
|
||||
|
||||
// 11. Send request
|
||||
resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if err != nil {
|
||||
if resp != nil && resp.Body != nil {
|
||||
_ = resp.Body.Close()
|
||||
|
||||
@@ -117,7 +117,7 @@ func (s *GatewayService) ForwardAsResponses(
|
||||
}
|
||||
|
||||
// 11. Send request
|
||||
resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if err != nil {
|
||||
if resp != nil && resp.Body != nil {
|
||||
_ = resp.Body.Close()
|
||||
|
||||
@@ -40,6 +40,7 @@ func newGatewayRecordUsageServiceForTest(usageRepo UsageLogRepository, userRepo
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -566,6 +566,7 @@ type GatewayService struct {
|
||||
debugModelRouting atomic.Bool
|
||||
debugClaudeMimic atomic.Bool
|
||||
debugGatewayBodyFile atomic.Pointer[os.File] // non-nil when SUB2API_DEBUG_GATEWAY_BODY is set
|
||||
tlsFPProfileService *TLSFingerprintProfileService
|
||||
}
|
||||
|
||||
// NewGatewayService creates a new GatewayService
|
||||
@@ -592,6 +593,7 @@ func NewGatewayService(
|
||||
rpmCache RPMCache,
|
||||
digestStore *DigestSessionStore,
|
||||
settingService *SettingService,
|
||||
tlsFPProfileService *TLSFingerprintProfileService,
|
||||
) *GatewayService {
|
||||
userGroupRateTTL := resolveUserGroupRateCacheTTL(cfg)
|
||||
modelsListTTL := resolveModelsListCacheTTL(cfg)
|
||||
@@ -623,6 +625,7 @@ func NewGatewayService(
|
||||
modelsListCache: gocache.New(modelsListTTL, time.Minute),
|
||||
modelsListCacheTTL: modelsListTTL,
|
||||
responseHeaderFilter: compileResponseHeaderFilter(cfg),
|
||||
tlsFPProfileService: tlsFPProfileService,
|
||||
}
|
||||
svc.userGroupRateResolver = newUserGroupRateResolver(
|
||||
userGroupRateRepo,
|
||||
@@ -4133,9 +4136,12 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
||||
proxyURL = account.Proxy.URL()
|
||||
}
|
||||
|
||||
// 解析 TLS 指纹 profile(同一请求生命周期内不变,避免重试循环中重复解析)
|
||||
tlsProfile := s.tlsFPProfileService.ResolveTLSProfile(account)
|
||||
|
||||
// 调试日志:记录即将转发的账号信息
|
||||
logger.LegacyPrintf("service.gateway", "[Forward] Using account: ID=%d Name=%s Platform=%s Type=%s TLSFingerprint=%v Proxy=%s",
|
||||
account.ID, account.Name, account.Platform, account.Type, account.IsTLSFingerprintEnabled(), proxyURL)
|
||||
account.ID, account.Name, account.Platform, account.Type, tlsProfile, proxyURL)
|
||||
// Pre-filter: strip empty text blocks (including nested in tool_result) to prevent upstream 400.
|
||||
body = StripEmptyTextBlocks(body)
|
||||
|
||||
@@ -4155,7 +4161,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, tlsProfile)
|
||||
if err != nil {
|
||||
if resp != nil && resp.Body != nil {
|
||||
_ = resp.Body.Close()
|
||||
@@ -4233,7 +4239,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
||||
retryReq, buildErr := s.buildUpstreamRequest(retryCtx, c, account, filteredBody, token, tokenType, reqModel, reqStream, shouldMimicClaudeCode)
|
||||
releaseRetryCtx()
|
||||
if buildErr == nil {
|
||||
retryResp, retryErr := s.httpUpstream.DoWithTLS(retryReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
retryResp, retryErr := s.httpUpstream.DoWithTLS(retryReq, proxyURL, account.ID, account.Concurrency, tlsProfile)
|
||||
if retryErr == nil {
|
||||
if retryResp.StatusCode < 400 {
|
||||
logger.LegacyPrintf("service.gateway", "Account %d: thinking block retry succeeded (blocks downgraded)", account.ID)
|
||||
@@ -4268,7 +4274,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
||||
retryReq2, buildErr2 := s.buildUpstreamRequest(retryCtx2, c, account, filteredBody2, token, tokenType, reqModel, reqStream, shouldMimicClaudeCode)
|
||||
releaseRetryCtx2()
|
||||
if buildErr2 == nil {
|
||||
retryResp2, retryErr2 := s.httpUpstream.DoWithTLS(retryReq2, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
retryResp2, retryErr2 := s.httpUpstream.DoWithTLS(retryReq2, proxyURL, account.ID, account.Concurrency, tlsProfile)
|
||||
if retryErr2 == nil {
|
||||
resp = retryResp2
|
||||
break
|
||||
@@ -4339,7 +4345,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
||||
budgetRetryReq, buildErr := s.buildUpstreamRequest(budgetRetryCtx, c, account, rectifiedBody, token, tokenType, reqModel, reqStream, shouldMimicClaudeCode)
|
||||
releaseBudgetRetryCtx()
|
||||
if buildErr == nil {
|
||||
budgetRetryResp, retryErr := s.httpUpstream.DoWithTLS(budgetRetryReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
budgetRetryResp, retryErr := s.httpUpstream.DoWithTLS(budgetRetryReq, proxyURL, account.ID, account.Concurrency, tlsProfile)
|
||||
if retryErr == nil {
|
||||
resp = budgetRetryResp
|
||||
break
|
||||
@@ -4645,7 +4651,7 @@ func (s *GatewayService) forwardAnthropicAPIKeyPassthroughWithInput(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if err != nil {
|
||||
if resp != nil && resp.Body != nil {
|
||||
_ = resp.Body.Close()
|
||||
@@ -5364,7 +5370,7 @@ func (s *GatewayService) executeBedrockUpstream(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, false)
|
||||
resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, nil)
|
||||
if err != nil {
|
||||
if resp != nil && resp.Body != nil {
|
||||
_ = resp.Body.Close()
|
||||
@@ -8044,7 +8050,7 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context,
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if err != nil {
|
||||
setOpsUpstreamError(c, 0, sanitizeUpstreamErrorMessage(err.Error()), "")
|
||||
s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Request failed")
|
||||
@@ -8072,7 +8078,7 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context,
|
||||
filteredBody := FilterThinkingBlocksForRetry(body)
|
||||
retryReq, buildErr := s.buildCountTokensRequest(ctx, c, account, filteredBody, token, tokenType, reqModel, shouldMimicClaudeCode)
|
||||
if buildErr == nil {
|
||||
retryResp, retryErr := s.httpUpstream.DoWithTLS(retryReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
retryResp, retryErr := s.httpUpstream.DoWithTLS(retryReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if retryErr == nil {
|
||||
resp = retryResp
|
||||
respBody, err = readUpstreamResponseBodyLimited(resp.Body, maxReadBytes)
|
||||
@@ -8161,7 +8167,7 @@ func (s *GatewayService) forwardCountTokensAnthropicAPIKeyPassthrough(ctx contex
|
||||
proxyURL = account.Proxy.URL()
|
||||
}
|
||||
|
||||
resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if err != nil {
|
||||
setOpsUpstreamError(c, 0, sanitizeUpstreamErrorMessage(err.Error()), "")
|
||||
appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -36,7 +37,7 @@ func (s *geminiCompatHTTPUpstreamStub) Do(req *http.Request, proxyURL string, ac
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (s *geminiCompatHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (s *geminiCompatHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return s.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,55 +1,24 @@
|
||||
package service
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
)
|
||||
|
||||
// HTTPUpstream 上游 HTTP 请求接口
|
||||
// 用于向上游 API(Claude、OpenAI、Gemini 等)发送请求
|
||||
// 这是一个通用接口,可用于任何基于 HTTP 的上游服务
|
||||
//
|
||||
// 设计说明:
|
||||
// - 支持可选代理配置
|
||||
// - 支持账户级连接池隔离
|
||||
// - 实现类负责连接池管理和复用
|
||||
// - 支持可选的 TLS 指纹伪装
|
||||
type HTTPUpstream interface {
|
||||
// Do 执行 HTTP 请求
|
||||
//
|
||||
// 参数:
|
||||
// - req: HTTP 请求对象,由调用方构建
|
||||
// - proxyURL: 代理服务器地址,空字符串表示直连
|
||||
// - accountID: 账户 ID,用于连接池隔离(隔离策略为 account 或 account_proxy 时生效)
|
||||
// - accountConcurrency: 账户并发限制,用于动态调整连接池大小
|
||||
//
|
||||
// 返回:
|
||||
// - *http.Response: HTTP 响应,调用方必须关闭 Body
|
||||
// - error: 请求错误(网络错误、超时等)
|
||||
//
|
||||
// 注意:
|
||||
// - 调用方必须关闭 resp.Body,否则会导致连接泄漏
|
||||
// - 响应体可能已被包装以跟踪请求生命周期
|
||||
// Do 执行 HTTP 请求(不启用 TLS 指纹)
|
||||
Do(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error)
|
||||
|
||||
// DoWithTLS 执行带 TLS 指纹伪装的 HTTP 请求
|
||||
//
|
||||
// 参数:
|
||||
// - req: HTTP 请求对象,由调用方构建
|
||||
// - proxyURL: 代理服务器地址,空字符串表示直连
|
||||
// - accountID: 账户 ID,用于连接池隔离和 TLS 指纹模板选择
|
||||
// - accountConcurrency: 账户并发限制,用于动态调整连接池大小
|
||||
// - enableTLSFingerprint: 是否启用 TLS 指纹伪装
|
||||
// profile 参数:
|
||||
// - nil: 不启用 TLS 指纹,行为与 Do 方法相同
|
||||
// - non-nil: 使用指定的 Profile 进行 TLS 指纹伪装
|
||||
//
|
||||
// 返回:
|
||||
// - *http.Response: HTTP 响应,调用方必须关闭 Body
|
||||
// - error: 请求错误(网络错误、超时等)
|
||||
//
|
||||
// TLS 指纹说明:
|
||||
// - 当 enableTLSFingerprint=true 时,使用 utls 库模拟 Claude CLI 的 TLS 指纹
|
||||
// - TLS 指纹模板根据 accountID % len(profiles) 自动选择
|
||||
// - 支持直连、HTTP/HTTPS 代理、SOCKS5 代理三种场景
|
||||
// - 如果 enableTLSFingerprint=false,行为与 Do 方法相同
|
||||
//
|
||||
// 注意:
|
||||
// - 调用方必须关闭 resp.Body,否则会导致连接泄漏
|
||||
// - TLS 指纹客户端与普通客户端使用不同的缓存键,互不影响
|
||||
DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error)
|
||||
// Profile 由调用方通过 TLSFingerprintProfileService 解析后传入,
|
||||
// 支持按账号绑定的数据库 profile 或内置默认 profile。
|
||||
DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -43,7 +44,7 @@ func (u *httpUpstreamRecorder) Do(req *http.Request, proxyURL string, accountID
|
||||
return u.resp, nil
|
||||
}
|
||||
|
||||
func (u *httpUpstreamRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (u *httpUpstreamRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return u.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -57,7 +58,7 @@ func (u *httpUpstreamSequenceRecorder) Do(req *http.Request, proxyURL string, ac
|
||||
return u.responses[len(u.responses)-1], nil
|
||||
}
|
||||
|
||||
func (u *httpUpstreamSequenceRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (u *httpUpstreamSequenceRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return u.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
|
||||
259
backend/internal/service/tls_fingerprint_profile_service.go
Normal file
259
backend/internal/service/tls_fingerprint_profile_service.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand/v2"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
)
|
||||
|
||||
// TLSFingerprintProfileRepository 定义 TLS 指纹模板的数据访问接口
|
||||
type TLSFingerprintProfileRepository interface {
|
||||
List(ctx context.Context) ([]*model.TLSFingerprintProfile, error)
|
||||
GetByID(ctx context.Context, id int64) (*model.TLSFingerprintProfile, error)
|
||||
Create(ctx context.Context, profile *model.TLSFingerprintProfile) (*model.TLSFingerprintProfile, error)
|
||||
Update(ctx context.Context, profile *model.TLSFingerprintProfile) (*model.TLSFingerprintProfile, error)
|
||||
Delete(ctx context.Context, id int64) error
|
||||
}
|
||||
|
||||
// TLSFingerprintProfileCache 定义 TLS 指纹模板的缓存接口
|
||||
type TLSFingerprintProfileCache interface {
|
||||
Get(ctx context.Context) ([]*model.TLSFingerprintProfile, bool)
|
||||
Set(ctx context.Context, profiles []*model.TLSFingerprintProfile) error
|
||||
Invalidate(ctx context.Context) error
|
||||
NotifyUpdate(ctx context.Context) error
|
||||
SubscribeUpdates(ctx context.Context, handler func())
|
||||
}
|
||||
|
||||
// TLSFingerprintProfileService TLS 指纹模板管理服务
|
||||
type TLSFingerprintProfileService struct {
|
||||
repo TLSFingerprintProfileRepository
|
||||
cache TLSFingerprintProfileCache
|
||||
|
||||
// 本地 ID→Profile 映射缓存,用于 DoWithTLS 热路径快速查找
|
||||
localCache map[int64]*model.TLSFingerprintProfile
|
||||
localMu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewTLSFingerprintProfileService 创建 TLS 指纹模板服务
|
||||
func NewTLSFingerprintProfileService(
|
||||
repo TLSFingerprintProfileRepository,
|
||||
cache TLSFingerprintProfileCache,
|
||||
) *TLSFingerprintProfileService {
|
||||
svc := &TLSFingerprintProfileService{
|
||||
repo: repo,
|
||||
cache: cache,
|
||||
localCache: make(map[int64]*model.TLSFingerprintProfile),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := svc.reloadFromDB(ctx); err != nil {
|
||||
logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to load profiles from DB on startup: %v", err)
|
||||
if fallbackErr := svc.refreshLocalCache(ctx); fallbackErr != nil {
|
||||
logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to load profiles from cache fallback on startup: %v", fallbackErr)
|
||||
}
|
||||
}
|
||||
|
||||
if cache != nil {
|
||||
cache.SubscribeUpdates(ctx, func() {
|
||||
if err := svc.refreshLocalCache(context.Background()); err != nil {
|
||||
logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to refresh cache on notification: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
// --- CRUD ---
|
||||
|
||||
// List 获取所有模板
|
||||
func (s *TLSFingerprintProfileService) List(ctx context.Context) ([]*model.TLSFingerprintProfile, error) {
|
||||
return s.repo.List(ctx)
|
||||
}
|
||||
|
||||
// GetByID 根据 ID 获取模板
|
||||
func (s *TLSFingerprintProfileService) GetByID(ctx context.Context, id int64) (*model.TLSFingerprintProfile, error) {
|
||||
return s.repo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// Create 创建模板
|
||||
func (s *TLSFingerprintProfileService) Create(ctx context.Context, profile *model.TLSFingerprintProfile) (*model.TLSFingerprintProfile, error) {
|
||||
if err := profile.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
created, err := s.repo.Create(ctx, profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refreshCtx, cancel := s.newCacheRefreshContext()
|
||||
defer cancel()
|
||||
s.invalidateAndNotify(refreshCtx)
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
// Update 更新模板
|
||||
func (s *TLSFingerprintProfileService) Update(ctx context.Context, profile *model.TLSFingerprintProfile) (*model.TLSFingerprintProfile, error) {
|
||||
if err := profile.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updated, err := s.repo.Update(ctx, profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refreshCtx, cancel := s.newCacheRefreshContext()
|
||||
defer cancel()
|
||||
s.invalidateAndNotify(refreshCtx)
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
// Delete 删除模板
|
||||
func (s *TLSFingerprintProfileService) Delete(ctx context.Context, id int64) error {
|
||||
if err := s.repo.Delete(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
refreshCtx, cancel := s.newCacheRefreshContext()
|
||||
defer cancel()
|
||||
s.invalidateAndNotify(refreshCtx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- 热路径:运行时 Profile 查找 ---
|
||||
|
||||
// GetProfileByID 根据 ID 从本地缓存获取 Profile(用于 DoWithTLS 热路径)
|
||||
// 返回 nil 表示未找到,调用方应 fallback 到内置默认 Profile
|
||||
func (s *TLSFingerprintProfileService) GetProfileByID(id int64) *tlsfingerprint.Profile {
|
||||
s.localMu.RLock()
|
||||
p, ok := s.localCache[id]
|
||||
s.localMu.RUnlock()
|
||||
|
||||
if ok && p != nil {
|
||||
return p.ToTLSProfile()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getRandomProfile 从本地缓存中随机选择一个 Profile
|
||||
func (s *TLSFingerprintProfileService) getRandomProfile() *tlsfingerprint.Profile {
|
||||
s.localMu.RLock()
|
||||
defer s.localMu.RUnlock()
|
||||
|
||||
if len(s.localCache) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 收集所有 profile
|
||||
profiles := make([]*model.TLSFingerprintProfile, 0, len(s.localCache))
|
||||
for _, p := range s.localCache {
|
||||
if p != nil {
|
||||
profiles = append(profiles, p)
|
||||
}
|
||||
}
|
||||
if len(profiles) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return profiles[rand.IntN(len(profiles))].ToTLSProfile()
|
||||
}
|
||||
|
||||
// ResolveTLSProfile 根据 Account 的配置解析出运行时 TLS Profile
|
||||
//
|
||||
// 逻辑:
|
||||
// 1. 未启用 TLS 指纹 → 返回 nil(不伪装)
|
||||
// 2. 启用 + 绑定了 profile_id → 从缓存查找对应 profile
|
||||
// 3. 启用 + 未绑定或找不到 → 返回空 Profile(使用代码内置默认值)
|
||||
func (s *TLSFingerprintProfileService) ResolveTLSProfile(account *Account) *tlsfingerprint.Profile {
|
||||
if account == nil || !account.IsTLSFingerprintEnabled() {
|
||||
return nil
|
||||
}
|
||||
id := account.GetTLSFingerprintProfileID()
|
||||
if id > 0 {
|
||||
if p := s.GetProfileByID(id); p != nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
if id == -1 {
|
||||
// 随机选择一个 profile
|
||||
if p := s.getRandomProfile(); p != nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
// TLS 启用但无绑定 profile → 空 Profile → dialer 使用内置默认值
|
||||
return &tlsfingerprint.Profile{Name: "Built-in Default (Node.js 24.x)"}
|
||||
}
|
||||
|
||||
// --- 缓存管理 ---
|
||||
|
||||
func (s *TLSFingerprintProfileService) refreshLocalCache(ctx context.Context) error {
|
||||
if s.cache != nil {
|
||||
if profiles, ok := s.cache.Get(ctx); ok {
|
||||
s.setLocalCache(profiles)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return s.reloadFromDB(ctx)
|
||||
}
|
||||
|
||||
func (s *TLSFingerprintProfileService) reloadFromDB(ctx context.Context) error {
|
||||
profiles, err := s.repo.List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.cache != nil {
|
||||
if err := s.cache.Set(ctx, profiles); err != nil {
|
||||
logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to set cache: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.setLocalCache(profiles)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TLSFingerprintProfileService) setLocalCache(profiles []*model.TLSFingerprintProfile) {
|
||||
m := make(map[int64]*model.TLSFingerprintProfile, len(profiles))
|
||||
for _, p := range profiles {
|
||||
m[p.ID] = p
|
||||
}
|
||||
|
||||
s.localMu.Lock()
|
||||
s.localCache = m
|
||||
s.localMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *TLSFingerprintProfileService) newCacheRefreshContext() (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), 3*time.Second)
|
||||
}
|
||||
|
||||
func (s *TLSFingerprintProfileService) invalidateAndNotify(ctx context.Context) {
|
||||
if s.cache != nil {
|
||||
if err := s.cache.Invalidate(ctx); err != nil {
|
||||
logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to invalidate cache: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.reloadFromDB(ctx); err != nil {
|
||||
logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to refresh local cache: %v", err)
|
||||
s.localMu.Lock()
|
||||
s.localCache = make(map[int64]*model.TLSFingerprintProfile)
|
||||
s.localMu.Unlock()
|
||||
}
|
||||
|
||||
if s.cache != nil {
|
||||
if err := s.cache.NotifyUpdate(ctx); err != nil {
|
||||
logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to notify cache update: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,6 +482,7 @@ var ProviderSet = wire.NewSet(
|
||||
NewUsageCache,
|
||||
NewTotpService,
|
||||
NewErrorPassthroughService,
|
||||
NewTLSFingerprintProfileService,
|
||||
NewDigestSessionStore,
|
||||
ProvideIdempotencyCoordinator,
|
||||
ProvideSystemOperationLockService,
|
||||
|
||||
Reference in New Issue
Block a user