diff --git a/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php b/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php index 79f1334..3476fc7 100644 --- a/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php +++ b/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php @@ -17,65 +17,107 @@ class Fail2BanJailModal extends ModalComponent /* ---------------- intern ---------------- */ +// protected function load(bool $force = false): void +// { +// $jail = $this->jail; +// +// [, $s] = $this->f2b('status '.escapeshellarg($jail)); +// $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: ''; +// $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : []; +// +// $defaultBantime = $this->getBantime($jail); +// +// $rows = []; +// foreach ($ips as $ip) { +// $banAt = null; $until = null; $remaining = null; +// +// // 1) Primärquelle: DB +// if ($info = $this->banInfoFromDb($jail, $ip)) { +// $banAt = $info['banned_at']; +// if ((int)$info['expire'] === -1) { +// $remaining = -1; // permanent +// } elseif ((int)$info['expire'] > 0) { +// $until = (int)$info['expire']; +// $remaining = max(0, $until - time()); +// } +// } +// +// // 2) Fallback: Log + Jail-Bantime +// if ($remaining === null) { +// $banAt = $banAt ?? $this->lastBanTimestamp($jail, $ip); +// if ($banAt !== null) { +// $remaining = max(0, $defaultBantime - (time() - $banAt)); +// $until = $remaining > 0 ? $banAt + $defaultBantime : null; +// } else { +// $remaining = -2; // ~approx +// } +// } +// +// // 3) Wenn 0 Sekunden, aber Fail2Ban hält die IP noch → „verlängert/unbekannt“ +// if ($remaining === 0 && $this->isStillBanned($jail, $ip)) { +// $remaining = -2; // markiere als „~ unbekannt/verlängert“ +// } +// +// [$timeText, $metaText, $boxClass] = $this->present($remaining, $banAt, $until); +// +// $rows[] = [ +// 'ip' => $ip, +// 'bantime' => $defaultBantime, +// 'banned_at' => $banAt, +// 'remaining' => $remaining, +// 'until' => $until, +// 'time_text' => $timeText, +// 'meta_text' => $metaText, +// 'box_class' => $boxClass, +// ]; +// } +// +// $this->rows = $rows; +// logger()->debug('rows='.json_encode($rows, JSON_UNESCAPED_SLASHES)); +// } + protected function load(bool $force = false): void { $jail = $this->jail; - [, $s] = $this->f2b('status '.escapeshellarg($jail)); - $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: ''; - $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : []; + // 1) Primär: DB + $rows = $this->activeBansFromDb($jail); - $defaultBantime = $this->getBantime($jail); + // 2) Fallback: status parsen, wenn DB leer (z. B. sehr alte F2B-Setups) + if (empty($rows)) { + [, $s] = $this->f2b('status '.escapeshellarg($jail)); + $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: ''; + $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : []; + $defaultBantime = $this->getBantime($jail); - $rows = []; - foreach ($ips as $ip) { - $banAt = null; $until = null; $remaining = null; - - // 1) Primärquelle: DB - if ($info = $this->banInfoFromDb($jail, $ip)) { - $banAt = $info['banned_at']; - if ((int)$info['expire'] === -1) { - $remaining = -1; // permanent - } elseif ((int)$info['expire'] > 0) { - $until = (int)$info['expire']; - $remaining = max(0, $until - time()); - } + foreach ($ips as $ip) { + $banAt = $this->lastBanTimestamp($jail, $ip); + $remaining = $banAt !== null ? max(0, $defaultBantime - (time() - $banAt)) : -2; + $until = ($remaining > 0 && $banAt) ? $banAt + $defaultBantime : null; + $rows[] = [ + 'ip' => $ip, + 'bantime' => $defaultBantime, + 'banned_at' => $banAt, + 'remaining' => $remaining, + 'until' => $until, + ]; } - - // 2) Fallback: Log + Jail-Bantime - if ($remaining === null) { - $banAt = $banAt ?? $this->lastBanTimestamp($jail, $ip); - if ($banAt !== null) { - $remaining = max(0, $defaultBantime - (time() - $banAt)); - $until = $remaining > 0 ? $banAt + $defaultBantime : null; - } else { - $remaining = -2; // ~approx - } - } - - // 3) Wenn 0 Sekunden, aber Fail2Ban hält die IP noch → „verlängert/unbekannt“ - if ($remaining === 0 && $this->isStillBanned($jail, $ip)) { - $remaining = -2; // markiere als „~ unbekannt/verlängert“ - } - - [$timeText, $metaText, $boxClass] = $this->present($remaining, $banAt, $until); - - $rows[] = [ - 'ip' => $ip, - 'bantime' => $defaultBantime, - 'banned_at' => $banAt, - 'remaining' => $remaining, - 'until' => $until, - 'time_text' => $timeText, - 'meta_text' => $metaText, - 'box_class' => $boxClass, - ]; } - $this->rows = $rows; - logger()->debug('rows='.json_encode($rows, JSON_UNESCAPED_SLASHES)); - } + // Präsentationswerte bauen (einheitlich) + $presented = []; + foreach ($rows as $r) { + [$timeText, $metaText, $boxClass] = $this->present($r['remaining'], $r['banned_at'], $r['until']); + $presented[] = $r + [ + 'time_text' => $timeText, + 'meta_text' => $metaText, + 'box_class' => $boxClass, + ]; + } + $this->rows = $presented; + logger()->debug('rows='.json_encode($presented, JSON_UNESCAPED_SLASHES)); + } /** ROBUST: findet Binaries automatisch */ private function bin(string $name): string { @@ -83,6 +125,62 @@ class Fail2BanJailModal extends ModalComponent return $p !== '' ? $p : $name; } + + private function activeBansFromDb(string $jail): array + { + $sudo = $this->bin('sudo'); + $sqlite = $this->bin('sqlite3'); + $db = $this->getDbFile(); + + $sql = << CAST(strftime('%s','now') AS INTEGER) +ORDER BY timeofban DESC; +SQL; + + $q = sprintf($sql, $this->sql($jail)); + $cmd = "$sudo -n $sqlite -readonly ".escapeshellarg($db).' '.escapeshellarg($q).' 2>&1'; + $out = trim((string)@shell_exec($cmd)); + if ($out === '') return []; + + $rows = []; + foreach (explode("\n", $out) as $line) { + $parts = explode('|', $line); + if (count($parts) < 5) continue; + [$ip, $banAt, $bantime, $remaining, $expire] = [ $parts[0], (int)$parts[1], (int)$parts[2], (int)$parts[3], ($parts[4] !== '' ? (int)$parts[4] : null) ]; + $rows[] = [ + 'ip' => trim($ip), + 'bantime' => $bantime, + 'banned_at' => $banAt ?: null, + 'remaining' => $remaining, + 'until' => $expire, + ]; + } + return $rows; + } + /** letzte "Ban "-Zeile → Unix-Timestamp (mit Fallbacks) */ private function lastBanTimestamp(string $jail, string $ip): ?int { @@ -248,11 +346,9 @@ class Fail2BanJailModal extends ModalComponent private function isStillBanned(string $jail, string $ip): bool { - // 1) Direktcheck: liefert 1/0 je nach Version, ist harmlos wenn 0 [, $out1] = $this->f2b('get '.escapeshellarg($jail).' banip '.escapeshellarg($ip)); if (preg_match('/\b1\b/', $out1)) return true; - // 2) Fallback: aus `status ` die Liste parsen (das hast du ohnehin) [, $out3] = $this->f2b('status '.escapeshellarg($jail)); $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $out3) ?: ''; return $ipList !== '' && preg_match('/(^|\s)'.preg_quote($ip,'/').'(\s|$)/', $ipList) === 1;