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

main v1.0.130
boban 2025-11-01 22:53:37 +01:00
parent 9acea7b89b
commit e3dc81ef73
4 changed files with 142 additions and 61 deletions

View File

@ -14,20 +14,37 @@ class Fail2banBanlist extends Component
*/
public ?string $jail = null;
/** @var array<int,string> */
public array $banned = [];
/**
* Struktur für Blade (reine Ausgabe, keine Logik im Blade):
* [
* [
* 'ip' => '1.2.3.4',
* 'jail' => 'recidive',
* 'permanent' => false,
* 'label' => 'Temporär', // oder 'Permanent'
* 'box' => 'border-amber-400/20 bg-white/3', // Kartenstil
* 'badge' => 'border-amber-400/30 bg-amber-500/10 text-amber-200',
* 'btn' => 'border-rose-400/30 bg-rose-500/10 text-rose-200 hover:border-rose-400/50',
* ],
* ...
* ]
*
* @var array<int,array{
* ip:string,jail:string,permanent:bool,label:string,box:string,badge:string,btn:string
* }>
*/
public array $rows = [];
#[On('f2b:refresh')]
public function refreshList(): void
{
$this->loadBannedIps();
$this->loadBanned();
}
public function mount(?string $jail = null): void
{
// erlaubt optionalen Param via <livewire ... :jail="'recidive'" />
$this->jail = $jail;
$this->loadBannedIps();
$this->loadBanned();
}
public function render()
@ -37,61 +54,119 @@ class Fail2banBanlist extends Component
/* ================= core ================= */
private function loadBannedIps(): void
private function loadBanned(): void
{
$jails = $this->jailList();
// ggf. nur ein bestimmtes Jail anzeigen
// ggf. nur ein bestimmtes Jail
if (is_string($this->jail) && $this->jail !== '' && $this->jail !== '*') {
$jails = in_array($this->jail, $jails, true) ? [$this->jail] : [];
}
$ips = [];
$rows = [];
foreach ($jails as $j) {
$out = $this->f2b("status ".escapeshellarg($j));
if (preg_match('/IP list:\s*(.+)$/mi', $out, $m)) {
$parts = preg_split('/\s+/', trim($m[1]));
foreach ($parts as $ip) {
if (filter_var($ip, FILTER_VALIDATE_IP)) $ips[] = $ip;
$out = $this->f2b("status " . escapeshellarg($j));
if (!preg_match('/IP list:\s*(.+)$/mi', $out, $m)) {
continue;
}
$ips = preg_split('/\s+/', trim($m[1])) ?: [];
foreach ($ips as $ip) {
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
continue;
}
$permanent = $this->isPermanent($j, $ip);
// Präsentationsklassen zentral definieren
if ($permanent) {
$box = 'border-rose-400/30 bg-rose-500/5';
$badge = 'border-rose-400/30 bg-rose-500/10 text-rose-200';
$label = 'Permanent';
} else {
$box = 'border-amber-400/20 bg-white/3';
$badge = 'border-amber-400/30 bg-amber-500/10 text-amber-200';
$label = 'Temporär';
}
$rows[] = [
'ip' => $ip,
'jail' => $j,
'permanent' => $permanent,
'label' => $label,
'box' => $box,
'badge' => $badge,
'btn' => 'border-rose-400/30 bg-rose-500/10 text-rose-200 hover:border-rose-400/50',
];
}
}
$this->banned = array_values(array_unique($ips));
// Sortierung: permanent oben, dann nach Jail, dann IP
usort($rows, function ($a, $b) {
if ($a['permanent'] !== $b['permanent']) return $a['permanent'] ? -1 : 1;
if ($a['jail'] !== $b['jail']) return strcmp($a['jail'], $b['jail']);
return strcmp($a['ip'], $b['ip']);
});
$this->rows = $rows;
}
/** Entbannt IP in allen passenden Jails */
public function unban(string $ip): void
/** Entbannt eine IP **im angegebenen Jail** (Button gibt Jail mit) */
public function unban(string $ip, string $jail): void
{
if (!filter_var($ip, FILTER_VALIDATE_IP)) return;
$jails = $this->jailList();
if (is_string($this->jail) && $this->jail !== '' && $this->jail !== '*') {
$jails = in_array($this->jail, $jails, true) ? [$this->jail] : [];
}
$cmd = sprintf(
'sudo -n /usr/bin/fail2ban-client set %s unbanip %s 2>&1',
escapeshellarg($jail),
escapeshellarg($ip)
);
@shell_exec($cmd);
$cnt = 0;
foreach ($jails as $j) {
$this->f2b("set ".escapeshellarg($j)." unbanip ".escapeshellarg($ip));
$cnt++;
}
$this->loadBannedIps();
$this->loadBanned();
$this->dispatch('toast',
type: 'done',
badge: 'Fail2Ban',
title: 'IP entbannt',
text: ($this->jail && $this->jail !== '*')
? "IP {$ip} in Jail „{$this->jail}“ entbannt."
: "IP {$ip} in {$cnt} Jail(s) entbannt.",
text: "IP {$ip} in Jail „{$jail}“ entbannt.",
duration: 5000,
);
}
/* ================= helpers ================= */
/** Liefert Liste aller Jails über fail2ban-client */
/** Prüft via SQLite, ob der **letzte** Ban für (jail, ip) permanent ist (bantime < 0). */
private function isPermanent(string $jail, string $ip): bool
{
$db = $this->getDbFile();
if ($db === '' || !is_readable($db)) {
// Fallback: Blacklist-Jail ist per Design permanent
return $jail === 'mailwolt-blacklist';
}
$q = <<<SQL
WITH last AS (
SELECT MAX(timeofban) AS t
FROM bans
WHERE jail = '$jail' AND ip = '$ip'
)
SELECT bantime
FROM bans, last
WHERE jail = '$jail' AND ip = '$ip' AND timeofban = last.t
LIMIT 1;
SQL;
$cmd = sprintf(
'sudo -n /usr/bin/sqlite3 -readonly %s %s 2>&1',
escapeshellarg($db),
escapeshellarg($q)
);
$out = trim((string)@shell_exec($cmd));
if ($out === '') return ($jail === 'mailwolt-blacklist'); // Fallback
return ((int)$out) < 0;
}
/** Liste aller Jails */
private function jailList(): array
{
$out = $this->f2b('status');
@ -102,9 +177,19 @@ class Fail2banBanlist extends Component
return [];
}
/** führt fail2ban-client via sudo aus und gibt stdout zurück */
/** fail2ban-client über sudo aufrufen */
private function f2b(string $args): string
{
return (string) @shell_exec('sudo -n /usr/bin/fail2ban-client '.$args.' 2>&1');
}
/** Pfad zur Fail2Ban-SQLite-DB holen */
private function getDbFile(): string
{
$out = $this->f2b('get dbfile');
$lines = array_values(array_filter(array_map('trim', preg_split('/\r?\n/', $out))));
$path = end($lines) ?: '';
$path = preg_replace('/^`?-?\s*/', '', $path);
return $path ?: '/var/lib/fail2ban/fail2ban.sqlite3';
}
}

View File

@ -3,6 +3,7 @@
namespace App\Livewire\Ui\Security;
use Livewire\Attributes\On;
use Livewire\Component;
use App\Models\Fail2banSetting;
use App\Models\Fail2banIpList;

View File

@ -1,39 +1,34 @@
<div class="space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Aktuell gebannte IPs</h3>
<h3 class="text-lg font-semibold text-white/90">Aktuell gebannte IPs</h3>
<button wire:click="refreshList"
class="px-3 py-1.5 text-sm bg-gray-700 text-white rounded hover:bg-gray-600">
class="inline-flex items-center gap-1.5 rounded-lg border border-white/10 bg-white/5 px-2.5 py-1 text-xs text-white/80 hover:text-white hover:border-white/20">
<i class="ph ph-arrows-counter-clockwise text-[14px]"></i>
Aktualisieren
</button>
</div>
@if (empty($banned))
<div class="text-gray-500 text-sm">
Keine aktiven Banns vorhanden.
</div>
@if (empty($rows))
<div class="text-white/50 text-sm">Keine aktiven Banns vorhanden.</div>
@else
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<table class="min-w-full text-sm text-left">
<thead class="bg-gray-100 dark:bg-gray-800">
<tr>
<th class="px-4 py-2 font-medium text-gray-700 dark:text-gray-300">IP-Adresse</th>
<th class="px-4 py-2 font-medium text-gray-700 dark:text-gray-300 w-32 text-right">Aktion</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@foreach ($banned as $ip)
<tr class="hover:bg-gray-50 dark:hover:bg-gray-900/30">
<td class="px-4 py-2 text-gray-800 dark:text-gray-100">{{ $ip }}</td>
<td class="px-4 py-2 text-right">
<button wire:click="unban('{{ $ip }}')"
class="px-2 py-1 text-xs bg-red-600 text-white rounded hover:bg-red-500">
Entbannen
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="space-y-3">
@foreach ($rows as $r)
<div class="flex items-center justify-between rounded-2xl border px-4 py-3 {{ $r['box'] }}">
<div class="flex items-center gap-3">
<div class="text-white/90 font-medium tracking-wide">{{ $r['ip'] }}</div>
<span class="text-xs rounded-full px-2 py-0.5 border {{ $r['badge'] }}">
{{ $r['style'] === 'permanent' ? 'Permanent' : 'Temporär' }}
</span>
<span class="text-xs text-white/50">Jail: {{ $r['jail'] }}</span>
</div>
<button
wire:click="unban('{{ $r['ip'] }}','{{ $r['jail'] }}')"
class="text-[13px] px-3 py-2 rounded-xl border border-rose-400/30 bg-rose-500/10 text-rose-200 hover:border-rose-400/50">
Entbannen
</button>
</div>
@endforeach
</div>
@endif
</div>

View File

@ -8,7 +8,7 @@
<span class="text-[11px] uppercase tracking-wide text-white/70">Fail2Ban Konfiguration</span>
</div>
<button wire:click="save"
class="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-1.5 text-white/80 hover:text-white hover:border-white/20">
class="inline-flex items-center gap-1.5 rounded-lg border border-white/10 bg-white/5 px-2.5 py-1 text-xs text-white/80 hover:text-white hover:border-white/20">
<i class="ph ph-floppy-disk text-[14px]"></i> Speichern & Reload
</button>
</div>