172 lines
5.0 KiB
Go
172 lines
5.0 KiB
Go
// Package tlsfingerprint provides TLS fingerprint simulation for HTTP clients.
|
|
package tlsfingerprint
|
|
|
|
import (
|
|
"log/slog"
|
|
"sort"
|
|
"sync"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
)
|
|
|
|
// DefaultProfileName is the name of the built-in Claude CLI profile.
|
|
const DefaultProfileName = "claude_cli_v2"
|
|
|
|
// Registry manages TLS fingerprint profiles.
|
|
// It holds a collection of profiles that can be used for TLS fingerprint simulation.
|
|
// Profiles are selected based on account ID using modulo operation.
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
profiles map[string]*Profile
|
|
profileNames []string // Sorted list of profile names for deterministic selection
|
|
}
|
|
|
|
// NewRegistry creates a new TLS fingerprint profile registry.
|
|
// It initializes with the built-in default profile.
|
|
func NewRegistry() *Registry {
|
|
r := &Registry{
|
|
profiles: make(map[string]*Profile),
|
|
profileNames: make([]string, 0),
|
|
}
|
|
|
|
// Register the built-in default profile
|
|
r.registerBuiltinProfile()
|
|
|
|
return r
|
|
}
|
|
|
|
// NewRegistryFromConfig creates a new registry and loads profiles from config.
|
|
// If the config has custom profiles defined, they will be merged with the built-in default.
|
|
func NewRegistryFromConfig(cfg *config.TLSFingerprintConfig) *Registry {
|
|
r := NewRegistry()
|
|
|
|
if cfg == nil || !cfg.Enabled {
|
|
slog.Debug("tls_registry_disabled", "reason", "disabled or no config")
|
|
return r
|
|
}
|
|
|
|
// Load custom profiles from config
|
|
for name, profileCfg := range cfg.Profiles {
|
|
profile := &Profile{
|
|
Name: profileCfg.Name,
|
|
EnableGREASE: profileCfg.EnableGREASE,
|
|
CipherSuites: profileCfg.CipherSuites,
|
|
Curves: profileCfg.Curves,
|
|
PointFormats: profileCfg.PointFormats,
|
|
}
|
|
|
|
// If the profile has empty values, they will use defaults in dialer
|
|
r.RegisterProfile(name, profile)
|
|
slog.Debug("tls_registry_loaded_profile", "key", name, "name", profileCfg.Name)
|
|
}
|
|
|
|
slog.Debug("tls_registry_initialized", "profile_count", len(r.profileNames), "profiles", r.profileNames)
|
|
return r
|
|
}
|
|
|
|
// registerBuiltinProfile adds the default Claude CLI profile to the registry.
|
|
func (r *Registry) registerBuiltinProfile() {
|
|
defaultProfile := &Profile{
|
|
Name: "Claude CLI 2.x (Node.js 20.x + OpenSSL 3.x)",
|
|
EnableGREASE: false, // Node.js does not use GREASE
|
|
// Empty slices will cause dialer to use built-in defaults
|
|
CipherSuites: nil,
|
|
Curves: nil,
|
|
PointFormats: nil,
|
|
}
|
|
r.RegisterProfile(DefaultProfileName, defaultProfile)
|
|
}
|
|
|
|
// RegisterProfile adds or updates a profile in the registry.
|
|
func (r *Registry) RegisterProfile(name string, profile *Profile) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
// Check if this is a new profile
|
|
_, exists := r.profiles[name]
|
|
r.profiles[name] = profile
|
|
|
|
if !exists {
|
|
r.profileNames = append(r.profileNames, name)
|
|
// Keep names sorted for deterministic selection
|
|
sort.Strings(r.profileNames)
|
|
}
|
|
}
|
|
|
|
// GetProfile returns a profile by name.
|
|
// Returns nil if the profile does not exist.
|
|
func (r *Registry) GetProfile(name string) *Profile {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
return r.profiles[name]
|
|
}
|
|
|
|
// GetDefaultProfile returns the built-in default profile.
|
|
func (r *Registry) GetDefaultProfile() *Profile {
|
|
return r.GetProfile(DefaultProfileName)
|
|
}
|
|
|
|
// GetProfileByAccountID returns a profile for the given account ID.
|
|
// The profile is selected using: profileNames[accountID % len(profiles)]
|
|
// This ensures deterministic profile assignment for each account.
|
|
func (r *Registry) GetProfileByAccountID(accountID int64) *Profile {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
if len(r.profileNames) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Use modulo to select profile index
|
|
// Use absolute value to handle negative IDs (though unlikely)
|
|
idx := accountID
|
|
if idx < 0 {
|
|
idx = -idx
|
|
}
|
|
selectedIndex := int(idx % int64(len(r.profileNames)))
|
|
selectedName := r.profileNames[selectedIndex]
|
|
|
|
return r.profiles[selectedName]
|
|
}
|
|
|
|
// ProfileCount returns the number of registered profiles.
|
|
func (r *Registry) ProfileCount() int {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
return len(r.profiles)
|
|
}
|
|
|
|
// ProfileNames returns a sorted list of all registered profile names.
|
|
func (r *Registry) ProfileNames() []string {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
// Return a copy to prevent modification
|
|
names := make([]string, len(r.profileNames))
|
|
copy(names, r.profileNames)
|
|
return names
|
|
}
|
|
|
|
// Global registry instance for convenience
|
|
var globalRegistry *Registry
|
|
var globalRegistryOnce sync.Once
|
|
|
|
// GlobalRegistry returns the global TLS fingerprint registry.
|
|
// The registry is lazily initialized with the default profile.
|
|
func GlobalRegistry() *Registry {
|
|
globalRegistryOnce.Do(func() {
|
|
globalRegistry = NewRegistry()
|
|
})
|
|
return globalRegistry
|
|
}
|
|
|
|
// InitGlobalRegistry initializes the global registry with configuration.
|
|
// This should be called during application startup.
|
|
// It is safe to call multiple times; subsequent calls will update the registry.
|
|
func InitGlobalRegistry(cfg *config.TLSFingerprintConfig) *Registry {
|
|
globalRegistryOnce.Do(func() {
|
|
globalRegistry = NewRegistryFromConfig(cfg)
|
|
})
|
|
return globalRegistry
|
|
}
|