From 437940091130a07ad7ea4185953e06de0fbdc697 Mon Sep 17 00:00:00 2001
From: yuhao7370 <53969724+yuhao7370@users.noreply.github.com>
Date: Fri, 27 Feb 2026 19:47:48 +0800
Subject: [PATCH] Fix auth flow with bag endpoint and pod hosts
---
main.py | 4 +-
reqs/store.py | 101 ++++++++++++++++++++++++++++++++++++++++++++------
2 files changed, 91 insertions(+), 14 deletions(-)
diff --git a/main.py b/main.py
index 82fdae3..4165eac 100755
--- a/main.py
+++ b/main.py
@@ -300,7 +300,7 @@ def authedPost(*args, **kwargs):
r = Store.sess.original_post(*args, **kwargs)
isAuthFail = False
try:
- d = plistlib.loads(r.content)
+ d = parse_plist_payload(r.content)
if str(d['failureType']) in ("2034", "1008"):
isAuthFail = True
except:
@@ -565,4 +565,4 @@ def main():
tool.tool_main()
if __name__ == '__main__':
- main()
\ No newline at end of file
+ main()
diff --git a/reqs/store.py b/reqs/store.py
index 2672c8f..1774159 100755
--- a/reqs/store.py
+++ b/reqs/store.py
@@ -2,8 +2,8 @@
import json
import pickle
import plistlib
+import re
import requests
-from reqs.schemas.store_authenticate_req import StoreAuthenticateReq
from reqs.schemas.store_authenticate_resp import StoreAuthenticateResp
from reqs.schemas.store_buyproduct_req import StoreBuyproductReq
from reqs.schemas.store_buyproduct_resp import StoreBuyproductResp
@@ -23,12 +23,56 @@ def __init__(self, req, resp, errMsg, errType=None):
#CONFIGURATOR_UA = "Configurator/2.0 (Macintosh; OS X 10.12.6; 16G29) AppleWebKit/2603.3.8"
CONFIGURATOR_UA = 'Configurator/2.0 (Macintosh; OS X 10.12.6; 16G29) AppleWebKit/2603.3.8 iOS/14.2 hwp/t8020'
+LEGACY_AUTH_ENDPOINT = "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/authenticate"
+INIT_BAG_ENDPOINT = "https://init.itunes.apple.com/bag.xml?guid=%s"
+APPSTORE_HOST = "buy.itunes.apple.com"
+APPSTORE_DOWNLOAD_PATH = "/WebObjects/MZFinance.woa/wa/volumeStoreDownloadProduct"
+APPSTORE_PURCHASE_PATH = "/WebObjects/MZFinance.woa/wa/buyProduct"
+
+DOCUMENT_XML_PATTERN = re.compile(br'(?is)]*>(.*)')
+PLIST_XML_PATTERN = re.compile(br'(?is)]*>.*?')
+DICT_XML_PATTERN = re.compile(br'(?is)]*>.*')
+
+def _wrap_dict_as_plist(dict_body: bytes) -> bytes:
+ return b"" + dict_body + b""
+
+def _normalize_plist_payload(body: bytes) -> bytes:
+ normalized = body.strip()
+ if not normalized:
+ return normalized
+
+ document_match = DOCUMENT_XML_PATTERN.search(normalized)
+ if document_match and document_match.group(1).strip():
+ normalized = document_match.group(1).strip()
+
+ plist_match = PLIST_XML_PATTERN.search(normalized)
+ if plist_match:
+ normalized = plist_match.group(0).strip()
+
+ dict_match = DICT_XML_PATTERN.search(normalized)
+ if dict_match:
+ return _wrap_dict_as_plist(dict_match.group(0).strip())
+
+ if b"" in normalized:
+ return _wrap_dict_as_plist(b"" + normalized + b"")
+
+ return normalized
+
+def parse_plist_payload(body: bytes):
+ try:
+ return plistlib.loads(body)
+ except Exception:
+ normalized = _normalize_plist_payload(body)
+ if normalized != body:
+ return plistlib.loads(normalized)
+ raise
class StoreClientAuth(object):
def __init__(self, appleId=None, password=None):
self.appleId = appleId
self.password = password
self.guid = None # the guid will not be used in itunes server mode
+ self.pod = None
self.accountName = None
self.authHeaders = None
self.authCookies = None
@@ -57,25 +101,51 @@ def _generateGuid(self, appleId):
guid = (defaultPart + hashPart).upper()
return guid
+ def _resolve_auth_endpoint(self, sess):
+ try:
+ r = sess.get(INIT_BAG_ENDPOINT % self.guid,
+ headers={
+ "Accept": "application/xml",
+ "User-Agent": CONFIGURATOR_UA,
+ },
+ timeout=15.0)
+ r.raise_for_status()
+ d = parse_plist_payload(r.content)
+ if isinstance(d, dict):
+ urlBag = d.get('urlBag')
+ if isinstance(urlBag, dict):
+ endpoint = urlBag.get('authenticateAccount')
+ if endpoint:
+ return endpoint
+ except Exception:
+ pass
+ return LEGACY_AUTH_ENDPOINT
+
def login(self, sess):
if not self.guid:
self.guid = self._generateGuid(self.appleId)
- req = StoreAuthenticateReq(appleId=self.appleId, password=self.password, attempt='4', createSession="true",
- guid=self.guid, rmp='0', why='signIn')
- url = "https://p46-buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/authenticate?guid=%s" % self.guid
+ req = {
+ "appleId": self.appleId,
+ "password": self.password,
+ "attempt": "4",
+ "guid": self.guid,
+ "rmp": "0",
+ "why": "signIn",
+ }
+ url = self._resolve_auth_endpoint(sess)
while True:
r = sess.post(url,
headers={
"Accept": "*/*",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": CONFIGURATOR_UA,
- }, data=plistlib.dumps(req.as_dict()), allow_redirects=False)
+ }, data=plistlib.dumps(req), allow_redirects=False)
if r.status_code == 302:
url = r.headers['Location']
continue
break
- d = plistlib.loads(r.content)
+ d = parse_plist_payload(r.content)
resp = StoreAuthenticateResp.from_dict(d)
if not resp.m_allowed:
raise StoreException("authenticate", d, resp.customerMessage, resp.failureType)
@@ -84,6 +154,7 @@ def login(self, sess):
self.authHeaders['X-Dsid'] = self.authHeaders['iCloud-Dsid'] = str(resp.download_queue_info.dsid)
self.authHeaders['X-Apple-Store-Front'] = r.headers.get('x-set-apple-store-front')
self.authHeaders['X-Token'] = resp.passwordToken
+ self.pod = r.headers.get('pod')
self.authCookies = pickle.dumps(sess.cookies).hex()
self.accountName = resp.accountInfo.address.firstName + " " + resp.accountInfo.address.lastName
@@ -103,6 +174,12 @@ def __init__(self, sess: requests.Session):
self.iTunes_provider = None
self.authInfo = None
+ def _get_appstore_domain(self):
+ pod = self.authInfo.pod if self.authInfo else None
+ if pod:
+ return f"p{pod}-{APPSTORE_HOST}"
+ return APPSTORE_HOST
+
def authenticate_load_session(self, sessionContent):
self.authInfo = StoreClientAuth.load(sessionContent)
if self.authInfo.authHeaders is None or self.authInfo.authCookies is None:
@@ -145,12 +222,12 @@ def volumeStoreDownloadProduct(self, appId, appVerId=""):
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": CONFIGURATOR_UA,
}
- url = "https://p25-buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/volumeStoreDownloadProduct?guid=%s" % self.authInfo.guid
+ url = "https://%s%s?guid=%s" % (self._get_appstore_domain(), APPSTORE_DOWNLOAD_PATH, self.authInfo.guid)
payload = req.as_dict()
r = self.sess.post(url,
headers=hdrs,
data=plistlib.dumps(payload))
- d = plistlib.loads(r.content)
+ d = parse_plist_payload(r.content)
resp = StoreDownloadResp.from_dict(d)
if resp.cancel_purchase_batch:
raise StoreException("volumeStoreDownloadProduct", d, resp.customerMessage, '%s-%s' % (resp.failureType, resp.metrics))
@@ -200,14 +277,14 @@ def buyProduct(self, appId, appVer='', productType='C', pricingParameters='STDQ'
data=plistlib.dumps(payload)
)
- d = plistlib.loads(r.content)
+ d = parse_plist_payload(r.content)
resp = StoreBuyproductResp.from_dict(d)
if resp.cancel_purchase_batch:
raise StoreException("buyProduct", d, resp.customerMessage, '%s-%s' % (resp.failureType, resp.metrics))
return resp
def buyProduct_purchase(self, appId, productType='C'):
- url = "https://buy.itunes.apple.com/WebObjects/MZBuy.woa/wa/buyProduct"
+ url = "https://%s%s" % (self._get_appstore_domain(), APPSTORE_PURCHASE_PATH)
req = StoreBuyproductReq(
guid=self.authInfo.guid,
salableAdamId=str(appId),
@@ -234,7 +311,7 @@ def buyProduct_purchase(self, appId, productType='C'):
if r.status_code == 500:
raise StoreException("buyProduct_purchase", None, 'purchased_before')
- d = plistlib.loads(r.content)
+ d = parse_plist_payload(r.content)
resp = StoreBuyproductResp.from_dict(d)
if resp.status != 0 or resp.jingleDocType != 'purchaseSuccess':
raise StoreException("buyProduct_purchase", d, resp.customerMessage,
@@ -251,4 +328,4 @@ def download(self, appId, appVer='', isRedownload=True):
if self.iTunes_provider:
return self.buyProduct(appId, appVer, pricingParameters='STDRDL' if isRedownload else 'STDQ')
else:
- return self.volumeStoreDownloadProduct(appId, appVer)
\ No newline at end of file
+ return self.volumeStoreDownloadProduct(appId, appVer)