initial aubox skeleton: web-UI, kirin DLOAD, firmware library
- FastAPI Web-UI auf 127.0.0.1:8080 mit Geräte-Live-Erkennung, sandboxed File-Browser, Firmware-Library (SQLite + Auto-Identifikation) - Huawei update.app Parser: extrahiert Hardware-ID, Section-Layout, BOOT/SYSTEM-Vorhandensein direkt aus den Headern - Kirin Download-Mode: hisi-idt-Protokoll-Implementation gegen pyusb - USB-Erkennung für Huawei (DLOAD/Fastboot-D), Google, MediaTek, Qualcomm EDL - Huawei-P10-Lite-Workflow (eRecovery + Testpoint-DLOAD-Pfade) - Docker-Compose mit USB-Passthrough (Major 189) für Re-Enumeration - udev-Regeln + Setup-Script für Debian/Ubuntu/Pi-OS Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
__version__ = "0.0.1"
|
||||
@@ -0,0 +1,3 @@
|
||||
from .cli import main
|
||||
|
||||
raise SystemExit(main())
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
"""CLI-Einstiegspunkt: `python -m aubox <command>`."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
from . import p10lite, usb
|
||||
from .kirin import KirinDload
|
||||
|
||||
|
||||
def cmd_detect(_args: argparse.Namespace) -> int:
|
||||
devs = usb.scan()
|
||||
if not devs:
|
||||
print("Kein bekanntes Hersteller-Gerät angeschlossen.")
|
||||
print("Probiere `lsusb` für die rohe Liste.")
|
||||
return 1
|
||||
for d in devs:
|
||||
print(d)
|
||||
if d.mode.notes:
|
||||
print(f" {d.mode.notes}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_p10lite_frp(args: argparse.Namespace) -> int:
|
||||
if args.method == "erecovery":
|
||||
print(p10lite.erecovery_instructions())
|
||||
return 0
|
||||
|
||||
if args.method == "dload-erase":
|
||||
try:
|
||||
plan = p10lite.dload_erase_plan()
|
||||
except FileNotFoundError as e:
|
||||
print(str(e), file=sys.stderr)
|
||||
return 2
|
||||
|
||||
print("Plan:")
|
||||
for stage in plan["stages"]:
|
||||
print(f" - {stage['label']}: {stage['file']} -> 0x{stage['addr']:08x}")
|
||||
print()
|
||||
print("Bitte jetzt Testpoint setzen und USB anschließen.")
|
||||
print("Warte auf Kirin DLOAD (12d1:1100)...")
|
||||
|
||||
for _ in range(60):
|
||||
if usb.find_first("kirin-dload") is not None:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
print("Timeout: kein Kirin DLOAD erschienen.", file=sys.stderr)
|
||||
return 3
|
||||
|
||||
print("Gerät erkannt. Sende Loader-Stages...")
|
||||
with KirinDload() as k:
|
||||
for stage in plan["stages"]:
|
||||
print(f" -> {stage['label']}")
|
||||
k.send_loader(stage["file"], stage["addr"])
|
||||
# Zwischen den Stages re-enumeriert das Gerät evtl.
|
||||
time.sleep(2)
|
||||
|
||||
print("Loader-Stages gesendet. Erwarteter Folgemodus:",
|
||||
plan["post"])
|
||||
print("Nächster Schritt (manuell, bis Workflow ausgereift):")
|
||||
print(" fastboot devices")
|
||||
print(f" fastboot erase {plan['frp_partition']}")
|
||||
return 0
|
||||
|
||||
print(f"Methode {args.method!r} noch nicht implementiert.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
def cmd_web(args: argparse.Namespace) -> int:
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"aubox.web.app:app",
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
reload=args.reload,
|
||||
log_level="info",
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser(prog="aubox")
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
sp_detect = sub.add_parser("detect", help="Angeschlossene Geräte erkennen")
|
||||
sp_detect.set_defaults(func=cmd_detect)
|
||||
|
||||
sp_web = sub.add_parser("web", help="Lokale Web-UI starten")
|
||||
sp_web.add_argument("--host", default="127.0.0.1",
|
||||
help="Bind-Adresse (Default: 127.0.0.1, im Container 0.0.0.0)")
|
||||
sp_web.add_argument("--port", type=int, default=8080)
|
||||
sp_web.add_argument("--reload", action="store_true",
|
||||
help="Auto-Reload bei Code-Änderungen (Entwicklung)")
|
||||
sp_web.set_defaults(func=cmd_web)
|
||||
|
||||
sp_p10 = sub.add_parser("p10lite", help="Workflows für Huawei P10 Lite")
|
||||
p10_sub = sp_p10.add_subparsers(dest="p10_cmd", required=True)
|
||||
|
||||
sp_frp = p10_sub.add_parser("frp-remove", help="Google-Account-Sperre entfernen")
|
||||
sp_frp.add_argument(
|
||||
"--method",
|
||||
choices=["erecovery", "dload-erase", "dload-flash"],
|
||||
default="erecovery",
|
||||
help="Welcher Pfad? Default: erecovery (sicherster, kein Loader nötig)",
|
||||
)
|
||||
sp_frp.set_defaults(func=cmd_p10lite_frp)
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Sandbox-fähiger Datei-Browser.
|
||||
|
||||
Egal welcher Pfad reinkommt — er wird gegen ein konfiguriertes Root-Verzeichnis
|
||||
geprüft. Path-Traversal (`../..`) ist damit ausgeschlossen.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class PathEscapeError(PermissionError):
|
||||
"""Pfad würde aus dem erlaubten Root ausbrechen."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Entry:
|
||||
name: str
|
||||
rel_path: str # relativ zum Root, mit '/' Separator
|
||||
is_dir: bool
|
||||
size: int # Bytes (0 für Dirs)
|
||||
|
||||
|
||||
def safe_resolve(root: Path, rel: str) -> Path:
|
||||
"""Pfad relativ zum Root auflösen und verifizieren, dass er drin bleibt."""
|
||||
root = root.resolve()
|
||||
rel_clean = rel.lstrip("/").strip()
|
||||
target = (root / rel_clean).resolve()
|
||||
try:
|
||||
target.relative_to(root)
|
||||
except ValueError as e:
|
||||
raise PathEscapeError(f"Pfad {rel!r} verlässt Root {root}") from e
|
||||
return target
|
||||
|
||||
|
||||
def list_dir(root: Path, rel: str = "") -> tuple[Path, list[Entry]]:
|
||||
"""Inhalt eines Verzeichnisses listen, Dirs zuerst, dann Files alphabetisch."""
|
||||
target = safe_resolve(root, rel)
|
||||
if not target.exists():
|
||||
raise FileNotFoundError(target)
|
||||
if not target.is_dir():
|
||||
raise NotADirectoryError(target)
|
||||
|
||||
entries: list[Entry] = []
|
||||
for child in sorted(target.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())):
|
||||
if child.name.startswith("."):
|
||||
continue
|
||||
rel_child = str(child.relative_to(root.resolve())).replace("\\", "/")
|
||||
try:
|
||||
size = child.stat().st_size if child.is_file() else 0
|
||||
except OSError:
|
||||
size = 0
|
||||
entries.append(Entry(
|
||||
name=child.name,
|
||||
rel_path=rel_child,
|
||||
is_dir=child.is_dir(),
|
||||
size=size,
|
||||
))
|
||||
return target, entries
|
||||
|
||||
|
||||
def breadcrumbs(rel: str) -> list[tuple[str, str]]:
|
||||
"""Liste von (Label, Pfad) für die Anzeige als Breadcrumb-Navigation."""
|
||||
parts = [p for p in rel.split("/") if p]
|
||||
out = [("/", "")]
|
||||
cur = ""
|
||||
for p in parts:
|
||||
cur = f"{cur}/{p}" if cur else p
|
||||
out.append((p, cur))
|
||||
return out
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
"""Kirin Download-Mode (HiSilicon) — hisi-idt-Protokoll.
|
||||
|
||||
Wenn ein Kirin-Gerät per Testpoint zur Erde gezogen und gebootet wird,
|
||||
landet es im "DLOAD"-Modus mit USB-VID:PID 12d1:1100. In diesem Modus
|
||||
hat es selbst noch keinen Speicher initialisiert — es wartet darauf,
|
||||
dass der Host einen *xloader* (kleiner sekundärer Bootloader) per USB
|
||||
schickt. Erst danach kommt der nächste Stage-Loader (`usb_loader.bin`),
|
||||
und dann fährt das Gerät als erweiterter Fastboot hoch.
|
||||
|
||||
Das Protokoll dafür heißt im Huawei-Kosmos schlicht "hisi-idt". Es ist
|
||||
nicht öffentlich dokumentiert, aber seit Jahren bekannt aus dem
|
||||
hisi-idt.py-Skript, das u.a. im Kirin-Tools-Umfeld kursiert. Hier eine
|
||||
saubere Implementation gegen pyusb.
|
||||
|
||||
WICHTIG:
|
||||
Die Loader-Dateien (`hisi-sec_usb_xloader.bin`, `usb_loader.bin`)
|
||||
sind Huawei-signierte Binaries. Du musst sie selbst beschaffen
|
||||
und in `loaders/kirin/<soc>/` ablegen. Siehe loaders/README.md.
|
||||
|
||||
Diese Implementation orientiert sich am öffentlichen Protokoll-Wissen.
|
||||
Auf realer P10-Lite-Hardware vor produktivem Einsatz validieren.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import usb.core
|
||||
import usb.util
|
||||
|
||||
KIRIN_DLOAD_VID = 0x12D1
|
||||
KIRIN_DLOAD_PID = 0x1100
|
||||
|
||||
# Protokoll-Konstanten (hisi-idt)
|
||||
HEAD_TAG = 0xFE
|
||||
TAIL_TAG = 0xFE
|
||||
TYPE_DATA = 0xDA
|
||||
TYPE_HEAD = 0xA5
|
||||
CHUNK = 0x800 # 2 KiB Payload pro Frame
|
||||
TIMEOUT_MS = 5000
|
||||
|
||||
|
||||
def _crc16_xmodem(data: bytes) -> int:
|
||||
"""CRC-16/XMODEM (Polynom 0x1021, init 0)."""
|
||||
crc = 0
|
||||
for b in data:
|
||||
crc ^= b << 8
|
||||
for _ in range(8):
|
||||
if crc & 0x8000:
|
||||
crc = (crc << 1) ^ 0x1021
|
||||
else:
|
||||
crc <<= 1
|
||||
crc &= 0xFFFF
|
||||
return crc
|
||||
|
||||
|
||||
def _frame(seq: int, payload: bytes, frame_type: int) -> bytes:
|
||||
"""Ein hisi-idt-Frame bauen.
|
||||
|
||||
Layout:
|
||||
HEAD (1) | TYPE (1) | SEQ (2 BE) | PAYLOAD | CRC16 (2 BE) | TAIL (1)
|
||||
"""
|
||||
body = struct.pack(">BBH", HEAD_TAG, frame_type, seq) + payload
|
||||
crc = _crc16_xmodem(body[1:]) # CRC ohne HEAD
|
||||
return body + struct.pack(">HB", crc, TAIL_TAG)
|
||||
|
||||
|
||||
class KirinDload:
|
||||
"""Verbindung zum Kirin im Download-Mode."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.dev: usb.core.Device | None = None
|
||||
self.ep_out = None
|
||||
self.ep_in = None
|
||||
|
||||
def open(self) -> None:
|
||||
dev = usb.core.find(idVendor=KIRIN_DLOAD_VID, idProduct=KIRIN_DLOAD_PID)
|
||||
if dev is None:
|
||||
raise RuntimeError(
|
||||
f"Kein Kirin-DLOAD-Gerät gefunden "
|
||||
f"({KIRIN_DLOAD_VID:04x}:{KIRIN_DLOAD_PID:04x}). "
|
||||
f"Testpoint sitzen? USB-Kabel datentauglich?"
|
||||
)
|
||||
if dev.is_kernel_driver_active(0):
|
||||
dev.detach_kernel_driver(0)
|
||||
dev.set_configuration()
|
||||
cfg = dev.get_active_configuration()
|
||||
intf = cfg[(0, 0)]
|
||||
self.ep_out = usb.util.find_descriptor(
|
||||
intf,
|
||||
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress)
|
||||
== usb.util.ENDPOINT_OUT,
|
||||
)
|
||||
self.ep_in = usb.util.find_descriptor(
|
||||
intf,
|
||||
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress)
|
||||
== usb.util.ENDPOINT_IN,
|
||||
)
|
||||
if self.ep_out is None:
|
||||
raise RuntimeError("Kein OUT-Endpunkt gefunden — falsche USB-Konfiguration?")
|
||||
self.dev = dev
|
||||
|
||||
def close(self) -> None:
|
||||
if self.dev is not None:
|
||||
usb.util.dispose_resources(self.dev)
|
||||
self.dev = None
|
||||
|
||||
def __enter__(self) -> "KirinDload":
|
||||
self.open()
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc) -> None:
|
||||
self.close()
|
||||
|
||||
def send_loader(self, path: Path, load_addr: int) -> None:
|
||||
"""Loader-Binary in einer Folge von Frames an das Gerät senden.
|
||||
|
||||
Args:
|
||||
path: Pfad zur xloader/usb_loader-Datei.
|
||||
load_addr: Zieladresse im SRAM. Pro SoC unterschiedlich.
|
||||
Für Kirin 658 (P10 Lite, xloader) typischerweise 0x07012000.
|
||||
In der Doku des SoCs verifizieren.
|
||||
"""
|
||||
data = path.read_bytes()
|
||||
self._send_header(load_addr, len(data))
|
||||
seq = 1
|
||||
for offset in range(0, len(data), CHUNK):
|
||||
chunk = data[offset : offset + CHUNK]
|
||||
self._write(_frame(seq, chunk, TYPE_DATA))
|
||||
seq = (seq + 1) & 0xFFFF
|
||||
# Kleines Delay — Kirin akzeptiert zu schnelle Bursts nicht zuverlässig
|
||||
time.sleep(0.001)
|
||||
# Sequenz abschließen: einige Implementationen senden ein finales Frame
|
||||
# mit Länge 0. Bei Bedarf hier ergänzen, je nach SoC-Verhalten.
|
||||
|
||||
def _send_header(self, addr: int, size: int) -> None:
|
||||
"""Header-Frame: sagt dem Gerät, wohin und wieviel."""
|
||||
payload = struct.pack(">II", addr, size)
|
||||
self._write(_frame(0, payload, TYPE_HEAD))
|
||||
|
||||
def _write(self, buf: bytes) -> None:
|
||||
assert self.dev is not None and self.ep_out is not None
|
||||
self.ep_out.write(buf, timeout=TIMEOUT_MS)
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Firmware-Library: SQLite-Index + Format-Identifikation.
|
||||
|
||||
Öffentliche API:
|
||||
db.connect(path) -> sqlite3.Connection
|
||||
db.upsert_firmware(conn, record)
|
||||
db.list_firmware(conn, ...)
|
||||
identify.identify(path) -> FirmwareInfo | None
|
||||
"""
|
||||
from . import db, identify # noqa: F401
|
||||
@@ -0,0 +1,94 @@
|
||||
"""SQLite-Index der Firmware-Library.
|
||||
|
||||
Die DB-Datei wandert standardmäßig nach `<firmware_root>/firmware.db`,
|
||||
damit sie zur Library gehört und beim Mounten automatisch da ist.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Iterator
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS firmware (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rel_path TEXT NOT NULL UNIQUE, -- relativ zum firmware-Root
|
||||
size INTEGER NOT NULL,
|
||||
mtime REAL NOT NULL,
|
||||
sha256 TEXT, -- erst nach erstem Hash gefüllt
|
||||
format TEXT, -- 'huawei-update.app', 'mtk-scatter', 'unknown', ...
|
||||
vendor TEXT,
|
||||
model TEXT, -- z.B. 'WAS-LX1'
|
||||
soc TEXT, -- z.B. 'kirin-658'
|
||||
version TEXT, -- z.B. '8.0.0.367'
|
||||
region TEXT, -- z.B. 'C432'
|
||||
extra_json TEXT, -- alles weitere als JSON
|
||||
added_at REAL DEFAULT (strftime('%s','now')),
|
||||
last_seen_at REAL DEFAULT (strftime('%s','now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_fw_model ON firmware(model);
|
||||
CREATE INDEX IF NOT EXISTS idx_fw_format ON firmware(format);
|
||||
"""
|
||||
|
||||
|
||||
def connect(db_path: Path) -> sqlite3.Connection:
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.executescript(SCHEMA)
|
||||
return conn
|
||||
|
||||
|
||||
@contextmanager
|
||||
def open_db(db_path: Path) -> Iterator[sqlite3.Connection]:
|
||||
"""Connection-Lifecycle für FastAPI-Handler: öffnet, gibt zurück, schließt."""
|
||||
conn = connect(db_path)
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def transaction(conn: sqlite3.Connection) -> Iterator[sqlite3.Connection]:
|
||||
try:
|
||||
yield conn
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
|
||||
|
||||
def upsert(conn: sqlite3.Connection, rec: dict) -> int:
|
||||
"""Einfügen oder bei Konflikt auf rel_path aktualisieren."""
|
||||
cols = ["rel_path", "size", "mtime", "sha256", "format",
|
||||
"vendor", "model", "soc", "version", "region", "extra_json"]
|
||||
placeholders = ",".join(f":{c}" for c in cols)
|
||||
update = ",".join(f"{c}=excluded.{c}" for c in cols if c != "rel_path")
|
||||
sql = (f"INSERT INTO firmware ({','.join(cols)}) VALUES ({placeholders}) "
|
||||
f"ON CONFLICT(rel_path) DO UPDATE SET {update}, "
|
||||
f"last_seen_at=strftime('%s','now')")
|
||||
cur = conn.execute(sql, {c: rec.get(c) for c in cols})
|
||||
return cur.lastrowid or 0
|
||||
|
||||
|
||||
def list_all(conn: sqlite3.Connection) -> list[sqlite3.Row]:
|
||||
return list(conn.execute(
|
||||
"SELECT * FROM firmware ORDER BY vendor, model, version"
|
||||
))
|
||||
|
||||
|
||||
def get_by_id(conn: sqlite3.Connection, fw_id: int) -> sqlite3.Row | None:
|
||||
cur = conn.execute("SELECT * FROM firmware WHERE id = ?", (fw_id,))
|
||||
return cur.fetchone()
|
||||
|
||||
|
||||
def delete_missing(conn: sqlite3.Connection, present_rel_paths: set[str]) -> int:
|
||||
"""Einträge entfernen, die beim letzten Scan nicht mehr da waren."""
|
||||
cur = conn.execute("SELECT id, rel_path FROM firmware")
|
||||
to_delete = [row["id"] for row in cur if row["rel_path"] not in present_rel_paths]
|
||||
if to_delete:
|
||||
conn.executemany("DELETE FROM firmware WHERE id = ?",
|
||||
[(i,) for i in to_delete])
|
||||
return len(to_delete)
|
||||
@@ -0,0 +1,148 @@
|
||||
"""Parser für Huawei `update.app`-Container.
|
||||
|
||||
Format-Beschreibung (öffentlich aus splituapp / Huawei Update Extractor):
|
||||
|
||||
Datei beginnt mit 0x5C Bytes Padding/Header, danach folgen Section-Header.
|
||||
Jeder Section-Header ist 98 Bytes:
|
||||
|
||||
uint32 magic = 0xA55AAA55
|
||||
uint32 header_length = 98
|
||||
uint32 unknown1
|
||||
char[8] hardware_id z.B. "WAS-LX1"
|
||||
uint32 file_sequence
|
||||
uint32 file_size
|
||||
char[16] file_date z.B. "2018.04.16"
|
||||
char[16] file_time z.B. "10:23:45"
|
||||
char[16] file_type z.B. "BOOT", "SYSTEM", "USERDATA", "CRC"
|
||||
char[16] blank1
|
||||
uint16 header_checksum
|
||||
uint16 block_size
|
||||
uint16 blank2
|
||||
|
||||
Danach `file_size` Bytes Section-Daten, gefolgt von einer CRC-Tabelle
|
||||
(eine uint16 pro `block_size` Bytes Daten), dann 4-Byte-Alignment auf
|
||||
die nächste Section-Header-Position.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
UPDATE_MAGIC = 0xA55AAA55
|
||||
HEADER_FMT = "<III8sII16s16s16s16sHHH"
|
||||
HEADER_SIZE = 98
|
||||
|
||||
|
||||
@dataclass
|
||||
class Section:
|
||||
sequence: int
|
||||
size: int
|
||||
date: str
|
||||
time: str
|
||||
type: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class UpdateAppInfo:
|
||||
hardware_id: str
|
||||
section_count: int
|
||||
sections: list[Section] = field(default_factory=list)
|
||||
has_boot: bool = False
|
||||
has_system: bool = False
|
||||
earliest_date: str | None = None
|
||||
|
||||
|
||||
def _cstr(b: bytes) -> str:
|
||||
return b.split(b"\x00", 1)[0].decode("ascii", errors="replace").strip()
|
||||
|
||||
|
||||
def parse(path: Path, max_sections: int = 500) -> UpdateAppInfo | None:
|
||||
"""Header der ersten N Sections einlesen, ohne das ganze Image zu lesen."""
|
||||
if not path.is_file():
|
||||
return None
|
||||
if path.stat().st_size < 0x100:
|
||||
return None
|
||||
|
||||
with path.open("rb") as f:
|
||||
# Magic suchen — sitzt meist bei 0x5C, manche Builds variieren
|
||||
head = f.read(0x200)
|
||||
idx = head.find(struct.pack("<I", UPDATE_MAGIC))
|
||||
if idx < 0:
|
||||
return None
|
||||
f.seek(idx)
|
||||
|
||||
sections: list[Section] = []
|
||||
hardware_id = ""
|
||||
earliest: str | None = None
|
||||
has_boot = has_system = False
|
||||
|
||||
for _ in range(max_sections):
|
||||
buf = f.read(HEADER_SIZE)
|
||||
if len(buf) < HEADER_SIZE:
|
||||
break
|
||||
(magic, _hlen, _u, hw, seq, size,
|
||||
date, time_, ftype, _b, _crc, blksz, _b2) = struct.unpack(HEADER_FMT, buf)
|
||||
if magic != UPDATE_MAGIC:
|
||||
break
|
||||
|
||||
hw_str = _cstr(hw)
|
||||
type_str = _cstr(ftype)
|
||||
date_str = _cstr(date)
|
||||
time_str = _cstr(time_)
|
||||
|
||||
if not hardware_id and hw_str:
|
||||
hardware_id = hw_str
|
||||
if earliest is None or (date_str and date_str < earliest):
|
||||
if date_str:
|
||||
earliest = date_str
|
||||
if type_str.upper() in ("BOOT", "FASTBOOT", "XLOADER", "BOOTLOADER"):
|
||||
has_boot = True
|
||||
if type_str.upper() in ("SYSTEM", "SYSTEM_IMAGE"):
|
||||
has_system = True
|
||||
|
||||
sections.append(Section(
|
||||
sequence=seq, size=size, date=date_str,
|
||||
time=time_str, type=type_str,
|
||||
))
|
||||
|
||||
# Daten + CRC + Padding überspringen
|
||||
if blksz <= 0:
|
||||
break
|
||||
crc_size = ((size + blksz - 1) // blksz) * 2
|
||||
cur_after_data = f.tell() + size + crc_size
|
||||
pad = (-cur_after_data) % 4
|
||||
f.seek(size + crc_size + pad, 1)
|
||||
|
||||
if not sections:
|
||||
return None
|
||||
return UpdateAppInfo(
|
||||
hardware_id=hardware_id,
|
||||
section_count=len(sections),
|
||||
sections=sections,
|
||||
has_boot=has_boot,
|
||||
has_system=has_system,
|
||||
earliest_date=earliest,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Modell -> SoC-Mapping. Wird mit der Zeit erweitert; was hier nicht steht,
|
||||
# kommt als 'unknown' raus und der User kann es manuell pflegen.
|
||||
HUAWEI_MODEL_SOC = {
|
||||
"WAS-LX1": "kirin-658",
|
||||
"WAS-LX2": "kirin-658",
|
||||
"WAS-LX3": "kirin-658",
|
||||
"PRA-LX1": "kirin-655", # P8 Lite 2017
|
||||
"VTR-L09": "kirin-960", # P10
|
||||
"VTR-L29": "kirin-960",
|
||||
"VKY-L09": "kirin-960", # P10 Plus
|
||||
"EML-L09": "kirin-970", # P20
|
||||
"EML-L29": "kirin-970",
|
||||
"CLT-L29": "kirin-970", # P20 Pro
|
||||
"ANE-LX1": "kirin-710", # P20 Lite
|
||||
}
|
||||
|
||||
|
||||
def soc_for(hardware_id: str) -> str | None:
|
||||
return HUAWEI_MODEL_SOC.get(hardware_id.upper())
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Format-Erkennung. Dispatcher: schaut auf Magic + Dateinamen, leitet an
|
||||
den passenden Parser weiter. Liefert ein einheitliches Dict mit den Feldern,
|
||||
die in die DB gehen.
|
||||
|
||||
Aktuell unterstützt:
|
||||
- Huawei update.app (vollständig)
|
||||
|
||||
Geplant (Stubs als Hinweis):
|
||||
- MediaTek scatter (txt + bin)
|
||||
- Samsung Odin (.tar.md5 mit AP/BL/CP/CSC)
|
||||
- Qualcomm rawprogram*.xml
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
from . import huawei
|
||||
|
||||
HUAWEI_MAGIC = struct.pack("<I", huawei.UPDATE_MAGIC)
|
||||
|
||||
|
||||
def _peek(path: Path, n: int = 0x200) -> bytes:
|
||||
try:
|
||||
with path.open("rb") as f:
|
||||
return f.read(n)
|
||||
except OSError:
|
||||
return b""
|
||||
|
||||
|
||||
def identify(path: Path, root: Path) -> dict:
|
||||
"""Rückgabe: dict für db.upsert. Enthält mindestens rel_path, size, mtime,
|
||||
format. Erkennungs-Misserfolg -> format='unknown'."""
|
||||
rel = str(path.resolve().relative_to(root.resolve())).replace("\\", "/")
|
||||
stat = path.stat()
|
||||
base: dict = {
|
||||
"rel_path": rel,
|
||||
"size": stat.st_size,
|
||||
"mtime": stat.st_mtime,
|
||||
"sha256": None,
|
||||
"format": "unknown",
|
||||
"vendor": None,
|
||||
"model": None,
|
||||
"soc": None,
|
||||
"version": None,
|
||||
"region": None,
|
||||
"extra_json": None,
|
||||
}
|
||||
|
||||
head = _peek(path)
|
||||
|
||||
# Huawei update.app — Magic taucht innerhalb der ersten 0x100 Bytes auf
|
||||
if HUAWEI_MAGIC in head[:0x200]:
|
||||
info = huawei.parse(path)
|
||||
if info is not None:
|
||||
base.update(
|
||||
format="huawei-update.app",
|
||||
vendor="Huawei",
|
||||
model=info.hardware_id or None,
|
||||
soc=huawei.soc_for(info.hardware_id) if info.hardware_id else None,
|
||||
version=info.earliest_date, # bessere Quelle wenn verfügbar
|
||||
region=_region_from_filename(path.name),
|
||||
extra_json=json.dumps({
|
||||
"section_count": info.section_count,
|
||||
"has_boot": info.has_boot,
|
||||
"has_system": info.has_system,
|
||||
"first_sections": [
|
||||
s.type for s in info.sections[:10]
|
||||
],
|
||||
}),
|
||||
)
|
||||
return base
|
||||
|
||||
# Fallback: Filename-Heuristik (MTK scatter etc. — TODO)
|
||||
name = path.name.lower()
|
||||
if name.startswith("mt") and name.endswith(".txt"):
|
||||
base["format"] = "mtk-scatter-candidate"
|
||||
elif name.endswith(".tar.md5"):
|
||||
base["format"] = "samsung-odin-candidate"
|
||||
base["vendor"] = "Samsung"
|
||||
|
||||
return base
|
||||
|
||||
|
||||
def _region_from_filename(name: str) -> str | None:
|
||||
"""Huawei-Region oft im Filename: '...C432B198...' -> 'C432'."""
|
||||
upper = name.upper()
|
||||
for marker in ("C432", "C636", "C185", "C233", "C605", "C10", "C461"):
|
||||
if marker in upper:
|
||||
return marker
|
||||
return None
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Workflows für Huawei P10 Lite (WAS-LX1, Kirin 658, EMUI 8.x).
|
||||
|
||||
Drei FRP-Removal-Pfade in absteigender Erfolgswahrscheinlichkeit:
|
||||
|
||||
1. erecovery — eRecovery + update.app von SD-Karte. Kein Loader nötig.
|
||||
Kein Bootloader-Unlock nötig. Funktioniert in der Regel.
|
||||
2. dload-erase — Testpoint -> Kirin DLOAD -> xloader -> frp-Partition leeren.
|
||||
Braucht passende Loader-Files (siehe loaders/README.md).
|
||||
3. dload-flash — Testpoint -> Kirin DLOAD -> kompletter Reflash via update.app.
|
||||
Maximaler Eingriff, höchstes Risiko, bricht Garantie endgültig.
|
||||
|
||||
Was hier *nicht* funktioniert:
|
||||
- ADB/Fastboot-FRP-Tricks: das Gerät ist im Setup-Wizard, ADB ist aus.
|
||||
- TalkBack/YouTube-Tricks: von Google/Huawei seit Jahren gepatcht.
|
||||
- Bootloader-Unlock-Code von Huawei: seit Juli 2018 abgeschaltet.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
LOADER_ROOT = Path(__file__).resolve().parent.parent / "loaders" / "kirin" / "kirin960_lite"
|
||||
# Kirin 658 wird intern oft mit dem Kirin-960-Loader-Set gefahren (Lite-Variante).
|
||||
# Für 100%-Sicherheit: Loader vom exakten WAS-LX1-Firmware-Dump verwenden.
|
||||
|
||||
XLOADER_NAME = "hisi-sec_usb_xloader.bin"
|
||||
USB_LOADER_NAME = "usb_loader.bin"
|
||||
FASTBOOT_BIN_NAME = "fastboot.bin"
|
||||
|
||||
# SRAM-Adressen für die Loader-Stages (öffentlich aus Kirin-Reverse-Engineering)
|
||||
# Im Zweifel mit dem Image abgleichen — manche WAS-Builds nutzen 0x07012000.
|
||||
XLOADER_ADDR = 0x07012000
|
||||
USB_LOADER_ADDR = 0x07012000
|
||||
|
||||
|
||||
def erecovery_instructions() -> str:
|
||||
"""Schritt-für-Schritt für den eRecovery-Pfad. Pure Anleitung, kein Code."""
|
||||
return """
|
||||
eRecovery-Methode (ohne Loader, ohne Bootloader-Unlock)
|
||||
========================================================
|
||||
|
||||
Du brauchst:
|
||||
- Eine FAT32-formatierte SD-Karte, max. 32 GB
|
||||
- Die *exakte* update.app deiner Region (z.B. WAS-LX1 EMEA C432)
|
||||
Bezug: huaweifirm.com, hovatek.com, stockromhuawei.com
|
||||
- USB-Ladegerät (kein Datenkabel nötig)
|
||||
|
||||
Schritte:
|
||||
1. Auf der SD-Karte einen Ordner `dload` (klein!) anlegen.
|
||||
2. update.app dort hineinkopieren. Pfad: SD:/dload/update.app
|
||||
3. SD-Karte in das ausgeschaltete P10 Lite stecken.
|
||||
4. Beim Einschalten gleichzeitig halten: Power + Volume-Up + Volume-Down.
|
||||
Halten bis das Huawei-Logo erscheint, dann nur noch Power weiter, andere lösen.
|
||||
5. eRecovery erkennt die SD-Karte und bietet "Software-Update" / "SD-Karten-Update".
|
||||
Bestätigen.
|
||||
6. Nach Abschluss bootet das Gerät neu, FRP-Partition ist neu geschrieben,
|
||||
Setup-Assistent startet ohne alten Google-Account.
|
||||
|
||||
Wenn eRecovery die Datei nicht erkennt:
|
||||
- Region passt nicht (häufigster Fehler)
|
||||
- update.app ist Multi-Part (.app + .lst). Beide nach /dload kopieren.
|
||||
- SD-Karte nicht FAT32 oder > 32 GB
|
||||
"""
|
||||
|
||||
|
||||
def dload_erase_plan(loader_dir: Path = LOADER_ROOT) -> dict:
|
||||
"""Plant den Testpoint -> DLOAD -> frp-erase-Pfad.
|
||||
|
||||
Liefert ein Dict mit Pfaden und Adressen, das vom CLI ausgeführt wird.
|
||||
Wirft FileNotFoundError, wenn die Loader-Files fehlen — dann landet der
|
||||
User bei loaders/README.md und weiß, was zu tun ist.
|
||||
"""
|
||||
xloader = loader_dir / XLOADER_NAME
|
||||
usb_loader = loader_dir / USB_LOADER_NAME
|
||||
|
||||
missing = [str(p) for p in (xloader, usb_loader) if not p.is_file()]
|
||||
if missing:
|
||||
raise FileNotFoundError(
|
||||
"Folgende Loader-Files fehlen:\n - "
|
||||
+ "\n - ".join(missing)
|
||||
+ "\nSiehe loaders/README.md für Beschaffung."
|
||||
)
|
||||
|
||||
return {
|
||||
"stages": [
|
||||
{"file": xloader, "addr": XLOADER_ADDR, "label": "xloader"},
|
||||
{"file": usb_loader, "addr": USB_LOADER_ADDR, "label": "usb_loader"},
|
||||
],
|
||||
"post": "huawei-fastboot-d", # erwarteter Modus nach dem Loaden
|
||||
"frp_partition": "frp",
|
||||
# Größe der FRP-Partition auf P10 Lite: meist 1 MiB. Beim ersten echten
|
||||
# Lauf gegen die ptable des Geräts gegenchecken (gpt-Dump via fastboot).
|
||||
"frp_size_bytes": 1024 * 1024,
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
"""USB-Geräte-Erkennung über VID:PID.
|
||||
|
||||
Erkennt die wichtigsten Hersteller-Modi und ordnet sie einem semantischen
|
||||
Status zu, damit Workflows wissen, in welchem Modus das Gerät gerade ist.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import usb.core
|
||||
import usb.util
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DeviceMode:
|
||||
label: str
|
||||
vendor: str
|
||||
notes: str = ""
|
||||
|
||||
|
||||
# (vid, pid) -> Modus
|
||||
KNOWN: dict[tuple[int, int], DeviceMode] = {
|
||||
# Huawei / Kirin
|
||||
(0x12D1, 0x1100): DeviceMode("kirin-dload", "Huawei",
|
||||
"Kirin Download-Mode, wartet auf xloader via hisi-idt"),
|
||||
(0x12D1, 0x1052): DeviceMode("huawei-fastboot-d", "Huawei",
|
||||
"Erweiterter Fastboot-Mode (nach xloader)"),
|
||||
(0x12D1, 0x3609): DeviceMode("kirin-dload-alt", "Huawei",
|
||||
"Kirin DLOAD alternative PID"),
|
||||
(0x12D1, 0x107E): DeviceMode("huawei-hisuite", "Huawei",
|
||||
"HiSuite/MTP-Modus, kein Recovery-Zugriff"),
|
||||
# Google / Android Standard
|
||||
(0x18D1, 0x4EE0): DeviceMode("fastboot", "Google", "Standard-Fastboot"),
|
||||
(0x18D1, 0x4EE7): DeviceMode("adb", "Google", "ADB"),
|
||||
# MediaTek
|
||||
(0x0E8D, 0x0003): DeviceMode("mtk-preloader", "MediaTek",
|
||||
"Preloader (DA-Mode möglich)"),
|
||||
(0x0E8D, 0x2000): DeviceMode("mtk-brom", "MediaTek",
|
||||
"BROM — Ziel für mtkclient/kamakiri"),
|
||||
# Qualcomm EDL
|
||||
(0x05C6, 0x9008): DeviceMode("qc-edl", "Qualcomm",
|
||||
"Emergency Download (Firehose-fähig)"),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class FoundDevice:
|
||||
vid: int
|
||||
pid: int
|
||||
mode: DeviceMode
|
||||
bus: int
|
||||
address: int
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (f"[{self.bus:03d}:{self.address:03d}] "
|
||||
f"{self.vid:04x}:{self.pid:04x} {self.mode.vendor} "
|
||||
f"{self.mode.label}")
|
||||
|
||||
|
||||
def scan() -> list[FoundDevice]:
|
||||
"""Alle bekannten Hersteller-Modi auflisten, die gerade angeschlossen sind."""
|
||||
found: list[FoundDevice] = []
|
||||
for dev in usb.core.find(find_all=True):
|
||||
key = (dev.idVendor, dev.idProduct)
|
||||
mode = KNOWN.get(key)
|
||||
if mode is None:
|
||||
continue
|
||||
found.append(FoundDevice(
|
||||
vid=dev.idVendor,
|
||||
pid=dev.idProduct,
|
||||
mode=mode,
|
||||
bus=dev.bus,
|
||||
address=dev.address,
|
||||
))
|
||||
return found
|
||||
|
||||
|
||||
def find_first(label: str) -> FoundDevice | None:
|
||||
for d in scan():
|
||||
if d.mode.label == label:
|
||||
return d
|
||||
return None
|
||||
@@ -0,0 +1,163 @@
|
||||
"""FastAPI-App. Lokale Web-UI für aubox.
|
||||
|
||||
Start:
|
||||
uvicorn aubox.web.app:app --host 127.0.0.1 --port 8080
|
||||
oder über CLI:
|
||||
python -m aubox web
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from .. import filebrowse, p10lite, usb
|
||||
from ..library import db as fwdb
|
||||
from ..library import identify as fwid
|
||||
|
||||
WEB_ROOT = Path(__file__).resolve().parent
|
||||
FIRMWARE_ROOT = Path(os.environ.get("AUBOX_FIRMWARE_ROOT", "./firmware")).resolve()
|
||||
LOADER_ROOT = Path(os.environ.get("AUBOX_LOADER_ROOT", "./loaders")).resolve()
|
||||
DB_PATH = FIRMWARE_ROOT / "firmware.db"
|
||||
|
||||
app = FastAPI(title="aubox")
|
||||
app.mount("/static", StaticFiles(directory=WEB_ROOT / "static"), name="static")
|
||||
templates = Jinja2Templates(directory=WEB_ROOT / "templates")
|
||||
|
||||
|
||||
def _human_size(n: int) -> str:
|
||||
for unit in ("B", "KB", "MB", "GB", "TB"):
|
||||
if n < 1024:
|
||||
return f"{n:.1f} {unit}" if unit != "B" else f"{n} B"
|
||||
n /= 1024
|
||||
return f"{n:.1f} PB"
|
||||
|
||||
|
||||
templates.env.filters["humansize"] = _human_size
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request):
|
||||
devices = usb.scan()
|
||||
with fwdb.open_db(DB_PATH) as conn:
|
||||
fw_count = conn.execute("SELECT COUNT(*) FROM firmware").fetchone()[0]
|
||||
return templates.TemplateResponse("index.html", {
|
||||
"request": request,
|
||||
"devices": devices,
|
||||
"fw_count": fw_count,
|
||||
"firmware_root": FIRMWARE_ROOT,
|
||||
"loader_root": LOADER_ROOT,
|
||||
})
|
||||
|
||||
|
||||
# ---------- Devices --------------------------------------------------------
|
||||
|
||||
@app.get("/devices", response_class=HTMLResponse)
|
||||
async def devices_page(request: Request):
|
||||
return templates.TemplateResponse("devices.html", {"request": request})
|
||||
|
||||
|
||||
@app.get("/api/devices/html", response_class=HTMLResponse)
|
||||
async def devices_partial(request: Request):
|
||||
return templates.TemplateResponse("_devices.html", {
|
||||
"request": request,
|
||||
"devices": usb.scan(),
|
||||
})
|
||||
|
||||
|
||||
# ---------- Firmware Library ----------------------------------------------
|
||||
|
||||
@app.get("/firmware", response_class=HTMLResponse)
|
||||
async def firmware_page(request: Request):
|
||||
with fwdb.open_db(DB_PATH) as conn:
|
||||
rows = fwdb.list_all(conn)
|
||||
return templates.TemplateResponse("firmware.html", {
|
||||
"request": request,
|
||||
"firmware": rows,
|
||||
"firmware_root": FIRMWARE_ROOT,
|
||||
})
|
||||
|
||||
|
||||
@app.post("/firmware/scan")
|
||||
async def firmware_scan():
|
||||
"""Walkt FIRMWARE_ROOT, identifiziert jede Datei, schreibt in DB."""
|
||||
if not FIRMWARE_ROOT.is_dir():
|
||||
raise HTTPException(404, f"Firmware-Root {FIRMWARE_ROOT} nicht gefunden")
|
||||
|
||||
seen: set[str] = set()
|
||||
added = updated = 0
|
||||
with fwdb.open_db(DB_PATH) as conn:
|
||||
with fwdb.transaction(conn):
|
||||
for path in FIRMWARE_ROOT.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
if path.name == "firmware.db" or path.name.startswith("."):
|
||||
continue
|
||||
rec = fwid.identify(path, FIRMWARE_ROOT)
|
||||
seen.add(rec["rel_path"])
|
||||
existed = conn.execute(
|
||||
"SELECT 1 FROM firmware WHERE rel_path = ?",
|
||||
(rec["rel_path"],),
|
||||
).fetchone()
|
||||
fwdb.upsert(conn, rec)
|
||||
if existed:
|
||||
updated += 1
|
||||
else:
|
||||
added += 1
|
||||
removed = fwdb.delete_missing(conn, seen)
|
||||
|
||||
return JSONResponse({
|
||||
"scanned": len(seen),
|
||||
"added": added,
|
||||
"updated": updated,
|
||||
"removed": removed,
|
||||
})
|
||||
|
||||
|
||||
@app.get("/firmware/{fw_id}", response_class=HTMLResponse)
|
||||
async def firmware_detail(request: Request, fw_id: int):
|
||||
with fwdb.open_db(DB_PATH) as conn:
|
||||
row = fwdb.get_by_id(conn, fw_id)
|
||||
if row is None:
|
||||
raise HTTPException(404)
|
||||
return templates.TemplateResponse("firmware_detail.html", {
|
||||
"request": request,
|
||||
"fw": row,
|
||||
"firmware_root": FIRMWARE_ROOT,
|
||||
})
|
||||
|
||||
|
||||
# ---------- File Browser (sandboxed auf FIRMWARE_ROOT) --------------------
|
||||
|
||||
@app.get("/browse", response_class=HTMLResponse)
|
||||
async def browse(request: Request, path: str = ""):
|
||||
try:
|
||||
target, entries = filebrowse.list_dir(FIRMWARE_ROOT, path)
|
||||
except (filebrowse.PathEscapeError, FileNotFoundError, NotADirectoryError) as e:
|
||||
raise HTTPException(400, str(e))
|
||||
return templates.TemplateResponse("browse.html", {
|
||||
"request": request,
|
||||
"rel": path,
|
||||
"entries": entries,
|
||||
"crumbs": filebrowse.breadcrumbs(path),
|
||||
"firmware_root": FIRMWARE_ROOT,
|
||||
})
|
||||
|
||||
|
||||
# ---------- Workflows ------------------------------------------------------
|
||||
|
||||
@app.get("/workflows/p10lite", response_class=HTMLResponse)
|
||||
async def workflow_p10lite(request: Request):
|
||||
return templates.TemplateResponse("p10lite.html", {
|
||||
"request": request,
|
||||
"instructions": p10lite.erecovery_instructions(),
|
||||
})
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"ok": True}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Live-Refresh für Container mit data-refresh="<url>" und data-interval="<ms>"
|
||||
function startAutoRefresh() {
|
||||
document.querySelectorAll("[data-refresh]").forEach((el) => {
|
||||
const url = el.dataset.refresh;
|
||||
const interval = parseInt(el.dataset.interval || "2000", 10);
|
||||
const tick = async () => {
|
||||
try {
|
||||
const r = await fetch(url, { headers: { "Accept": "text/html" } });
|
||||
if (r.ok) el.innerHTML = await r.text();
|
||||
} catch (e) { /* netzkurz weg, nicht weiter schlimm */ }
|
||||
};
|
||||
tick();
|
||||
setInterval(tick, interval);
|
||||
});
|
||||
}
|
||||
|
||||
// Forms mit data-action posten und Antwort in data-target rendern
|
||||
function wireScanForms() {
|
||||
document.querySelectorAll("form[data-action]").forEach((form) => {
|
||||
form.addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
const btn = form.querySelector("button[type=submit]");
|
||||
const target = document.querySelector(form.dataset.target);
|
||||
btn.disabled = true;
|
||||
if (target) target.textContent = "läuft…";
|
||||
try {
|
||||
const r = await fetch(form.dataset.action, { method: "POST" });
|
||||
const j = await r.json().catch(() => null);
|
||||
if (target) {
|
||||
if (j) {
|
||||
target.textContent = `gescannt: ${j.scanned} · neu: ${j.added} · aktualisiert: ${j.updated} · entfernt: ${j.removed}`;
|
||||
} else {
|
||||
target.textContent = r.ok ? "fertig" : "Fehler";
|
||||
}
|
||||
}
|
||||
// Seite neu laden, damit die Tabelle die neuen Einträge zeigt
|
||||
if (r.ok) setTimeout(() => location.reload(), 800);
|
||||
} catch (e) {
|
||||
if (target) target.textContent = "Fehler: " + e;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
startAutoRefresh();
|
||||
wireScanForms();
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
:root {
|
||||
--bg: #1c1f24;
|
||||
--bg-card: #262a31;
|
||||
--fg: #e6e6e6;
|
||||
--muted: #8a8f98;
|
||||
--accent: #6cb4ff;
|
||||
--accent-hover: #8fc8ff;
|
||||
--border: #353a44;
|
||||
--ok: #7adf7a;
|
||||
--warn: #ffb86c;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
header {
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
header h1 { margin: 0; font-size: 1.4rem; }
|
||||
header h1 a { color: var(--fg); text-decoration: none; }
|
||||
|
||||
nav { display: flex; gap: 1.5rem; }
|
||||
nav a { color: var(--muted); text-decoration: none; font-weight: 500; }
|
||||
nav a:hover { color: var(--accent); }
|
||||
|
||||
main { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
||||
|
||||
footer {
|
||||
text-align: center; padding: 2rem; color: var(--muted);
|
||||
border-top: 1px solid var(--border); margin-top: 4rem;
|
||||
}
|
||||
|
||||
a { color: var(--accent); }
|
||||
a:hover { color: var(--accent-hover); }
|
||||
|
||||
code, pre {
|
||||
font-family: "JetBrains Mono", Menlo, Consolas, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
pre {
|
||||
background: var(--bg-card);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
code { background: var(--bg-card); padding: 0.1em 0.4em; border-radius: 3px; }
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.card h2 { margin-top: 0; font-size: 1rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.card .big { font-size: 2.5rem; font-weight: 700; margin: 0.5rem 0; }
|
||||
|
||||
.info { margin-top: 3rem; }
|
||||
.info ul { list-style: none; padding: 0; }
|
||||
.info li { margin: 0.5rem 0; }
|
||||
|
||||
table.grid {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--bg-card);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
table.grid th, table.grid td {
|
||||
text-align: left;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
table.grid th { background: rgba(255,255,255,0.04); font-weight: 600; }
|
||||
table.grid tbody tr:last-child td { border-bottom: none; }
|
||||
table.grid tbody tr:hover { background: rgba(255,255,255,0.03); }
|
||||
|
||||
table.kv { border-collapse: collapse; }
|
||||
table.kv th { text-align: left; padding: 0.5rem 1rem 0.5rem 0; color: var(--muted); font-weight: 500; }
|
||||
table.kv td { padding: 0.5rem 0; }
|
||||
|
||||
button {
|
||||
background: var(--accent);
|
||||
color: #0d1117;
|
||||
border: 0;
|
||||
padding: 0.6rem 1.2rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
button:hover { background: var(--accent-hover); }
|
||||
button:disabled { opacity: 0.5; cursor: wait; }
|
||||
|
||||
.empty {
|
||||
background: var(--bg-card);
|
||||
border: 1px dashed var(--border);
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.muted { color: var(--muted); }
|
||||
|
||||
.crumbs {
|
||||
background: var(--bg-card);
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
font-family: "JetBrains Mono", Menlo, monospace;
|
||||
}
|
||||
.crumbs a { color: var(--fg); text-decoration: none; }
|
||||
.crumbs a:hover { color: var(--accent); }
|
||||
|
||||
#scan-result { margin-left: 1rem; color: var(--muted); }
|
||||
@@ -0,0 +1,20 @@
|
||||
{% if not devices %}
|
||||
<p class="empty">Kein bekanntes Hersteller-Gerät am USB.</p>
|
||||
{% else %}
|
||||
<table class="grid">
|
||||
<thead>
|
||||
<tr><th>Bus:Addr</th><th>VID:PID</th><th>Hersteller</th><th>Modus</th><th>Hinweis</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in devices %}
|
||||
<tr>
|
||||
<td>{{ "%03d"|format(d.bus) }}:{{ "%03d"|format(d.address) }}</td>
|
||||
<td><code>{{ "%04x"|format(d.vid) }}:{{ "%04x"|format(d.pid) }}</code></td>
|
||||
<td>{{ d.mode.vendor }}</td>
|
||||
<td><strong>{{ d.mode.label }}</strong></td>
|
||||
<td>{{ d.mode.notes }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{% block title %}aubox{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1><a href="/">aubox</a></h1>
|
||||
<nav>
|
||||
<a href="/">Übersicht</a>
|
||||
<a href="/devices">Geräte</a>
|
||||
<a href="/firmware">Firmware</a>
|
||||
<a href="/browse">Dateien</a>
|
||||
<a href="/workflows/p10lite">P10 Lite</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<footer>
|
||||
<small>aubox · lokale Web-UI</small>
|
||||
</footer>
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dateien · aubox{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Datei-Browser</h2>
|
||||
<p class="muted">Sandboxed auf <code>{{ firmware_root }}</code> — Path-Traversal blockiert.</p>
|
||||
|
||||
<nav class="crumbs">
|
||||
{% for label, p in crumbs %}
|
||||
<a href="/browse?path={{ p }}">{{ label }}</a>
|
||||
{% if not loop.last %} / {% endif %}
|
||||
{% endfor %}
|
||||
</nav>
|
||||
|
||||
{% if not entries %}
|
||||
<p class="empty">Verzeichnis ist leer.</p>
|
||||
{% else %}
|
||||
<table class="grid">
|
||||
<thead><tr><th>Name</th><th>Typ</th><th>Größe</th></tr></thead>
|
||||
<tbody>
|
||||
{% for e in entries %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if e.is_dir %}
|
||||
<a href="/browse?path={{ e.rel_path }}">{{ e.name }}/</a>
|
||||
{% else %}
|
||||
{{ e.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ "DIR" if e.is_dir else "FILE" }}</td>
|
||||
<td>{{ e.size|humansize if not e.is_dir else "—" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Geräte · aubox{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Angeschlossene Geräte</h2>
|
||||
<p>Aktualisiert sich alle 2 Sekunden.</p>
|
||||
<div id="devices" data-refresh="/api/devices/html" data-interval="2000">
|
||||
Lade…
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Firmware · aubox{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Firmware-Library</h2>
|
||||
<p>Quelle: <code>{{ firmware_root }}</code></p>
|
||||
|
||||
<form id="scan-form" data-action="/firmware/scan" data-target="#scan-result">
|
||||
<button type="submit">Library scannen</button>
|
||||
<span id="scan-result"></span>
|
||||
</form>
|
||||
|
||||
{% if not firmware %}
|
||||
<p class="empty">Noch keine Einträge. Lege Firmware-Dateien unter
|
||||
<code>{{ firmware_root }}</code> ab und klicke "Library scannen".</p>
|
||||
{% else %}
|
||||
<table class="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Vendor</th><th>Modell</th><th>SoC</th><th>Region</th>
|
||||
<th>Format</th><th>Größe</th><th>Pfad</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for fw in firmware %}
|
||||
<tr>
|
||||
<td>{{ fw.vendor or "—" }}</td>
|
||||
<td><a href="/firmware/{{ fw.id }}">{{ fw.model or "—" }}</a></td>
|
||||
<td>{{ fw.soc or "—" }}</td>
|
||||
<td>{{ fw.region or "—" }}</td>
|
||||
<td>{{ fw.format }}</td>
|
||||
<td>{{ fw.size|humansize }}</td>
|
||||
<td><code>{{ fw.rel_path }}</code></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ fw.model or fw.rel_path }} · aubox{% endblock %}
|
||||
{% block content %}
|
||||
<h2>{{ fw.model or "Unbekannt" }} <small>{{ fw.format }}</small></h2>
|
||||
|
||||
<table class="kv">
|
||||
<tr><th>Pfad</th><td><code>{{ fw.rel_path }}</code></td></tr>
|
||||
<tr><th>Vendor</th><td>{{ fw.vendor or "—" }}</td></tr>
|
||||
<tr><th>Modell</th><td>{{ fw.model or "—" }}</td></tr>
|
||||
<tr><th>SoC</th><td>{{ fw.soc or "—" }}</td></tr>
|
||||
<tr><th>Region</th><td>{{ fw.region or "—" }}</td></tr>
|
||||
<tr><th>Version</th><td>{{ fw.version or "—" }}</td></tr>
|
||||
<tr><th>Größe</th><td>{{ fw.size|humansize }}</td></tr>
|
||||
<tr><th>SHA-256</th><td><code>{{ fw.sha256 or "noch nicht berechnet" }}</code></td></tr>
|
||||
</table>
|
||||
|
||||
{% if fw.extra_json %}
|
||||
<h3>Format-spezifische Daten</h3>
|
||||
<pre>{{ fw.extra_json }}</pre>
|
||||
{% endif %}
|
||||
|
||||
<p><a href="/firmware">← zurück</a></p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}aubox · Übersicht{% endblock %}
|
||||
{% block content %}
|
||||
<section class="cards">
|
||||
<article class="card">
|
||||
<h2>Geräte</h2>
|
||||
<p class="big">{{ devices|length }}</p>
|
||||
<p>{{ "angeschlossen" if devices else "keins erkannt" }}</p>
|
||||
<p><a href="/devices">öffnen →</a></p>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<h2>Firmware-Library</h2>
|
||||
<p class="big">{{ fw_count }}</p>
|
||||
<p>Einträge in der Datenbank</p>
|
||||
<p><a href="/firmware">öffnen →</a></p>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<h2>Dateien</h2>
|
||||
<p>Sandbox-Browser für die Firmware-Library</p>
|
||||
<p><a href="/browse">öffnen →</a></p>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<h2>Huawei P10 Lite</h2>
|
||||
<p>FRP-Removal · WAS-LX1 · Kirin 658</p>
|
||||
<p><a href="/workflows/p10lite">öffnen →</a></p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="info">
|
||||
<h3>Aktive Pfade im Container</h3>
|
||||
<ul>
|
||||
<li><strong>Firmware:</strong> <code>{{ firmware_root }}</code></li>
|
||||
<li><strong>Loader:</strong> <code>{{ loader_root }}</code></li>
|
||||
</ul>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Huawei P10 Lite · aubox{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Huawei P10 Lite — FRP-Removal</h2>
|
||||
<p class="muted">WAS-LX1 · Kirin 658 · EMUI 8.x</p>
|
||||
|
||||
<h3>Methode 1: eRecovery + SD-Karte (empfohlen)</h3>
|
||||
<pre>{{ instructions }}</pre>
|
||||
|
||||
<h3>Methode 2: Testpoint + Kirin DLOAD</h3>
|
||||
<p>Über CLI vorbereitet, Web-UI-Button folgt:</p>
|
||||
<pre>python -m aubox p10lite frp-remove --method dload-erase</pre>
|
||||
<p>Voraussetzung: Loader-Files in <code>/loaders/kirin/kirin960_lite/</code>
|
||||
(siehe Loader-README im Repo).</p>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user