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:
Stefan Hacker
2026-04-26 12:09:39 +02:00
parent d0386b3c53
commit fb3534553b
35 changed files with 1883 additions and 0 deletions
View File
+163
View File
@@ -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}
+50
View File
@@ -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();
});
+136
View File
@@ -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); }
+20
View File
@@ -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 %}
+27
View File
@@ -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>
+36
View File
@@ -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 %}
+9
View File
@@ -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 %}
+38
View File
@@ -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 %}
+23
View File
@@ -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 %}
+39
View File
@@ -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 %}
+15
View File
@@ -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 %}