diff --git a/.github/workflows/docker-hub-publish.yml b/.github/workflows/docker-hub-publish.yml index da51b4b..a068611 100644 --- a/.github/workflows/docker-hub-publish.yml +++ b/.github/workflows/docker-hub-publish.yml @@ -22,7 +22,7 @@ jobs: - name: Build the Docker image - run: docker build . --file Dockerfile -t mobilesecurity/mdast_cli:2026.03.02 -t mobilesecurity/mdast_cli:latest + run: docker build . --file Dockerfile -t mobilesecurity/mdast_cli:2026.6.1 -t mobilesecurity/mdast_cli:latest - name: Docker Hub push latest image @@ -30,7 +30,7 @@ jobs: - name: Docker Hub push tagged image - run: docker push mobilesecurity/mdast_cli:2026.03.02 + run: docker push mobilesecurity/mdast_cli:2026.6.1 diff --git a/mdast_cli/distribution_systems/appstore_client/store.py b/mdast_cli/distribution_systems/appstore_client/store.py index ef4348d..519a2ad 100755 --- a/mdast_cli/distribution_systems/appstore_client/store.py +++ b/mdast_cli/distribution_systems/appstore_client/store.py @@ -9,8 +9,28 @@ # Apple "bag" service: returns endpoint definitions (auth URL). Required since ~2025. BAG_URL_TEMPLATE = "https://init.itunes.apple.com/bag.xml?guid=%s" -DEFAULT_AUTH_URL_TEMPLATE = "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/authenticate?guid=%s" +# Apple's 26HOTFIX24 (June 2026) moved login to the native auth endpoint. The bag now +# advertises ".../auth/v1/native" (no "/fast"); the endpoint only works WITH the "/fast/" +# sub-path AND a trailing slash, otherwise Apple replies 301 + an HTML redirect page that +# breaks the plist parser. See majd/ipatool#486. +AUTH_HOST = "auth.itunes.apple.com" +NATIVE_FAST_PATH = "/auth/v1/native/fast/" +DEFAULT_AUTH_URL = "https://" + AUTH_HOST + NATIVE_FAST_PATH BUY_DOMAIN = "buy.itunes.apple.com" + + +def _normalize_auth_endpoint(endpoint: Optional[str]) -> Optional[str]: + """Ensure the native auth endpoint carries the '/fast/' sub-path with a trailing slash. + + Apple's bag advertises ".../auth/v1/native"; without "/fast/" the request gets a + 301 + HTML redirect that the plist parser chokes on (majd/ipatool#486). + """ + if endpoint and AUTH_HOST in endpoint: + if not (endpoint.endswith("/fast") or endpoint.endswith("/fast/")): + endpoint = endpoint.rstrip("/") + "/fast" + if not endpoint.endswith("/"): + endpoint = endpoint + "/" + return endpoint PURCHASE_PATH = "/WebObjects/MZBuy.woa/wa/buyProduct" DOWNLOAD_PATH = "/WebObjects/MZFinance.woa/wa/volumeStoreDownloadProduct" @@ -45,9 +65,14 @@ def _parse_bag_response(content: bytes) -> Optional[str]: try: data = plistlib.loads(content) if isinstance(data, dict): - url_bag = data.get("urlBag") or data.get("URLBag") - if isinstance(url_bag, dict): - return url_bag.get("authenticateAccount") or url_bag.get("authenticate") + # 'authenticateAccount' moved to the bag root (majd/ipatool#486); older bags + # keep it under 'urlBag'. Check the root first, then fall back to urlBag. + endpoint = data.get("authenticateAccount") or data.get("authenticate") + if not endpoint: + url_bag = data.get("urlBag") or data.get("URLBag") + if isinstance(url_bag, dict): + endpoint = url_bag.get("authenticateAccount") or url_bag.get("authenticate") + return endpoint return None except Exception: pass @@ -121,13 +146,13 @@ def get_bag(self) -> str: "Bag request failed: status=%s, falling back to default auth URL", r.status_code, ) - return DEFAULT_AUTH_URL_TEMPLATE % self.guid - auth_endpoint = _parse_bag_response(r.content) + return DEFAULT_AUTH_URL + auth_endpoint = _normalize_auth_endpoint(_parse_bag_response(r.content)) if auth_endpoint: logger.debug("Using auth endpoint from bag: %s", auth_endpoint[:60] + "...") return auth_endpoint logger.warning("Could not parse bag response, falling back to default auth URL") - return DEFAULT_AUTH_URL_TEMPLATE % self.guid + return DEFAULT_AUTH_URL def authenticate(self, appleId, password): if not self.guid: diff --git a/mdast_cli/distribution_systems/google_play_apkeep.py b/mdast_cli/distribution_systems/google_play_apkeep.py index dc3e87e..fc68084 100644 --- a/mdast_cli/distribution_systems/google_play_apkeep.py +++ b/mdast_cli/distribution_systems/google_play_apkeep.py @@ -111,7 +111,8 @@ async def download_app( if proc.returncode != 0: raise RuntimeError(sanitized_output_text.strip() or 'apkeep returned non-zero exit code') - if f'{package_name} downloaded successfully!' not in output: + success_marker_found = f'{package_name} downloaded successfully!' in output + if not success_marker_found: logger.warning(f'Google Play - success marker not found in output, continuing artifact check (package: {package_name})') split_dir = os.path.join(download_dir, package_name) @@ -279,8 +280,26 @@ async def download_app( except Exception as ex: logger.debug(f'Google Play - failed to scan directory for candidates: {ex} (package: {package_name})') - logger.error(f'Google Play - artifact not found after successful download (package: {package_name})') - raise RuntimeError('Google Play: download reports success but artifact not found') + if not success_marker_found: + # apkeep exited without printing the "downloaded successfully!" marker and produced no file. + # This is how Google Play behaves when it refuses to deliver the APK for the requested + # device profile: most commonly the app has no native build for the device's CPU ABI + # (e.g. requesting an x86_64 emulator profile for an arm64-only app), but it can also mean + # the app is unavailable for the account's region or is not acquired in its library. + # Not a real download failure on our side. + logger.error(f'Google Play - apkeep produced no success marker and no artifact; ' + f'Google Play did not deliver the app for the requested device profile ' + f'(likely CPU ABI/architecture mismatch, e.g. no x86_64 build for an arm64-only app; ' + f'or region/availability/acquisition) (package: {package_name})') + raise RuntimeError( + 'Google Play: apkeep finished without a success marker and produced no artifact - ' + 'Google Play did not deliver the app for the requested device profile. Most likely the app ' + 'has no native build for the requested CPU ABI (e.g. requesting x86_64 for an arm64-only app); ' + 'also check region/availability and that the app is acquired in the account library' + ) + + logger.error(f'Google Play - success marker present but artifact not found (package: {package_name})') + raise RuntimeError('Google Play: apkeep reported success but the artifact could not be located') def _ensure_dir_exists(path: str) -> None: diff --git a/setup.py b/setup.py index 10b6bb8..d0fccce 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="mdast_cli", - version='2026.03.02', + version='2026.6.1', python_requires='>=3.12',