From eae036a9fce5a88940d5b838195bb0e7de748f20 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 30 Sep 2026 01:54:38 +0200 Subject: [PATCH] feat: Fern-Einrichtung des Agents per ADB (kein Abtippen am Fire TV) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Am Fire TV gibt es keine Kamera (kein QR) und Host/Token mit der Fernbedienung einzugeben ist eine Qual. Der Satellit hat aber ohnehin schon eine ADB-Verbindung — darueber bekommt der Agent seine Zugangsdaten direkt. - Android: MainActivity.applyProvisioning() liest Intent-Extras (host/port/tls/ token/name/control/reply_mode), speichert sie, startet den Dienst und zeigt 'Von ARIA eingerichtet' (Transparenz). Auch via onNewIntent. - Satellit: adb.provision (am start mit Extras) und adb.status (installiert? laeuft? welche Version?). adb.install richtet direkt mit ein, wenn rvs_host/ rvs_token mitkommen -> ein Klick = installieren + einrichten + online. - Diagnostic: Checkbox 'Gleich einrichten' (default an) + 'Status pruefen'-Button. Der RVS-Zugang wird SERVERSEITIG ergaenzt (params.provision) — das Token steht nie im Browser-Formular. - Allowlists (Code + .env.example) und README ergaenzt, inkl. Sicherheitshinweis. Android-Version 0.0.1.1. Co-Authored-By: Claude Opus 4.8 --- diagnostic/index.html | 43 +++++- diagnostic/server.js | 13 +- host-agent/android/README.md | 19 +++ .../de/hackersoft/ariaagent/MainActivity.kt | 47 ++++++- satellite/.env.example | 5 +- satellite/satellite.py | 125 +++++++++++++++++- 6 files changed, 244 insertions(+), 8 deletions(-) diff --git a/diagnostic/index.html b/diagnostic/index.html index 7505ca2..341d69e 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -1616,8 +1616,17 @@ Aus dem Gitea-Release. Der Satellit lädt die Datei selbst (einmal) und installiert sie auf dem Gerät. + +
+ Spart das Abtippen auf dem Fernseher: Host und Token gehen direkt vom + Server an den Agenten — QR-Scan geht am Fire TV ja nicht. +
+
@@ -4742,8 +4751,26 @@ '(Haken bei „Immer erlauben"). Ich warte bis zu 90 Sekunden darauf und\n' + 'installiere dann automatisch weiter. Kein zweiter Klick nötig.\n\n' + 'Danach: APK laden + installieren, zusammen 1–3 Minuten.'; + const params = { ip: apkTarget.ip, url }; + // Server ergaenzt Host/Port/Token — steht bewusst NICHT im Browser-Formular. + if (document.getElementById('apk-provision').checked) { + params.provision = true; + params.name = apkTarget.name; + } send({ action: 'sat_command', satellite: apkTarget.satId, cmd: 'adb.install', - requestId: apkRequestId, params: { ip: apkTarget.ip, url } }); + requestId: apkRequestId, params }); + } + + /** Prüft, ob der Agent auf dem Gerät installiert ist und läuft. */ + function runApkStatus() { + if (!apkTarget) return; + apkRequestId = 'apk_' + Date.now() + '_' + Math.floor(Math.random() * 100000); + document.getElementById('apk-status').innerHTML = 'prüfe …'; + const log = document.getElementById('apk-log'); + log.style.display = 'block'; + log.textContent = 'Frage das Gerät über ADB ab …'; + send({ action: 'sat_command', satellite: apkTarget.satId, cmd: 'adb.status', + requestId: apkRequestId, params: { ip: apkTarget.ip } }); } function onSatResult(p) { if (!apkRequestId || !p || p.requestId !== apkRequestId) return; @@ -4753,7 +4780,19 @@ if (p.ok) { const r = p.result || {}; status.innerHTML = '✓ erfolgreich'; - log.textContent = r.message || JSON.stringify(r, null, 2); + let txt = r.message || JSON.stringify(r, null, 2); + if (typeof r.installed !== 'undefined') { // adb.status + txt = (r.running ? '🟢 Agent läuft' : r.installed ? '🟡 installiert, läuft nicht' : '🔴 nicht installiert') + + (r.version ? '\nVersion: ' + r.version : '') + + '\nPaket: ' + (r.package || '') + '\nGerät: ' + (r.target || ''); + if (!r.running && r.installed) { + txt += '\n\nTipp: Auf dem Gerät die App öffnen und „Speichern & Verbinden" —' + + '\noder hier „Gleich einrichten" nutzen, dann startet sie von selbst.'; + } + } else if (r.provisioned === false) { + status.innerHTML = '⚠ installiert, Einrichtung offen'; + } + log.textContent = txt; } else { status.innerHTML = '✗ fehlgeschlagen'; log.textContent = p.error || 'Unbekannter Fehler'; diff --git a/diagnostic/server.js b/diagnostic/server.js index 6e91f1f..f842af8 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -2819,13 +2819,24 @@ wss.on("connection", (ws) => { } else if (msg.action === "sat_command") { // Browser loest eine Satelliten-Aktion aus (z.B. APK auf Fire TV ausrollen). // Die Antwort (sat_result) kommt ueber die rvsWs zurueck. + const params = msg.params && typeof msg.params === "object" ? msg.params : {}; + // Fern-Einrichtung: der RVS-Zugang wird SERVERSEITIG ergaenzt — so muss + // niemand Host/Token im Browser eintippen (und das Token steht nicht im + // Browser-Formular). Gilt fuer adb.provision und adb.install mit Setup. + if (params.provision === true) { + delete params.provision; + params.rvs_host = RVS_HOST; + params.rvs_port = Number(RVS_PORT) || 443; + params.rvs_tls = RVS_TLS === "true"; + params.rvs_token = RVS_TOKEN; + } sendToRVS_raw({ type: "sat_command", payload: { satellite: String(msg.satellite || ""), device: String(msg.device || ""), action: String(msg.cmd || ""), - params: msg.params && typeof msg.params === "object" ? msg.params : {}, + params, requestId: String(msg.requestId || ""), }, timestamp: Date.now(), diff --git a/host-agent/android/README.md b/host-agent/android/README.md index ebaa934..64b9adc 100644 --- a/host-agent/android/README.md +++ b/host-agent/android/README.md @@ -87,6 +87,25 @@ Statt die APK von Hand aufs Gerät zu schaufeln, kann ein **Satellit** sie per A und einen **📦**-Button. 3. 📦 → APK-URL (aus dem Gitea-Release) → **Jetzt ausrollen**. Beim ersten Mal erscheint auf dem TV „USB-Debugging zulassen?" → bestätigen (Haken „Immer erlauben"). + Der Rollout **wartet darauf** und macht dann automatisch weiter. + +**Einrichtung ohne Tippen:** Am Fire TV gibt es keine Kamera (kein QR-Scan) und +Host/Token mit der Fernbedienung einzugeben ist eine Qual. Deshalb ist im Rollout- +Dialog **„Gleich einrichten"** vorausgewählt: die Diagnostic kennt den RVS-Zugang +selbst und gibt ihn dem Agenten über dieselbe ADB-Verbindung mit +(`adb.provision` → `am start` mit Intent-Extras). Der Agent speichert die Daten, +startet den Dienst und ist **sofort online** — null Eingaben am Fernseher. +Das Token geht dabei **serverseitig** raus, es steht nie im Browser-Formular. + +Mit **„Status prüfen"** (`adb.status`) siehst Du jederzeit, ob der Agent auf dem +Gerät installiert ist, welche Version läuft und ob der Dienst aktiv ist. + +> **Sicherheitshinweis:** Die Einrichtung per Intent geht an die (exportierte) +> Haupt-Activity. Wer ADB-Zugriff auf das Gerät hat, kann den Agenten damit +> umkonfigurieren — wer ADB hat, kontrolliert das Gerät ohnehin. Die App zeigt +> beim Empfang einen Hinweis („Von ARIA eingerichtet"), damit es nicht unbemerkt +> passiert. Auf einem dedizierten Stick ist das unkritisch; schalte ADB-Debugging +> nach dem Ausrollen ab, wenn Du ganz sicher gehen willst. ARIA kann dasselbe: `satellite_command` mit `action='adb.install'`, `params={'ip':'…','url':'…'}`. Der Satellit lädt die APK **einmal** und kann sie diff --git a/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/MainActivity.kt b/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/MainActivity.kt index 6bacec4..1fab72e 100644 --- a/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/MainActivity.kt +++ b/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/MainActivity.kt @@ -122,7 +122,10 @@ class MainActivity : AppCompatActivity() { } } - loadIntoUi(AgentConfig.load(this)) + // Fern-Einrichtung (z.B. Fire TV, wo QR-Scan und Tippen wegfallen): der + // Satellit startet die App per ADB mit den Zugangsdaten als Extras. + // Hat das geklappt, ist die UI schon befuellt — sonst normal laden. + if (!applyProvisioning(intent)) loadIntoUi(AgentConfig.load(this)) // Fire TV: kein Touchscreen — der QR-Scan ist dort sinnlos (keine Kamera), // und der erste Fokus muss gesetzt sein, damit die Fernbedienung greift. @@ -209,6 +212,48 @@ class MainActivity : AppCompatActivity() { statusView.text = "Status: ${AgentService.status} $dot" } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + if (intent != null && applyProvisioning(intent)) return + } + + /** + * Fern-Einrichtung per Intent-Extras (vom Satelliten via ADB gestartet). + * + * Auf einem Fire TV gibt es keine Kamera (kein QR) und Tippen mit der + * Fernbedienung ist muehsam — deshalb kann der Satellit die Zugangsdaten + * direkt mitgeben: + * adb shell am start -n de.hackersoft.ariaagent/.MainActivity \ + * -a de.hackersoft.ariaagent.PROVISION \ + * -e host rvs.example.com --ei port 443 --ez tls true -e token XYZ + * + * Gibt true zurueck, wenn eingerichtet wurde (dann startet der Dienst gleich). + */ + private fun applyProvisioning(intent: Intent): Boolean { + val host = intent.getStringExtra("host")?.trim().orEmpty() + val token = intent.getStringExtra("token")?.trim().orEmpty() + if (host.isEmpty() || token.isEmpty()) return false + + val old = AgentConfig.load(this) + val cfg = AgentConfig( + host = host, + port = intent.getIntExtra("port", old.port.takeIf { it > 0 } ?: 443), + tls = intent.getBooleanExtra("tls", old.tls), + token = token, + name = intent.getStringExtra("name")?.trim().takeUnless { it.isNullOrEmpty() } ?: old.name, + controlEnabled = intent.getBooleanExtra("control", true), + replyMode = intent.getStringExtra("reply_mode")?.trim() + .takeUnless { it.isNullOrEmpty() } ?: old.replyMode, + ) + cfg.save(this) + AgentService.start(this) + // Sichtbar machen, dass die Konfiguration von aussen kam (Transparenz). + toast("Von ARIA eingerichtet: ${cfg.host} — Agent gestartet") + loadIntoUi(cfg) + refreshStatus() + return true + } + /** Sprech-Taste: startet/stoppt die Aufnahme im laufenden Dienst. */ private fun talkToAria() { if (!AgentConfig.load(this).isValid()) { toast("Erst verbinden"); return } diff --git a/satellite/.env.example b/satellite/.env.example index 24ca4d3..d0f5bf4 100644 --- a/satellite/.env.example +++ b/satellite/.env.example @@ -45,9 +45,12 @@ CONTROL_ENABLED=true # adb.connect ADB-Verbindung aufbauen (params: ip) # adb.devices verbundene ADB-Geraete auflisten # adb.shell Shell-Kommando auf dem Android-Geraet (params: ip, cmd) +# adb.provision Agent FERN einrichten: RVS-Zugang uebergeben, statt ihn mit der +# Fernbedienung abzutippen (params: ip, rvs_host, rvs_token, ...) +# adb.status laeuft/installiert der Agent auf dem Geraet? (params: ip) # ACHTUNG: Diese Liste ueberschreibt den Code-Default. Wird eine neue Aktion # ergaenzt, MUSS sie hier nachgetragen werden — sonst lehnt der Satellit sie ab. -CONTROL_ALLOWLIST=dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer,snmp.ports,snmp.info,fritzbox.info,fritzbox.hosts,ssh.exec,adb.devices,adb.connect,adb.install,adb.shell +CONTROL_ALLOWLIST=dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer,snmp.ports,snmp.info,fritzbox.info,fritzbox.hosts,ssh.exec,adb.devices,adb.connect,adb.install,adb.shell,adb.provision,adb.status # ─── ADB / Android-Rollout (optional) ────────────────────────────── # Fire TV Sticks & Android-Geraete im LAN: APK ausrollen ohne Datei-Kopieren. diff --git a/satellite/satellite.py b/satellite/satellite.py index d81e448..66e1c6b 100644 --- a/satellite/satellite.py +++ b/satellite/satellite.py @@ -135,7 +135,7 @@ CONTROL_ALLOWLIST = [ os.environ.get("CONTROL_ALLOWLIST", "dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer," "snmp.ports,snmp.info,fritzbox.info,fritzbox.hosts,ssh.exec," - "adb.devices,adb.connect,adb.install,adb.shell").split(",") + "adb.devices,adb.connect,adb.install,adb.shell,adb.provision,adb.status").split(",") if a.strip() ] @@ -633,6 +633,10 @@ async def _control(action: str, params: dict, devices: list[dict]) -> dict: return await loop.run_in_executor(None, _do_adb_install, params) if action == "adb.shell": return await loop.run_in_executor(None, _do_adb_shell, params) + if action == "adb.provision": + return await loop.run_in_executor(None, _do_adb_provision, params) + if action == "adb.status": + return await loop.run_in_executor(None, _do_adb_status, params) return {"ok": False, "error": f"Aktion '{action}' nicht implementiert."} except Exception as exc: return {"ok": False, "error": f"{action} fehlgeschlagen: {exc}"} @@ -1222,6 +1226,110 @@ def _do_adb_connect(params: dict) -> dict: return {"ok": True, "result": {"target": target, "state": state, "message": msg}} +AGENT_PACKAGE = os.environ.get("AGENT_PACKAGE", "de.hackersoft.ariaagent") + + +def _adb_app_installed(target: str, package: str) -> bool: + rc, out, _ = _adb_run(["-s", target, "shell", "pm", "list", "packages", package], + timeout=30) + return rc == 0 and f"package:{package}" in out + + +def _adb_app_running(target: str, package: str) -> bool: + """Laeuft der Vordergrund-Dienst des Agents? (Fire TV / Android).""" + rc, out, _ = _adb_run(["-s", target, "shell", "dumpsys", "activity", "services", + package], timeout=30) + return rc == 0 and "ServiceRecord" in out + + +def _do_adb_status(params: dict) -> dict: + """Ist der ARIA-Agent auf dem Geraet installiert und laeuft er?""" + target = _adb_target(params) + if not target: + return {"ok": False, "error": "ip/host erforderlich."} + package = (params.get("package") or AGENT_PACKAGE).strip() + ok, msg = _adb_connect(target) + if not ok: + return {"ok": False, "error": msg} + installed = _adb_app_installed(target, package) + running = _adb_app_running(target, package) if installed else False + ver = "" + if installed: + rc, out, _ = _adb_run(["-s", target, "shell", "dumpsys", "package", package], + timeout=30) + m = re.search(r"versionName=(\S+)", out or "") + ver = m.group(1) if m else "" + return {"ok": True, "result": { + "target": target, "package": package, "installed": installed, + "running": running, "version": ver, + "message": ("Agent laeuft" if running else + "installiert, laeuft aber nicht" if installed else + "nicht installiert")}} + + +def _do_adb_provision(params: dict) -> dict: + """Richtet den ARIA-Agent auf dem Geraet FERN ein (RVS-Zugang uebergeben). + + Auf einem Fire TV gibt es keine Kamera (kein QR-Scan) und Tippen mit der + Fernbedienung ist muehsam. Der Satellit startet die App deshalb per ADB + direkt mit den Zugangsdaten als Intent-Extras. + params: {ip, rvs_host, rvs_token, rvs_port?, rvs_tls?, name?, control?, + reply_mode?, package?} + """ + target = _adb_target(params) + if not target: + return {"ok": False, "error": "ip/host erforderlich."} + rvs_host = (params.get("rvs_host") or "").strip() + rvs_token = (params.get("rvs_token") or "").strip() + if not rvs_host or not rvs_token: + return {"ok": False, "error": "rvs_host und rvs_token erforderlich."} + package = (params.get("package") or AGENT_PACKAGE).strip() + + ok, msg = _adb_connect(target) + state = _adb_state(target) + wait = float(params.get("auth_wait") or ADB_AUTH_WAIT_SEC) + if (not ok or state != "device") and wait > 0: + authed, state = _adb_wait_authorized(target, wait) + if not authed: + return {"ok": False, "error": + "Nicht autorisiert — 'USB-Debugging zulassen?' auf dem " + f"Bildschirm bestaetigen. (Zustand: '{state or '?'}')"} + elif not ok: + return {"ok": False, "error": msg} + + if not _adb_app_installed(target, package): + return {"ok": False, "error": f"'{package}' ist nicht installiert — " + "erst adb.install ausfuehren."} + + args = ["-s", target, "shell", "am", "start", + "-n", f"{package}/.MainActivity", + "-a", f"{package}.PROVISION", + "-e", "host", rvs_host, + "-e", "token", rvs_token, + "--ei", "port", str(int(params.get("rvs_port") or 443)), + "--ez", "tls", "true" if _as_bool(params.get("rvs_tls"), True) else "false", + "--ez", "control", "true" if _as_bool(params.get("control"), True) else "false"] + if params.get("name"): + args += ["-e", "name", str(params["name"])] + if params.get("reply_mode"): + args += ["-e", "reply_mode", str(params["reply_mode"])] + + rc, out, err = _adb_run(args, timeout=60) + if rc != 0 or "Error" in f"{out} {err}": + return {"ok": False, "error": (f"{out} {err}".strip() or f"exit {rc}")[:400]} + return {"ok": True, "result": { + "target": target, "package": package, + "message": f"Agent eingerichtet (RVS {rvs_host}) und gestartet"}} + + +def _as_bool(v, default: bool) -> bool: + if v is None: + return default + if isinstance(v, bool): + return v + return str(v).strip().lower() in ("1", "true", "yes", "on", "ja") + + def _do_adb_shell(params: dict) -> dict: """Shell-Kommando auf dem Android-Geraet (Diagnose, z.B. 'pm list packages').""" target = _adb_target(params) @@ -1343,8 +1451,19 @@ def _do_adb_install(params: dict) -> dict: txt = f"{out} {err2}".strip() size_mb = round(os.path.getsize(apk) / 1024 / 1024, 1) if rc == 0 and "success" in txt.lower(): - return {"ok": True, "result": {"target": target, "apk_mb": size_mb, - "message": f"APK installiert ({size_mb} MB)"}} + res = {"target": target, "apk_mb": size_mb, + "message": f"APK installiert ({size_mb} MB)"} + # Direkt einrichten, wenn die RVS-Daten mitkamen — auf einem Fire TV + # spart das der Fernbedienung das Abtippen von Host und Token. + if params.get("rvs_host") and params.get("rvs_token"): + prov = _do_adb_provision(params) + res["provisioned"] = bool(prov.get("ok")) + if prov.get("ok"): + res["message"] += " und eingerichtet — der Agent sollte gleich online sein" + else: + res["message"] += (f". Einrichtung fehlgeschlagen: " + f"{prov.get('error', '?')}") + return {"ok": True, "result": res} low = txt.lower() if "signatures do not match" in low or "update_incompatible" in low: return {"ok": False, "error": "Signatur passt nicht zur installierten Version — "