Skip to content

Fix/appstore auth legacy fallback - #149

Merged
npinaev merged 6 commits into
Dynamic-Mobile-Security:mainfrom
SergeyOstrouhov:fix/appstore-auth-legacy-fallback
Aug 26, 2026
Merged

Fix/appstore auth legacy fallback#149
npinaev merged 6 commits into
Dynamic-Mobile-Security:mainfrom
SergeyOstrouhov:fix/appstore-auth-legacy-fallback

Conversation

@SergeyOstrouhov

Copy link
Copy Markdown
Contributor

Проблема

Интеграция с 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 your
request.» — для любого приложения. Лицензия не создавалась никогда, и следующий
volumeStoreDownloadProduct возвращал failureType 9610 (License not found).

Проверка, один и тот же payload:

Путь Ответ Apple
MZBuy.woa/wa/buyProduct (было) m-allowed=False, «Unable to process your request.»
MZFinance.woa/wa/buyProduct (стало) jingleDocType=purchaseSuccess, status=0

3. Отказ Apple логировался как успех

Проверялся только HTTP-статус, поэтому в логе было «App was successfully purchased»
там, где Apple отказала. Теперь разбирается plist, failureType и customerMessage
пробрасываются наружу.

4. login(force=True) уничтожал рабочую сессию

Его вызывал get_app_info при каждом обращении, а download_app — при любой ошибке
Store. При нынешней нестабильности аутентификации потеря сессии делала сбой
невосстановимым в рамках запуска.

5. Голые 301/302 без заголовка Location

Edge Apple регулярно отдаёт их; такой ответ считался нештатным и обрывал логин
вместо перехода к следующему эндпоинту.

6. failureType 5002 трактовался как «протухшая сессия»

Эта трактовка взята из ipatool#468 и на живом API не подтверждается. Замер — пять
запросов подряд в одной сессии по двум приложениям:

Попытка App A App B
1 5002 5002
2–4 ok ok
5 5002 ok

Свежий логин код 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;
  • статусы редиректов и 500/502 добавлены в AUTH_FALLBACK_STATUSES;
  • экспоненциальный backoff между раундами (20 → 40 → 80 → 150 с) с джиттером;
  • PURCHASE_PATH переведён на MZFinance; purchase() возвращает True/False
    и бросает StoreException с текстом Apple при отказе;
  • _download_info_with_retries: 9610 → покупка и повтор, 5002 → повтор запроса;
  • принудительный релогин только при failureType 1008/2034/2042;
  • sanitize_file_name() вырезает категории Unicode Cc/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:

Приложение Размер Способ
Facebook 375 МБ Python API
Instagram 484 МБ CLI
Instagram 462 МБ CLI, независимый запуск
WhatsApp 270 МБ CLI, через ретрай 5002

У всех проверены целостность 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 стоит закладывать
таймаут с запасом и не считать единичный отказ логина регрессией.

Sergey Ostroukhov 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.
@npinaev
npinaev merged commit 5d31aa2 into Dynamic-Mobile-Security:main Aug 26, 2026
1 check passed
@SergeyOstrouhov
SergeyOstrouhov deleted the fix/appstore-auth-legacy-fallback branch August 26, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants