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:
+10
-2
@@ -1225,9 +1225,17 @@ META_TOOLS = [
|
||||
"Aktionen: 'wol' (params={'mac':'...'}) zum Aufwecken, "
|
||||
"'snmp.printer' (params={'ip':'<drucker-ip>'}) — BEVORZUGT fuer Drucker-"
|
||||
"Tinte/Toner: liest die Fuellstaende zuverlaessig als Prozent aus der "
|
||||
"Printer-MIB (kein HTML-Scrapen). 'snmp.get'/'snmp.walk' "
|
||||
"Printer-MIB (kein HTML-Scrapen). 'snmp.ports' (params={'ip':'...'}) — "
|
||||
"Switch/Router-Interfaces: aktive/freie Ports + Linkspeed (beantwortet "
|
||||
"'sind noch Ports frei'). 'snmp.info' (params={'ip':'...'}) — Modell, "
|
||||
"Seriennummer, INSTALLIERTE Firmware/Software-Version (ob ein Update "
|
||||
"existiert, weiss SNMP NICHT). 'snmp.get'/'snmp.walk' "
|
||||
"(params={'ip':'...','oid':'...','community':'public'}) fuer beliebige "
|
||||
"SNMP-Werte. "
|
||||
"SNMP-Werte. 'fritzbox.info' / 'fritzbox.hosts' (params={'ip':'<fritzbox>'}) "
|
||||
"— Internetverbindung/Datenrate/externe IP bzw. verbundene Geraete (braucht "
|
||||
"hinterlegten FritzBox-Login). Fuer Geraete mit hinterlegten Zugangsdaten "
|
||||
"(community/v3/Login) nutzt der Satellit diese automatisch — Du musst keine "
|
||||
"Passwoerter mitgeben. "
|
||||
"'http.get'/'http.post' (params={'url':'...'}) fuer lokale Webhooks UND "
|
||||
"um Geraete-Statusseiten zu lesen (Fallback fuer Tinte, NAS ...). Bei grossen "
|
||||
"Seiten NICHT blind paginieren: setze params['contains'] (String oder "
|
||||
|
||||
+143
-2
@@ -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;">×</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 = [];
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -80,6 +80,10 @@ const ALLOWED_TYPES = new Set([
|
||||
// sat_hello, liefern Geraete-Inventar (sat_devices) auf sat_discover und
|
||||
// fuehren Aktionen aus (sat_command → sat_result).
|
||||
"sat_hello", "sat_discover", "sat_devices", "sat_command", "sat_result",
|
||||
// Satelliten-Credential-Store: Diagnostic legt pro Geraet Zugangsdaten ab
|
||||
// (SNMP/HTTP/FritzBox), der Satellit speichert sie verschluesselt.
|
||||
"sat_creds_set", "sat_creds_delete", "sat_creds_list",
|
||||
"sat_creds_result", "sat_creds_list_result",
|
||||
// Compute-Flotte (AI-Boxen): Worker (f5tts/whisper/voxtral/llm-adapter) melden
|
||||
// sich per worker_hello an und pingen per worker_ping; der Diagnostic-Server
|
||||
// aggregiert das und broadcastet worker_update/worker_list an die Browser-UI.
|
||||
|
||||
+12
-1
@@ -29,7 +29,18 @@ CONTROL_ENABLED=true
|
||||
# snmp.get einzelner SNMP-Wert (params: ip, oid)
|
||||
# snmp.walk SNMP-Teilbaum (params: ip, oid)
|
||||
# snmp.printer Drucker-Fuellstaende (Tinte/Toner) aus der Printer-MIB (params: ip)
|
||||
CONTROL_ALLOWLIST=dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer
|
||||
# snmp.ports Switch/Router-Interfaces: aktive/freie Ports (params: ip)
|
||||
# snmp.info Modell/Seriennummer/Firmware-Version (params: ip)
|
||||
# fritzbox.info FritzBox: Verbindung/Datenrate/externe IP (TR-064, braucht Login)
|
||||
# fritzbox.hosts FritzBox: verbundene Geraete (TR-064, braucht Login)
|
||||
CONTROL_ALLOWLIST=dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer,snmp.ports,snmp.info,fritzbox.info,fritzbox.hosts
|
||||
|
||||
# ─── Credential-Store (optional) ───────────────────────────────────
|
||||
# Pro Geraet koennen im Diagnostic Zugangsdaten hinterlegt werden (SNMP-Community/
|
||||
# v3, HTTP-Basic, FritzBox-Login). Der Satellit speichert sie VERSCHLUESSELT im
|
||||
# Bind-Volume ./data. Der Schluessel wird beim ersten Start automatisch erzeugt
|
||||
# (./data/creds.key) — oder hier fest vorgeben (Fernet-Key, base64):
|
||||
# CREDS_KEY=
|
||||
|
||||
# ─── SNMP (optional) ───────────────────────────────────────────────
|
||||
# Defaults fuer die snmp.*-Aktionen; pro Request per params ueberschreibbar.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.env
|
||||
# Verschluesselter Credential-Store + Schluessel (nie einchecken!)
|
||||
data/
|
||||
@@ -21,3 +21,6 @@ services:
|
||||
network_mode: host
|
||||
env_file: .env
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# Persistenter Credential-Store (verschluesselt) + Schluesseldatei.
|
||||
- ./data:/data
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
websockets>=12.0
|
||||
zeroconf>=0.131.0
|
||||
requests>=2.31.0
|
||||
cryptography>=42.0 # Verschluesselung des Geraete-Credential-Stores (Fernet)
|
||||
|
||||
+389
-12
@@ -114,7 +114,8 @@ CONTROL_ENABLED = _env_bool("CONTROL_ENABLED", False)
|
||||
CONTROL_ALLOWLIST = [
|
||||
a.strip() for a in
|
||||
os.environ.get("CONTROL_ALLOWLIST",
|
||||
"dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer").split(",")
|
||||
"dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer,"
|
||||
"snmp.ports,snmp.info,fritzbox.info,fritzbox.hosts").split(",")
|
||||
if a.strip()
|
||||
]
|
||||
|
||||
@@ -156,6 +157,80 @@ SNMP_SYS_OIDS = {
|
||||
"location": "1.3.6.1.2.1.1.6.0", # sysLocation
|
||||
}
|
||||
|
||||
# ─── Geraete-Credential-Store (verschluesselt, pro IP) ─────────────
|
||||
# Diagnostic legt via sat_creds_set pro Geraet Zugangsdaten ab (SNMP-Community/
|
||||
# v3, HTTP-Basic, FritzBox-Login). Der Satellit nutzt sie automatisch bei snmp.*/
|
||||
# http/fritzbox. Persistiert verschluesselt (Fernet) in einem Bind-Volume.
|
||||
CREDS_PATH = os.environ.get("CREDS_PATH", "/data/credentials.json.enc")
|
||||
CREDS_KEY_PATH = os.environ.get("CREDS_KEY_PATH", "/data/creds.key")
|
||||
_CREDS: dict = {} # {ip: {snmp:{...}, http:{...}, fritzbox:{...}}}
|
||||
_creds_fernet = None # Fernet-Instanz (lazy)
|
||||
|
||||
|
||||
def _creds_cipher():
|
||||
"""Fernet-Instanz; Schluessel aus CREDS_KEY (env) oder Schluesseldatei im
|
||||
Volume (wird beim ersten Start erzeugt, 0600)."""
|
||||
global _creds_fernet
|
||||
if _creds_fernet is not None:
|
||||
return _creds_fernet
|
||||
from cryptography.fernet import Fernet
|
||||
key = os.environ.get("CREDS_KEY", "").strip().encode() or None
|
||||
if not key:
|
||||
kp = Path(CREDS_KEY_PATH)
|
||||
if kp.exists():
|
||||
key = kp.read_bytes().strip()
|
||||
else:
|
||||
key = Fernet.generate_key()
|
||||
kp.parent.mkdir(parents=True, exist_ok=True)
|
||||
kp.write_bytes(key)
|
||||
try:
|
||||
os.chmod(kp, 0o600)
|
||||
except OSError:
|
||||
pass
|
||||
logger.info("[creds] neuer Verschluesselungs-Schluessel erzeugt: %s", CREDS_KEY_PATH)
|
||||
_creds_fernet = Fernet(key)
|
||||
return _creds_fernet
|
||||
|
||||
|
||||
def _creds_load() -> None:
|
||||
global _CREDS
|
||||
p = Path(CREDS_PATH)
|
||||
if not p.exists():
|
||||
_CREDS = {}
|
||||
return
|
||||
try:
|
||||
blob = p.read_bytes()
|
||||
raw = _creds_cipher().decrypt(blob)
|
||||
_CREDS = json.loads(raw.decode("utf-8")) or {}
|
||||
logger.info("[creds] %d Geraete-Eintraege geladen", len(_CREDS))
|
||||
except Exception as exc:
|
||||
logger.warning("[creds] laden fehlgeschlagen (%s) — starte leer", exc)
|
||||
_CREDS = {}
|
||||
|
||||
|
||||
def _creds_save() -> None:
|
||||
p = Path(CREDS_PATH)
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
blob = _creds_cipher().encrypt(json.dumps(_CREDS).encode("utf-8"))
|
||||
p.write_bytes(blob)
|
||||
try:
|
||||
os.chmod(p, 0o600)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
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."""
|
||||
out = []
|
||||
for ip, entry in sorted(_CREDS.items()):
|
||||
types = [t for t in ("snmp", "http", "fritzbox") if entry.get(t)]
|
||||
out.append({"ip": ip, "types": types})
|
||||
return out
|
||||
|
||||
HEARTBEAT_SEC = 25
|
||||
|
||||
# mDNS-Servicetypen, die fuer ARIA interessant sind.
|
||||
@@ -459,6 +534,12 @@ async def _control(action: str, params: dict, devices: list[dict]) -> dict:
|
||||
return await loop.run_in_executor(None, _do_snmp, action, params)
|
||||
if action == "snmp.printer":
|
||||
return await loop.run_in_executor(None, _do_snmp_printer, params)
|
||||
if action == "snmp.ports":
|
||||
return await loop.run_in_executor(None, _do_snmp_ports, params)
|
||||
if action == "snmp.info":
|
||||
return await loop.run_in_executor(None, _do_snmp_info, params)
|
||||
if action in ("fritzbox.info", "fritzbox.hosts"):
|
||||
return await loop.run_in_executor(None, _do_fritzbox, action, params)
|
||||
return {"ok": False, "error": f"Aktion '{action}' nicht implementiert."}
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": f"{action} fehlgeschlagen: {exc}"}
|
||||
@@ -529,8 +610,22 @@ def _do_http(action: str, params: dict) -> dict:
|
||||
if not url.startswith(("http://", "https://")):
|
||||
return {"ok": False, "error": "url (http/https) erforderlich."}
|
||||
method = "GET" if action == "http.get" else "POST"
|
||||
# HTTP-Basic-Auth: explizite params > gespeicherte http-Creds fuer den Host.
|
||||
auth = None
|
||||
hcreds = {}
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
host = urlparse(url).hostname or ""
|
||||
hcreds = _creds_for(host).get("http", {})
|
||||
except Exception:
|
||||
pass
|
||||
user = params.get("user") or hcreds.get("user")
|
||||
pw = params.get("pass") or params.get("password") or hcreds.get("pass")
|
||||
if user:
|
||||
auth = (str(user), str(pw or ""))
|
||||
r = requests.request(method, url, data=params.get("body"),
|
||||
headers=params.get("headers"), timeout=HTTP_TIMEOUT_SEC)
|
||||
headers=params.get("headers"), auth=auth,
|
||||
timeout=HTTP_TIMEOUT_SEC)
|
||||
text = r.text
|
||||
total = len(text)
|
||||
|
||||
@@ -581,11 +676,27 @@ def _snmp_run(args: list, timeout: float) -> tuple:
|
||||
return True, r.stdout
|
||||
|
||||
|
||||
def _snmp_base_args(params: dict) -> list:
|
||||
community = str(params.get("community") or SNMP_COMMUNITY)
|
||||
version = str(params.get("version") or SNMP_VERSION)
|
||||
# -t Timeout(s), -r Retries: schnell scheitern statt haengen.
|
||||
return ["-v", version, "-c", community, "-t", "2", "-r", "1"]
|
||||
def _snmp_base_args(params: dict, ip: str = "") -> list:
|
||||
"""Version/Community bzw. v3-Auth. Prioritaet: explizite params > gespeicherte
|
||||
Creds fuer die IP > globale Defaults. OHNE -t/-r (haengt der Aufrufer an)."""
|
||||
creds = _creds_for(ip).get("snmp", {}) if ip else {}
|
||||
version = str(params.get("version") or creds.get("version") or SNMP_VERSION)
|
||||
if version == "3":
|
||||
v3 = creds.get("v3", {}) or {}
|
||||
user = str(params.get("user") or v3.get("user") or "")
|
||||
level = str(params.get("level") or v3.get("level") or "authPriv")
|
||||
args = ["-v", "3", "-u", user, "-l", level]
|
||||
ap = params.get("authProto") or v3.get("authProto")
|
||||
ak = params.get("authKey") or v3.get("authKey")
|
||||
pp = params.get("privProto") or v3.get("privProto")
|
||||
pk = params.get("privKey") or v3.get("privKey")
|
||||
if ap and ak:
|
||||
args += ["-a", str(ap), "-A", str(ak)]
|
||||
if pp and pk:
|
||||
args += ["-x", str(pp), "-X", str(pk)]
|
||||
return args
|
||||
community = str(params.get("community") or creds.get("community") or SNMP_COMMUNITY)
|
||||
return ["-v", version, "-c", community]
|
||||
|
||||
|
||||
def _snmp_target(params: dict) -> str:
|
||||
@@ -603,7 +714,7 @@ def _do_snmp(action: str, params: dict) -> dict:
|
||||
return {"ok": False, "error": "oid erforderlich (z.B. 1.3.6.1.2.1.1.5.0 fuer sysName)."}
|
||||
tool = "snmpwalk" if action == "snmp.walk" else "snmpget"
|
||||
# -OQ: OID = Wert, ohne Typannotation; numerische OIDs brauchen keine MIB-Files.
|
||||
args = [tool, "-OQ", *_snmp_base_args(params), ip, oid]
|
||||
args = [tool, "-OQ", *_snmp_base_args(params, ip), "-t", "2", "-r", "1", ip, oid]
|
||||
ok, out = _snmp_run(args, SNMP_TIMEOUT_SEC)
|
||||
if not ok:
|
||||
return {"ok": False, "error": out}
|
||||
@@ -625,7 +736,7 @@ def _do_snmp_printer(params: dict) -> dict:
|
||||
ip = _snmp_target(params)
|
||||
if not ip:
|
||||
return {"ok": False, "error": "ip/host erforderlich."}
|
||||
base = _snmp_base_args(params)
|
||||
base = [*_snmp_base_args(params, ip), "-t", "2", "-r", "1"]
|
||||
descs = _snmp_walk_values(ip, base, SNMP_SUPPLY_DESC)
|
||||
if not descs:
|
||||
return {"ok": False, "error":
|
||||
@@ -648,6 +759,209 @@ def _do_snmp_printer(params: dict) -> dict:
|
||||
return {"ok": True, "result": {"ip": ip, "supplies": supplies}}
|
||||
|
||||
|
||||
# ifTable (RFC 1213) Spalten:
|
||||
_IF_DESCR = "1.3.6.1.2.1.2.2.1.2"
|
||||
_IF_TYPE = "1.3.6.1.2.1.2.2.1.3"
|
||||
_IF_SPEED = "1.3.6.1.2.1.2.2.1.5"
|
||||
_IF_ADMIN = "1.3.6.1.2.1.2.2.1.7" # up(1) down(2)
|
||||
_IF_OPER = "1.3.6.1.2.1.2.2.1.8" # up(1) down(2) ...
|
||||
_IF_ALIAS = "1.3.6.1.2.1.31.1.1.1.18" # ifAlias (ifXTable, optional)
|
||||
|
||||
|
||||
def _do_snmp_ports(params: dict) -> dict:
|
||||
"""Interface-Uebersicht eines Switches/Routers: welche Ports sind aktiv (Link),
|
||||
welche frei. params: {ip|host, community?/v3?}. ethernetCsmacd(6)=echte Ports;
|
||||
Loopback/VLAN etc. werden als 'other' markiert, nicht als freier Port gezaehlt."""
|
||||
ip = _snmp_target(params)
|
||||
if not ip:
|
||||
return {"ok": False, "error": "ip/host erforderlich."}
|
||||
base = [*_snmp_base_args(params, ip), "-t", "2", "-r", "1"]
|
||||
descr = _snmp_walk_values(ip, base, _IF_DESCR)
|
||||
if not descr:
|
||||
return {"ok": False, "error":
|
||||
"Keine Interface-Daten (kein SNMP / falsche Credentials / kein Switch)."}
|
||||
types = _snmp_walk_values(ip, base, _IF_TYPE)
|
||||
opers = _snmp_walk_values(ip, base, _IF_OPER)
|
||||
admins = _snmp_walk_values(ip, base, _IF_ADMIN)
|
||||
speeds = _snmp_walk_values(ip, base, _IF_SPEED)
|
||||
aliases = _snmp_walk_values(ip, base, _IF_ALIAS)
|
||||
ports = []
|
||||
up = down_free = disabled = 0
|
||||
for i, name in enumerate(descr):
|
||||
itype = _to_int(types[i]) if i < len(types) else None
|
||||
oper = _to_int(opers[i]) if i < len(opers) else None
|
||||
admin = _to_int(admins[i]) if i < len(admins) else None
|
||||
speed = _to_int(speeds[i]) if i < len(speeds) else None
|
||||
is_eth = (itype == 6) # ethernetCsmacd
|
||||
state = ("up" if oper == 1 else
|
||||
"disabled" if admin == 2 else "down")
|
||||
if is_eth:
|
||||
if state == "up":
|
||||
up += 1
|
||||
elif state == "disabled":
|
||||
disabled += 1
|
||||
else:
|
||||
down_free += 1
|
||||
ports.append({
|
||||
"name": name.strip('"'),
|
||||
"alias": (aliases[i].strip('"') if i < len(aliases) else ""),
|
||||
"physical": is_eth,
|
||||
"state": state,
|
||||
"speedMbps": round(speed / 1_000_000) if speed else None,
|
||||
})
|
||||
return {"ok": True, "result": {
|
||||
"ip": ip,
|
||||
"summary": {"physical_ports": up + down_free + disabled,
|
||||
"up": up, "free": down_free, "disabled": disabled},
|
||||
"ports": ports,
|
||||
}}
|
||||
|
||||
|
||||
# entPhysicalTable (RFC 4133) — Modell/Serie/Firmware:
|
||||
_ENT_MODEL = "1.3.6.1.2.1.47.1.1.1.1.13" # entPhysicalModelName
|
||||
_ENT_SERIAL = "1.3.6.1.2.1.47.1.1.1.1.11" # entPhysicalSerialNum
|
||||
_ENT_SWREV = "1.3.6.1.2.1.47.1.1.1.1.10" # entPhysicalSoftwareRev
|
||||
_ENT_FWREV = "1.3.6.1.2.1.47.1.1.1.1.9" # entPhysicalFirmwareRev
|
||||
|
||||
|
||||
def _do_snmp_info(params: dict) -> dict:
|
||||
"""Geraeteinfo: sysName/sysDescr + (falls vorhanden) Modell, Seriennummer,
|
||||
Firmware-/Software-Version aus der Entity-MIB. Sagt die INSTALLIERTE Version —
|
||||
ob ein Update existiert, weiss SNMP nicht (Hersteller-Sache)."""
|
||||
ip = _snmp_target(params)
|
||||
if not ip:
|
||||
return {"ok": False, "error": "ip/host erforderlich."}
|
||||
sysinfo = _snmp_system(ip, str(params.get("community") or ""), str(params.get("version") or ""))
|
||||
base = [*_snmp_base_args(params, ip), "-t", "2", "-r", "1"]
|
||||
|
||||
def _first(oid):
|
||||
vals = [v for v in _snmp_walk_values(ip, base, oid)
|
||||
if v and "No Such" not in v]
|
||||
return vals[0] if vals else None
|
||||
|
||||
result = {
|
||||
"ip": ip,
|
||||
"name": (sysinfo or {}).get("name"),
|
||||
"descr": (sysinfo or {}).get("descr"),
|
||||
"location": (sysinfo or {}).get("location"),
|
||||
"uptime": (sysinfo or {}).get("uptime"),
|
||||
"model": _first(_ENT_MODEL),
|
||||
"serial": _first(_ENT_SERIAL),
|
||||
"firmware": _first(_ENT_FWREV) or _first(_ENT_SWREV),
|
||||
}
|
||||
if not any(result[k] for k in ("name", "descr", "model", "firmware")):
|
||||
return {"ok": False, "error": "Kein SNMP / keine verwertbaren Infos."}
|
||||
return {"ok": True, "result": result}
|
||||
|
||||
|
||||
# ─── FritzBox (TR-064) ─────────────────────────────────────────────
|
||||
# TR-064 ist SOAP+Digest-Auth — zu fummelig fuer on-the-fly http.post, daher ein
|
||||
# schlanker Reader. Braucht FritzBox-Login (Credential-Store, Typ 'fritzbox').
|
||||
|
||||
def _tr064(ip: str, user: str, pw: str, service: str, control: str,
|
||||
action: str, args: Optional[dict] = None) -> dict:
|
||||
"""Ein TR-064-SOAP-Call. Gibt {ok, fields|error}. fields = alle <NewX>-Tags."""
|
||||
import requests
|
||||
from requests.auth import HTTPDigestAuth
|
||||
body = "".join(f"<{k}>{v}</{k}>" for k, v in (args or {}).items())
|
||||
envelope = (
|
||||
'<?xml version="1.0"?>'
|
||||
'<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" '
|
||||
's:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>'
|
||||
f'<u:{action} xmlns:u="{service}">{body}</u:{action}>'
|
||||
'</s:Body></s:Envelope>'
|
||||
)
|
||||
url = f"http://{ip}:49000{control}"
|
||||
try:
|
||||
r = requests.post(url, data=envelope.encode("utf-8"),
|
||||
headers={"Content-Type": 'text/xml; charset="utf-8"',
|
||||
"SOAPAction": f"{service}#{action}"},
|
||||
auth=HTTPDigestAuth(user, pw), timeout=HTTP_TIMEOUT_SEC)
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": f"TR-064 nicht erreichbar: {exc}"}
|
||||
if r.status_code == 401:
|
||||
return {"ok": False, "error": "TR-064 Auth fehlgeschlagen (FritzBox-Login pruefen)."}
|
||||
if r.status_code != 200:
|
||||
return {"ok": False, "error": f"TR-064 HTTP {r.status_code}"}
|
||||
fields = {m.group(1): m.group(2) for m in
|
||||
re.finditer(r"<(New[^>/]+)>(.*?)</\1>", r.text, re.DOTALL)}
|
||||
return {"ok": True, "fields": fields}
|
||||
|
||||
|
||||
def _do_fritzbox(action: str, params: dict) -> dict:
|
||||
"""fritzbox.info -> Modell/Firmware/Verbindung/externe IP/Datenrate.
|
||||
fritzbox.hosts -> Liste der bekannten Geraete (Name/IP/MAC/aktiv)."""
|
||||
ip = _snmp_target(params)
|
||||
if not ip:
|
||||
return {"ok": False, "error": "ip/host erforderlich."}
|
||||
fb = _creds_for(ip).get("fritzbox", {})
|
||||
user = str(params.get("user") or fb.get("user") or "")
|
||||
pw = str(params.get("pass") or params.get("password") or fb.get("pass") or "")
|
||||
if not pw:
|
||||
return {"ok": False, "error":
|
||||
"Kein FritzBox-Login hinterlegt. In der Geraeteliste Credentials "
|
||||
"(Typ 'fritzbox') fuer diese IP setzen."}
|
||||
|
||||
if action == "fritzbox.hosts":
|
||||
p = _tr064(ip, user, pw, "urn:dslforum-org:service:Hosts:1",
|
||||
"/upnp/control/hosts", "X_AVM-DE_GetHostListPath")
|
||||
if not p.get("ok"):
|
||||
return p
|
||||
path = p["fields"].get("NewX_AVM-DE_HostListPath", "")
|
||||
if not path:
|
||||
return {"ok": False, "error": "FritzBox lieferte keinen Host-Listen-Pfad."}
|
||||
import requests
|
||||
from requests.auth import HTTPDigestAuth
|
||||
try:
|
||||
r = requests.get(f"http://{ip}:49000{path}",
|
||||
auth=HTTPDigestAuth(user, pw), timeout=HTTP_TIMEOUT_SEC)
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": f"Host-Liste nicht abrufbar: {exc}"}
|
||||
hosts = []
|
||||
for item in re.finditer(r"<Item>(.*?)</Item>", r.text, re.DOTALL):
|
||||
blk = item.group(1)
|
||||
|
||||
def _t(tag):
|
||||
m = re.search(rf"<{tag}>(.*?)</{tag}>", blk, re.DOTALL)
|
||||
return m.group(1) if m else ""
|
||||
hosts.append({"name": _t("HostName"), "ip": _t("IPAddress"),
|
||||
"mac": _t("MACAddress"),
|
||||
"active": _t("Active") in ("1", "true")})
|
||||
return {"ok": True, "result": {"ip": ip, "count": len(hosts), "hosts": hosts}}
|
||||
|
||||
# fritzbox.info (Default): mehrere Services, Teil-Fehler tolerieren.
|
||||
info = {"ip": ip}
|
||||
dev = _tr064(ip, user, pw, "urn:dslforum-org:service:DeviceInfo:1",
|
||||
"/upnp/control/deviceinfo", "GetInfo")
|
||||
if dev.get("ok"):
|
||||
f = dev["fields"]
|
||||
info.update({"model": f.get("NewModelName"), "firmware": f.get("NewSoftwareVersion"),
|
||||
"serial": f.get("NewSerialNumber"), "uptime_s": _to_int(f.get("NewUpTime"))})
|
||||
st = _tr064(ip, user, pw, "urn:dslforum-org:service:WANIPConnection:1",
|
||||
"/upnp/control/wanipconnection1", "GetStatusInfo")
|
||||
if st.get("ok"):
|
||||
info["connection"] = st["fields"].get("NewConnectionStatus")
|
||||
info["connection_uptime_s"] = _to_int(st["fields"].get("NewUptime"))
|
||||
ext = _tr064(ip, user, pw, "urn:dslforum-org:service:WANIPConnection:1",
|
||||
"/upnp/control/wanipconnection1", "GetExternalIPAddress")
|
||||
if ext.get("ok"):
|
||||
info["external_ip"] = ext["fields"].get("NewExternalIPAddress")
|
||||
link = _tr064(ip, user, pw, "urn:dslforum-org:service:WANCommonInterfaceConfig:1",
|
||||
"/upnp/control/wancommonifconfig1", "GetCommonLinkProperties")
|
||||
if link.get("ok"):
|
||||
f = link["fields"]
|
||||
dn = _to_int(f.get("NewLayer1DownstreamMaxBitRate"))
|
||||
upr = _to_int(f.get("NewLayer1UpstreamMaxBitRate"))
|
||||
info["downstream_mbit"] = round(dn / 1_000_000, 1) if dn else None
|
||||
info["upstream_mbit"] = round(upr / 1_000_000, 1) if upr else None
|
||||
info["physical_link"] = f.get("NewPhysicalLinkStatus")
|
||||
if len(info) == 1:
|
||||
return {"ok": False, "error":
|
||||
"FritzBox antwortet nicht auf TR-064 (Login/Rechte pruefen; TR-064 in "
|
||||
"der FritzBox unter Heimnetz > Netzwerkeinstellungen aktivieren)."}
|
||||
return {"ok": True, "result": info}
|
||||
|
||||
|
||||
def _to_int(s: str):
|
||||
try:
|
||||
return int(str(s).strip())
|
||||
@@ -658,9 +972,14 @@ def _to_int(s: str):
|
||||
def _snmp_system(ip: str, community: str = "", version: str = "") -> Optional[dict]:
|
||||
"""Fragt die SNMP-System-Group eines Hosts ab (ein snmpget, alle 6 OIDs).
|
||||
Gibt {descr,name,contact,location,uptime,objectid} oder None (kein SNMP).
|
||||
Kurzer Timeout, keine Retries -> Nicht-SNMP-Hosts scheitern schnell."""
|
||||
base = ["-v", str(version or SNMP_VERSION), "-c", str(community or SNMP_COMMUNITY),
|
||||
"-t", "1", "-r", "0"]
|
||||
Nutzt gespeicherte Creds fuer die IP; kurzer Timeout, keine Retries ->
|
||||
Nicht-SNMP-Hosts scheitern schnell."""
|
||||
params = {}
|
||||
if community:
|
||||
params["community"] = community
|
||||
if version:
|
||||
params["version"] = version
|
||||
base = [*_snmp_base_args(params, ip), "-t", "1", "-r", "0"]
|
||||
keys = list(SNMP_SYS_OIDS.keys())
|
||||
ok, out = _snmp_run(["snmpget", "-Oqv", *base, ip, *SNMP_SYS_OIDS.values()],
|
||||
SNMP_DISCOVERY_TIMEOUT)
|
||||
@@ -850,6 +1169,63 @@ class Satellite:
|
||||
"timestamp": int(time.time() * 1000),
|
||||
})
|
||||
|
||||
elif mtype == "sat_creds_set":
|
||||
if not self._for_me(payload):
|
||||
return
|
||||
ip = (payload.get("ip") or "").strip()
|
||||
creds = payload.get("creds") or {}
|
||||
ok = False
|
||||
if ip and isinstance(creds, dict):
|
||||
entry = _CREDS.setdefault(ip, {})
|
||||
for t in ("snmp", "http", "fritzbox"):
|
||||
if t in creds:
|
||||
if creds[t]: # leeres Objekt = Typ loeschen
|
||||
entry[t] = creds[t]
|
||||
else:
|
||||
entry.pop(t, None)
|
||||
if not entry:
|
||||
_CREDS.pop(ip, None)
|
||||
try:
|
||||
_creds_save()
|
||||
ok = True
|
||||
except Exception as exc:
|
||||
logger.warning("[creds] speichern fehlgeschlagen: %s", exc)
|
||||
await self._send({"type": "sat_creds_result",
|
||||
"payload": {"requestId": payload.get("requestId", ""),
|
||||
"satellite": SATELLITE_ID, "ok": ok, "ip": ip},
|
||||
"timestamp": int(time.time() * 1000)})
|
||||
|
||||
elif mtype == "sat_creds_delete":
|
||||
if not self._for_me(payload):
|
||||
return
|
||||
ip = (payload.get("ip") or "").strip()
|
||||
ctype = (payload.get("type") or "").strip()
|
||||
if ip in _CREDS:
|
||||
if ctype:
|
||||
_CREDS[ip].pop(ctype, None)
|
||||
if not _CREDS[ip]:
|
||||
_CREDS.pop(ip, None)
|
||||
else:
|
||||
_CREDS.pop(ip, None)
|
||||
try:
|
||||
_creds_save()
|
||||
except Exception as exc:
|
||||
logger.warning("[creds] speichern fehlgeschlagen: %s", exc)
|
||||
await self._send({"type": "sat_creds_result",
|
||||
"payload": {"requestId": payload.get("requestId", ""),
|
||||
"satellite": SATELLITE_ID, "ok": True, "ip": ip},
|
||||
"timestamp": int(time.time() * 1000)})
|
||||
|
||||
elif mtype == "sat_creds_list":
|
||||
if not self._for_me(payload):
|
||||
return
|
||||
await self._send({"type": "sat_creds_list_result",
|
||||
"payload": {"requestId": payload.get("requestId", ""),
|
||||
"satellite": SATELLITE_ID,
|
||||
"location": SATELLITE_LOCATION,
|
||||
"items": _creds_public_summary()},
|
||||
"timestamp": int(time.time() * 1000)})
|
||||
|
||||
async def _periodic_scan(self) -> None:
|
||||
while True:
|
||||
try:
|
||||
@@ -871,6 +1247,7 @@ class Satellite:
|
||||
if not RVS_HOST or not RVS_TOKEN:
|
||||
logger.error("RVS_HOST und RVS_TOKEN sind Pflicht (siehe .env.example).")
|
||||
return
|
||||
_creds_load()
|
||||
asyncio.create_task(self._periodic_scan())
|
||||
backoff = 1
|
||||
while True:
|
||||
|
||||
Reference in New Issue
Block a user