Files
gpt-plus-gpt/pkg/stripe/fingerprint.go
2026-03-15 20:48:19 +08:00

223 lines
7.1 KiB
Go

package stripe
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"strings"
"time"
"gpt-plus/pkg/httpclient"
)
// Fingerprint holds Stripe anti-fraud device IDs.
type Fingerprint struct {
GUID string
MUID string
SID string
}
const fingerprintMaxRetries = 3
// GetFingerprint sends payload to m.stripe.com/6 and retrieves device fingerprint.
// Uses the provided (proxied) client. If it fails, returns an error — never fakes GUID.
// If browserFP is provided, real fingerprint data is used in the payload.
func GetFingerprint(client *httpclient.Client, userAgent, domain, tagVersion string, browserFP ...*BrowserFingerprint) (*Fingerprint, error) {
var bfp *BrowserFingerprint
if len(browserFP) > 0 {
bfp = browserFP[0]
}
payload := CreateInitPayload(userAgent, domain, tagVersion, bfp)
encoded, err := EncodePayload(payload)
if err != nil {
return nil, fmt.Errorf("encode payload: %w", err)
}
var lastErr error
for attempt := 1; attempt <= fingerprintMaxRetries; attempt++ {
fp, err := doFingerprintRequest(nil, client, encoded, userAgent)
if err == nil {
fp = retryForMUIDSID(fp, nil, client, encoded, userAgent)
return fp, nil
}
lastErr = err
if isRetryableError(err) {
log.Printf("[stripe] fingerprint attempt %d/%d failed (retryable): %v", attempt, fingerprintMaxRetries, err)
if attempt < fingerprintMaxRetries {
time.Sleep(time.Duration(attempt*3) * time.Second)
}
continue
}
return nil, fmt.Errorf("send fingerprint request: %w", err)
}
return nil, fmt.Errorf("m.stripe.com unreachable after %d retries: %w", fingerprintMaxRetries, lastErr)
}
// GetFingerprintDirect sends payload to m.stripe.com/6 using a direct (no-proxy) HTTPS connection.
// This bypasses the SOCKS proxy which may block m.stripe.com.
// The direct connection is safe: m.stripe.com is Stripe's telemetry endpoint
// and does not leak your real IP to OpenAI — it only communicates with Stripe.
func GetFingerprintDirect(userAgent, domain, tagVersion string) (*Fingerprint, error) {
payload := CreateInitPayload(userAgent, domain, tagVersion)
encoded, err := EncodePayload(payload)
if err != nil {
return nil, fmt.Errorf("encode payload: %w", err)
}
directClient := &http.Client{
Timeout: 15 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
}).DialContext,
},
}
var lastErr error
for attempt := 1; attempt <= fingerprintMaxRetries; attempt++ {
fp, err := doFingerprintRequest(directClient, nil, encoded, userAgent)
if err == nil {
fp = retryForMUIDSID(fp, directClient, nil, encoded, userAgent)
return fp, nil
}
lastErr = err
if isRetryableError(err) {
log.Printf("[stripe] fingerprint-direct attempt %d/%d failed (retryable): %v", attempt, fingerprintMaxRetries, err)
if attempt < fingerprintMaxRetries {
time.Sleep(time.Duration(attempt*2) * time.Second)
}
continue
}
return nil, fmt.Errorf("send fingerprint-direct request: %w", err)
}
return nil, fmt.Errorf("m.stripe.com unreachable (direct) after %d retries: %w", fingerprintMaxRetries, lastErr)
}
// retryForMUIDSID retries fingerprint request if GUID is present but MUID/SID are empty.
// Makes up to 2 additional attempts with increasing delay, merging results.
func retryForMUIDSID(fp *Fingerprint, directClient *http.Client, proxiedClient *httpclient.Client, encoded, userAgent string) *Fingerprint {
if fp.GUID == "" || (fp.MUID != "" && fp.SID != "") {
return fp
}
log.Printf("[stripe] GUID present but MUID/SID empty, retrying to fill...")
for i := 0; i < 2; i++ {
time.Sleep(time.Duration(500+i*500) * time.Millisecond)
retryFP, err := doFingerprintRequest(directClient, proxiedClient, encoded, userAgent)
if err != nil {
log.Printf("[stripe] MUID/SID retry %d failed: %v", i+1, err)
continue
}
if fp.MUID == "" && retryFP.MUID != "" {
fp.MUID = retryFP.MUID
}
if fp.SID == "" && retryFP.SID != "" {
fp.SID = retryFP.SID
}
if fp.MUID != "" && fp.SID != "" {
log.Printf("[stripe] MUID/SID filled after retry %d", i+1)
break
}
}
return fp
}
// GetFingerprintAuto tries the proxied client first; if it fails, falls back to direct connection.
func GetFingerprintAuto(ctx context.Context, client *httpclient.Client, userAgent, domain, tagVersion string) (*Fingerprint, error) {
fp, err := GetFingerprint(client, userAgent, domain, tagVersion)
if err == nil {
return fp, nil
}
log.Printf("[stripe] proxy fingerprint failed (%v), falling back to direct connection", err)
return GetFingerprintDirect(userAgent, domain, tagVersion)
}
// doFingerprintRequest executes a single POST to m.stripe.com/6.
// Exactly one of directClient / proxiedClient must be non-nil.
func doFingerprintRequest(directClient *http.Client, proxiedClient *httpclient.Client, encodedPayload, userAgent string) (*Fingerprint, error) {
req, err := http.NewRequest("POST", "https://m.stripe.com/6", strings.NewReader(encodedPayload))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
// Set proper headers — missing these caused silent rejections
req.Header.Set("User-Agent", userAgent)
req.Header.Set("Content-Type", "text/plain;charset=UTF-8")
req.Header.Set("Origin", "https://js.stripe.com")
req.Header.Set("Referer", "https://js.stripe.com/")
req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
var resp *http.Response
if directClient != nil {
resp, err = directClient.Do(req)
} else {
resp, err = proxiedClient.Do(req)
}
if err != nil {
return nil, err
}
defer resp.Body.Close() // safe: one request per call, no loop
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("fingerprint request failed: status %d, body: %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("parse fingerprint response: %w", err)
}
fp := &Fingerprint{}
if v, ok := result["guid"].(string); ok {
fp.GUID = v
}
if v, ok := result["muid"].(string); ok {
fp.MUID = v
}
if v, ok := result["sid"].(string); ok {
fp.SID = v
}
// GUID is critical — if missing, Stripe WILL trigger 3DS or decline
if fp.GUID == "" {
return nil, fmt.Errorf("m.stripe.com returned empty GUID (response: %s)", string(body))
}
// MUID/SID can sometimes be empty on first call, but GUID is the key signal
if fp.MUID == "" {
log.Printf("[stripe] WARNING: MUID empty in fingerprint response, proceeding with GUID only")
}
if fp.SID == "" {
log.Printf("[stripe] WARNING: SID empty in fingerprint response, proceeding with GUID only")
}
return fp, nil
}
// isRetryableError checks if the error is a transient network issue worth retrying.
func isRetryableError(err error) bool {
if err == nil {
return false
}
msg := err.Error()
return strings.Contains(msg, "connection refused") ||
strings.Contains(msg, "connection reset") ||
strings.Contains(msg, "i/o timeout") ||
strings.Contains(msg, "no such host") ||
strings.Contains(msg, "network is unreachable")
}