增加随机指纹

This commit is contained in:
2026-07-07 12:03:52 +08:00
parent 10c312eb09
commit 67354c3b8c
4 changed files with 145 additions and 52 deletions
+105 -46
View File
@@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"io"
"math/rand"
"net/url"
"strings"
"time"
@@ -60,11 +61,11 @@ func (c *Client) SetProxy(proxy string) {
}
func (c *Client) ExchangeCookie(ctx context.Context, cookie string) (*CookieExchangeResult, error) {
client, err := c.newTLSClient()
sess, err := c.newTLSClient()
if err != nil {
return nil, err
}
return exchangeCookieWithTLSClient(ctx, client, cookie)
return exchangeCookieWithTLSClient(ctx, sess, cookie)
}
// uploadMaxRetries is how many extra in-place attempts a transient upload
@@ -106,7 +107,7 @@ func (c *Client) UploadImage(ctx context.Context, token string, content []byte,
// body plus whether a failure is retryable (transport error / 429/451/5xx).
// Auth failures (401/403) and other non-200s are not retryable.
func (c *Client) uploadImageOnce(ctx context.Context, token string, content []byte, contentType, engine string) ([]byte, error, bool) {
client, err := c.newTLSClient()
sess, err := c.newTLSClient()
if err != nil {
return nil, err, false
}
@@ -125,7 +126,7 @@ func (c *Client) uploadImageOnce(ctx context.Context, token string, content []by
"x-api-key": {c.apiKey},
"content-type": {defaultString(contentType, "image/png")},
"accept": {"*/*"},
"user-agent": {defaultUserAgent},
"user-agent": {sess.fp.userAgent},
http.HeaderOrderKey: {
"authorization",
"x-api-key",
@@ -135,7 +136,7 @@ func (c *Client) uploadImageOnce(ctx context.Context, token string, content []by
},
}
resp, err := client.Do(req)
resp, err := sess.client.Do(req)
if err != nil {
// Transport error (incl. Client.Timeout on the storage endpoint) — a
// network blip, retryable on a fresh connection.
@@ -160,7 +161,7 @@ func (c *Client) uploadImageOnce(ctx context.Context, token string, content []by
}
func (c *Client) GenerateImage(ctx context.Context, token, modelID, prompt, aspectRatio, resolution string, blobIDs []string) ([]byte, map[string]any, error) {
client, err := c.newTLSClient()
sess, err := c.newTLSClient()
if err != nil {
return nil, nil, err
}
@@ -178,9 +179,9 @@ func (c *Client) GenerateImage(ctx context.Context, token, modelID, prompt, aspe
candidates = BuildImagePayloadCandidates(modelID, prompt, aspectRatio, resolution, blobIDs)
}
for _, payload := range candidates {
respBody, pollURL, err := c.submitImage(ctx, client, token, prompt, endpoint, payload)
respBody, pollURL, err := c.submitImage(ctx, sess, token, prompt, endpoint, payload)
if err == nil {
meta, data, pollErr := c.pollImage(ctx, client, token, pollURL)
meta, data, pollErr := c.pollImage(ctx, sess, token, pollURL)
if pollErr != nil {
return nil, nil, pollErr
}
@@ -208,7 +209,7 @@ func (c *Client) GenerateImage(ctx context.Context, token, modelID, prompt, aspe
// in meta["video_url"] — used by the async /v1/videos job, which proxies that URL
// on /content instead of persisting the file.
func (c *Client) GenerateVideo(ctx context.Context, token, engine, prompt, aspectRatio string, durationSeconds int, resolution, referenceMode, upstreamModel string, blobIDs []string, downloadResult bool) ([]byte, map[string]any, error) {
client, err := c.newTLSClient()
sess, err := c.newTLSClient()
if err != nil {
return nil, nil, err
}
@@ -218,12 +219,12 @@ func (c *Client) GenerateVideo(ctx context.Context, token, engine, prompt, aspec
if engine == "firefly-video" {
endpoint = fireflyVideoSubmitURL
}
respBody, pollURL, err := c.submitVideo(ctx, client, token, endpoint, payload)
respBody, pollURL, err := c.submitVideo(ctx, sess, token, endpoint, payload)
if err != nil {
return nil, nil, err
}
_ = respBody
meta, data, pollErr := c.pollVideo(ctx, client, token, pollURL, downloadResult)
meta, data, pollErr := c.pollVideo(ctx, sess, token, pollURL, downloadResult)
if pollErr != nil {
return nil, nil, pollErr
}
@@ -235,7 +236,7 @@ func (c *Client) FetchAccountProfile(ctx context.Context, token string) (map[str
if token == "" {
return map[string]any{}, nil
}
client, err := c.newTLSClient()
sess, err := c.newTLSClient()
if err != nil {
return nil, err
}
@@ -249,7 +250,7 @@ func (c *Client) FetchAccountProfile(ctx context.Context, token string) (map[str
req.Header = http.Header{
"authorization": {"Bearer " + token},
"accept": {"application/json"},
"user-agent": {defaultUserAgent},
"user-agent": {sess.fp.userAgent},
http.HeaderOrderKey: {
"authorization",
"accept",
@@ -257,7 +258,7 @@ func (c *Client) FetchAccountProfile(ctx context.Context, token string) (map[str
},
}
resp, err := client.Do(req)
resp, err := sess.client.Do(req)
if err != nil {
continue
}
@@ -320,7 +321,7 @@ func (c *Client) FetchCreditsBalance(ctx context.Context, token string) (map[str
}, nil
}
client, err := c.newTLSClient()
sess, err := c.newTLSClient()
if err != nil {
return nil, err
}
@@ -335,7 +336,7 @@ func (c *Client) FetchCreditsBalance(ctx context.Context, token string) (map[str
"x-account-id": {accountID},
"accept": {"application/json"},
"content-type": {"application/json"},
"user-agent": {defaultUserAgent},
"user-agent": {sess.fp.userAgent},
http.HeaderOrderKey: {
"authorization",
"x-api-key",
@@ -346,7 +347,7 @@ func (c *Client) FetchCreditsBalance(ctx context.Context, token string) (map[str
},
}
resp, err := client.Do(req)
resp, err := sess.client.Do(req)
if err != nil {
return map[string]any{
"remaining": nil,
@@ -401,7 +402,7 @@ func (c *Client) FetchCreditsBalance(ctx context.Context, token string) (map[str
}, nil
}
func (c *Client) submitImage(ctx context.Context, client tlsclient.HttpClient, token, prompt, endpoint string, payload map[string]any) ([]byte, string, error) {
func (c *Client) submitImage(ctx context.Context, sess *tlsSession, token, prompt, endpoint string, payload map[string]any) ([]byte, string, error) {
body, err := json.Marshal(payload)
if err != nil {
return nil, "", err
@@ -419,13 +420,13 @@ func (c *Client) submitImage(ctx context.Context, client tlsclient.HttpClient, t
"origin": {"https://firefly.adobe.com"},
"referer": {"https://firefly.adobe.com/"},
"accept-language": {"en-US,en;q=0.9"},
"sec-ch-ua": {defaultSecCHUA},
"sec-ch-ua": {sess.fp.secCHUA},
"sec-ch-ua-mobile": {"?0"},
"sec-ch-ua-platform": {`"Windows"`},
"sec-ch-ua-platform": {sess.fp.platform},
"sec-fetch-site": {"same-site"},
"sec-fetch-mode": {"cors"},
"sec-fetch-dest": {"empty"},
"user-agent": {defaultUserAgent},
"user-agent": {sess.fp.userAgent},
"x-arp-session-id": {buildARPSessionID()},
http.HeaderOrderKey: {
"authorization",
@@ -450,7 +451,7 @@ func (c *Client) submitImage(ctx context.Context, client tlsclient.HttpClient, t
req.Header.Set("x-nonce", nonce)
}
resp, err := client.Do(req)
resp, err := sess.client.Do(req)
if err != nil {
return nil, "", fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
}
@@ -497,7 +498,7 @@ func (c *Client) submitImage(ctx context.Context, client tlsclient.HttpClient, t
return respBody, "", errors.New("submit ok but no poll url")
}
func (c *Client) pollImage(ctx context.Context, client tlsclient.HttpClient, token, pollURL string) (map[string]any, []byte, error) {
func (c *Client) pollImage(ctx context.Context, sess *tlsSession, token, pollURL string) (map[string]any, []byte, error) {
start := time.Now()
for {
if time.Since(start) > 3*time.Minute {
@@ -514,7 +515,7 @@ func (c *Client) pollImage(ctx context.Context, client tlsclient.HttpClient, tok
"accept": {"*/*"},
"origin": {"https://firefly.adobe.com"},
"referer": {"https://firefly.adobe.com/"},
"user-agent": {defaultUserAgent},
"user-agent": {sess.fp.userAgent},
http.HeaderOrderKey: {
"authorization",
"accept",
@@ -524,7 +525,7 @@ func (c *Client) pollImage(ctx context.Context, client tlsclient.HttpClient, tok
},
}
resp, err := client.Do(req)
resp, err := sess.client.Do(req)
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
}
@@ -551,7 +552,7 @@ func (c *Client) pollImage(ctx context.Context, client tlsclient.HttpClient, tok
if first, ok := outputs[0].(map[string]any); ok {
if image, ok := first["image"].(map[string]any); ok {
if url := strings.TrimSpace(stringValue(image["presignedUrl"])); url != "" {
data, err := c.download(ctx, client, url)
data, err := c.download(ctx, sess, url)
if err != nil {
return nil, nil, err
}
@@ -569,7 +570,7 @@ func (c *Client) pollImage(ctx context.Context, client tlsclient.HttpClient, tok
}
}
func (c *Client) submitVideo(ctx context.Context, client tlsclient.HttpClient, token, endpoint string, payload map[string]any) ([]byte, string, error) {
func (c *Client) submitVideo(ctx context.Context, sess *tlsSession, token, endpoint string, payload map[string]any) ([]byte, string, error) {
body, err := json.Marshal(payload)
if err != nil {
return nil, "", err
@@ -587,13 +588,13 @@ func (c *Client) submitVideo(ctx context.Context, client tlsclient.HttpClient, t
"origin": {"https://firefly.adobe.com"},
"referer": {"https://firefly.adobe.com/"},
"accept-language": {"en-US,en;q=0.9"},
"sec-ch-ua": {defaultSecCHUA},
"sec-ch-ua": {sess.fp.secCHUA},
"sec-ch-ua-mobile": {"?0"},
"sec-ch-ua-platform": {`"Windows"`},
"sec-ch-ua-platform": {sess.fp.platform},
"sec-fetch-site": {"same-site"},
"sec-fetch-mode": {"cors"},
"sec-fetch-dest": {"empty"},
"user-agent": {defaultUserAgent},
"user-agent": {sess.fp.userAgent},
"x-arp-session-id": {buildARPSessionID()},
http.HeaderOrderKey: {
"authorization",
@@ -621,7 +622,7 @@ func (c *Client) submitVideo(ctx context.Context, client tlsclient.HttpClient, t
}
}
resp, err := client.Do(req)
resp, err := sess.client.Do(req)
if err != nil {
return nil, "", fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
}
@@ -671,7 +672,7 @@ func (c *Client) submitVideo(ctx context.Context, client tlsclient.HttpClient, t
return respBody, "", errors.New("video submit ok but no poll url")
}
func (c *Client) pollVideo(ctx context.Context, client tlsclient.HttpClient, token, pollURL string, downloadResult bool) (map[string]any, []byte, error) {
func (c *Client) pollVideo(ctx context.Context, sess *tlsSession, token, pollURL string, downloadResult bool) (map[string]any, []byte, error) {
start := time.Now()
for {
if time.Since(start) > 10*time.Minute {
@@ -688,7 +689,7 @@ func (c *Client) pollVideo(ctx context.Context, client tlsclient.HttpClient, tok
"accept": {"*/*"},
"origin": {"https://firefly.adobe.com"},
"referer": {"https://firefly.adobe.com/"},
"user-agent": {defaultUserAgent},
"user-agent": {sess.fp.userAgent},
http.HeaderOrderKey: {
"authorization",
"accept",
@@ -698,7 +699,7 @@ func (c *Client) pollVideo(ctx context.Context, client tlsclient.HttpClient, tok
},
}
resp, err := client.Do(req)
resp, err := sess.client.Do(req)
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
}
@@ -732,7 +733,7 @@ func (c *Client) pollVideo(ctx context.Context, client tlsclient.HttpClient, tok
if !downloadResult {
return payload, nil, nil
}
data, err := c.download(ctx, client, raw)
data, err := c.download(ctx, sess, raw)
if err != nil {
return nil, nil, err
}
@@ -754,7 +755,7 @@ func (c *Client) pollVideo(ctx context.Context, client tlsclient.HttpClient, tok
// asset is already produced at this point, so a transient network hiccup
// (connection EOF/reset mid-body, S3 accelerate 5xx) must not fail the whole
// generation — retry with backoff before giving up.
func (c *Client) download(ctx context.Context, client tlsclient.HttpClient, url string) ([]byte, error) {
func (c *Client) download(ctx context.Context, sess *tlsSession, url string) ([]byte, error) {
var data []byte
var err error
for _, wait := range []time.Duration{0, 1 * time.Second, 2 * time.Second, 5 * time.Second, 10 * time.Second} {
@@ -765,7 +766,7 @@ func (c *Client) download(ctx context.Context, client tlsclient.HttpClient, url
case <-time.After(wait):
}
}
data, err = c.downloadOnce(ctx, client, url)
data, err = c.downloadOnce(ctx, sess, url)
if err == nil || !errors.Is(err, ErrTemporaryUpstream) {
break
}
@@ -773,7 +774,7 @@ func (c *Client) download(ctx context.Context, client tlsclient.HttpClient, url
return data, err
}
func (c *Client) downloadOnce(ctx context.Context, client tlsclient.HttpClient, url string) ([]byte, error) {
func (c *Client) downloadOnce(ctx context.Context, sess *tlsSession, url string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
@@ -781,13 +782,13 @@ func (c *Client) downloadOnce(ctx context.Context, client tlsclient.HttpClient,
req = req.WithContext(ctx)
req.Header = http.Header{
"accept": {"*/*"},
"user-agent": {defaultUserAgent},
"user-agent": {sess.fp.userAgent},
http.HeaderOrderKey: {
"accept",
"user-agent",
},
}
resp, err := client.Do(req)
resp, err := sess.client.Do(req)
if err != nil {
// Network-level failure (EOF, reset, timeout) — transient, worth a retry.
return nil, fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
@@ -808,20 +809,78 @@ func (c *Client) downloadOnce(ctx context.Context, client tlsclient.HttpClient,
return data, nil
}
func (c *Client) newTLSClient() (tlsclient.HttpClient, error) {
// fingerprint bundles a TLS ClientProfile with the browser headers that match
// it, so the JA3/JA4 handshake and the advertised User-Agent / client hints
// stay internally consistent within a single request.
type fingerprint struct {
profile profiles.ClientProfile
userAgent string
secCHUA string
platform string // quoted sec-ch-ua-platform value, e.g. `"Windows"`
}
const (
osWindows = iota
osMac
)
func newChromeFingerprint(profile profiles.ClientProfile, major, osKind int) fingerprint {
ua := fmt.Sprintf("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%d.0.0.0 Safari/537.36", major)
platform := `"Windows"`
if osKind == osMac {
ua = fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%d.0.0.0 Safari/537.36", major)
platform = `"macOS"`
}
return fingerprint{
profile: profile,
userAgent: ua,
secCHUA: fmt.Sprintf(`"Not?A_Brand";v="99", "Google Chrome";v="%d", "Chromium";v="%d"`, major, major),
platform: platform,
}
}
// adobeFingerprints is a small pool of recent desktop Chrome builds across
// Windows/macOS. Each request draws one at random so the outbound TLS
// fingerprint and headers vary from call to call instead of presenting a single
// static signature to Adobe's anti-bot layer.
var adobeFingerprints = []fingerprint{
newChromeFingerprint(profiles.Chrome_133, 133, osWindows),
newChromeFingerprint(profiles.Chrome_131, 131, osWindows),
newChromeFingerprint(profiles.Chrome_124, 124, osWindows),
newChromeFingerprint(profiles.Chrome_133, 133, osMac),
newChromeFingerprint(profiles.Chrome_131, 131, osMac),
}
func randomFingerprint() fingerprint {
return adobeFingerprints[rand.Intn(len(adobeFingerprints))]
}
// tlsSession pairs a configured TLS client with the fingerprint it was built
// for, so downstream header builders advertise the matching UA / client hints.
type tlsSession struct {
client tlsclient.HttpClient
fp fingerprint
}
func (c *Client) newTLSClient() (*tlsSession, error) {
fp := randomFingerprint()
options := []tlsclient.HttpClientOption{
tlsclient.WithTimeoutSeconds(60),
tlsclient.WithClientProfile(profiles.Chrome_133),
tlsclient.WithClientProfile(fp.profile),
tlsclient.WithNotFollowRedirects(),
tlsclient.WithRandomTLSExtensionOrder(),
}
if c.proxy != "" {
options = append(options, tlsclient.WithProxyUrl(c.proxy))
}
return tlsclient.NewHttpClient(tlsclient.NewNoopLogger(), options...)
client, err := tlsclient.NewHttpClient(tlsclient.NewNoopLogger(), options...)
if err != nil {
return nil, err
}
return &tlsSession{client: client, fp: fp}, nil
}
func exchangeCookieWithTLSClient(ctx context.Context, client tlsclient.HttpClient, cookie string) (*CookieExchangeResult, error) {
func exchangeCookieWithTLSClient(ctx context.Context, sess *tlsSession, cookie string) (*CookieExchangeResult, error) {
cookie = normalizeCookie(cookie)
if cookie == "" {
return nil, ErrAdobeCookieEmpty
@@ -840,7 +899,7 @@ func exchangeCookieWithTLSClient(ctx context.Context, client tlsclient.HttpClien
"cookie": {cookie},
"origin": {"https://firefly.adobe.com"},
"referer": {"https://firefly.adobe.com/"},
"user-agent": {defaultUserAgent},
"user-agent": {sess.fp.userAgent},
http.HeaderOrderKey: {
"accept",
"accept-language",
@@ -851,7 +910,7 @@ func exchangeCookieWithTLSClient(ctx context.Context, client tlsclient.HttpClien
"user-agent",
},
}
resp, err := client.Do(req)
resp, err := sess.client.Do(req)
if err != nil {
return nil, fmt.Errorf("adobe cookie exchange network error: %w", err)
}
-5
View File
@@ -12,11 +12,6 @@ import (
"github.com/google/uuid"
)
const (
defaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"
defaultSecCHUA = `"Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"`
)
func stringValue(v any) string {
switch x := v.(type) {
case string:
+18
View File
@@ -256,7 +256,23 @@ func (s *AdminReadService) Dashboard(ctx context.Context) (map[string]any, error
now := time.Now()
dayCut := now.Add(-24 * time.Hour)
weekCut := now.Add(-3 * 24 * time.Hour) // "week" key = last 3 days (per admin request)
// "today" = since local midnight (Asia/Shanghai, the app's business tz), not a
// rolling 24h — so 今日消耗/生成 resets at 00:00 the way the admin expects.
loc, lerr := time.LoadLocation("Asia/Shanghai")
if lerr != nil {
loc = time.Local
}
nowLocal := now.In(loc)
todayCut := time.Date(nowLocal.Year(), nowLocal.Month(), nowLocal.Day(), 0, 0, 0, 0, loc)
today, err := s.events.WindowStats(ctx, todayCut)
if err != nil {
return nil, err
}
todayDau, err := s.events.DistinctUsersSince(ctx, todayCut)
if err != nil {
return nil, err
}
day, err := s.events.WindowStats(ctx, dayCut)
if err != nil {
return nil, err
@@ -345,6 +361,8 @@ func (s *AdminReadService) Dashboard(ctx context.Context) (map[string]any, error
}
return map[string]any{
"today": today,
"today_dau": todayDau,
"day": day,
"week": week,
"prev_day_total": prevDay,