feat: Fern-Einrichtung des Agents per ADB (kein Abtippen am Fire TV)
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 <noreply@anthropic.com>
This commit is contained in:
+41
-2
@@ -1616,8 +1616,17 @@
|
||||
Aus dem Gitea-Release. Der Satellit lädt die Datei selbst (einmal) und
|
||||
installiert sie auf dem Gerät.
|
||||
</div>
|
||||
<label style="display:flex;align-items:center;gap:8px;margin-top:12px;cursor:pointer;">
|
||||
<input type="checkbox" id="apk-provision" checked>
|
||||
<span>Gleich einrichten (RVS-Zugang übertragen)</span>
|
||||
</label>
|
||||
<div style="color:#666;font-size:11px;margin-left:26px;">
|
||||
Spart das Abtippen auf dem Fernseher: Host und Token gehen direkt vom
|
||||
Server an den Agenten — QR-Scan geht am Fire TV ja nicht.
|
||||
</div>
|
||||
<div style="margin-top:14px;">
|
||||
<button class="btn" onclick="runApkDeploy()">📦 Jetzt ausrollen</button>
|
||||
<button class="btn secondary" onclick="runApkStatus()" style="margin-left:6px;">Status prüfen</button>
|
||||
<span id="apk-status" style="margin-left:10px;color:#8888AA;"></span>
|
||||
</div>
|
||||
<pre id="apk-log" style="margin-top:12px;background:#0D0D1A;border-radius:6px;padding:10px;max-height:240px;overflow:auto;white-space:pre-wrap;color:#8888AA;font-size:12px;display:none;"></pre>
|
||||
@@ -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 = '<span style="color:#FFD60A;">prüfe …</span>';
|
||||
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 = '<span style="color:#34C759;">✓ erfolgreich</span>';
|
||||
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 = '<span style="color:#FF9500;">⚠ installiert, Einrichtung offen</span>';
|
||||
}
|
||||
log.textContent = txt;
|
||||
} else {
|
||||
status.innerHTML = '<span style="color:#FF6E6E;">✗ fehlgeschlagen</span>';
|
||||
log.textContent = p.error || 'Unbekannter Fehler';
|
||||
|
||||
+12
-1
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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.
|
||||
|
||||
+122
-3
@@ -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 — "
|
||||
|
||||
Reference in New Issue
Block a user