Files
MUA-Mikes-Unraid-Agent/scripts/mua.page
T

179 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
Menu="Utilities"
Type="xmenu"
Title="MUA"
Icon="cubes"
---
<?php
/** MUA WebGUI — sicherheitsorientierte Verwaltung. */
$port = 3002;
$config_port = 3013;
$pidfile = '/var/run/mua.pid';
$binary = '/usr/local/bin/mua';
$admin_token_file = '/var/run/mua-admin.token';
$admin_token = is_readable($admin_token_file) ? trim(file_get_contents($admin_token_file)) : '';
$unraid_csrf = $var['csrf_token'] ?? '';
$host_ip = $_SERVER['SERVER_ADDR'] ?? '127.0.0.1';
$endpoint = 'http://' . $host_ip . ':' . $port . '/mcp';
function mua_api($method, $path, $payload, $token, $port) {
if (!$token) return [0, null];
$ch = curl_init('http://127.0.0.1:' . $port . $path);
$headers = ['X-MUA-Admin-Token: ' . $token];
$options = [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_CONNECTTIMEOUT => 2, CURLOPT_HTTPHEADER => $headers];
if ($method === 'POST') {
$options[CURLOPT_POST] = true;
if ($payload !== null) {
$options[CURLOPT_POSTFIELDS] = json_encode($payload);
$headers[] = 'Content-Type: application/json';
$options[CURLOPT_HTTPHEADER] = $headers;
}
}
curl_setopt_array($ch, $options);
$raw = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [$code, $raw !== false ? json_decode($raw, true) : null];
}
function csrf_value($token, $offset = 0) {
return hash_hmac('sha256', gmdate('Y-m-d-H', time() + $offset * 3600), $token);
}
function mua_health($port) {
$ch = curl_init('http://127.0.0.1:' . $port . '/health');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 3, CURLOPT_CONNECTTIMEOUT => 2]);
$raw = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $code === 200 && $raw !== false ? json_decode($raw, true) : null;
}
$running = false;
$pid = null;
if (file_exists($pidfile)) {
$pid = trim(file_get_contents($pidfile));
$running = $pid && is_numeric($pid) && @posix_kill((int)$pid, 0);
}
$health_data = mua_health($port);
$health_ok = is_array($health_data) && ($health_data['status'] ?? '') === 'ok';
$running_version = $health_data['version'] ?? '';
[$config_code, $config] = mua_api('GET', '/config', null, $admin_token, $config_port);
$config_ok = $config_code === 200 && is_array($config);
$flash_msg = '';
$flash_type = '';
$new_api_key = '';
$csrf = csrf_value($admin_token);
$readonly = [
'unraid_docker_list', 'unraid_docker_inspect', 'unraid_docker_logs',
'unraid_docker_analyze_logs', 'unraid_docker_processes', 'unraid_docker_stats',
'unraid_docker_info', 'unraid_network_inventory', 'unraid_network_list',
'unraid_network_inspect', 'unraid_network_host_state', 'unraid_system_connection_test',
'unraid_system_shell_readonly', 'unraid_docker_update_status',
'unraid_system_health', 'unraid_storage_status', 'unraid_disk_health',
'unraid_notifications_list', 'unraid_shares_list', 'unraid_share_inspect',
];
$operator = array_merge($readonly, [
'unraid_network_audit_tcp', 'unraid_network_lan_probe',
'unraid_ca_search', 'unraid_ca_install_preview',
'unraid_docker_start', 'unraid_docker_stop', 'unraid_docker_restart',
]);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$posted_csrf = $_POST['mua_csrf'] ?? '';
$csrf_ok = $admin_token && (hash_equals(csrf_value($admin_token), $posted_csrf) || hash_equals(csrf_value($admin_token, -1), $posted_csrf));
if (!$csrf_ok) {
$flash_msg = 'Sicherheitsprüfung fehlgeschlagen. Seite neu laden und erneut versuchen.';
$flash_type = 'err';
} else {
$action = $_POST['action'] ?? '';
$payload = null;
if ($action === 'generate_key') $payload = ['generate' => true];
if ($action === 'preset_readonly') $payload = ['enabledTools' => $readonly, 'allToolsEnabled' => false];
if ($action === 'preset_operator') $payload = ['enabledTools' => $operator, 'allToolsEnabled' => false];
if ($action === 'preset_none') $payload = ['enabledTools' => [], 'allToolsEnabled' => false];
if ($action === 'preset_all') $payload = ['enabledTools' => [], 'allToolsEnabled' => true];
if ($action === 'save_tools') {
$selected = [];
foreach (($config['allTools'] ?? []) as $tool) {
$name = is_array($tool) ? ($tool['name'] ?? '') : $tool;
if ($name && isset($_POST['tool_' . $name])) $selected[] = $name;
}
$payload = ['enabledTools' => $selected, 'allToolsEnabled' => false];
}
if ($action === 'restart') {
[$code, $result] = mua_api('POST', '/restart', null, $admin_token, $config_port);
$flash_msg = $code === 200 ? 'MUA wird neu gestartet.' : 'Neustart fehlgeschlagen (HTTP ' . $code . ').';
$flash_type = $code === 200 ? 'ok' : 'err';
} elseif ($payload !== null) {
[$code, $result] = mua_api('POST', '/config', $payload, $admin_token, $config_port);
if ($code === 200 && is_array($result) && ($result['ok'] ?? false)) {
$new_api_key = $result['generatedApiKey'] ?? '';
$flash_msg = $new_api_key ? 'Neuer API-Key erzeugt. Jetzt kopieren – er wird nur dieses eine Mal angezeigt.' : 'Sicherheitseinstellungen gespeichert.';
$flash_type = 'ok';
[$config_code, $config] = mua_api('GET', '/config', null, $admin_token, $config_port);
$config_ok = $config_code === 200 && is_array($config);
} else {
$flash_msg = 'Speichern fehlgeschlagen (HTTP ' . $code . ').';
$flash_type = 'err';
}
}
}
}
$enabled_tools = $config['enabledTools'] ?? [];
$all_tools_enabled = (bool)($config['allToolsEnabled'] ?? false);
$all_tools = $config['allTools'] ?? [];
$risk_groups = ['read' => [], 'active' => [], 'write' => [], 'critical' => []];
foreach ($all_tools as $tool) {
if (!is_array($tool)) $tool = ['name' => $tool, 'description' => '', 'risk' => 'read'];
$risk = $tool['risk'] ?? 'read';
if (!isset($risk_groups[$risk])) $risk = 'read';
$risk_groups[$risk][] = $tool;
}
$risk_labels = [
'read' => ['Nur Lesen', 'Liest Status und Diagnoseinformationen.', 'mua-risk-safe'],
'active' => ['Aktive Prüfungen', 'Baut gezielt Netzwerkverbindungen für Tests auf.', 'mua-risk-notice'],
'write' => ['Betrieb steuern', 'Startet, stoppt oder startet Container neu.', 'mua-risk-warn'],
'critical' => ['Kritischer Zugriff', 'Verändert Container oder führt uneingeschränkte Root-Befehle aus.', 'mua-risk-danger'],
];
$active_count = $all_tools_enabled ? count($all_tools) : count($enabled_tools);
?>
<style>
.mua-wrap{max-width:1280px}.mua-hero{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;margin:12px 0 20px}.mua-title h2{margin:0 0 5px;font-size:1.55rem}.mua-muted{color:#777}.mua-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(285px,1fr));gap:14px;margin:14px 0;align-items:start}.mua-card{border:1px solid #d8d8d8;border-radius:9px;padding:16px;background:rgba(255,255,255,.03)}.mua-card h3{margin:0 0 8px}.mua-status{padding:11px 14px;border-radius:7px;margin:10px 0;border:1px solid}.mua-status.ok,.mua-risk-safe{border-color:#43a047;background:rgba(67,160,71,.11)}.mua-status.err,.mua-risk-danger{border-color:#e53935;background:rgba(229,57,53,.11)}.mua-status.warn,.mua-risk-warn{border-color:#fb8c00;background:rgba(251,140,0,.12)}.mua-risk-notice{border-color:#1e88e5;background:rgba(30,136,229,.10)}.mua-pill{display:inline-block;padding:3px 9px;border-radius:20px;font-size:.82em;font-weight:600;border:1px solid currentColor}.mua-endpoint{font-size:1.05em;word-break:break-all}.mua-key{display:block;padding:12px;border:1px solid #43a047;border-radius:6px;word-break:break-all;font-size:1.04em;background:rgba(67,160,71,.08)}.mua-actions{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}.mua-btn{padding:8px 14px;border-radius:5px;border:1px solid #aaa;cursor:pointer}.mua-btn-primary{background:#168bd2;color:#fff;border-color:#168bd2}.mua-btn-danger{background:#c62828;color:#fff;border-color:#c62828}.mua-tool{display:grid;grid-template-columns:22px minmax(0,1fr);gap:8px;padding:9px 0;border-top:1px solid rgba(128,128,128,.2);align-items:start}.mua-tool:first-of-type{border-top:0}.mua-tool code{font-size:.86em;overflow-wrap:anywhere}.mua-tool small{display:block;color:#777;margin-top:3px;line-height:1.35}.mua-section{margin-top:24px}.mua-summary{font-size:1.08em}.mua-preset{margin:0}.mua-details td:first-child{width:190px;font-weight:600}@media(max-width:700px){.mua-hero{display:block}.mua-actions{display:grid}.mua-btn{width:100%}}
</style>
<div class="mua-wrap">
<div class="mua-hero"><div class="mua-title"><h2>MUA · Mikes Unraid Agent</h2><div class="mua-muted">Sicherer MCP-Zugriff auf Docker, Netzwerk und Unraid-Systemfunktionen</div></div><div><span class="mua-pill <?= $running && $health_ok ? 'mua-risk-safe' : 'mua-risk-danger' ?>"><?= $running && $health_ok ? '● Dienst bereit' : '● Dienst gestört' ?></span></div></div>
<?php if ($flash_msg): ?><div class="mua-status <?= htmlspecialchars($flash_type) ?>"><?= htmlspecialchars($flash_msg) ?></div><?php endif; ?>
<?php if ($new_api_key): ?><div class="mua-status ok"><strong>Neuer API-Key – einmalige Anzeige</strong><code class="mua-key" id="mua-new-key"><?= htmlspecialchars($new_api_key) ?></code><button type="button" class="mua-btn" onclick="navigator.clipboard.writeText(document.getElementById('mua-new-key').textContent)">In Zwischenablage kopieren</button></div><?php endif; ?>
<div class="mua-grid">
<div class="mua-card"><h3>Dienst</h3><p class="mua-summary"><?= $health_ok ? '✅ MCP-Server antwortet' : '❌ Health-Check fehlgeschlagen' ?></p><div class="mua-muted">Version <?= htmlspecialchars($running_version ?: '—') ?> · PID <?= htmlspecialchars((string)($pid ?: '—')) ?></div></div>
<div class="mua-card"><h3>Zugriffsprofil</h3><p class="mua-summary"><strong><?= $active_count ?></strong> von <?= count($all_tools) ?> Werkzeugen aktiv</p><div class="mua-muted"><?= $all_tools_enabled ? 'Vollzugriff – einschließlich kritischer Werkzeuge' : ($active_count === 0 ? 'Komplett gesperrt' : 'Benutzerdefinierte Freigabe') ?></div></div>
<div class="mua-card"><h3>Authentifizierung</h3><p class="mua-summary"><?= $config_ok ? '✅ Bearer-Key aktiv' : '❌ Verwaltung nicht erreichbar' ?></p><div class="mua-muted"><?= htmlspecialchars($config['apiKeyMasked'] ?? 'nicht verfügbar') ?></div></div>
</div>
<div class="mua-card"><h3>MCP-Endpunkt</h3><code class="mua-endpoint"><?= htmlspecialchars($endpoint) ?></code><p class="mua-muted">Streamable HTTP · JSON-RPC 2.0 · Bearer-Authentifizierung erforderlich</p></div>
<div class="mua-section"><h2>Schnelle Sicherheitsprofile</h2><p class="mua-muted">„Nur Lesen“ ist die empfohlene Dauerstellung. Weitergehende Rechte nur bei konkretem Bedarf freigeben.</p><div class="mua-actions">
<?php foreach ([['preset_readonly','Nur Lesen (empfohlen)',''],['preset_operator','Betrieb + Diagnose',''],['preset_none','Alles sperren',''],['preset_all','Vollzugriff','danger']] as $preset): ?>
<form method="post" class="mua-preset" onsubmit="<?= $preset[0] === 'preset_all' ? "return confirm('Vollzugriff aktiviert auch Container-Umbau und uneingeschränkte Root-Shell. Wirklich freigeben?');" : '' ?>"><input type="hidden" name="csrf_token" value="<?= htmlspecialchars($unraid_csrf) ?>"><input type="hidden" name="mua_csrf" value="<?= htmlspecialchars($csrf) ?>"><input type="hidden" name="action" value="<?= $preset[0] ?>"><button class="mua-btn <?= $preset[2] === 'danger' ? 'mua-btn-danger' : ($preset[0] === 'preset_readonly' ? 'mua-btn-primary' : '') ?>" type="submit"><?= $preset[1] ?></button></form>
<?php endforeach; ?></div></div>
<div class="mua-section"><h2>Werkzeuge im Detail</h2><form method="post"><input type="hidden" name="csrf_token" value="<?= htmlspecialchars($unraid_csrf) ?>"><input type="hidden" name="mua_csrf" value="<?= htmlspecialchars($csrf) ?>"><input type="hidden" name="action" value="save_tools"><div class="mua-grid">
<?php foreach ($risk_groups as $risk => $tools): $meta=$risk_labels[$risk]; ?>
<div class="mua-card <?= $meta[2] ?>"><h3><?= $meta[0] ?> <span class="mua-pill"><?= count($tools) ?></span></h3><p class="mua-muted"><?= $meta[1] ?></p>
<?php foreach ($tools as $tool): $name=$tool['name']; $checked=$all_tools_enabled || in_array($name,$enabled_tools,true); ?>
<label class="mua-tool"><input type="checkbox" name="tool_<?= htmlspecialchars($name) ?>" <?= $checked ? 'checked' : '' ?>><span><code><?= htmlspecialchars($name) ?></code><small><?= htmlspecialchars($tool['description'] ?? '') ?></small></span></label>
<?php endforeach; ?></div><?php endforeach; ?>
</div><button type="submit" class="mua-btn mua-btn-primary">Benutzerdefinierte Auswahl speichern</button></form></div>
<div class="mua-section"><h2>API-Key</h2><div class="mua-card"><p>Der vollständige Schlüssel wird nicht dauerhaft angezeigt. Beim Erzeugen erscheint er genau einmal; danach ist nur noch die Endung sichtbar.</p><form method="post" onsubmit="return confirm('Der bisherige Schlüssel wird sofort ungültig. Neuen Schlüssel erzeugen?');"><input type="hidden" name="csrf_token" value="<?= htmlspecialchars($unraid_csrf) ?>"><input type="hidden" name="mua_csrf" value="<?= htmlspecialchars($csrf) ?>"><input type="hidden" name="action" value="generate_key"><button type="submit" class="mua-btn">Neuen API-Key erzeugen</button></form></div></div>
<div class="mua-section"><h2>Technische Details</h2><table class="mua-details"><tr><td>Version</td><td><?= htmlspecialchars($running_version ?: '—') ?></td></tr><tr><td>MCP-Port</td><td><?= $port ?></td></tr><tr><td>Verwaltung</td><td>127.0.0.1:<?= $config_port ?> · separater Admin-Token</td></tr><tr><td>Konfiguration</td><td><code>/boot/config/plugins/mua/mua.conf</code></td></tr><tr><td>Audit-Protokoll</td><td><code>/var/log/plugins/mua-audit.log</code> (keine Argumente oder Inhalte)</td></tr></table><form method="post" onsubmit="return confirm('MUA-Service neu starten?');"><input type="hidden" name="csrf_token" value="<?= htmlspecialchars($unraid_csrf) ?>"><input type="hidden" name="mua_csrf" value="<?= htmlspecialchars($csrf) ?>"><input type="hidden" name="action" value="restart"><button type="submit" class="mua-btn">Dienst neu starten</button></form></div>
</div>