diff --git a/backend/internal/provider/adobe/client.go b/backend/internal/provider/adobe/client.go index 4db0783..2deca64 100644 --- a/backend/internal/provider/adobe/client.go +++ b/backend/internal/provider/adobe/client.go @@ -58,9 +58,8 @@ var profileURLs = []string{ } type Client struct { - apiKey string - proxy string - arpSessionID string // cached per-client, reused across requests (matches adobe2api) + apiKey string + proxy string } func NewClient(apiKey, proxy string) *Client { @@ -70,17 +69,6 @@ func NewClient(apiKey, proxy string) *Client { } } -// getARPSessionID returns a cached ARP session id matching adobe2api's format: -// base64({"sid":"","ftr":"___dUAL43-mnts-ants-d4_31ck__tt"}) -// Generated once per client and reused — adobe2api reuses the same session id per -// token/profile instead of rotating every request. -func (c *Client) getARPSessionID() string { - if c.arpSessionID != "" { - return c.arpSessionID - } - c.arpSessionID = buildARPSessionID() - return c.arpSessionID -} func (c *Client) SetProxy(proxy string) { c.proxy = strings.TrimSpace(proxy) @@ -300,6 +288,7 @@ func (c *Client) FetchAccountProfile(ctx context.Context, token string) (map[str "user-agent", }, } + defer ReleasePID(token) resp, err := sess.client.Do(req) if err != nil { @@ -470,7 +459,7 @@ func (c *Client) submitImage(ctx context.Context, sess *tlsSession, token, promp "sec-fetch-mode": {"cors"}, "sec-fetch-dest": {"empty"}, "user-agent": {sess.fp.userAgent}, - "x-arp-session-id": {c.getARPSessionID()}, + "x-arp-session-id": {buildARPSessionID(token)}, http.HeaderOrderKey: { "authorization", "x-api-key", @@ -569,6 +558,7 @@ func (c *Client) pollImage(ctx context.Context, sess *tlsSession, token, pollURL "user-agent", }, } + defer ReleasePID(token) resp, err := sess.client.Do(req) if err != nil { @@ -648,7 +638,7 @@ func (c *Client) submitVideo(ctx context.Context, sess *tlsSession, token, endpo "sec-fetch-mode": {"cors"}, "sec-fetch-dest": {"empty"}, "user-agent": {sess.fp.userAgent}, - "x-arp-session-id": {c.getARPSessionID()}, + "x-arp-session-id": {buildARPSessionID(token)}, http.HeaderOrderKey: { "authorization", "x-api-key", @@ -753,6 +743,7 @@ func (c *Client) pollVideo(ctx context.Context, sess *tlsSession, token, pollURL "user-agent", }, } + defer ReleasePID(token) resp, err := sess.client.Do(req) if err != nil { diff --git a/backend/internal/provider/adobe/util.go b/backend/internal/provider/adobe/util.go index 3a58fcb..5672605 100644 --- a/backend/internal/provider/adobe/util.go +++ b/backend/internal/provider/adobe/util.go @@ -7,15 +7,23 @@ import ( "encoding/json" "math/big" "net/url" - "os" "regexp" "strconv" "strings" + "sync" "time" "github.com/google/uuid" ) +// arpPIDPool maps access tokens to unique PIDs so the same account always +// reuses its PID and different accounts never collide. Guarded by arpPIDMu. +var ( + arpPIDMu sync.Mutex + arpTokenPID = map[string]int{} // token → pid + arpPIDToken = map[int]string{} // pid → token +) + // adobeUserIDPat matches Adobe IMS user IDs embedded in cookies (e.g. // "4BDA81F069FC6DA40A495FAB@AdobeID"). var adobeUserIDPat = regexp.MustCompile(`[A-Fa-f0-9]{20,}@AdobeID`) @@ -96,12 +104,11 @@ func decodeJWTPayload(token string) map[string]any { return out } -func buildARPSessionID() string { +func buildARPSessionID(token string) string { // Matches adobe2api's format exactly: // base64({"sid":"","ftr":"___dUAL43-mnts-ants-d4_31ck__tt"}) // Two fields only (no "ark") — mirrors what a real browser session sends. - pid := os.Getpid() - ftr := randomHex(16) + "_" + strconv.FormatInt(time.Now().UnixMilli(), 10) + "_" + strconv.Itoa(pid) + "_dUAL43-mnts-ants-d4_31ck__tt" + ftr := randomHex(16) + "_" + strconv.FormatInt(time.Now().UnixMilli(), 10) + "_" + strconv.Itoa(allocPID(token)) + "_dUAL43-mnts-ants-d4_31ck__tt" raw := map[string]any{ "sid": uuid.NewString(), "ftr": ftr, @@ -110,6 +117,39 @@ func buildARPSessionID() string { return base64.StdEncoding.EncodeToString(b) } +// allocPID returns a unique PID bound to token. Same token always gets the +// same PID; different tokens never share a PID. Picks randomly from +// [1000, 99999] and retries on collision. +func allocPID(token string) int { + arpPIDMu.Lock() + defer arpPIDMu.Unlock() + + if pid, ok := arpTokenPID[token]; ok { + return pid + } + for { + pid := randomInt(1000, 99999) + if _, used := arpPIDToken[pid]; !used { + arpPIDToken[pid] = token + arpTokenPID[token] = pid + return pid + } + } +} + +// ReleasePID releases the PID bound to token so it can be reused by another +// account. Call this when a token/session is finished (e.g. after the Adobe +// API request completes or on token expiry). +func ReleasePID(token string) { + arpPIDMu.Lock() + defer arpPIDMu.Unlock() + + if pid, ok := arpTokenPID[token]; ok { + delete(arpPIDToken, pid) + delete(arpTokenPID, token) + } +} + func randomHex(n int) string { if n <= 0 { return ""