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:
122
backend/internal/repository/tls_fingerprint_profile_cache.go
Normal file
122
backend/internal/repository/tls_fingerprint_profile_cache.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const (
|
||||
tlsFPProfileCacheKey = "tls_fingerprint_profiles"
|
||||
tlsFPProfilePubSubKey = "tls_fingerprint_profiles_updated"
|
||||
tlsFPProfileCacheTTL = 24 * time.Hour
|
||||
)
|
||||
|
||||
type tlsFingerprintProfileCache struct {
|
||||
rdb *redis.Client
|
||||
localCache []*model.TLSFingerprintProfile
|
||||
localMu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewTLSFingerprintProfileCache 创建 TLS 指纹模板缓存
|
||||
func NewTLSFingerprintProfileCache(rdb *redis.Client) service.TLSFingerprintProfileCache {
|
||||
return &tlsFingerprintProfileCache{
|
||||
rdb: rdb,
|
||||
}
|
||||
}
|
||||
|
||||
// Get 从缓存获取模板列表
|
||||
func (c *tlsFingerprintProfileCache) Get(ctx context.Context) ([]*model.TLSFingerprintProfile, bool) {
|
||||
c.localMu.RLock()
|
||||
if c.localCache != nil {
|
||||
profiles := c.localCache
|
||||
c.localMu.RUnlock()
|
||||
return profiles, true
|
||||
}
|
||||
c.localMu.RUnlock()
|
||||
|
||||
data, err := c.rdb.Get(ctx, tlsFPProfileCacheKey).Bytes()
|
||||
if err != nil {
|
||||
if err != redis.Nil {
|
||||
slog.Warn("tls_fp_profile_cache_get_failed", "error", err)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var profiles []*model.TLSFingerprintProfile
|
||||
if err := json.Unmarshal(data, &profiles); err != nil {
|
||||
slog.Warn("tls_fp_profile_cache_unmarshal_failed", "error", err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
c.localMu.Lock()
|
||||
c.localCache = profiles
|
||||
c.localMu.Unlock()
|
||||
|
||||
return profiles, true
|
||||
}
|
||||
|
||||
// Set 设置缓存
|
||||
func (c *tlsFingerprintProfileCache) Set(ctx context.Context, profiles []*model.TLSFingerprintProfile) error {
|
||||
data, err := json.Marshal(profiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.rdb.Set(ctx, tlsFPProfileCacheKey, data, tlsFPProfileCacheTTL).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.localMu.Lock()
|
||||
c.localCache = profiles
|
||||
c.localMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Invalidate 使缓存失效
|
||||
func (c *tlsFingerprintProfileCache) Invalidate(ctx context.Context) error {
|
||||
c.localMu.Lock()
|
||||
c.localCache = nil
|
||||
c.localMu.Unlock()
|
||||
|
||||
return c.rdb.Del(ctx, tlsFPProfileCacheKey).Err()
|
||||
}
|
||||
|
||||
// NotifyUpdate 通知其他实例刷新缓存
|
||||
func (c *tlsFingerprintProfileCache) NotifyUpdate(ctx context.Context) error {
|
||||
return c.rdb.Publish(ctx, tlsFPProfilePubSubKey, "refresh").Err()
|
||||
}
|
||||
|
||||
// SubscribeUpdates 订阅缓存更新通知
|
||||
func (c *tlsFingerprintProfileCache) SubscribeUpdates(ctx context.Context, handler func()) {
|
||||
go func() {
|
||||
sub := c.rdb.Subscribe(ctx, tlsFPProfilePubSubKey)
|
||||
defer func() { _ = sub.Close() }()
|
||||
|
||||
ch := sub.Channel()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
slog.Debug("tls_fp_profile_cache_subscriber_stopped", "reason", "context_done")
|
||||
return
|
||||
case msg := <-ch:
|
||||
if msg == nil {
|
||||
slog.Warn("tls_fp_profile_cache_subscriber_stopped", "reason", "channel_closed")
|
||||
return
|
||||
}
|
||||
c.localMu.Lock()
|
||||
c.localCache = nil
|
||||
c.localMu.Unlock()
|
||||
|
||||
handler()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
Reference in New Issue
Block a user