package stripe import ( "crypto/tls" "fmt" "io" "log" "net" "net/http" "regexp" "sync" "time" ) // StripeConstants holds dynamically fetched Stripe.js build constants. type StripeConstants struct { RV string // 40-char hex from stripe.js (STRIPE_JS_BUILD_SALT) SV string // 64-char hex from stripe.js (STRIPE_JS_BUILD_SALT) RVTS string // date string e.g. "2024-01-01 00:00:00 -0000" BuildHash string // first 10 chars of RV (module 6179) TagVersion string // from m.stripe.network/inner.html out-X.X.X.js } // Fallback values — used when dynamic fetch fails. var fallbackConstants = StripeConstants{ RV: "e5b328e98e63961074bfff3e3ac7f85ffe37b12b", SV: "663ce80473de6178ef298eecdc3e16645c1463af7afe64fff89400f5e02aa0c7", RVTS: "2024-01-01 00:00:00 -0000", BuildHash: "ede17ac9fd", TagVersion: "4.5.43", } var ( cachedConstants *StripeConstants cachedAt time.Time constantsMu sync.Mutex constantsTTL = 1 * time.Hour ) // SetFallbackConstants allows overriding fallback values from config. func SetFallbackConstants(buildHash, tagVersion string) { if buildHash != "" { fallbackConstants.BuildHash = buildHash } if tagVersion != "" { fallbackConstants.TagVersion = tagVersion } } // FetchStripeConstants returns the current Stripe.js build constants. // Uses a 1-hour cache. Falls back to hardcoded/config values on failure. func FetchStripeConstants() *StripeConstants { constantsMu.Lock() defer constantsMu.Unlock() if cachedConstants != nil && time.Since(cachedAt) < constantsTTL { return cachedConstants } sc, err := doFetchStripeConstants() if err != nil { log.Printf("[stripe-constants] fetch failed: %v, using fallback values", err) fb := fallbackConstants // copy return &fb } cachedConstants = sc cachedAt = time.Now() log.Printf("[stripe-constants] fetched: RV=%s, SV=%s..., BuildHash=%s, TagVersion=%s, RVTS=%s", sc.RV, sc.SV[:16], sc.BuildHash, sc.TagVersion, sc.RVTS) return sc } // directHTTPClient creates a direct (no-proxy) HTTP client for fetching Stripe.js. func directHTTPClient() *http.Client { return &http.Client{ Timeout: 15 * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12}, DialContext: (&net.Dialer{ Timeout: 10 * time.Second, }).DialContext, }, } } // Regex patterns for extracting constants from stripe.js bundle. var ( reRV = regexp.MustCompile(`STRIPE_JS_BUILD_SALT\s*=\s*\[\s*"([0-9a-f]{40})"`) reSV = regexp.MustCompile(`STRIPE_JS_BUILD_SALT\s*=\s*\[[^]]*"[0-9a-f]{40}"\s*,\s*"([0-9a-f]{64})"`) reRVTS = regexp.MustCompile(`STRIPE_JS_BUILD_SALT\s*=\s*\[[^]]*"[0-9a-f]{40}"\s*,\s*"[0-9a-f]{64}"\s*,\s*"(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+[-+]\d{4})"`) reBuildHash = regexp.MustCompile(`"([0-9a-f]{10})"`) reTagVersion = regexp.MustCompile(`out-(\d+\.\d+\.\d+)\.js`) ) func doFetchStripeConstants() (*StripeConstants, error) { client := directHTTPClient() // Step 1: Fetch js.stripe.com/v3/ to extract RV, SV, RVTS, BuildHash stripeJS, err := fetchURL(client, "https://js.stripe.com/v3/") if err != nil { return nil, fmt.Errorf("fetch stripe.js: %w", err) } sc := &StripeConstants{} if m := reRV.FindStringSubmatch(stripeJS); len(m) > 1 { sc.RV = m[1] } else { re40 := regexp.MustCompile(`"([0-9a-f]{40})"`) matches := re40.FindAllStringSubmatch(stripeJS, -1) if len(matches) > 0 { sc.RV = matches[0][1] } } if m := reSV.FindStringSubmatch(stripeJS); len(m) > 1 { sc.SV = m[1] } else { re64 := regexp.MustCompile(`"([0-9a-f]{64})"`) matches := re64.FindAllStringSubmatch(stripeJS, -1) if len(matches) > 0 { sc.SV = matches[0][1] } } if m := reRVTS.FindStringSubmatch(stripeJS); len(m) > 1 { sc.RVTS = m[1] } if sc.RV != "" && len(sc.RV) >= 10 { sc.BuildHash = sc.RV[:10] } if sc.RV == "" || sc.SV == "" { return nil, fmt.Errorf("failed to extract RV/SV from stripe.js (len=%d)", len(stripeJS)) } if sc.RVTS == "" { sc.RVTS = fallbackConstants.RVTS } if sc.BuildHash == "" { sc.BuildHash = fallbackConstants.BuildHash } // Step 2: Fetch m.stripe.network/inner.html to extract tagVersion innerHTML, err := fetchURL(client, "https://m.stripe.network/inner.html") if err != nil { log.Printf("[stripe-constants] fetch inner.html failed: %v, using fallback tagVersion", err) sc.TagVersion = fallbackConstants.TagVersion } else { if m := reTagVersion.FindStringSubmatch(innerHTML); len(m) > 1 { sc.TagVersion = m[1] } else { log.Printf("[stripe-constants] tagVersion not found in inner.html, using fallback") sc.TagVersion = fallbackConstants.TagVersion } } return sc, nil } func fetchURL(client *http.Client, url string) (string, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return "", err } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36") req.Header.Set("Accept", "*/*") resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("status %d for %s", resp.StatusCode, url) } body, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("read body: %w", err) } return string(body), nil }