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:
2026-10-31 16:48:44 +01:00
co-authored by Claude Opus 5.5
parent 3a3ef81835
commit c934866bba
3 changed files with 181 additions and 31 deletions
+19 -2
View File
@@ -836,6 +836,9 @@ function satelliteList() {
}));
}
// requestId → Browser-Socket, der Klartext-Zugangsdaten angefordert hat
const credsRevealWaiters = new Map();
function broadcastSatellites() {
broadcast({ type: "sat_update", satellites: satelliteList() });
}
@@ -1425,7 +1428,16 @@ function connectRVS(forcePlain) {
location: p.location || "", devices: p.devices || [] });
} else if (msg.type === "sat_creds_list_result" || msg.type === "sat_creds_result") {
// Credential-Store-Antworten eines Satelliten → an den Browser.
broadcast({ type: msg.type, payload: msg.payload || {} });
// Mit Klartext-Passwoertern (Auge im Formular) NUR an den Browser, der
// gefragt hat — nie an alle offenen Diagnostic-Tabs.
const p = msg.payload || {};
if (p.revealed) {
const asker = credsRevealWaiters.get(p.requestId);
credsRevealWaiters.delete(p.requestId);
if (asker && asker.readyState === WebSocket.OPEN) asker.send(JSON.stringify({ type: msg.type, payload: p }));
} else {
broadcast({ type: msg.type, payload: p });
}
} else if (msg.type === "worker_hello") {
// Ein Compute-Worker (GPU-Dienst) meldet sich mit seiner Identitaet.
const p = msg.payload || {};
@@ -3411,8 +3423,13 @@ wss.on("connection", (ws) => {
requestId: "dn_" + Date.now() },
timestamp: Date.now() });
} else if (msg.action === "sat_creds_list") {
const requestId = "dc_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
if (msg.reveal) {
credsRevealWaiters.set(requestId, ws);
setTimeout(() => credsRevealWaiters.delete(requestId), 30000);
}
sendToRVS_raw({ type: "sat_creds_list",
payload: { satellite: msg.satellite || "", requestId: "dc_" + Date.now() },
payload: { satellite: msg.satellite || "", ip: msg.ip || "", reveal: !!msg.reveal, requestId },
timestamp: Date.now() });
} else if (msg.action === "sat_creds_set") {
sendToRVS_raw({ type: "sat_creds_set",