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

main v1.0.120
boban 2025-10-31 04:26:34 +01:00
parent a3a4ec4d06
commit 834f173bb9
1 changed files with 147 additions and 51 deletions

View File

@ -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 = <<<SQL
WITH last AS (
SELECT ip, MAX(timeofban) AS t
FROM bans
WHERE jail=%s
GROUP BY ip
),
curr AS (
SELECT b.ip, b.timeofban, b.bantime
FROM bans b
JOIN last l ON l.ip=b.ip AND l.t=b.timeofban
)
SELECT
ip,
timeofban,
bantime,
CASE WHEN bantime < 0 THEN -1
ELSE (timeofban + bantime) - CAST(strftime('%s','now') AS INTEGER)
END AS remaining,
CASE WHEN bantime < 0 THEN NULL
ELSE (timeofban + bantime)
END AS expire
FROM curr
WHERE bantime < 0
OR (timeofban + bantime) > 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 <IP>"-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 <jail>` 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;