// 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 }