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

187 lines
5.3 KiB
Go

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
}