package stripe import ( "crypto/rand" "encoding/json" "fmt" "log" "math/big" "os" "path/filepath" "strings" "sync" ) // BrowserFingerprint holds real browser fingerprint data harvested from a real browser. type BrowserFingerprint struct { CookieSupport bool `json:"cookie_support"` DoNotTrack bool `json:"do_not_track"` Language string `json:"language"` Platform string `json:"platform"` ScreenSize string `json:"screen_size"` UserAgent string `json:"user_agent"` Plugins string `json:"plugins"` WebGLVendor string `json:"webgl_vendor"` WebGLRenderer string `json:"webgl_renderer"` FontsBits string `json:"fonts_bits"` CanvasHash string `json:"canvas_hash"` OaiDID string `json:"oai_did"` CfClearance string `json:"cf_clearance"` CsrfToken string `json:"csrf_token"` Region string `json:"region"` StripeFpID string `json:"stripe_fingerprint_id"` } // BrowserFingerprintPool manages a pool of pre-harvested browser fingerprints. type BrowserFingerprintPool struct { mu sync.Mutex fingerprints []*BrowserFingerprint index int } // NewBrowserFingerprintPool loads all fingerprint JSON files from a directory. // If filterLang is not empty, only loads fingerprints matching that language prefix (e.g. "ko"). func NewBrowserFingerprintPool(dir string, filterLang ...string) (*BrowserFingerprintPool, error) { files, err := filepath.Glob(filepath.Join(dir, "*.json")) if err != nil { return nil, fmt.Errorf("glob fingerprint dir: %w", err) } if len(files) == 0 { return nil, fmt.Errorf("no fingerprint files found in %s", dir) } langPrefix := "" if len(filterLang) > 0 && filterLang[0] != "" { langPrefix = strings.ToLower(filterLang[0]) } var fps []*BrowserFingerprint for _, f := range files { data, err := os.ReadFile(f) if err != nil { log.Printf("[fingerprint] warning: skip %s: %v", f, err) continue } var fp BrowserFingerprint if err := json.Unmarshal(data, &fp); err != nil { log.Printf("[fingerprint] warning: skip %s: %v", f, err) continue } if fp.CanvasHash == "" || fp.UserAgent == "" { log.Printf("[fingerprint] warning: skip %s: missing canvas_hash or user_agent", f) continue } // Filter by language if specified if langPrefix != "" && !strings.HasPrefix(strings.ToLower(fp.Language), langPrefix) { log.Printf("[fingerprint] skip %s: language %q doesn't match %q", filepath.Base(f), fp.Language, langPrefix) continue } fps = append(fps, &fp) } if len(fps) == 0 { return nil, fmt.Errorf("no valid fingerprints loaded from %s", dir) } // Shuffle for i := len(fps) - 1; i > 0; i-- { n, _ := rand.Int(rand.Reader, big.NewInt(int64(i+1))) j := int(n.Int64()) fps[i], fps[j] = fps[j], fps[i] } log.Printf("[fingerprint] loaded %d browser fingerprints from %s", len(fps), dir) return &BrowserFingerprintPool{fingerprints: fps}, nil } // Get returns the next fingerprint in round-robin order. func (p *BrowserFingerprintPool) Get() *BrowserFingerprint { p.mu.Lock() defer p.mu.Unlock() fp := p.fingerprints[p.index%len(p.fingerprints)] p.index++ return fp } // Count returns the number of fingerprints in the pool. func (p *BrowserFingerprintPool) Count() int { return len(p.fingerprints) }