parent
a3a4ec4d06
commit
834f173bb9
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue