// Gemeinsame Basis für Verwaltungs-Tabs über SSH (Webserver, Zertifikate, Samba/AD): Verbindung, root-Rechte // (direkt als root oder per sudo), Skripte als root, Konfigurationsdateien mit Backup, Prüfung und automatischem Zurückrollen. // Unterklassen setzen NAME (für Fehlermeldungen), OPS (aufrufbare Operationen) und implementieren read(id). const i18n = require('../i18n'); const { execOn } = require('./docker'); const { acquireSudo } = require('./sudo'); const lastLine = (s) => String(s || '').split('\n').map((l) => l.trim()).filter(Boolean).slice(-1)[0] || ''; const b64 = (s) => Buffer.from(String(s)).toString('base64'); const q = (s) => `'${String(s).replace(/'/g, "'\\''")}'`; // Shell-Quoting in einfachen Anführungszeichen const check = (v, rx, what) => { if (!rx.test(String(v ?? ''))) throw new Error(i18n.t('Invalid value for {field}: {value}', { field: what, value: v })); return String(v); }; // Ausgabe mit @@ABSCHNITT-Markern in { ABSCHNITT: text } zerlegen function sections(out) { const res = {}; let cur = null; for (const line of String(out).split('\n')) { const m = line.match(/^@@(\w+)$/); if (m) { cur = m[1]; res[cur] = []; continue; } if (cur) res[cur].push(line); } return Object.fromEntries(Object.entries(res).map(([k, v]) => [k, v.join('\n').trim()])); } // Paketverwaltung des Hosts erkennen (für Installationsbefehle im Terminal) const PKG_DETECT = 'if command -v apt-get >/dev/null 2>&1; then echo apt; elif command -v dnf >/dev/null 2>&1; then echo dnf; elif command -v yum >/dev/null 2>&1; then echo yum; elif command -v pacman >/dev/null 2>&1; then echo pacman; elif command -v zypper >/dev/null 2>&1; then echo zypper; elif command -v apk >/dev/null 2>&1; then echo apk; fi'; function installCmd(pkg, packages) { const p = packages.join(' '); return { apt: `apt-get update && apt-get install -y ${p}`, dnf: `dnf install -y ${p}`, yum: `yum install -y ${p}`, pacman: `pacman -S --needed --noconfirm ${p}`, zypper: `zypper install -y ${p}`, apk: `apk add ${p}` }[pkg] || ''; } class AdminManager { constructor(ssh) { this.ssh = ssh; this.sessions = new Map(); // id -> { conn, jumps, host, sudo, sudoPw, ... } } get(id) { const s = this.sessions.get(id); if (!s) throw new Error(i18n.t('Session not found')); return s; } // Shell-Skript als root ausführen (base64-kodiert, damit kein Quoting-Problem entsteht). // stdin: weitere Eingabe für das Skript (z. B. Passwörter, die nicht in der Kommandozeile stehen sollen) root(s, script, stdin = '') { const cmd = `sh -c "$(printf %s ${b64(`export PATH=$PATH:/usr/sbin:/sbin:/usr/local/sbin\n${script}`)} | base64 -d)"`; if (!s.sudo) return execOn(s.conn, cmd, stdin); return execOn(s.conn, `sudo -S -p '' ${cmd}`, `${s.sudoPw || ''}\n${stdin}`); } async rootOk(s, script, stdin) { const r = await this.root(s, script, stdin); if (r.code) throw new Error(lastLine(r.err) || lastLine(r.out) || i18n.t('Command failed with code {code}', { code: r.code })); return r.out; } async open(id, host, onClose) { const { conn, jumps } = await this.ssh.connect(host, id); const s = { conn, jumps, host, sudo: false }; this.sessions.set(id, s); conn.on('close', () => { if (this.sessions.get(id) === s) { this.close(id); onClose(); } }); conn.on('error', () => {}); if ((await execOn(conn, 'id -u')).out.trim() !== '0') { s.sudo = true; s.sudoPw = await acquireSudo(this.ssh, id, host, (c, stdin) => execOn(conn, c, stdin)); if (s.sudoPw == null) throw new Error(i18n.t('Root privileges are required. Log in as root or enter the password of a user with sudo rights.')); } s.pkg = (await this.root(s, PKG_DETECT)).out.trim(); return this.read(id); } // Präfix für Befehle, die im Terminal-Tab laufen (dort fragt sudo selbst nach dem Passwort) termPrefix(s) { return s.sudo ? 'sudo ' : ''; } termCmd(s, cmd) { return s.sudo ? `sudo sh -c ${q(cmd)}` : cmd; } // Datei schreiben mit Backup; validate (Shell) muss erfolgreich sein, sonst wird der alte Stand wiederhergestellt. // after: Befehl nach erfolgreicher Prüfung (z. B. reload). Liefert die Ausgabe der Prüfung. async writeChecked(s, path, content, { validate = 'true', after = '', mode = '' } = {}) { const r = await this.root(s, `f=${q(path)} had=0; [ -e "$f" ] && { cp -a "$f" "$f.mrterm-bak" || exit 1; had=1; } mkdir -p "$(dirname "$f")" || exit 1 printf %s ${b64(content)} | base64 -d > "$f" || exit 1 ${mode ? `chmod ${mode} "$f"` : ''} if ! out=$( (${validate}) 2>&1 ); then if [ $had = 1 ]; then mv -f "$f.mrterm-bak" "$f"; else rm -f "$f"; fi printf '%s\\n' "$out" >&2; exit 4 fi printf '%s\\n' "$out" ${after ? `(${after}) 2>&1 || exit 6` : ''} exit 0`); if (r.code === 4) throw new Error(i18n.t('The configuration is invalid, the previous version was restored: {err}', { err: r.err.trim().split('\n').slice(-6).join('\n') })); if (r.code === 6) throw new Error(i18n.t('Saved, but reloading failed: {err}', { err: lastLine(r.out) })); if (r.code) throw new Error(lastLine(r.err) || i18n.t('Command failed with code {code}', { code: r.code })); return r.out.trim(); } // Generischer Einstieg für die Oberfläche: nur Operationen aus OPS sind erlaubt op(id, name, ...args) { if (!this.constructor.OPS.includes(name)) throw new Error('Invalid operation'); return this[name](this.get(id), ...args); } close(id) { const s = this.sessions.get(id); if (!s) return; this.sessions.delete(id); try { s.conn.end(); } catch {} s.jumps?.forEach((c) => { try { c.end(); } catch {} }); } } module.exports = { AdminManager, sections, lastLine, b64, q, check, installCmd };