parent
9aa9475387
commit
77f22518c8
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue