增加随机指纹
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user