diff --git a/Dockerfile b/Dockerfile index dd67332..b80ff26 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,16 @@ FROM debian:bookworm-slim # hfsprogs (mkfs.hfsplus) liegt in Debians "non-free"-Bereich, das offizielle -# bookworm-slim-Image aktiviert davon aber nur "main". Eigene sources.list.d -# Datei ergaenzt non-free, unabhaengig davon ob die Basis-Sources im alten -# Ein-Zeilen- oder im neuen deb822-Format vorliegen (apt liest beide parallel). -RUN echo "deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware" \ - > /etc/apt/sources.list.d/non-free.list +# bookworm-slim-Image aktiviert davon aber nur "main". WICHTIG: bookworm-slim +# nutzt seit einiger Zeit das neue deb822-Format (/etc/apt/sources.list.d/ +# debian.sources) und deklariert dort bereits "Signed-By" fuer deb.debian.org. +# Eine ZUSAETZLICHE separate sources.list-Datei fuer dieselbe URI ohne +# Signed-By fuehrt zu "Conflicting values set for option Signed-By" und +# bricht apt-get update komplett ab (live getestet). Deshalb hier die +# bestehende deb822-Datei in-place um contrib/non-free erweitern statt eine +# zweite Quelle anzulegen. +RUN sed -i 's/^Components: main$/Components: main contrib non-free non-free-firmware/' \ + /etc/apt/sources.list.d/debian.sources RUN apt-get update && apt-get install -y --no-install-recommends \ python3 python3-pip \ @@ -21,24 +26,36 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl \ && rm -rf /var/lib/apt/lists/* -# pbzx: entpackt Apples pbzx-komprimierte Installer-Payloads (macOS Big Sur+). -# Bewusst aus dem gepflegten Upstream-Repo gebaut statt selbst nachgebaut -- -# das Binaerformat ist reverse-engineered und ein fertiges, getestetes Tool -# ist zuverlaessiger als eine selbstgeschriebene Parser-Vermutung. -RUN git clone --depth 1 https://github.com/mackyle/pbzx.git /tmp/pbzx \ - && cd /tmp/pbzx && make && install -m 755 pbzx /usr/local/bin/pbzx \ - && rm -rf /tmp/pbzx - # xar: existiert als Debian-Paket erst ab trixie (bookworm hat es gar nicht # im Archiv, auch nicht in non-free). Statt die ganze Basis-Distro zu wechseln -# (Risiko fuer alle anderen Pakete) bauen wir es aus dem selben, von Apple- -# Tooling-Communities genutzten Upstream-Fork wie pbzx (gleicher Maintainer). +# (Risiko fuer alle anderen Pakete) bauen wir es aus dem mackyle/xar-Upstream. +# MUSS vor pbzx gebaut werden -- pbzx linkt gegen -lxar und xar/xar.h. +# Live getestet: configure.ac prüft die Existenz von OpenSSL via +# AC_CHECK_LIB([crypto], [OpenSSL_add_all_ciphers]) -- diese Funktion ist in +# OpenSSL 3.x (Debian bookworm) nur noch ein no-op-Praeprozessor-Makro in +# evp.h, kein linkbares Symbol mehr, der Autoconf-Test schlaegt daher fehl +# ("Cannot build without libcrypto"), obwohl libssl-dev korrekt installiert +# ist. Fix: den Probe-Funktionsnamen per sed auf EVP_EncryptInit umbiegen -- +# eine seit jeher stabil exportierte Funktion, verifiziert per nm gegen +# libcrypto.so.3 in genau diesem Image. RUN git clone --depth 1 https://github.com/mackyle/xar.git /tmp/xar-src \ && cd /tmp/xar-src/xar \ + && sed -i 's/OpenSSL_add_all_ciphers/EVP_EncryptInit/g' configure.ac \ && ./autogen.sh && ./configure && make && make install \ && ldconfig \ && rm -rf /tmp/xar-src +# pbzx: entpackt Apples pbzx-komprimierte Installer-Payloads (macOS Big Sur+). +# WICHTIG: mackyle/pbzx existiert nicht (mehr) auf GitHub (404, live geprueft +# -- git meldet dafuer irrefuehrend "could not read Username", weil GitHub +# bei git-upload-pack aus Anti-Enumeration-Gruenden 401 statt 404 zurueckgibt). +# Community-Standard ist der Fork NiklasRosenstein/pbzx (u.a. in OSX-KVM +# genutzt), live gegen GitHub verifiziert und hier gegen das zuvor gebaute +# xar gelinkt. +RUN git clone --depth 1 https://github.com/NiklasRosenstein/pbzx.git /tmp/pbzx \ + && cd /tmp/pbzx && make && install -m 755 pbzx /usr/local/bin/pbzx \ + && rm -rf /tmp/pbzx + WORKDIR /app COPY backend/requirements.txt /app/backend/requirements.txt RUN pip3 install --no-cache-dir --break-system-packages -r /app/backend/requirements.txt diff --git a/README.md b/README.md index 1c47579..a6b9bb4 100644 --- a/README.md +++ b/README.md @@ -69,13 +69,71 @@ Block-Device. Wenn du das falsche Geraet auswaehlst, sind die Daten weg. Vor dem Start immer pruefen, welcher Stick tatsaechlich gemeint ist (Groesse und Modell in der GUI vergleichen). -## Build-Hinweis (hfsprogs / xar) -`hfsprogs` liegt in Debians non-free-Bereich (Dockerfile aktiviert das -gezielt ueber eine eigene sources.list.d-Datei), `xar` gibt es in -`bookworm` ueberhaupt nicht als Paket — das wird deshalb aus dem -Upstream-Repo (`mackyle/xar`, selber Maintainer wie das schon genutzte -`pbzx`) selbst gebaut. Macht den ersten Build minimal langsamer, aber -zuverlaessiger als ein Distro-Wechsel. +## Build-Hinweis (hfsprogs / xar / pbzx) +Alles unten wurde am 2026-07-18 live gegen einen echten `docker compose build` ++ `up` auf einem echten Docker-Host getestet (nicht nur behauptet): + +- `hfsprogs` liegt in Debians non-free-Bereich. WICHTIG: `bookworm-slim` + nutzt das neue **deb822**-Sourcen-Format (`/etc/apt/sources.list.d/ + debian.sources` statt der alten `sources.list`). Eine zusaetzliche + separate `sources.list`-Datei fuer dieselbe `deb.debian.org`-URI OHNE + `Signed-By` fuehrt zu `Conflicting values set for option Signed-By` und + bricht `apt-get update` komplett ab. Der Dockerfile patcht deshalb die + bestehende `debian.sources` per `sed` (Components-Zeile um + `contrib non-free non-free-firmware` erweitert) statt eine zweite Quelle + anzulegen. +- `xar` gibt es in `bookworm` ueberhaupt nicht als Paket, wird aus dem + Upstream-Repo `mackyle/xar` gebaut. Dessen `configure.ac` prueft + OpenSSL ueber `AC_CHECK_LIB([crypto], [OpenSSL_add_all_ciphers])` -- + diese Funktion ist in OpenSSL 3.x (Debian bookworm) nur noch ein + no-op-Praeprozessor-Makro in `evp.h`, kein linkbares Symbol mehr. Der + Autoconf-Test schlaegt deshalb fehl ("Cannot build without libcrypto"), + obwohl `libssl-dev` korrekt installiert ist. Fix: der Dockerfile biegt + den Probe-Funktionsnamen per `sed` auf `EVP_EncryptInit` um (gegen + `libcrypto.so.3` per `nm` verifiziert, seit jeher stabil exportiert). +- `xar` MUSS vor `pbzx` gebaut werden -- `pbzx` linkt gegen `-lxar` und + `xar/xar.h`. +- `pbzx`: das Repo `mackyle/pbzx` existiert nicht (mehr) auf GitHub (404). + Git meldet das irrefuehrend als `could not read Username for + 'https://github.com'`, weil GitHub bei `git-upload-pack`-Requests gegen + nichtexistente/private Repos aus Anti-Enumeration-Gruenden 401 statt 404 + zurueckgibt. Fix: Community-Standard-Fork + [`NiklasRosenstein/pbzx`](https://github.com/NiklasRosenstein/pbzx) + (u.a. in OSX-KVM genutzt) wird stattdessen geklont. + +Macht den ersten Build minimal langsamer, aber zuverlaessiger als ein +Distro-Wechsel. + +## Katalog-Bugs (gefunden + gefixt, 2026-07-18) +Zwei echte Bugs in `backend/catalog.py`, beide live gegen den echten +Apple-Katalog (`swscan.apple.com`) reproduziert und verifiziert: + +1. **Doppeltes Entpacken:** Apples Server schickt den Katalog mit + `Content-Encoding: x-gzip`. `requests`/`urllib3` dekomprimiert das + bereits TRANSPARENT -- der Code rief zusaetzlich `gzip.decompress()` + drauf auf, was mit `BadGzipFile: Not a gzipped file` crashte (die + Gzip-Magic-Bytes waren durch die automatische Dekompression laengst + weg). Fix: nur noch entpacken wenn die Bytes wirklich noch mit der + Gzip-Magic (`\x1f\x8b`) anfangen. +2. **Big Sur+ wurde komplett ausgefiltert:** `_is_macos_installer()` prüfte + nur auf das klassische Schema + (`ExtendedMetaInfo.InstallAssistantPackageIdentifiers.OSInstall == + "com.apple.mpkg.OSInstall"`, gueltig bis Catalina). Ab Big Sur nutzt + Apple stattdessen einen `SharedSupport`-Key + (`com.apple.pkg.InstallAssistant.macOSBigSur`, `...macOSTahoe`, etc.) -- + ohne den alten Key. Der Filter hat dadurch STILLSCHWEIGEND alle + Big-Sur-bis-Tahoe-Installer verworfen, obwohl sie im selben Katalog + laengst enthalten waren (live verifiziert: 24 InstallAssistant.pkg- + Eintraege im Katalog, 0 davon trafen den alten Filter). Genau der + `recovery`-Pfad, den dieses Tool eigentlich anbieten soll. Fix: + `_is_macos_installer` erkennt jetzt beide Schemata; `_fetch_product_ + metadata` faellt bei fehlendem `ServerMetadataURL` (Big Sur+ hat keins) + auf den Namen aus dem `SharedSupport`-Identifier zurueck. Nach dem Fix: + 37 statt 13 Installer gefunden, inkl. macOS Tahoe. + +Beide Fixes wurden nicht nur im Code geaendert, sondern gegen den echten, +frisch gebauten Container end-to-end verifiziert (`/api/versions` liefert +jetzt tatsaechlich Big Sur bis Tahoe zurueck). ## Voraussetzungen auf dem Host - Docker + Docker Compose diff --git a/backend/catalog.py b/backend/catalog.py index f8732c3..336048c 100644 --- a/backend/catalog.py +++ b/backend/catalog.py @@ -75,15 +75,61 @@ def find_latest_catalog_url(timeout: float = 8) -> str: def _fetch_catalog_products(url: str, timeout: float = 60) -> dict: r = requests.get(url, timeout=timeout) r.raise_for_status() - raw = gzip.decompress(r.content) + # Live gegen swscan.apple.com verifiziert (2026-07-18): der Server + # schickt "Content-Encoding: x-gzip" -- requests/urllib3 dekomprimiert + # das bereits transparent, r.content ist also schon reines XML. Ein + # zusaetzliches gzip.decompress() darauf crasht mit BadGzipFile, weil + # die Gzip-Magic-Bytes (\x1f\x8b) dann laengst weg sind. Manche + # Server/Proxies liefern aber ggf. noch echte rohe Gzip-Bytes ohne + # das Encoding transparent aufzuloesen -- daher robust an den echten + # Magic-Bytes pruefen statt blind zu (de-)komprimieren. + raw = r.content + if raw[:2] == b"\x1f\x8b": + raw = gzip.decompress(raw) data = plistlib.loads(raw) return data.get("Products", {}) +# Bekannte Marketing-Versionsnummern nach Codename. Nur eine Anzeige-Hilfe -- +# taucht ein UNS unbekannter zukuenftiger Codename auf (n plane naechstes Jahr: +# neuer Nachfolger von Tahoe), wird er trotzdem gelistet (siehe +# _split_shared_support_name), nur ohne huebsche Versionsnummer. +KNOWN_MACOS_VERSIONS = { + "bigsur": "11", "monterey": "12", "ventura": "13", "sonoma": "14", + "sequoia": "15", "tahoe": "26", +} + + +def _split_shared_support_name(shared_support_id: str): + """'com.apple.pkg.InstallAssistant.macOSBigSur' -> ('Big Sur', '11').""" + name = shared_support_id.rsplit(".macOS", 1)[-1] + if not name: + return None, None + # CamelCase -> "Big Sur" (Ziffern/Grossbuchstaben-Grenzen als Trenner) + import re + + pretty = re.sub(r"(? bool: emi = product.get("ExtendedMetaInfo", {}) or {} ids = emi.get("InstallAssistantPackageIdentifiers", {}) or {} - return ids.get("OSInstall") == "com.apple.mpkg.OSInstall" + # Klassisches Schema bis Catalina: eigener "OSInstall"-Schluessel. + if ids.get("OSInstall") == "com.apple.mpkg.OSInstall": + return True + # Ab Big Sur (live gegen swscan.apple.com verifiziert, 2026-07-18): kein + # "OSInstall"-Schluessel mehr, stattdessen "SharedSupport": + # "com.apple.pkg.InstallAssistant.macOS" (z.B. macOSBigSur, + # macOSMonterey, ..., macOSTahoe). Der alte Filter uebersah dadurch + # STILLSCHWEIGEND alle Big-Sur-und-neuer-Installer -- genau die, die per + # InstallAssistant.pkg (statt BaseSystem.dmg) geliefert werden und die + # dieses Tool eigentlich explizit unterstuetzen soll. + shared = ids.get("SharedSupport", "") + return isinstance(shared, str) and shared.startswith( + "com.apple.pkg.InstallAssistant.macOS" + ) def _fetch_product_metadata(product: dict, timeout: float = 20): @@ -100,6 +146,19 @@ def _fetch_product_metadata(product: dict, timeout: float = 20): title = en.get("title") except Exception as exc: # noqa: BLE001 log.warning("Metadaten-Fetch fehlgeschlagen fuer %s: %s", smd_url, exc) + if title: + return title, version + # Big Sur+ Produkte haben KEIN ServerMetadataURL (live verifiziert) -- + # Name kommt stattdessen aus dem SharedSupport-Identifier. + emi = product.get("ExtendedMetaInfo", {}) or {} + ids = emi.get("InstallAssistantPackageIdentifiers", {}) or {} + shared = ids.get("SharedSupport", "") + if isinstance(shared, str) and shared.startswith( + "com.apple.pkg.InstallAssistant.macOS" + ): + name, ver = _split_shared_support_name(shared) + if name: + return f"macOS {name}", (version or ver) return title, version