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

main v1.0.127
boban 2025-11-01 22:05:14 +01:00
parent 9aa9475387
commit 77f22518c8
4 changed files with 226 additions and 38 deletions

View File

@ -0,0 +1,77 @@
<?php
namespace App\Livewire\Ui\Security;
use Livewire\Attributes\On;
use Livewire\Component;
use Illuminate\Support\Str;
class Fail2banBanlist extends Component
{
public array $banned = [];
public string $jail = 'mailwolt-blacklist'; // Dein dedizierter Jail
#[On('f2b:refresh')]
public function refreshList(): void
{
$this->loadBannedIps();
}
public function mount(): void
{
$this->loadBannedIps();
}
private function loadBannedIps(): void
{
$output = @shell_exec(sprintf('sudo -n /usr/bin/fail2ban-client status %s 2>&1', escapeshellarg($this->jail)));
if (!$output) {
$this->banned = [];
return;
}
// Beispielausgabe:
// Status for the jail: mailwolt-blacklist
// |- Filter
// | |- Currently failed: 0
// | `- Total failed: 0
// `- Actions
// |- Currently banned: 2
// | `- IP list: 203.0.113.45 198.51.100.22
// `- Total banned: 2
if (preg_match('/IP list:\s*(.+)$/mi', $output, $m)) {
$ips = preg_split('/\s+/', trim($m[1]));
$this->banned = array_values(array_filter($ips, fn($ip) => filter_var($ip, FILTER_VALIDATE_IP)));
} else {
$this->banned = [];
}
}
public function unban(string $ip): void
{
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
return;
}
$ipEsc = escapeshellarg($ip);
$cmd = sprintf('sudo -n /usr/bin/fail2ban-client set %s unbanip %s 2>&1', escapeshellarg($this->jail), $ipEsc);
@shell_exec($cmd);
$this->dispatch('toast',
type: 'info',
badge: 'Fail2Ban',
title: 'IP entbannt',
text: "Die IP {$ip} wurde erfolgreich entbannt.",
duration: 6000,
);
$this->refreshList();
}
public function render()
{
return view('livewire.ui.security.fail2ban-banlist');
}
}

View File

@ -74,50 +74,111 @@ class Fail2BanJailModal extends ModalComponent
//
// $this->rows = $rows;
// logger()->debug('rows='.json_encode($rows, JSON_UNESCAPED_SLASHES));
// }
// protected function load(bool $force = false): void
// {
// $jail = $this->jail;
//
// // 1) Primär: DB
// $rows = $this->activeBansFromDb($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);
//
// 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,
// ];
// }
// }
//
// // 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));
// }
protected function load(bool $force = false): void
{
$jail = $this->jail;
// 1) Primär: DB
$rows = $this->activeBansFromDb($jail);
// 1) IP-Liste IMMER aus "status <jail>" holen (Quelle der Wahrheit)
[, $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)))) : [];
// 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);
$defaultBantime = $this->getBantime($jail);
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,
];
$rows = [];
foreach ($ips as $ip) {
$banAt = null; $until = null; $remaining = null;
// 2) DB-Info pro IP (optional, je nach Schema belegt)
if ($info = $this->banInfoFromDb($jail, $ip)) {
$banAt = $info['banned_at'];
if ((int)$info['expire'] === -1) {
$remaining = -1;
} elseif ((int)$info['expire'] > 0) {
$until = (int)$info['expire'];
$remaining = max(0, $until - time());
}
}
// 3) Fallback: Log + Default-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; // ~unbekannt/approx
}
}
// 4) Safety: falls 0s, aber F2B hält sie noch → als „~unbekannt“ kennzeichnen
if ($remaining === 0 && $this->isStillBanned($jail, $ip)) {
$remaining = -2;
}
[$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,
];
}
// 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));
$this->rows = $rows;
logger()->debug('rows='.json_encode($rows, JSON_UNESCAPED_SLASHES));
}
/** ROBUST: findet Binaries automatisch */
private function bin(string $name): string
{

View File

@ -0,0 +1,39 @@
<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>
<button wire:click="refreshList"
class="px-3 py-1.5 text-sm bg-gray-700 text-white rounded hover:bg-gray-600">
Aktualisieren
</button>
</div>
@if (empty($banned))
<div class="text-gray-500 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>
@endif
</div>

View File

@ -45,7 +45,8 @@
<div class="md:col-span-2">
<label class="inline-flex items-center gap-2 cursor-pointer select-none group">
<input type="checkbox" wire:model.defer="bantime_increment" class="peer sr-only">
<span class="w-5 h-5 flex items-center justify-center rounded-md border border-white/15 bg-white/5 peer-checked:bg-emerald-500/20 peer-checked:border-emerald-400/40">
<span
class="w-5 h-5 flex items-center justify-center rounded-md border border-white/15 bg-white/5 peer-checked:bg-emerald-500/20 peer-checked:border-emerald-400/40">
<i class="ph ph-check text-[12px] text-emerald-300 opacity-0 peer-checked:opacity-100"></i>
</span>
<span class="text-white/80 text-sm">Bantime dynamisch erhöhen (increment)</span>
@ -61,6 +62,11 @@
</div>
</div>
<div class="glass-card p-5">
<livewire:ui.security.fail2ban-banlist/>
</div>
<div class="glass-card p-5">
<div class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1 mb-4">
<i class="ph ph-info text-white/70 text-[13px]"></i>
@ -75,6 +81,7 @@
<li>Alle Änderungen hier werden nach Klick auf <em>„Speichern & Reload“</em> sofort aktiv.</li>
</ul>
</div>
</div>
{{-- RIGHT 1/3 --}}
@ -86,7 +93,8 @@
</div>
@forelse($whitelist as $ip)
<div class="flex items-center justify-between rounded-lg border border-white/10 bg-white/[0.03] px-3 py-2 mb-2">
<div
class="flex items-center justify-between rounded-lg border border-white/10 bg-white/[0.03] px-3 py-2 mb-2">
<span class="text-white/80 text-sm">{{ $ip }}</span>
<button class="text-[12px] px-2 py-0.5 rounded border border-white/10 hover:border-white/20"
wire:click="$dispatch('openModal',{component:'ui.security.modal.fail2ban-ip-modal',arguments:{mode:'remove',type:'whitelist',ip:'{{ $ip }}'}})">
@ -104,13 +112,15 @@
</div>
<div class="glass-card p-5">
<div class="inline-flex items-center gap-2 rounded-full bg-rose-500/10 border border-rose-400/30 px-2.5 py-1 mb-3">
<div
class="inline-flex items-center gap-2 rounded-full bg-rose-500/10 border border-rose-400/30 px-2.5 py-1 mb-3">
<i class="ph ph-hand text-rose-300 text-[13px]"></i>
<span class="text-[11px] uppercase tracking-wide text-rose-300">Blacklist</span>
</div>
@forelse($blacklist as $ip)
<div class="flex items-center justify-between rounded-lg border border-white/10 bg-white/[0.03] px-3 py-2 mb-2">
<div
class="flex items-center justify-between rounded-lg border border-white/10 bg-white/[0.03] px-3 py-2 mb-2">
<span class="text-white/80 text-sm">{{ $ip }}</span>
<button class="text-[12px] px-2 py-0.5 rounded border border-white/10 hover:border-white/20"
wire:click="$dispatch('openModal',{component:'ui.security.modal.fail2ban-ip-modal',arguments:{mode:'remove',type:'blacklist',ip:'{{ $ip }}'}})">
@ -121,8 +131,9 @@
<div class="text-sm text-white/50">Keine Einträge.</div>
@endforelse
<button class="text-[13px] w-full px-3 py-2 rounded-xl border border-rose-400/30 bg-rose-500/10 text-rose-200 hover:border-rose-400/50"
wire:click="$dispatch('openModal',{component:'ui.security.modal.fail2ban-ip-modal',arguments:{type:'blacklist'}})">
<button
class="text-[13px] w-full px-3 py-2 rounded-xl border border-rose-400/30 bg-rose-500/10 text-rose-200 hover:border-rose-400/50"
wire:click="$dispatch('openModal',{component:'ui.security.modal.fail2ban-ip-modal',arguments:{type:'blacklist'}})">
IP hinzufügen
</button>
</div>