Fix/appstore auth legacy fallback - #149
Merged
npinaev merged 6 commits intoAug 26, 2026
Merged
Conversation
added 6 commits
August 25, 2026 18:39
…ль 2026) Apple перестала отвечать валидным plist на native-эндпоинте /auth/v1/native/fast/ — приходит пустое тело с плавающим статусом 204/301/403/404/503. Логин падал сразу, из-за чего не работали ни get_app_info, ни download_app. Рабочая последовательность (как в ipatool v2.3.2, PR majd/ipatool#514): native /auth/v1/native/fast/ -> 204/403/404/503 legacy MZFinance authenticate -> 302 Location: pN-buy.itunes.apple.com повторный POST того же plist на pod-URL -> 200 + plist Изменения: - перебор эндпоинтов: bag -> native -> legacy, с несколькими раундами и backoff (edge Apple отвечает нестабильно, логин может пройти не с первой попытки); - редирект на pod обрабатывается вручную: тело запроса переотправляется без изменений, attempt остаётся равным 1 (инкремент ломал запрос); - однократный ретрай при failureType -5000 на первой попытке; - pod определяется из заголовка pod либо из URL редиректа; - X-Apple-Store-Front выставляется только при наличии заголовка; - внятная диагностика: явно сообщаем, что отказ со стороны Apple/сети, а не проблема с учётными данными. Добавлены регрессионные тесты на весь сценарий (tests/test_appstore_auth.py).
…страиваются Наблюдения на живом трафике: edge Apple регулярно отдаёт голые 301/302 вообще без заголовка Location (ipatool падает на этом с "failed to retrieve redirect location"). Такой ответ считался нештатным и обрывал аутентификацию вместо перехода к следующему эндпоинту/раунду. - статусы редиректов, а также 500/502 добавлены в AUTH_FALLBACK_STATUSES; - бюджет ретраев вынесен в переменные окружения MDAST_APPSTORE_AUTH_ROUNDS / MDAST_APPSTORE_AUTH_BACKOFF — пока эндпоинт Apple нестабилен, эксплуатации нужно крутить его без правки кода.
…не проглатываются Главная причина, по которой ничего не скачивалось: buyProduct вызывался на пути /WebObjects/MZBuy.woa/wa/buyProduct. Apple отвечает на него HTTP 200, но с m-allowed=False и cancel-purchase-batch=True («Unable to process your request.») — для ЛЮБОГО приложения. Лицензия не создавалась никогда, а последующий volumeStoreDownloadProduct возвращал failureType 9610 («License not found»), что в логах выглядело как «Failed to get app download info! Check your parameters». Проверено на живом API (август 2026): тот же payload на /WebObjects/MZFinance.woa/wa/buyProduct возвращает jingleDocType=purchaseSuccess, status=0, после чего приложение скачивается. Этот же путь использует ipatool. - PURCHASE_PATH переведён на MZFinance; - purchase() разбирает plist и возвращает True (лицензия создана) / False (уже была), а при отказе бросает StoreException с сообщением Apple: раньше любой HTTP 200 логировался как «successfully purchased»; - download() больше не возвращает пустой ответ молча — failureType и customerMessage от Apple пробрасываются наружу; - при 9610 выполняется повторная покупка и один retry скачивания; - добавлены константы failureType (1008/2034/2042/2059/5002/9610); - get_app_info больше не делает login(force=True): принудительный логин удалял рабочий кэш сессии, а аутентификация у Apple сейчас нестабильна. +7 регрессионных тестов, всего 201 passed.
download_app при ЛЮБОЙ StoreException повторял попытку с login(force=True), а тот удаляет каталог с сохранённой сессией. Учитывая, что аутентификация у Apple сейчас проходит далеко не с первого раза, потеря рабочей сессии из-за посторонней ошибки (например, отсутствия лицензии) делала скачивание невосстановимым в рамках запуска. Повторный логин выполняется только если Apple сообщил именно о протухшей сессии (failureType 1008/2034/2042). Остальные ошибки пробрасываются с текстом от Apple вместо прежнего гадания «app_id не существует или приложение не было куплено».
…eType 5002 Два наблюдения на живом API (август 2026): 1. Apple троттлит эндпоинт аутентификации по ЧАСТОТЕ запросов, а не по их числу. 80 плотно идущих запросов дали 0 пригодных ответов, а один запрос после 120 секунд тишины залогинился сразу. Прежняя стратегия (интервал 1.5с)сама провоцировала блокировку. Backoff переведён на экспоненту 20 -> 40 -> 80 -> 150с с джиттером, раундов по умолчанию 8. Джиттер разводит параллельные запуски CLI, чтобы они не складывались в собственную пачку. Пределы настраиваются: MDAST_APPSTORE_AUTH_ROUNDS, MDAST_APPSTORE_AUTH_BACKOFF, MDAST_APPSTORE_AUTH_MAX_BACKOFF. 2. failureType 5002 контекстно-зависим. На buyProduct это «приложение уже куплено» (успех), а на volumeStoreDownloadProduct тем же кодом Apple сообщает о протухшей сессии, и лечится он только новым логином. Заведены отдельные наборы FAILURES_NEEDING_REAUTH и FAILURES_NEEDING_REAUTH_ON_DOWNLOAD; так же это трактует ipatool (majd/ipatool#468). Проверено сквозным прогоном CLI: --download_only --distribution_system appstore --appstore_app_id 389801252 -> Instagram-444.0.0.ipa, 484 МБ, exit code 0.
Замер на живом API: пять запросов подряд в одной сессии по двум приложениям — попытка 1 дала 5002 для обоих, попытки 2-4 прошли для обоих, попытка 5 снова дала 5002 для одного. Свежий логин (13 минут ожидания backoff) код 5002 НЕ лечит. То есть на volumeStoreDownloadProduct это случайный сбой Apple, а не протухшая сессия. Прежняя трактовка была взята из ipatool (#468) без проверки и обходилась дорого: принудительный логин удалял рабочую сессию, тратил минуты и всё равно завершался тем же 5002. - 5002 убран из FAILURES_NEEDING_REAUTH, заведён DOWNLOAD_RETRY_FAILURES; - _download_info_with_retries повторяет тот же запрос (5 попыток, пауза 6с), 9610 по-прежнему ведёт к покупке и повтору; - настраивается через MDAST_APPSTORE_DOWNLOAD_ATTEMPTS / _PAUSE. Также: sanitize_file_name для имени файла. Apple кладёт bidi-символы в bundleDisplayName (у WhatsApp это U+200E в начале), и путь, начинающийся с невидимого символа, тихо ломает всё ниже по конвейеру. Вырезаются категории Cc/Cf и разделители пути. Проверено сквозным прогоном: WhatsApp-26.33.73.ipa, 270 МБ, exit 0, в логе видно «failureType 5002 (attempt 1/5), retrying in 6s», имя файла начинается с 'W' (0x57), а не с 0xE2808E.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Проблема
Интеграция с App Store не работала полностью: не проходила аутентификация, а при
успешном логине скачивание всё равно падало с невнятным сообщением
Failed to get app download info! Check your parameters.Часть причин — на стороне Apple (июль 2026, ipatool сломался так же: majd/ipatool#513).
Часть — собственные дефекты, которые до сих пор маскировались отказом на этапе логина.
Причины
Все выводы получены измерениями на живом API Apple в августе 2026, а не из документации.
1. Логин сдавался на первом отказе Apple
Bag отдаёт native-эндпоинт
/auth/v1/native/fast/, который теперь отвечает204/301/403/404/503 с пустым, не-plist телом. Рабочая последовательность:
native /auth/v1/native/fast/ → 204/403/404/503
legacy MZFinance authenticate → 302 Location: pN-buy.itunes.apple.com/...?Pod=N
повторный POST того же plist на pod-URL → 200 + plist
Критично: при переотправке на pod-хост поле
attemptдолжно остаться равным1.Прежний код его инкрементировал, из-за чего Apple отклоняла запрос (majd/ipatool#514).
2. Покупка выполнялась не на том эндпоинте — главная причина
buyProductвызывался на/WebObjects/MZBuy.woa/wa/buyProduct. Apple отвечает на негоHTTP 200, но с
m-allowed=False,cancel-purchase-batch=Trueи «Unable to process yourrequest.» — для любого приложения. Лицензия не создавалась никогда, и следующий
volumeStoreDownloadProductвозвращалfailureType 9610(License not found).Проверка, один и тот же payload:
MZBuy.woa/wa/buyProduct(было)m-allowed=False, «Unable to process your request.»MZFinance.woa/wa/buyProduct(стало)jingleDocType=purchaseSuccess,status=03. Отказ Apple логировался как успех
Проверялся только HTTP-статус, поэтому в логе было «App was successfully purchased»
там, где Apple отказала. Теперь разбирается plist,
failureTypeиcustomerMessageпробрасываются наружу.
4.
login(force=True)уничтожал рабочую сессиюЕго вызывал
get_app_infoпри каждом обращении, аdownload_app— при любой ошибкеStore. При нынешней нестабильности аутентификации потеря сессии делала сбой
невосстановимым в рамках запуска.
5. Голые 301/302 без заголовка
LocationEdge Apple регулярно отдаёт их; такой ответ считался нештатным и обрывал логин
вместо перехода к следующему эндпоинту.
6.
failureType 5002трактовался как «протухшая сессия»Эта трактовка взята из ipatool#468 и на живом API не подтверждается. Замер — пять
запросов подряд в одной сессии по двум приложениям:
Свежий логин код 5002 не лечит (проверено: 13 минут ожидания backoff → тот же 5002).
На
volumeStoreDownloadProductэто случайный сбой Apple, лечится повтором того жезапроса за секунды. Прежнее поведение было хуже бездействия: тратило минуты на
бесполезный логин, удаляло рабочую сессию и всё равно падало.
Обратите внимание: на
buyProductтот же код 5002 означает «приложение уже куплено»(успех). Коды контекстно-зависимы и разведены на два набора.
7. Bidi-символы в имени файла
Apple кладёт управляющие символы в
bundleDisplayName— у WhatsApp это U+200E в начале.Символ попадал в имя файла:
00000000: e280 8e57 6861 7473 4170 70 ...
^^^^^^^^ U+200E LEFT-TO-RIGHT MARK
Путь, начинающийся с невидимого символа, тихо ломает всё ниже по конвейеру — передачу
в сканер, сравнение путей, скрипты CI. Внешне не видно ничего.
8. Несуществующие примеры в справке CLI
--appstore_bundle_idпредлагалcom.instagram.iosиcom.whatsapp.WhatsApp— обоихне существует. Заменены на реальные
com.burbn.instagramиnet.whatsapp.WhatsApp.Изменения
bag → native → legacyс ручной обработкой pod-редиректаи сохранением
attempt=1;AUTH_FALLBACK_STATUSES;PURCHASE_PATHпереведён на MZFinance;purchase()возвращаетTrue/Falseи бросает
StoreExceptionс текстом Apple при отказе;_download_info_with_retries: 9610 → покупка и повтор, 5002 → повтор запроса;failureType1008/2034/2042;sanitize_file_name()вырезает категории UnicodeCc/Cfи разделители пути;failureType(1008/2034/2042/2059/5002/9610).Настраивается через переменные окружения:
MDAST_APPSTORE_AUTH_ROUNDS,MDAST_APPSTORE_AUTH_BACKOFF,MDAST_APPSTORE_AUTH_MAX_BACKOFF,MDAST_APPSTORE_DOWNLOAD_ATTEMPTS,MDAST_APPSTORE_DOWNLOAD_PAUSE.Комментарии в коде фиксируют именно измерения, а не предположения — включая места,
где поведение расходится с ipatool.
Проверка
Сквозные прогоны с реальным скачиванием
.ipa:У всех проверены целостность ZIP, наличие
Payload/,.sinfиiTunesMetadata.plist.Тесты: 208 passed, из них 26 новых в
tests/test_appstore_auth.py—на pod-редирект с
attempt=1, фоллбэк эндпоинтов, отличие 5002 на purchase и download,докупку лицензии при 9610 и очистку имени файла.
Изменено: 4 файла, +595 / −117.
Известное ограничение
Первый логин на холодную остаётся вероятностным: в замерах он занимал от 2 секунд до
5 минут, а один раз не прошёл за 6 минут вовсе. Это поведение самой Apple — эталонный
ipatool v2.3.2 отказывает в тех же условиях. Код переживает такие отказы и повторяет
попытки, но гарантировать проход с первой попытки нельзя. Для CI стоит закладывать
таймаут с запасом и не считать единичный отказ логина регрессией.