r002: API-Key-Auth + Einstellungs-Maske (API-Key generieren, Tool-Checkboxen)

- src/auth.ts: Config-Management (API-Key + Tool-Filter), timing-safe Auth
- src/index.ts: Auth-Check auf /mcp, Tool-Filter in tools/list + tools/call,
  Config-Server auf 127.0.0.1:3003 (nur localhost)
- scripts/mua.page: Einstellungs-UI (API-Key generieren + Tool-Checkboxen)
- scripts/rc.mua: Config-Dir sicherstellen
- Version: 2026.08.18.r002
This commit is contained in:
Mikei386
2026-08-18 10:53:07 +02:00
parent ea6762810c
commit 8a3ecaf39f
12 changed files with 492 additions and 27 deletions
+198
View File
@@ -0,0 +1,198 @@
/**
* MUA — Mikes Unraid Agent
* auth.ts — API-Key-Auth + Tool-Filter (Config-Management)
*
* Config-Datei: /boot/config/plugins/mua/mua.conf (chmod 600)
* Format (INI):
* MUA_API_KEY=<64 hex chars>
* MUA_ENABLED_TOOLS=all | <comma-separated tool names>
*
* Auth-Flow:
* Client sendet: Authorization: Bearer ***
* Server prüft: timing-safe comparison (timing-attack-sicher)
* Fehlschlag: 401 Unauthorized
*
* /health bleibt ohne Auth (Monitoring).
* /mcp (POST + DELETE) erfordert gültiges Token.
*
* Tool-Filter:
* MUA_ENABLED_TOOLS=all → alle Tools aktiv
* MUA_ENABLED_TOOLS=a,b,c → nur diese Tools (tools/list + tools/call)
*/
import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
import { randomBytes } from "node:crypto";
const CONFIG_DIR = process.env["MUA_CONFIG_DIR"] ?? "/boot/config/plugins/mua";
const CONFIG_FILE = `${CONFIG_DIR}/mua.conf`;
// ── Config-Struktur ─────────────────────────────────────────────────────
interface MUAConfig {
apiKey: string;
enabledTools: string[]; // leer = alle aktiv
}
// ── Config laden ────────────────────────────────────────────────────────
let cachedConfig: MUAConfig | null = null;
function parseConfig(content: string): MUAConfig {
const cfg: MUAConfig = { apiKey: "", enabledTools: [] };
for (const line of content.split("\n")) {
const trimmed = line.trim();
if (trimmed.startsWith("#") || trimmed === "") continue;
const eq = trimmed.indexOf("=");
if (eq < 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1).trim();
if (key === "MUA_API_KEY") {
cfg.apiKey = value;
} else if (key === "MUA_ENABLED_TOOLS") {
if (value === "all" || value === "") {
cfg.enabledTools = [];
} else {
cfg.enabledTools = value
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
}
}
return cfg;
}
function loadConfig(): MUAConfig {
if (cachedConfig) return cachedConfig;
// 1. Env-Override (für Testing / Docker)
const envToken = process.env["MUA_API_KEY"];
if (envToken && envToken.length > 0) {
cachedConfig = { apiKey: envToken, enabledTools: [] };
return cachedConfig;
}
// 2. Aus Config-Datei laden
try {
const content = readFileSync(CONFIG_FILE, "utf-8");
const cfg = parseConfig(content);
if (cfg.apiKey.length > 0) {
cachedConfig = cfg;
return cachedConfig;
}
} catch {
// Datei nicht vorhanden — generiere neue Config
}
// 3. Neue Config generieren
const newToken = randomBytes(32).toString("hex"); // 64 hex chars
cachedConfig = { apiKey: newToken, enabledTools: [] };
try {
mkdirSync(CONFIG_DIR, { recursive: true });
writeConfigFile(cachedConfig);
console.log(`[MUA] Neue Config generiert: ${CONFIG_FILE}`);
} catch (e) {
console.error(`[MUA] WARNUNG: Config konnte nicht gespeichert werden: ${String(e)}`);
console.error(`[MUA] API-Key (nur in diesem Log): ${newToken}`);
}
return cachedConfig;
}
// ── Config schreiben ────────────────────────────────────────────────────
function writeConfigFile(cfg: MUAConfig): void {
const toolsLine =
cfg.enabledTools.length === 0
? "MUA_ENABLED_TOOLS=all"
: `MUA_ENABLED_TOOLS=${cfg.enabledTools.join(",")}`;
const content = [
"# MUA Configuration",
"# API-Key für MCP-HTTP-Endpunkt (Port 3002)",
"# Format: Authorization: Bearer ***",
`MUA_API_KEY=${cfg.apiKey}`,
"# Aktive Tools (all = alle, oder kommagetrennte Tool-Namen)",
toolsLine,
"",
].join("\n");
writeFileSync(CONFIG_FILE, content, { mode: 0o600 });
chmodSync(CONFIG_FILE, 0o600);
}
export function saveConfig(cfg: MUAConfig): void {
cachedConfig = cfg;
writeConfigFile(cfg);
}
// ── API-Key ─────────────────────────────────────────────────────────────
export function getApiKey(): string {
return loadConfig().apiKey;
}
export function setApiKey(newKey: string): void {
const cfg = loadConfig();
cfg.apiKey = newKey;
saveConfig(cfg);
}
export function generateApiKey(): string {
const newKey = randomBytes(32).toString("hex");
setApiKey(newKey);
return newKey;
}
// ── Tool-Filter ─────────────────────────────────────────────────────────
/**
* Liefert die Liste aktiver Tool-Namen.
* Leer-Array = alle Tools aktiv.
*/
export function getEnabledTools(): string[] {
return loadConfig().enabledTools;
}
/**
* Setzt die aktiven Tools.
* Leer-Array = alle Tools aktiv.
*/
export function setEnabledTools(names: string[]): void {
const cfg = loadConfig();
cfg.enabledTools = names;
saveConfig(cfg);
}
/**
* Prüft, ob ein Tool aktiv ist.
* true = aktiv, false = deaktiviert.
*/
export function isToolEnabled(toolName: string): boolean {
const enabled = getEnabledTools();
if (enabled.length === 0) return true; // alle aktiv
return enabled.includes(toolName);
}
// ── Timing-safe Token-Check ─────────────────────────────────────────────
export function checkAuth(req: Request): boolean {
const token = getApiKey();
const authHeader = req.headers.get("authorization") ?? "";
// Format: "Bearer <token>"
if (!authHeader.startsWith("Bearer ")) {
return false;
}
const provided = authHeader.slice(7).trim();
// Timing-safe comparison (verhindert Timing-Attacks)
if (provided.length !== token.length) {
return false;
}
let diff = 0;
for (let i = 0; i < token.length; i++) {
diff |= token.charCodeAt(i) ^ provided.charCodeAt(i);
}
return diff === 0;
}
// ── Token für WebGUI-Tab (nur lesbar) ───────────────────────────────────
export function getTokenForDisplay(): string {
return getApiKey();
}