Initial sanitized code sync
This commit is contained in:
222
pkg/stripe/fingerprint.go
Normal file
222
pkg/stripe/fingerprint.go
Normal file
@@ -0,0 +1,222 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user