feat(satellite): Credential-Store + snmp.ports/info + FritzBox-Reader

Baut SNMP zur generellen Geraete-Auskunft aus und macht Zugangsdaten pro Geraet
hinterlegbar, damit der Satellit tiefer lesen kann.

Satellit:
- Verschluesselter Credential-Store (Fernet) pro IP im Bind-Volume ./data:
  SNMP (community/v2c oder v3 user/auth/priv), HTTP-Basic, FritzBox-Login.
  Neue Messages sat_creds_set/list/delete + *_result; Secrets werden NIE
  zurueckgeliefert (list gibt nur Typen). snmp.*/http/fritzbox nutzen die
  Creds automatisch (params haben Vorrang).
- snmp.ports: ifTable -> aktive/freie Ports, Linkspeed ('sind noch Ports frei').
- snmp.info: sysName/Descr + Entity-MIB Modell/Serial/Firmware (installierte
  Version; Update-Check ist Hersteller-Sache, kein SNMP).
- fritzbox.info/hosts: TR-064 (SOAP+Digest) — Verbindung/Datenrate/externe IP
  bzw. verbundene Geraete. Bewusst als Reader, weil TR-064 fuer on-the-fly
  http.post zu fummelig ist.
- CONTROL_ALLOWLIST-Default + .env.example erweitert; data/ ge-gitignore-t.

RVS: sat_creds_* in ALLOWED_TYPES (sonst verworfen).

Diagnostic: pro entdecktem Geraet ein Schluessel-Button -> Modal (SNMP v2c/v3,
HTTP-Basic, FritzBox); Speichern/Loeschen via RVS an den Satelliten; gesetzte
Typen werden pro Geraet angezeigt (🔑 ✓). Server relayed die Cred-Messages.

Brain: satellite_command-Tool um snmp.ports/info + fritzbox.* erweitert; ARIA
muss keine Passwoerter mitgeben (Satellit nutzt den Store).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 17:16:06 +02:00
co-authored by Claude Opus 4.8
parent 8b1555f926
commit f9cc10433a
9 changed files with 582 additions and 17 deletions
+143 -2
View File
@@ -196,6 +196,66 @@
</div>
</div>
<!-- Geraete-Credentials Modal -->
<div id="dev-creds-modal" style="display:none;position:fixed;inset:0;z-index:1002;background:rgba(0,0,0,0.8);align-items:center;justify-content:center;">
<div style="background:#0D0D1A;border:1px solid #2A2A3E;border-radius:10px;padding:18px;max-width:520px;width:94%;max-height:92vh;overflow:auto;display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;align-items:center;justify-content:space-between;">
<h3 style="margin:0;color:#fff;">🔑 Zugangsdaten: <span id="dev-creds-title"></span></h3>
<button onclick="closeDeviceCreds()" style="background:none;border:none;color:#8888AA;font-size:22px;cursor:pointer;">&times;</button>
</div>
<p style="color:#8888AA;font-size:11px;margin:0;">Wird verschluesselt auf dem Satelliten gespeichert. Leere Felder speichern nichts / loeschen den Typ. Der Satellit nutzt sie automatisch bei SNMP / HTTP / FritzBox.</p>
<div style="border:1px solid #1E1E2E;border-radius:6px;padding:10px;">
<div style="color:#E0E0F0;font-weight:600;margin-bottom:6px;">SNMP</div>
<label style="font-size:11px;color:#8888AA;">Version</label>
<select id="dc-snmp-version" onchange="dcToggleV3()" style="width:100%;margin-bottom:6px;">
<option value="">— nicht setzen —</option>
<option value="2c">v2c (Community)</option>
<option value="1">v1 (Community)</option>
<option value="3">v3 (User/Auth)</option>
</select>
<div id="dc-snmp-community-row">
<label style="font-size:11px;color:#8888AA;">Community</label>
<input id="dc-snmp-community" placeholder="public" style="width:100%;margin-bottom:4px;">
</div>
<div id="dc-snmp-v3" style="display:none;">
<input id="dc-v3-user" placeholder="v3 User" style="width:100%;margin-bottom:4px;">
<select id="dc-v3-level" style="width:100%;margin-bottom:4px;">
<option value="authPriv">authPriv</option>
<option value="authNoPriv">authNoPriv</option>
<option value="noAuthNoPriv">noAuthNoPriv</option>
</select>
<div style="display:flex;gap:4px;margin-bottom:4px;">
<select id="dc-v3-authproto" style="flex:1;"><option value="SHA">SHA</option><option value="MD5">MD5</option><option value="SHA-256">SHA-256</option></select>
<input id="dc-v3-authkey" placeholder="Auth-Passwort" style="flex:2;">
</div>
<div style="display:flex;gap:4px;">
<select id="dc-v3-privproto" style="flex:1;"><option value="AES">AES</option><option value="DES">DES</option><option value="AES-256">AES-256</option></select>
<input id="dc-v3-privkey" placeholder="Priv-Passwort" style="flex:2;">
</div>
</div>
</div>
<div style="border:1px solid #1E1E2E;border-radius:6px;padding:10px;">
<div style="color:#E0E0F0;font-weight:600;margin-bottom:6px;">HTTP-Basic</div>
<input id="dc-http-user" placeholder="Benutzer" style="width:100%;margin-bottom:4px;">
<input id="dc-http-pass" type="password" placeholder="Passwort" style="width:100%;">
</div>
<div style="border:1px solid #1E1E2E;border-radius:6px;padding:10px;">
<div style="color:#E0E0F0;font-weight:600;margin-bottom:6px;">FritzBox (TR-064)</div>
<input id="dc-fb-user" placeholder="FritzBox-Benutzer" style="width:100%;margin-bottom:4px;">
<input id="dc-fb-pass" type="password" placeholder="FritzBox-Passwort" style="width:100%;">
</div>
<div style="display:flex;gap:8px;justify-content:flex-end;">
<button class="btn secondary" onclick="deleteDeviceCreds()" style="color:#FF6E6E;">Alle loeschen</button>
<button class="btn" onclick="saveDeviceCreds()">Speichern</button>
</div>
<div id="dc-status" style="font-size:11px;color:#8888AA;min-height:14px;"></div>
</div>
</div>
<!-- Voice-Preview Modal -->
<div id="voice-preview-modal" style="display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,0.7);align-items:center;justify-content:center;">
<div style="background:#1A1A2E;border:1px solid #2A2A3E;border-radius:10px;padding:20px;max-width:560px;width:90%;display:flex;flex-direction:column;gap:12px;">
@@ -2062,7 +2122,29 @@
return;
}
if (msg.type === 'sat_update') { satellites = msg.satellites || []; renderSatellites(); return; }
if (msg.type === 'sat_update') {
satellites = msg.satellites || [];
renderSatellites();
satellites.forEach(s => { if (s.online !== false) requestSatCreds(s.id); });
return;
}
if (msg.type === 'sat_creds_list_result') {
const p = msg.payload || msg;
const sat = p.satellite || '';
const map = {};
(p.items || []).forEach(it => { map[it.ip] = it.types || []; });
satCreds[sat] = map;
renderSatellites();
return;
}
if (msg.type === 'sat_creds_result') {
const p = msg.payload || msg;
const el = document.getElementById('dc-status');
if (el) el.textContent = p.ok ? '✓ Gespeichert.' : '⚠ Fehlgeschlagen.';
if (p.ok && p.satellite) requestSatCreds(p.satellite);
if (p.ok) setTimeout(closeDeviceCreds, 700);
return;
}
if (msg.type === 'worker_update') { workers = msg.workers || []; renderWorkers(); if (typeof refreshLocalLlmModelChoices === 'function') refreshLocalLlmModelChoices(); if (typeof renderLlmCatalog === 'function') renderLlmCatalog(); return; }
if (msg.type === 'llm_response') {
const p = msg.payload || {};
@@ -4455,10 +4537,16 @@
let satellites = []; // [{id, location, caps, control, online}]
let satDevices = {}; // id → {devices, location, ts}
let satScanning = null; // id des gerade scannenden Satelliten
let satCreds = {}; // satId → { ip → [typen] } (welche Creds gesetzt sind, OHNE Secrets)
let credsModalSat = ''; // aktuell im Modal bearbeiteter Satellit
let credsModalIp = '';
function requestSatellites() {
send({ action: 'sat_list' });
}
function requestSatCreds(id) {
send({ action: 'sat_creds_list', satellite: id });
}
function scanSatellite(id) {
satScanning = id;
renderSatellites();
@@ -4490,13 +4578,20 @@
dev.devices.map(d => {
const tags = [];
if (d.model) tags.push(escapeHtml(d.model));
if (d.snmp && d.snmp.descr) tags.push(escapeHtml(String(d.snmp.descr).slice(0, 40)));
if (d.dialAppUrl) tags.push('DIAL');
if (d.mac) tags.push(escapeHtml(d.mac));
const tagStr = tags.length ? ' <span style="color:#666;">(' + tags.join(' · ') + ')</span>' : '';
return '<div style="padding:3px 6px;background:#0D0D1A;border-radius:4px;">' +
const hasCreds = d.ip && (satCreds[s.id] || {})[d.ip] && (satCreds[s.id][d.ip] || []).length;
const keyBtn = d.ip ? '<button class="btn secondary" title="Zugangsdaten (SNMP / HTTP / FritzBox)" ' +
'onclick="openDeviceCreds(\'' + escapeHtml(s.id) + '\',\'' + escapeHtml(d.ip) + '\',\'' + escapeHtml(d.name || d.ip) + '\')" ' +
'style="padding:1px 6px;font-size:11px;margin-left:8px;">' + (hasCreds ? '🔑 ✓' : '🔑') + '</button>' : '';
return '<div style="padding:3px 6px;background:#0D0D1A;border-radius:4px;display:flex;align-items:center;">' +
'<div style="flex:1;min-width:0;">' +
'<span style="color:#E0E0F0;">' + escapeHtml(d.name || d.id || '?') + '</span> ' +
'<span style="color:#0096FF;">[' + escapeHtml(d.type || '?') + ']</span> ' +
'<span style="color:#8888AA;">' + escapeHtml(d.ip || '') + '</span>' + tagStr +
'</div>' + keyBtn +
'</div>';
}).join('') + '</div>';
}
@@ -4519,6 +4614,52 @@
}).join('');
}
// ── Geraete-Credentials ────────────────────────────────
function dcToggleV3() {
const v = document.getElementById('dc-snmp-version').value;
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) {
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'].forEach(id => document.getElementById(id).value = '');
document.getElementById('dc-snmp-version').value = '';
dcToggleV3();
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';
}
function closeDeviceCreds() { document.getElementById('dev-creds-modal').style.display = 'none'; }
function saveDeviceCreds() {
const v = document.getElementById('dc-snmp-version').value;
const creds = {};
if (v === '3') {
const user = document.getElementById('dc-v3-user').value.trim();
if (user) creds.snmp = { version: '3', v3: {
user, level: document.getElementById('dc-v3-level').value,
authProto: document.getElementById('dc-v3-authproto').value,
authKey: document.getElementById('dc-v3-authkey').value,
privProto: document.getElementById('dc-v3-privproto').value,
privKey: document.getElementById('dc-v3-privkey').value } };
} else if (v === '1' || v === '2c') {
const c = document.getElementById('dc-snmp-community').value.trim();
if (c) creds.snmp = { version: v, community: c };
}
const hu = document.getElementById('dc-http-user').value.trim();
if (hu) creds.http = { user: hu, pass: document.getElementById('dc-http-pass').value };
const fu = document.getElementById('dc-fb-user').value.trim();
if (fu) creds.fritzbox = { user: fu, pass: document.getElementById('dc-fb-pass').value };
if (!Object.keys(creds).length) { document.getElementById('dc-status').textContent = 'Nichts zu speichern — mindestens einen Typ ausfuellen.'; return; }
send({ action: 'sat_creds_set', satellite: credsModalSat, ip: credsModalIp, creds });
document.getElementById('dc-status').textContent = 'Speichere …';
}
function deleteDeviceCreds() {
send({ action: 'sat_creds_delete', satellite: credsModalSat, ip: credsModalIp });
document.getElementById('dc-status').textContent = 'Loesche …';
}
// ── Triggers-Verwaltung ────────────────────────────────
let triggersCache = [];
+17
View File
@@ -1071,6 +1071,9 @@ function connectRVS(forcePlain) {
if (p.satellite && satellites.has(p.satellite)) satellites.get(p.satellite).last_seen = Date.now();
broadcast({ type: "sat_devices", satellite: p.satellite || "",
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 || {} });
} else if (msg.type === "worker_hello") {
// Ein Compute-Worker (GPU-Dienst) meldet sich mit seiner Identitaet.
const p = msg.payload || {};
@@ -2759,6 +2762,20 @@ wss.on("connection", (ws) => {
sendToRVS_raw({ type: "sat_discover",
payload: { satellite: msg.satellite || "", force: true },
timestamp: Date.now() });
} else if (msg.action === "sat_creds_list") {
sendToRVS_raw({ type: "sat_creds_list",
payload: { satellite: msg.satellite || "", requestId: "dc_" + Date.now() },
timestamp: Date.now() });
} else if (msg.action === "sat_creds_set") {
sendToRVS_raw({ type: "sat_creds_set",
payload: { satellite: msg.satellite || "", ip: msg.ip || "",
creds: msg.creds || {}, requestId: "dc_" + Date.now() },
timestamp: Date.now() });
} else if (msg.action === "sat_creds_delete") {
sendToRVS_raw({ type: "sat_creds_delete",
payload: { satellite: msg.satellite || "", ip: msg.ip || "",
type: msg.credType || "", requestId: "dc_" + Date.now() },
timestamp: Date.now() });
} else if (msg.action === "test_proxy") {
testProxy(msg.text);
} else if (msg.action === "check_proxy_auth") {