180 lines
14 KiB
Plaintext
180 lines
14 KiB
Plaintext
---
|
||
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',
|
||
'unraid_files_inventory',
|
||
];
|
||
$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>
|