module_fn('pi_control', 'table', $name); $assets = app()->assets(); if ($assets) { $assets->addStyle('/module/pi_control/asset?file=pi_control.css'); $assets->addScript('/module/pi_control/asset?file=hosts.js', 'footer', true); } $notice = null; $error = null; if (isset($_GET['status_json'])) { require_admin(); $id = (int)($_GET['id'] ?? 0); $stmt = $pdo->prepare('SELECT * FROM ' . $table('hosts') . ' WHERE id = :id LIMIT 1'); $stmt->execute(['id' => $id]); $host = $stmt->fetch(PDO::FETCH_ASSOC); if (!$host) { header('Content-Type: application/json; charset=utf-8'); echo json_encode(['ok' => false, 'error' => 'not_found']); exit; } $settings = modules()->settings('pi_control'); $strictHostKey = !empty($settings['terminal_strict_hostkey']); $reachable = hostReachable((string)($host['host'] ?? ''), (int)($host['port'] ?? 22)); $authOk = $reachable ? hostAuthOk($host, $strictHostKey) : false; $status = !$reachable ? 'down' : ($authOk ? 'ok' : 'auth'); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['ok' => true, 'status' => $status]); exit; } if (isset($_GET['update_json'])) { require_admin(); $id = (int)($_GET['id'] ?? 0); $stmt = $pdo->prepare('SELECT * FROM ' . $table('hosts') . ' WHERE id = :id LIMIT 1'); $stmt->execute(['id' => $id]); $host = $stmt->fetch(PDO::FETCH_ASSOC); if (!$host) { header('Content-Type: application/json; charset=utf-8'); echo json_encode(['ok' => false, 'error' => 'not_found']); exit; } $settings = modules()->settings('pi_control'); $strictHostKey = !empty($settings['terminal_strict_hostkey']); $updateCmd = <<<'SH' sh -lc 'if ! command -v apt-get >/dev/null 2>&1; then echo "__ERR__NO_APT"; exit 2; fi; if sudo -n apt update -qq >/dev/null 2>&1; then echo "__APT_UPDATE__=1"; else echo "__APT_UPDATE__=0"; fi; count=$(apt-get -s dist-upgrade 2>/dev/null | grep -c "^Inst "); echo "__COUNT__=$count"' SH; $upgradeCmd = <<<'SH' sh -lc 'id="$(. /etc/os-release 2>/dev/null && echo "${ID:-}")"; current="$(. /etc/os-release 2>/dev/null && echo "${VERSION_CODENAME:-}")"; if [ "$id" != "debian" ] || [ -z "$current" ]; then echo "__UPGRADE__=0"; exit 0; fi; latest="$( (command -v curl >/dev/null 2>&1 && curl -fsSL https://deb.debian.org/debian/dists/stable/Release) || (command -v wget >/dev/null 2>&1 && wget -qO- https://deb.debian.org/debian/dists/stable/Release) )"; latest="$(printf "%s" "$latest" | awk -F": " "/^Codename:/{print $2}")"; if [ -z "$latest" ]; then echo "__UPGRADE__=0"; echo "__RAW__=NO_FETCH"; exit 0; fi; if [ "$current" != "$latest" ]; then echo "__UPGRADE__=1"; else echo "__UPGRADE__=0"; fi' SH; [$updExit, $updOut, $updErr] = runSshCommandCapture($host, $updateCmd, $strictHostKey, 20); $updOutStr = (string)$updOut; $updErrStr = (string)$updErr; $updateCount = null; $updatePreview = ''; $updateErr = str_contains($updOutStr, '__ERR__NO_APT'); if ($updExit === 0 && !$updateErr) { if (preg_match('/^__COUNT__=(\d+)$/m', $updOutStr, $m)) { $updateCount = (int)$m[1]; } } [$upgExit, $upgOut, $upgErr] = runSshCommandCapture($host, $upgradeCmd, $strictHostKey, 25); $upgOutStr = (string)$upgOut; $upgErrStr = (string)$upgErr; $upgradeAvailable = null; $upgradeErr = str_contains($upgOutStr, '__ERR__'); if ($upgExit === 0 && !$upgradeErr) { if (preg_match('/^__UPGRADE__=(0|1)$/m', $upgOutStr, $m)) { $upgradeAvailable = $m[1] === '1'; } } $driver = (string)$pdo->getAttribute(PDO::ATTR_DRIVER_NAME); $nowExpr = $driver === 'pgsql' ? 'NOW()' : "DATETIME('now')"; $updCountVal = $updateCount; $updErrVal = $updExit === 0 && !$updateErr ? null : trim($updErrStr ?: $updOutStr); $upgAvailVal = $upgradeAvailable; $upgErrVal = $upgExit === 0 && !$upgradeErr ? null : trim($upgErrStr ?: $upgOutStr); $stmt = $pdo->prepare( 'UPDATE ' . $table('hosts') . ' SET update_checked_at = ' . $nowExpr . ', update_count = :update_count, update_preview = :update_preview, update_error = :update_error, upgrade_available = :upgrade_available, upgrade_raw = :upgrade_raw, upgrade_error = :upgrade_error WHERE id = :id' ); $stmt->execute([ 'update_count' => $updCountVal, 'update_preview' => $updatePreview !== '' ? $updatePreview : null, 'update_error' => $updErrVal, 'upgrade_available' => $upgAvailVal === null ? null : ($upgAvailVal ? 1 : 0), 'upgrade_raw' => $upgExit === 0 ? trim($upgOutStr) : null, 'upgrade_error' => $upgErrVal, 'id' => $id, ]); header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'ok' => true, 'updates' => [ 'count' => $updateCount, 'preview' => $updatePreview, 'error' => $updExit === 0 && !$updateErr ? '' : trim($updErrStr ?: $updOutStr), ], 'os' => [ 'available' => $upgradeAvailable, 'raw' => $upgExit === 0 ? trim($upgOutStr) : '', 'error' => $upgExit === 0 && !$upgradeErr ? '' : trim($upgErrStr ?: $upgOutStr), ], 'checked_at' => date('c'), ]); exit; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { require_admin(); $deleteId = (int)($_POST['delete_id'] ?? 0); $editId = (int)($_POST['id'] ?? 0); $name = trim((string)($_POST['name'] ?? '')); $host = trim((string)($_POST['host'] ?? '')); $port = (int)($_POST['port'] ?? 22); $username = trim((string)($_POST['username'] ?? '')); $authType = trim((string)($_POST['auth_type'] ?? 'key')); $keyPath = trim((string)($_POST['key_path'] ?? '')); $password = trim((string)($_POST['password'] ?? '')); $imageUrl = trim((string)($_POST['image_url'] ?? '')); if ($deleteId > 0) { $stmt = $pdo->prepare('DELETE FROM ' . $table('hosts') . ' WHERE id = :id'); $stmt->execute(['id' => $deleteId]); $notice = 'Host gelöscht.'; } else { if ($name === '' || $host === '' || $username === '') { $error = 'Bitte Name, Host und Benutzer angeben.'; } else { if ($editId > 0) { $stmt = $pdo->prepare('SELECT * FROM ' . $table('hosts') . ' WHERE id = :id LIMIT 1'); $stmt->execute(['id' => $editId]); $existing = $stmt->fetch(PDO::FETCH_ASSOC) ?: []; $passwordToStore = $password !== '' ? $password : ($existing['password'] ?? null); $stmt = $pdo->prepare( 'UPDATE ' . $table('hosts') . ' SET name = :name, host = :host, port = :port, username = :username, auth_type = :auth_type, key_path = :key_path, password = :password, image_url = :image_url WHERE id = :id' ); $stmt->execute([ 'id' => $editId, 'name' => $name, 'host' => $host, 'port' => $port > 0 ? $port : 22, 'username' => $username, 'auth_type' => $authType !== '' ? $authType : 'key', 'key_path' => $keyPath !== '' ? $keyPath : null, 'password' => $passwordToStore, 'image_url' => $imageUrl !== '' ? $imageUrl : null, ]); $notice = 'Host aktualisiert.'; } else { $stmt = $pdo->prepare( 'INSERT INTO ' . $table('hosts') . ' (name, host, port, username, auth_type, key_path, password, image_url) VALUES (:name, :host, :port, :username, :auth_type, :key_path, :password, :image_url)' ); $stmt->execute([ 'name' => $name, 'host' => $host, 'port' => $port > 0 ? $port : 22, 'username' => $username, 'auth_type' => $authType !== '' ? $authType : 'key', 'key_path' => $keyPath !== '' ? $keyPath : null, 'password' => $password !== '' ? $password : null, 'image_url' => $imageUrl !== '' ? $imageUrl : null, ]); $notice = 'Host gespeichert.'; } } } } $hosts = $pdo->query('SELECT * FROM ' . $table('hosts') . ' ORDER BY id DESC')->fetchAll(PDO::FETCH_ASSOC); $settings = modules()->settings('pi_control'); $strictHostKey = !empty($settings['terminal_strict_hostkey']); function hostReachable(string $host, int $port): bool { $errno = 0; $errstr = ''; $fp = @fsockopen($host, $port, $errno, $errstr, 1.2); if ($fp) { fclose($fp); return true; } return false; } function runSshCommand(string $cmd, int $timeoutSec): int { $descriptors = [ 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $process = proc_open($cmd, $descriptors, $pipes); if (!is_resource($process)) { return 255; } stream_set_blocking($pipes[1], false); stream_set_blocking($pipes[2], false); $start = time(); while (true) { $status = proc_get_status($process); if (!$status['running']) { $code = (int)$status['exitcode']; proc_close($process); return $code; } if (time() - $start > $timeoutSec) { proc_terminate($process, 9); proc_close($process); return 124; } usleep(100000); } } function runSshCommandCapture(array $host, string $command, bool $strictHostKey, int $timeoutSec): array { $hostAddr = (string)($host['host'] ?? ''); $user = (string)($host['username'] ?? ''); $port = (int)($host['port'] ?? 22); $authType = (string)($host['auth_type'] ?? 'key'); $keyPath = (string)($host['key_path'] ?? ''); $password = (string)($host['password'] ?? ''); $opts = $strictHostKey ? '-o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts' : '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'; $opts .= ' -o ConnectTimeout=6 -o NumberOfPasswordPrompts=1'; $target = escapeshellarg($user . '@' . $hostAddr); $remote = '/bin/bash -lc ' . escapeshellarg($command); $cmd = 'ssh ' . $opts . ' -p ' . (int)$port . ' '; if ($authType === 'key' && $keyPath !== '') { $cmd .= '-i ' . escapeshellarg($keyPath) . ' -o BatchMode=yes '; } elseif ($authType === 'key') { $cmd .= '-o BatchMode=yes '; } $cmd .= $target . ' -- ' . $remote; if ($authType === 'pass' && $password !== '') { $cmd = 'sshpass -p ' . escapeshellarg($password) . ' ' . $cmd; } $descriptors = [ 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $process = proc_open($cmd, $descriptors, $pipes); if (!is_resource($process)) { return [255, '', 'proc_open failed']; } stream_set_blocking($pipes[1], false); stream_set_blocking($pipes[2], false); $out = ''; $err = ''; $start = time(); while (true) { $status = proc_get_status($process); $out .= stream_get_contents($pipes[1]); $err .= stream_get_contents($pipes[2]); if (!$status['running']) { $exit = (int)$status['exitcode']; proc_close($process); return [$exit, $out, $err]; } if (time() - $start > $timeoutSec) { proc_terminate($process, 9); proc_close($process); return [124, $out, $err]; } usleep(100000); } } function hostAuthOk(array $host, bool $strictHostKey): bool { $hostAddr = (string)($host['host'] ?? ''); $user = (string)($host['username'] ?? ''); $port = (int)($host['port'] ?? 22); $authType = (string)($host['auth_type'] ?? 'key'); $keyPath = (string)($host['key_path'] ?? ''); $password = (string)($host['password'] ?? ''); if ($hostAddr === '' || $user === '') { return false; } $opts = $strictHostKey ? '-o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts' : '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'; $opts .= ' -o ConnectTimeout=2 -o NumberOfPasswordPrompts=1'; $target = escapeshellarg($user . '@' . $hostAddr); $cmd = 'ssh ' . $opts . ' -p ' . (int)$port . ' '; if ($authType === 'key' && $keyPath !== '') { $cmd .= '-i ' . escapeshellarg($keyPath) . ' -o BatchMode=yes '; } elseif ($authType === 'key') { $cmd .= '-o BatchMode=yes '; } $cmd .= $target . ' -- true'; if ($authType === 'pass') { if ($password === '') { return false; } $cmd = 'sshpass -p ' . escapeshellarg($password) . ' ' . $cmd; } $exitCode = runSshCommand($cmd, 3); return $exitCode === 0; } ?>
Verwalte die Raspberry Pis, die du steuern möchtest.