223 lines
7.1 KiB
Go
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")
|
|
}
|