187 lines
5.3 KiB
Go
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
|
|
}
|