Wird verschluesselt auf dem Satelliten gespeichert. Leere Felder speichern nichts / loeschen den Typ. Der Satellit nutzt sie automatisch bei SNMP / HTTP / FritzBox.
+
Wird verschlüsselt auf dem Satelliten gespeichert. Gespeicherte Passwörter stehen als •••••• da — 👁 zeigt sie an. Ein leeres Passwortfeld lässt das gespeicherte Passwort unverändert. Der Satellit nutzt die Daten automatisch bei SNMP / HTTP / FritzBox / SSH.
SNMP
@@ -299,7 +299,7 @@
-
+
@@ -310,11 +310,11 @@
-
+
-
+
@@ -322,13 +322,14 @@
HTTP-Basic
-
+
FritzBox (TR-064)
-
-
+
+
+
@@ -337,8 +338,8 @@
-
-
+
+
@@ -2695,9 +2696,20 @@
if (msg.type === 'sat_creds_list_result') {
const p = msg.payload || msg;
const sat = p.satellite || '';
- const map = {};
+ let map = {};
+ if (p.ip) {
+ // Antwort fuer EIN Geraet (Formular) — Liste der anderen behalten
+ map = { ...(satCreds[sat] || {}) };
+ delete map[p.ip];
+ }
(p.items || []).forEach(it => { map[it.ip] = it.types || []; });
satCreds[sat] = map;
+ const modalOpen = _dcEl('dev-creds-modal').style.display === 'flex';
+ const it = (p.items || []).find(x => x.ip === credsModalIp);
+ if (modalOpen && sat === credsModalSat && it && it.detail && (p.revealed || !dcFilled)) {
+ dcFill(it.detail, !!p.revealed);
+ _dcEl('dc-status').textContent = 'Hinterlegt: ' + (it.types || []).join(', ') + (p.revealed ? ' — im Klartext angezeigt.' : '');
+ }
renderSatellites();
return;
}
@@ -6060,6 +6072,7 @@
requestId: apkRequestId, params: { ip: apkTarget.ip } });
}
function onSatResult(p) {
+ if (p && dcFbReq && p.requestId === dcFbReq) { dcOnFritzUsers(p); return; }
if (!apkRequestId || !p || p.requestId !== apkRequestId) return;
const status = document.getElementById('apk-status');
const log = document.getElementById('apk-log');
@@ -6252,7 +6265,8 @@
'style="padding:1px 6px;font-size:11px;margin-left:6px;">✎' : '';
const hasCreds = d.ip && (satCreds[s.id] || {})[d.ip] && (satCreds[s.id][d.ip] || []).length;
const keyBtn = d.ip ? '' : '';
// APK ausrollen: bei ADB-faehigen Geraeten (Fire TV & Co.).
const canAdb = d.ip && (d.adbCapable || ['firetv', 'android', 'android-adb', 'androidtv', 'dial'].includes(d.type));
@@ -6306,16 +6320,90 @@
document.getElementById('dc-snmp-v3').style.display = (v === '3') ? 'block' : 'none';
document.getElementById('dc-snmp-community-row').style.display = (v === '1' || v === '2c') ? 'block' : 'none';
}
- function openDeviceCreds(satId, ip, name) {
+ // Gespeichertes wird beim Oeffnen angezeigt: Benutzer/Port/Version im
+ // Klartext, Passwoerter nur als "gespeichert". Das Auge holt sie im Klartext
+ // (geht nur an diesen Browser, nicht an andere offene Diagnostic-Tabs).
+ const DC_SECRET_FIELDS = {
+ 'dc-snmp-community': ['snmp', 'community'], 'dc-v3-authkey': ['snmp', 'v3', 'authKey'],
+ 'dc-v3-privkey': ['snmp', 'v3', 'privKey'], 'dc-http-pass': ['http', 'pass'],
+ 'dc-fb-pass': ['fritzbox', 'pass'], 'dc-ssh-pass': ['ssh', 'pass'], 'dc-ssh-key': ['ssh', 'key'],
+ };
+ let dcStored = {}, dcRevealed = false, dcFilled = false, dcFbReq = '';
+ const _dcEl = id => document.getElementById(id);
+ function openDeviceCreds(satId, ip, name, isFritz) {
credsModalSat = satId; credsModalIp = ip;
- document.getElementById('dev-creds-title').textContent = (name || ip) + ' (' + ip + ')';
- // Felder leeren — Secrets werden NIE zurueckgeliefert, nur neu gesetzt.
- ['dc-snmp-community','dc-v3-user','dc-v3-authkey','dc-v3-privkey','dc-http-user','dc-http-pass','dc-fb-user','dc-fb-pass','dc-ssh-user','dc-ssh-port','dc-ssh-pass','dc-ssh-key'].forEach(id => document.getElementById(id).value = '');
- document.getElementById('dc-snmp-version').value = '';
+ dcStored = {}; dcRevealed = false; dcFilled = false; dcFbReq = '';
+ _dcEl('dev-creds-title').textContent = (name || ip) + ' (' + ip + ')';
+ ['dc-snmp-community','dc-v3-user','dc-v3-authkey','dc-v3-privkey','dc-http-user','dc-http-pass','dc-fb-user','dc-fb-pass','dc-ssh-user','dc-ssh-port','dc-ssh-pass','dc-ssh-key'].forEach(id => {
+ const el = _dcEl(id);
+ el.value = '';
+ if (el.dataset.ph == null) el.dataset.ph = el.placeholder;
+ el.placeholder = el.dataset.ph;
+ if (DC_SECRET_FIELDS[id] && el.tagName === 'INPUT') el.type = 'password';
+ });
+ _dcEl('dc-snmp-version').value = '';
dcToggleV3();
+ _dcEl('dc-fb-detect').style.display = isFritz ? '' : 'none';
const set = ((satCreds[satId] || {})[ip]) || [];
- document.getElementById('dc-status').textContent = set.length ? ('Bereits hinterlegt: ' + set.join(', ')) : 'Noch keine Zugangsdaten hinterlegt.';
- document.getElementById('dev-creds-modal').style.display = 'flex';
+ _dcEl('dc-status').textContent = set.length ? ('Bereits hinterlegt: ' + set.join(', ') + ' — lade …') : 'Noch keine Zugangsdaten hinterlegt.';
+ _dcEl('dev-creds-modal').style.display = 'flex';
+ if (set.length) send({ action: 'sat_creds_list', satellite: satId, ip });
+ }
+ function dcFill(detail, revealed) {
+ const g = path => path.reduce((o, k) => (o ? o[k] : undefined), detail || {});
+ if (!dcFilled) {
+ const sn = detail.snmp || {};
+ if (sn.version) { _dcEl('dc-snmp-version').value = sn.version; dcToggleV3(); }
+ const v3 = sn.v3 || {};
+ if (v3.user) _dcEl('dc-v3-user').value = v3.user;
+ if (v3.level) _dcEl('dc-v3-level').value = v3.level;
+ if (v3.authProto) _dcEl('dc-v3-authproto').value = v3.authProto;
+ if (v3.privProto) _dcEl('dc-v3-privproto').value = v3.privProto;
+ _dcEl('dc-http-user').value = (detail.http || {}).user || '';
+ _dcEl('dc-fb-user').value = (detail.fritzbox || {}).user || '';
+ _dcEl('dc-ssh-user').value = (detail.ssh || {}).user || '';
+ _dcEl('dc-ssh-port').value = (detail.ssh || {}).port || '';
+ }
+ for (const [id, path] of Object.entries(DC_SECRET_FIELDS)) {
+ const has = !!g([...path.slice(0, -1), 'has_' + path[path.length - 1]]);
+ const el = _dcEl(id);
+ dcStored[id] = has;
+ if (!has) continue;
+ if (revealed) { el.value = g(path) || ''; if (el.tagName === 'INPUT') el.type = 'text'; }
+ else if (!el.value) el.placeholder = '•••••• gespeichert';
+ }
+ dcFilled = true;
+ if (revealed) dcRevealed = true;
+ }
+ function dcEye(id) {
+ const el = _dcEl(id);
+ if (dcStored[id] && !dcRevealed && !el.value) {
+ _dcEl('dc-status').textContent = 'Hole gespeicherte Zugangsdaten …';
+ send({ action: 'sat_creds_list', satellite: credsModalSat, ip: credsModalIp, reveal: true });
+ return;
+ }
+ if (el.tagName === 'INPUT') el.type = el.type === 'password' ? 'text' : 'password';
+ }
+ // FritzBox: login_sid.lua listet die Benutzer auch ohne Anmeldung. Wer sich
+ // im Browser nur mit Passwort anmeldet, hat trotzdem einen (fritz1234).
+ function dcDetectFritzUser() {
+ dcFbReq = 'fb_' + Date.now();
+ _dcEl('dc-status').textContent = 'Frage die FritzBox nach ihren Benutzern …';
+ send({ action: 'sat_command', satellite: credsModalSat, cmd: 'http.get', requestId: dcFbReq,
+ params: { url: 'http://' + credsModalIp + '/login_sid.lua?version=2' } });
+ }
+ function dcOnFritzUsers(p) {
+ dcFbReq = '';
+ const body = String(((p.result || {}).body) || '');
+ const users = [...body.matchAll(/]*)>([^<]+)<\/User>/g)].map(m => ({ name: m[2].trim(), last: /last="1"/.test(m[1]) }));
+ const st = _dcEl('dc-status');
+ if (!p.ok) { st.textContent = '⚠ FritzBox nicht erreichbar: ' + (p.error || 'Fehler'); return; }
+ if (!users.length) { st.textContent = 'Die Box nennt keine Benutzer — dann reicht das Passwort, Benutzer leer lassen.'; return; }
+ const pick = users.find(u => u.last) || users[0];
+ _dcEl('dc-fb-user').value = pick.name;
+ st.textContent = users.length > 1
+ ? `Benutzer auf der Box: ${users.map(u => u.name).join(', ')} — eingetragen: ${pick.name} (zuletzt angemeldet).`
+ : `Benutzer ermittelt: ${pick.name}`;
}
function closeDeviceCreds() { document.getElementById('dev-creds-modal').style.display = 'none'; }
function saveDeviceCreds() {
@@ -6332,21 +6420,26 @@
} else if (v === '1' || v === '2c') {
const c = document.getElementById('dc-snmp-community').value.trim();
if (c) creds.snmp = { version: v, community: c };
+ else if (dcStored['dc-snmp-community']) creds.snmp = { version: v }; // Community bleibt
}
+ if (creds.snmp && creds.snmp.v3) { // leere Schluesselfelder = gespeicherte behalten
+ if (!creds.snmp.v3.authKey) delete creds.snmp.v3.authKey;
+ if (!creds.snmp.v3.privKey) delete creds.snmp.v3.privKey;
+ }
+ // Leeres Passwortfeld = gespeichertes Passwort behalten (Satellit fuellt es auf).
// HTTP-Basic: Benutzer optional (manche Geraete nutzen Password-only).
const hu = document.getElementById('dc-http-user').value.trim();
const hp = document.getElementById('dc-http-pass').value;
- if (hu || hp) creds.http = { user: hu, pass: hp };
- // FritzBox: Benutzer optional (Password-only-Boxen haben keinen) — es reicht
- // ein Passwort. Gespeichert wird, sobald User ODER Passwort gesetzt ist.
+ if (hu || hp || dcStored['dc-http-pass']) { creds.http = { user: hu }; if (hp) creds.http.pass = hp; }
+ // FritzBox: Benutzer optional — es reicht ein Passwort.
const fu = document.getElementById('dc-fb-user').value.trim();
const fp = document.getElementById('dc-fb-pass').value;
- if (fu || fp) creds.fritzbox = { user: fu, pass: fp };
+ if (fu || fp || dcStored['dc-fb-pass']) { creds.fritzbox = { user: fu }; if (fp) creds.fritzbox.pass = fp; }
// SSH: Benutzer + (Passwort ODER Key). Port optional.
const su = document.getElementById('dc-ssh-user').value.trim();
const sp = document.getElementById('dc-ssh-pass').value;
const sk = document.getElementById('dc-ssh-key').value.trim();
- if (su && (sp || sk)) {
+ if (su && (sp || sk || dcStored['dc-ssh-pass'] || dcStored['dc-ssh-key'])) {
creds.ssh = { user: su };
if (sp) creds.ssh.pass = sp;
if (sk) creds.ssh.key = sk;
diff --git a/diagnostic/server.js b/diagnostic/server.js
index 7f25507..04777aa 100644
--- a/diagnostic/server.js
+++ b/diagnostic/server.js
@@ -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",
diff --git a/satellite/satellite.py b/satellite/satellite.py
index 19c6b8b..850bfbf 100644
--- a/satellite/satellite.py
+++ b/satellite/satellite.py
@@ -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_=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: