Fix: Mailbox Stats über Dovecot mit config/mailpool.php

main v1.0.108
boban 2025-10-31 00:43:16 +01:00
parent e77d9f64bb
commit d9867db546
3 changed files with 212 additions and 21 deletions

View File

@ -3,6 +3,7 @@
namespace App\Livewire\Ui\Security;
use Livewire\Attributes\On;
use Livewire\Component;
class Fail2BanCard extends Component
@ -22,6 +23,7 @@ class Fail2BanCard extends Component
return view('livewire.ui.security.fail2-ban-card');
}
#[On('f2b:refresh-banlist')]
public function refresh(): void
{
$this->load(true);
@ -35,67 +37,205 @@ class Fail2BanCard extends Component
/* ------------------- intern ------------------- */
// protected function load(bool $force = false): void
// {
// $bin = trim((string)@shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
// if ($bin === '') {
// $this->available = false;
// $this->permDenied = false;
// $this->activeBans = 0;
// $this->jails = [];
// return;
// }
//
// [$ok, $raw] = $this->f2b('ping');
// if (!$ok && stripos($raw, 'permission denied') !== false) {
// $this->available = true;
// $this->permDenied = true;
// $this->activeBans = 0;
// $this->jails = [];
// return;
// }
//
// [, $status] = $this->f2b('status');
// $jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status);
// $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : [];
//
// $rows = [];
// $sum = 0;
// foreach ($jails as $j) {
// [, $s] = $this->f2b('status '.escapeshellarg($j));
// $bantime = $this->getBantime($j);
// $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
// $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : [];
//
// $ipDetails = $this->buildIpDetails($j, $ips, $bantime);
//
// [, $s] = $this->f2b('status ' . escapeshellarg($j));
// $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
// $bantime = $this->getBantime($j);
// $rows[] = ['name' => $j, 'banned' => $banned, 'bantime' => $bantime];
// $sum += $banned;
// }
//
// $this->available = true;
// $this->permDenied = false;
// $this->activeBans = $sum;
// $this->jails = $rows;
// }
protected function load(bool $force = false): void
{
$bin = trim((string)@shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
if ($bin === '') {
$this->available = false;
$this->available = false;
$this->permDenied = false;
$this->activeBans = 0;
$this->jails = [];
$this->jails = [];
return;
}
// Rechte prüfen
[$ok, $raw] = $this->f2b('ping');
if (!$ok && stripos($raw, 'permission denied') !== false) {
$this->available = true;
$this->available = true;
$this->permDenied = true;
$this->activeBans = 0;
$this->jails = [];
$this->jails = [];
return;
}
// Jail-Liste
[, $status] = $this->f2b('status');
$jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status);
$jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : [];
$jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : [];
$rows = [];
$sum = 0;
$sum = 0;
foreach ($jails as $j) {
[, $s] = $this->f2b('status ' . escapeshellarg($j));
$banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
$bantime = $this->getBantime($j);
$rows[] = ['name' => $j, 'banned' => $banned, 'bantime' => $bantime];
$jEsc = escapeshellarg($j);
[, $s] = $this->f2b("status {$jEsc}");
$banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
$bantime = $this->getBantime($j);
$ipListLine = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
$ips = $ipListLine !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipListLine)))) : [];
// Details inkl. Restzeit je IP
$ipDetails = $this->buildIpDetails($j, $ips, $bantime);
$rows[] = [
'name' => $j,
'banned' => $banned,
'bantime' => $bantime, // Sek. (-1 = permanent)
'ips' => $ipDetails, // [['ip'=>..., 'remaining'=>..., 'until'=>...], ...]
];
$sum += $banned;
}
$this->available = true;
$this->available = true;
$this->permDenied = false;
$this->activeBans = $sum;
$this->jails = $rows;
$this->jails = $rows;
}
// private function f2b(string $args): array
// {
// $sudo = '/usr/bin/sudo';
// $f2b = '/usr/bin/fail2ban-client';
// $out = (string)@shell_exec("timeout 2 $sudo -n $f2b $args 2>&1");
// $ok = str_contains($out, 'Status') || str_contains($out, 'Jail list') || str_contains($out, 'pong');
// return [$ok, $out];
// }
private function f2b(string $args): array
{
$sudo = '/usr/bin/sudo';
$f2b = '/usr/bin/fail2ban-client';
$out = (string)@shell_exec("timeout 2 $sudo -n $f2b $args 2>&1");
$ok = str_contains($out, 'Status') || str_contains($out, 'Jail list') || str_contains($out, 'pong');
$f2b = '/usr/bin/fail2ban-client';
$cmd = "timeout 3 $sudo -n $f2b $args 2>&1";
$out = (string)@shell_exec($cmd);
$ok = stripos($out, 'Status') !== false
|| stripos($out, 'Jail list') !== false
|| stripos($out, 'pong') !== false;
return [$ok, $out];
}
/** konfig. Bantime des Jails in Sekunden (-1 = permanent) */
private function getBantime(string $jail): int
{
[, $out] = $this->f2b('get ' . escapeshellarg($jail) . ' bantime');
[, $out] = $this->f2b('get '.escapeshellarg($jail).' bantime');
$val = trim($out);
if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0];
return 600;
return 600; // konservativer Fallback
}
// private function getBantime(string $jail): int
// {
// [, $out] = $this->f2b('get ' . escapeshellarg($jail) . ' bantime');
// $val = trim($out);
// if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0];
// return 600;
// }
/** baut Detail-Liste inkl. Restzeit */
private function buildIpDetails(string $jail, array $ips, int $bantime): array
{
$now = time(); $out = [];
foreach ($ips as $ip) {
$banAt = $this->lastBanTimestamp($jail, $ip);
$remaining = null; $until = null;
if ($bantime === -1) {
$remaining = -1; // permanent
} elseif ($banAt !== null) {
$remaining = max(0, $bantime - ($now - $banAt));
$until = $remaining > 0 ? ($banAt + $bantime) : null;
}
$out[] = ['ip' => $ip, 'remaining' => $remaining, 'until' => $until];
}
return $out;
}
/** letzte "Ban <IP>"-Zeile finden (Log + Rotation + journald), liefert Unix-Timestamp oder null */
private function lastBanTimestamp(string $jail, string $ip): ?int
{
// 1) zgrep in /var/log/fail2ban.log*
$line = (string)@shell_exec(
"zgrep -h -E '\\[{$jail}\\]\\s+Ban\\s+{$ip}' /var/log/fail2ban.log* 2>/dev/null | tail -n 1"
);
$line = trim($line);
// 2) Fallback: journald
if ($line === '') {
$line = (string)@shell_exec(
"journalctl -u fail2ban --since '14 days ago' 2>/dev/null | grep -F '[{$jail}] Ban {$ip}' | tail -n 1"
);
$line = trim($line);
}
if ($line === '') return null;
// "YYYY-MM-DD HH:MM:SS,mmm ..." oder ISO im Text
if (preg_match('/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})/', $line, $m)
|| preg_match('/(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})/', $line, $m)) {
$ts = strtotime($m[1].' '.$m[2]);
return $ts ?: null;
}
return null;
}
private function firstMatch(string $pattern, string $haystack): ?string
{
return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
}
// private function firstMatch(string $pattern, string $haystack): ?string
// {
// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
// }
}

View File

@ -146,15 +146,41 @@ class Fail2BanJailModal extends ModalComponent
/** letzte "Ban <IP>"-Zeile parsen -> Unix-Timestamp */
private function lastBanTimestamp(string $jail, string $ip): ?int
{
$cmd = 'grep -F ' . escapeshellarg("[$jail] Ban $ip")
. ' /var/log/fail2ban.log 2>/dev/null | tail -n 1';
$line = trim((string) @shell_exec($cmd));
$jailQ = escapeshellarg($jail);
$ipQ = escapeshellarg($ip);
// 1) Durchsuche fail2ban.log + rotierte + .gz
$cmdFiles = "sh -c 'ls -1 /var/log/fail2ban.log* 2>/dev/null | wc -l'";
$haveFiles = ((int) @shell_exec($cmdFiles)) > 0;
$line = '';
if ($haveFiles) {
// zgrep -h: ohne Dateinamen; -E für Regex; tail -n1: letzte Ban-Zeile
$pattern = escapeshellarg(sprintf('\\[%s\\] Ban %s', $jail, $ip));
$cmd = "zgrep -h -E \"\\[${jail}\\]\\s+Ban\\s+${ip}\" /var/log/fail2ban.log* 2>/dev/null | tail -n 1";
$line = (string) @shell_exec($cmd);
}
// 2) Fallback: journald (wenn kein Dateilog)
if (trim($line) === '') {
// seit 14 Tagen scannen, Format wie im file-log
$cmdJ = "journalctl -u fail2ban --since '14 days ago' 2>/dev/null | grep -F \"[{$jail}] Ban {$ip}\" | tail -n 1";
$line = (string) @shell_exec($cmdJ);
}
$line = trim($line);
if ($line === '') return null;
// Datum am Anfang: 2025-10-29 18:07:11,436 ...
if (preg_match('/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})/', $line, $m)) {
$ts = strtotime($m[1].' '.$m[2]);
return $ts ?: null;
}
// journald kann anderes Präfix haben; versuche generischen ISO-Zeitstempel irgendwo in der Zeile
if (preg_match('/(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})/', $line, $m)) {
$ts = strtotime($m[1].' '.$m[2]);
return $ts ?: null;
}
return null;
}
@ -163,6 +189,31 @@ class Fail2BanJailModal extends ModalComponent
return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
}
private function buildIpDetails(string $jail, array $ips, int $bantime): array
{
$out = [];
$now = time();
foreach ($ips as $ip) {
$banAt = $this->lastBanTimestamp($jail, $ip);
$remaining = null; $until = null;
if ($bantime === -1) {
$remaining = -1; // permanent
} elseif ($banAt !== null) {
$remaining = max(0, $bantime - ($now - $banAt));
$until = $remaining > 0 ? ($banAt + $bantime) : null;
}
$out[] = [
'ip' => $ip,
'remaining' => $remaining, // -1=permanent, null=unbekannt, 0..N Sekunden
'until' => $until,
];
}
return $out;
}
private function fmtSecs(int $s): string
{
// kompakt: 1h 23m 45s / 05m 10s / 12s

View File

@ -32,7 +32,7 @@
@push('modal.footer')
<div class="px-5 py-3 border-t border-white/10 backdrop-blur rounded-b-2xl">
<div class="flex items-center gap-2 justify-end">
<button wire:click="refresh" wire:loading.attr="disabled"
<button wire:click="$dispatch('f2b:refresh-banlist')" wire:loading.attr="disabled"
class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">
<i class="ph ph-arrows-clockwise text-[13px]"></i>
<span wire:loading.remove>Neu prüfen</span>