feat(satellite): gespeicherte Zugangsdaten anzeigen, Passwort per Auge, FritzBox-Benutzer ermitteln
Das Zugangsdaten-Formular war beim erneuten Oeffnen leer, obwohl der Satellit alles gespeichert hatte. Satellit: - sat_creds_list mit ip liefert je Typ die Felder ohne Geheimnisse (Benutzer, Port, SNMP-Version/v3-Stufe) und has_<feld> fuer Passwoerter; mit reveal (nur zusammen mit ip) auch die Geheimnisse dieses Geraets. - sat_creds_set behaelt ein gespeichertes Geheimnis, wenn das Feld nicht mitgeschickt wird — Benutzer aendern, ohne das Passwort neu zu tippen. Diagnostic-Server: Antworten mit Klartext (revealed) gehen nur an den Browser, der gefragt hat, nicht an alle offenen Tabs. Formular: Benutzer/Port/Version stehen beim Oeffnen drin, Passwoerter als "•••••• gespeichert"; 👁 holt sie im Klartext bzw. schaltet um. Leeres Passwortfeld = gespeichertes behalten. FritzBox: "Benutzer ermitteln" (nur bei als FritzBox erkannten Geraeten) liest login_sid.lua?version=2 — die Box nennt ihre Benutzer ohne Anmeldung; eingetragen wird der zuletzt angemeldete. Getestet: Satelliten-Logik per Unit-Test, Formular in headless Chromium (Anzeige, Speichern ohne Passwort, Auge, Benutzer ermitteln). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+46
-6
@@ -290,12 +290,49 @@ def _creds_for(ip: str) -> dict:
|
||||
return _CREDS.get((ip or "").strip(), {}) if ip else {}
|
||||
|
||||
|
||||
def _creds_public_summary() -> list:
|
||||
"""Fuer sat_creds_list: welche Geraete welche Cred-Typen haben — OHNE Secrets."""
|
||||
_CRED_SECRETS = ("pass", "key", "community", "authKey", "privKey")
|
||||
|
||||
|
||||
def _creds_detail(d: dict, reveal: bool) -> dict:
|
||||
"""Ein Cred-Eintrag fuer die Diagnostic: Benutzer, Port, Version … immer;
|
||||
Geheimes nur als has_<feld>=True — ausser reveal (Auge im Formular)."""
|
||||
out = {}
|
||||
for k, v in (d or {}).items():
|
||||
if isinstance(v, dict):
|
||||
out[k] = _creds_detail(v, reveal)
|
||||
elif k in _CRED_SECRETS:
|
||||
out["has_" + k] = bool(v)
|
||||
if reveal:
|
||||
out[k] = v
|
||||
else:
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def _creds_keep_secrets(new: dict, old: dict) -> dict:
|
||||
"""Beim Speichern: fehlt ein Geheimnis im neuen Eintrag (Feld unangetastet),
|
||||
bleibt das alte. So muss man ein Passwort nicht neu tippen, um nur den
|
||||
Benutzer zu aendern."""
|
||||
out = dict(new)
|
||||
for k, v in (old or {}).items():
|
||||
if isinstance(v, dict) and isinstance(out.get(k), dict):
|
||||
out[k] = _creds_keep_secrets(out[k], v)
|
||||
elif k in _CRED_SECRETS and k not in out:
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def _creds_public_summary(ip: str = "", reveal: bool = False) -> list:
|
||||
"""Fuer sat_creds_list: welche Geraete welche Cred-Typen haben, dazu die
|
||||
Felder OHNE Secrets (Benutzer, Port, SNMP-Version). Mit ip + reveal auch
|
||||
die Secrets dieses einen Geraets."""
|
||||
out = []
|
||||
for ip, entry in sorted(_CREDS.items()):
|
||||
for dev_ip, entry in sorted(_CREDS.items()):
|
||||
if ip and dev_ip != ip:
|
||||
continue
|
||||
types = [t for t in ("snmp", "http", "fritzbox", "ssh") if entry.get(t)]
|
||||
out.append({"ip": ip, "types": types})
|
||||
out.append({"ip": dev_ip, "types": types,
|
||||
"detail": {t: _creds_detail(entry[t], reveal and bool(ip)) for t in types}})
|
||||
return out
|
||||
|
||||
HEARTBEAT_SEC = 25
|
||||
@@ -2150,7 +2187,7 @@ class Satellite:
|
||||
for t in ("snmp", "http", "fritzbox", "ssh"):
|
||||
if t in creds:
|
||||
if creds[t]: # leeres Objekt = Typ loeschen
|
||||
entry[t] = creds[t]
|
||||
entry[t] = _creds_keep_secrets(creds[t], entry.get(t) or {})
|
||||
else:
|
||||
entry.pop(t, None)
|
||||
if not entry:
|
||||
@@ -2189,11 +2226,14 @@ class Satellite:
|
||||
elif mtype == "sat_creds_list":
|
||||
if not self._for_me(payload):
|
||||
return
|
||||
ip = (payload.get("ip") or "").strip()
|
||||
reveal = bool(payload.get("reveal")) and bool(ip)
|
||||
await self._send({"type": "sat_creds_list_result",
|
||||
"payload": {"requestId": payload.get("requestId", ""),
|
||||
"satellite": SATELLITE_ID,
|
||||
"location": SATELLITE_LOCATION,
|
||||
"items": _creds_public_summary()},
|
||||
"ip": ip, "revealed": reveal,
|
||||
"items": _creds_public_summary(ip, reveal)},
|
||||
"timestamp": int(time.time() * 1000)})
|
||||
|
||||
async def _periodic_scan(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user