307 lines
11 KiB
Go
307 lines
11 KiB
Go
package stripe
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"math/rand"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"gpt-plus/pkg/captcha"
|
|
"gpt-plus/pkg/httpclient"
|
|
)
|
|
|
|
// TelemetrySession holds state for simulating Stripe.js telemetry events.
|
|
type TelemetrySession struct {
|
|
Client *httpclient.Client
|
|
CheckoutSessionID string
|
|
PublishableKey string
|
|
StripeJsID string
|
|
UserAgent string
|
|
Timezone string // e.g. "America/Chicago"
|
|
Currency string // e.g. "usd"
|
|
Merchant string // e.g. "acct_1HOrSwC6h1nxGoI3"
|
|
|
|
startTime int64 // controller load time (epoch ms)
|
|
eventCount int
|
|
stripeObjID string
|
|
sessionID string // elements_session_id
|
|
}
|
|
|
|
// NewTelemetrySession creates a telemetry session for simulating stripe.js events.
|
|
func NewTelemetrySession(client *httpclient.Client, checkoutSessionID, publishableKey, stripeJsID, userAgent, timezone, currency string) *TelemetrySession {
|
|
return &TelemetrySession{
|
|
Client: client,
|
|
CheckoutSessionID: checkoutSessionID,
|
|
PublishableKey: publishableKey,
|
|
StripeJsID: stripeJsID,
|
|
UserAgent: userAgent,
|
|
Timezone: timezone,
|
|
Currency: currency,
|
|
Merchant: "acct_1HOrSwC6h1nxGoI3",
|
|
startTime: time.Now().UnixMilli() - int64(rand.Intn(2000)+500),
|
|
stripeObjID: "sobj-" + uuid.New().String(),
|
|
sessionID: "elements_session_" + randomAlphaNum(11),
|
|
}
|
|
}
|
|
|
|
// PassiveCaptchaSiteKey is the primary hCaptcha site key used for Stripe's passive captcha.
|
|
// Updated from packet capture (2025-06): confirmed key for confirm requests.
|
|
const PassiveCaptchaSiteKey = "24ed0064-62cf-4d42-9960-5dd1a41d4e29"
|
|
|
|
// TelemetryStatusFunc is a callback for printing progress to terminal.
|
|
type TelemetryStatusFunc func(format string, args ...interface{})
|
|
|
|
// SendPreConfirmEvents sends telemetry events and solves passive captcha.
|
|
// Returns the passive captcha token to include in confirm request.
|
|
// Sends 50+ events across 12 batches to match real browser telemetry volume.
|
|
func (ts *TelemetrySession) SendPreConfirmEvents(ctx context.Context, solver *captcha.Solver, sf TelemetryStatusFunc) (passiveToken string, err error) {
|
|
log.Printf("[telemetry] sending pre-confirm events for checkout %s", ts.CheckoutSessionID)
|
|
|
|
now := time.Now().UnixMilli()
|
|
|
|
// Batch 1: Init events (5)
|
|
ts.sendBatch([]event{
|
|
ts.makeEvent("elements.controller.load", now-12000),
|
|
ts.makeEvent("elements.event.load", now-11800),
|
|
ts.makeEvent("rum.stripejs", now-11600),
|
|
ts.makeEvent("elements.create", now-11400),
|
|
ts.makeEvent("elements.init_payment_page", now-11200),
|
|
})
|
|
time.Sleep(time.Duration(200+rand.Intn(300)) * time.Millisecond)
|
|
|
|
// Batch 2: Mount + ready events (5)
|
|
ts.sendBatch([]event{
|
|
ts.makeEvent("elements.mount", now-10500),
|
|
ts.makeEvent("elements.event.ready", now-10300),
|
|
ts.makeEvent("elements.init_payment_page.success", now-10100),
|
|
ts.makeEvent("elements.retrieve_elements_session.success", now-9900),
|
|
ts.makeEvent("elements.get_elements_state", now-9700),
|
|
})
|
|
time.Sleep(time.Duration(200+rand.Intn(300)) * time.Millisecond)
|
|
|
|
// Batch 3: Card number field interaction (4)
|
|
ts.sendBatch([]event{
|
|
ts.makeEvent("elements.event.focus", now-9000),
|
|
ts.makeEvent("elements.event.change", now-8200),
|
|
ts.makeEvent("elements.event.blur", now-7800),
|
|
ts.makeEvent("elements.update", now-7700),
|
|
})
|
|
time.Sleep(time.Duration(150+rand.Intn(250)) * time.Millisecond)
|
|
|
|
// Batch 4: Card expiry field interaction (4)
|
|
ts.sendBatch([]event{
|
|
ts.makeEvent("elements.event.focus", now-7200),
|
|
ts.makeEvent("elements.event.change", now-6800),
|
|
ts.makeEvent("elements.event.blur", now-6400),
|
|
ts.makeEvent("elements.update", now-6300),
|
|
})
|
|
time.Sleep(time.Duration(100+rand.Intn(200)) * time.Millisecond)
|
|
|
|
// Batch 5: Card CVC field interaction (4)
|
|
ts.sendBatch([]event{
|
|
ts.makeEvent("elements.event.focus", now-5800),
|
|
ts.makeEvent("elements.event.change", now-5400),
|
|
ts.makeEvent("elements.event.blur", now-5000),
|
|
ts.makeEvent("elements.update", now-4900),
|
|
})
|
|
time.Sleep(time.Duration(100+rand.Intn(200)) * time.Millisecond)
|
|
|
|
// Batch 6: Name field interaction (4)
|
|
ts.sendBatch([]event{
|
|
ts.makeEvent("elements.event.focus", now-4500),
|
|
ts.makeEvent("elements.event.change", now-4100),
|
|
ts.makeEvent("elements.event.blur", now-3800),
|
|
ts.makeEvent("elements.update", now-3700),
|
|
})
|
|
time.Sleep(time.Duration(100+rand.Intn(200)) * time.Millisecond)
|
|
|
|
// Batch 7: Address field interaction (4)
|
|
ts.sendBatch([]event{
|
|
ts.makeEvent("elements.event.focus", now-3300),
|
|
ts.makeEvent("elements.event.change", now-2900),
|
|
ts.makeEvent("elements.event.blur", now-2600),
|
|
ts.makeEvent("elements.update", now-2500),
|
|
})
|
|
time.Sleep(time.Duration(100+rand.Intn(200)) * time.Millisecond)
|
|
|
|
// Batch 8: Postal code field interaction + address validation (5)
|
|
ts.sendBatch([]event{
|
|
ts.makeEvent("elements.event.focus", now-2200),
|
|
ts.makeEvent("elements.event.change", now-1900),
|
|
ts.makeEvent("elements.event.blur", now-1600),
|
|
ts.makeEvent("elements.update", now-1500),
|
|
ts.makeEvent("elements.update_checkout_session", now-1400),
|
|
})
|
|
time.Sleep(time.Duration(100+rand.Intn(200)) * time.Millisecond)
|
|
|
|
// Batch 9: Session state refresh (4)
|
|
ts.sendBatch([]event{
|
|
ts.makeEvent("elements.retrieve_elements_session.success", now-1200),
|
|
ts.makeEvent("elements.get_elements_state", now-1100),
|
|
ts.makeEvent("elements.update", now-1000),
|
|
ts.makeEvent("elements.validate_elements", now-900),
|
|
})
|
|
time.Sleep(time.Duration(100+rand.Intn(200)) * time.Millisecond)
|
|
|
|
// Batch 10: Additional focus/blur cycles (6)
|
|
ts.sendBatch([]event{
|
|
ts.makeEvent("elements.event.focus", now-800),
|
|
ts.makeEvent("elements.event.blur", now-700),
|
|
ts.makeEvent("elements.event.focus", now-650),
|
|
ts.makeEvent("elements.event.blur", now-550),
|
|
ts.makeEvent("elements.event.focus", now-500),
|
|
ts.makeEvent("elements.event.blur", now-400),
|
|
})
|
|
time.Sleep(time.Duration(100+rand.Intn(200)) * time.Millisecond)
|
|
|
|
// Batch 11: Passive captcha init (2)
|
|
ts.sendBatch([]event{
|
|
ts.makeEvent("elements.captcha.passive.init", now-300),
|
|
ts.makeEvent("elements.captcha.passive.load", now-250),
|
|
})
|
|
|
|
// Solve invisible hCaptcha via captcha solver
|
|
if solver != nil {
|
|
if sf != nil {
|
|
sf(" → 解 Passive hCaptcha (invisible, site_key=%s)...", PassiveCaptchaSiteKey[:8])
|
|
}
|
|
log.Printf("[telemetry] solving passive (invisible) hCaptcha, site_key=%s", PassiveCaptchaSiteKey)
|
|
passiveToken, _, err = solver.SolveInvisibleHCaptcha(ctx, PassiveCaptchaSiteKey, "https://js.stripe.com")
|
|
if err != nil {
|
|
if sf != nil {
|
|
sf(" ⚠ Passive hCaptcha 失败 (非致命): %v", err)
|
|
}
|
|
log.Printf("[telemetry] passive captcha failed (non-fatal): %v", err)
|
|
} else {
|
|
if sf != nil {
|
|
sf(" ✓ Passive hCaptcha 成功, token 长度=%d", len(passiveToken))
|
|
}
|
|
log.Printf("[telemetry] passive captcha token obtained, len=%d", len(passiveToken))
|
|
}
|
|
}
|
|
|
|
// Batch 12: Passive captcha completion + pre-confirm (5)
|
|
ts.sendBatch([]event{
|
|
ts.makeEvent("elements.captcha.passive.execute", now-100),
|
|
ts.makeEvent("elements.captcha.passive.success", now-80),
|
|
ts.makeEvent("elements.get_elements_state", now-60),
|
|
ts.makeEvent("elements.custom_checkout.confirm", now-30),
|
|
ts.makeEvent("elements.validate_elements", now-10),
|
|
})
|
|
|
|
log.Printf("[telemetry] sent %d events total across 12 batches", ts.eventCount)
|
|
return passiveToken, nil
|
|
}
|
|
|
|
// SendPostConfirmEvents sends events after a successful confirm.
|
|
func (ts *TelemetrySession) SendPostConfirmEvents() {
|
|
now := time.Now().UnixMilli()
|
|
ts.sendBatch([]event{
|
|
ts.makeEvent("elements.confirm_payment_page", now),
|
|
ts.makeEvent("elements.confirm_payment_page.success", now+100),
|
|
})
|
|
}
|
|
|
|
// ─── Internal helpers ───
|
|
|
|
type event map[string]interface{}
|
|
|
|
func (ts *TelemetrySession) makeEvent(name string, created int64) event {
|
|
ts.eventCount++
|
|
e := event{
|
|
"event_name": name,
|
|
"created": created,
|
|
"batching_enabled": true,
|
|
"event_count": fmt.Sprintf("%d", ts.eventCount),
|
|
"os": "Windows",
|
|
"browserFamily": "Chrome",
|
|
"version": FetchStripeConstants().BuildHash,
|
|
"event_id": uuid.New().String(),
|
|
"team_identifier": "t_0",
|
|
"deploy_status": "main",
|
|
"browserClassification": "modern",
|
|
"browser_classification_v2": "2024",
|
|
"connection_rtt": fmt.Sprintf("%d", 50+rand.Intn(200)),
|
|
"connection_downlink": fmt.Sprintf("%.1f", 1.0+rand.Float64()*9),
|
|
"connection_effective_type": "4g",
|
|
"key": ts.PublishableKey,
|
|
"key_mode": "live",
|
|
"referrer": "https://chatgpt.com",
|
|
"betas": "custom_checkout_server_updates_1 custom_checkout_manual_approval_1",
|
|
"stripe_js_id": ts.StripeJsID,
|
|
"stripe_obj_id": ts.stripeObjID,
|
|
"controller_load_time": fmt.Sprintf("%d", ts.startTime),
|
|
"stripe_js_release_train": "basil",
|
|
"wrapper": "react-stripe-js",
|
|
"wrapper_version": "3.10.0",
|
|
"es_module": "true",
|
|
"es_module_version": "7.9.0",
|
|
"browser_timezone": ts.Timezone,
|
|
"checkout_session_id": ts.CheckoutSessionID,
|
|
"elements_init_source": "custom_checkout",
|
|
"decoupled_intent": "true",
|
|
"merchant": ts.Merchant,
|
|
"elements_session_id": ts.sessionID,
|
|
}
|
|
|
|
switch {
|
|
case strings.Contains(name, "focus"), strings.Contains(name, "blur"),
|
|
strings.Contains(name, "mount"), strings.Contains(name, "ready"):
|
|
e["element"] = "payment"
|
|
e["element_id"] = "payment-" + uuid.New().String()
|
|
e["frame_width"] = fmt.Sprintf("%d", 400+rand.Intn(200))
|
|
case strings.Contains(name, "confirm"):
|
|
e["currency"] = ts.Currency
|
|
e["frame_width"] = fmt.Sprintf("%d", 800+rand.Intn(200))
|
|
e["livemode"] = "true"
|
|
e["uiMode"] = "custom"
|
|
e["m_sdk_confirm"] = "1"
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func (ts *TelemetrySession) sendBatch(events []event) error {
|
|
eventsJSON, err := json.Marshal(events)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal telemetry events: %w", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("client_id", "stripe-js")
|
|
form.Set("num_requests", fmt.Sprintf("%d", len(events)))
|
|
form.Set("events", string(eventsJSON))
|
|
|
|
headers := map[string]string{
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
"Origin": "https://js.stripe.com",
|
|
"Referer": "https://js.stripe.com/",
|
|
"Accept": "application/json",
|
|
"Accept-Language": DefaultAcceptLanguage,
|
|
"User-Agent": ts.UserAgent,
|
|
}
|
|
|
|
resp, err := ts.Client.PostForm("https://r.stripe.com/b", form, headers)
|
|
if err != nil {
|
|
log.Printf("[telemetry] send batch failed: %v", err)
|
|
return err
|
|
}
|
|
httpclient.ReadBody(resp)
|
|
return nil
|
|
}
|
|
|
|
func randomAlphaNum(n int) string {
|
|
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
b := make([]byte, n)
|
|
for i := range b {
|
|
b[i] = chars[rand.Intn(len(chars))]
|
|
}
|
|
return string(b)
|
|
}
|