Files
image2api/backend/internal/provider/adobe/arp_shape_test.go
T
chiyiandDevin ae49b95e0f fix(adobe): 优先使用 cookie 里的 sherlockToken 作 arp,补齐会话一致性
- ExchangeCookie 提取 cookie 中 sherlockToken/forter/arkose/bfp/sid 重组
  arp 并按 token 缓存;无 ims_sid 的游客 token 直接拒绝,不再入池
- getARPSessionID 复用缓存 arp 并刷新 ftr 时间戳;sid/devHex/bfp 按
  token 保持稳定,不再每次重随机
- 新增 ARPFromCookie/refreshARPTimestamp 及形状单测

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-14 12:18:05 +08:00

163 lines
5.3 KiB
Go

package adobe
import (
"encoding/base64"
"encoding/json"
"regexp"
"strings"
"testing"
)
// captured from live firefly.adobe.com traffic:
//
// ark: 91818c89a54748463.1048135404|r=ap-southeast-1|…|rid=84|ag=101|…
// ftr: 40d7ba7c70ff478e9c212073ec8254c4_1791795834777_27064_UDF43-mnf-a4_31ck_s/Clo68HDL4=-3629-v2_tt
//
// The Arkose slot is deliberately emitted empty rather than synthesized — see
// buildARPSessionID — so the expected ftr ends in "_31ck__tt".
var (
arkPat = regexp.MustCompile(`^[0-9a-f]{17}\.[1-9][0-9]{9}\|r=ap-southeast-1\|.*\|rid=[0-9]{1,2}\|ag=101\|`)
ftrPat = regexp.MustCompile(`^[0-9a-f]{32}_[0-9]{13}_[0-9]{4,5}_UDF43-mnf-a4_31ck__tt$`)
)
func TestARPSessionIDShape(t *testing.T) {
raw, err := base64.StdEncoding.DecodeString(buildARPSessionID("tok-a"))
if err != nil {
t.Fatalf("not base64: %v", err)
}
var got struct{ Sid, Ark, Ftr, Bfp string }
if err := json.Unmarshal(raw, &got); err != nil {
t.Fatalf("not json: %v", err)
}
if len(got.Bfp) != 36 {
t.Errorf("bfp not a uuid: %q", got.Bfp)
}
if !arkPat.MatchString(got.Ark) {
t.Errorf("ark shape mismatch:\n%s", got.Ark)
}
if !ftrPat.MatchString(got.Ftr) {
t.Errorf("ftr shape mismatch:\n%s", got.Ftr)
}
if len(got.Sid) != 36 {
t.Errorf("sid not a uuid: %q", got.Sid)
}
}
// ark must differ per call — a frozen blob is a cross-account correlation key.
func TestARKVariesAcrossCalls(t *testing.T) {
if buildARKBlob() == buildARKBlob() {
t.Error("ark is constant across calls")
}
}
// pid is stable per token but distinct across tokens.
func TestPIDStablePerToken(t *testing.T) {
defer ReleasePID("tok-x")
defer ReleasePID("tok-y")
if allocPID("tok-x") != allocPID("tok-x") {
t.Error("pid changed for the same token")
}
if allocPID("tok-x") == allocPID("tok-y") {
t.Error("two tokens share a pid")
}
}
// A real browser keeps sid / devHex / bfp for the whole session; only the ftr
// timestamp advances. Re-randomizing them per call is a bot signal.
func TestIdentityStablePerToken(t *testing.T) {
decode := func(token string) map[string]string {
raw, err := base64.StdEncoding.DecodeString(buildARPSessionID(token))
if err != nil {
t.Fatalf("not base64: %v", err)
}
var m map[string]string
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatalf("not json: %v", err)
}
return m
}
a, b := decode("tok-s"), decode("tok-s")
if a["sid"] != b["sid"] || a["bfp"] != b["bfp"] {
t.Error("sid/bfp changed across calls for the same token")
}
if a["ftr"][:39] != b["ftr"][:39] {
t.Error("ftr device hex changed across calls for the same token")
}
}
// CacheARP stores a cookie-derived arp which getARPSessionID prefers over
// synthesis.
func TestCacheARPPreferred(t *testing.T) {
c := NewClient("", "")
CacheARP("tok-c", "eyJjb29raWUiOiJhYXJkZWYa")
if got := c.getARPSessionID("tok-c"); got != "eyJjb29raWUiOiJhYXJkZWYa" {
t.Errorf("cached arp not preferred: %q", got)
}
}
// refreshARPTimestamp bumps only the ftr timestamp segment; devHex, pid, the
// Arkose slot, and every other payload field pass through untouched.
func TestRefreshARPTimestamp(t *testing.T) {
mk := func(ftr string) string {
b, _ := json.Marshal(map[string]string{
"sid": "4e54df9a-79cf-4569-b40d-fa793e38b727",
"ftr": ftr,
"ark": "ark_blob",
"bfp": "bf",
})
return base64.StdEncoding.EncodeToString(b)
}
old := "40d7ba7c70ff478e9c212073ec8254c4_1791795834777_27064_UDF43-mnf-a4_31ck__tt"
raw, err := base64.StdEncoding.DecodeString(refreshARPTimestamp(mk(old)))
if err != nil {
t.Fatalf("refreshed arp not base64: %v", err)
}
var m map[string]string
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatalf("not json: %v", err)
}
parts := strings.Split(m["ftr"], "_")
if len(parts) != 7 {
t.Fatalf("ftr segment count changed: %q", m["ftr"])
}
if parts[0] != "40d7ba7c70ff478e9c212073ec8254c4" || parts[2] != "27064" || parts[4] != "31ck" || parts[6] != "tt" {
t.Errorf("ftr fields mangled: %q", m["ftr"])
}
if parts[1] == "1791795834777" {
t.Error("ftr timestamp not bumped")
}
if m["sid"] != "4e54df9a-79cf-4569-b40d-fa793e38b727" || m["ark"] != "ark_blob" || m["bfp"] != "bf" {
t.Errorf("non-ftr fields mangled: %v", m)
}
// malformed inputs pass through unchanged
if got := refreshARPTimestamp("!!!not-base64"); got != "!!!not-base64" {
t.Errorf("garbage input changed: %q", got)
}
if got := refreshARPTimestamp(mk("too_short")); got != mk("too_short") {
t.Error("short ftr changed")
}
}
// ARPFromCookie returns the sherlockToken verbatim when present, and rebuilds
// the JSON from component cookies otherwise.
func TestARPFromCookie(t *testing.T) {
if got := ARPFromCookie("a=1; sherlockToken=eyJhIjoxfQ==; b=2"); got != "eyJhIjoxfQ==" {
t.Errorf("sherlockToken not returned verbatim: %q", got)
}
if got := ARPFromCookie("a=1"); got != "" {
t.Errorf("expected empty for cookie without arp pieces, got %q", got)
}
raw, err := base64.StdEncoding.DecodeString(ARPFromCookie(
"ff_session_guid=4e54df9a-79cf-4569-b40d-fa793e38b727; forterToken=ft_x%3D-v2; arkose=ark_blob; bfp=bf"))
if err != nil {
t.Fatalf("reassembled arp not base64: %v", err)
}
var m map[string]string
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatalf("not json: %v", err)
}
if m["sid"] != "4e54df9a-79cf-4569-b40d-fa793e38b727" || m["ftr"] != "ft_x=-v2" || m["ark"] != "ark_blob" || m["bfp"] != "bf" {
t.Errorf("reassembled arp mismatch: %v", m)
}
}