diff --git a/app/Livewire/Ui/Security/Fail2banSettings.php b/app/Livewire/Ui/Security/Fail2banSettings.php index 390719e..66f950c 100644 --- a/app/Livewire/Ui/Security/Fail2banSettings.php +++ b/app/Livewire/Ui/Security/Fail2banSettings.php @@ -3,10 +3,10 @@ namespace App\Livewire\Ui\Security; -use Livewire\Attributes\On; use Livewire\Component; use App\Models\Fail2banSetting; use App\Models\Fail2banIpList; +use Illuminate\Validation\ValidationException; class Fail2banSettings extends Component { @@ -31,13 +31,10 @@ class Fail2banSettings extends Component { $this->whitelist = Fail2banIpList::visibleWhitelist()->pluck('ip')->toArray(); $this->blacklist = Fail2banIpList::visibleBlacklist()->pluck('ip')->toArray(); -// $this->whitelist = Fail2banIpList::where('type', 'whitelist')->pluck('ip')->toArray(); -// $this->blacklist = Fail2banIpList::where('type', 'blacklist')->pluck('ip')->toArray(); } public function mount(): void { - // Setting holen oder Defaults anlegen $this->settings = Fail2banSetting::first() ?? Fail2banSetting::create([ 'bantime' => 3600, 'max_bantime' => 43200, @@ -50,7 +47,6 @@ class Fail2banSettings extends Component 'external_mode' => false, ]); - // Properties befüllen $this->fill([ 'bantime' => (int)$this->settings->bantime, 'max_bantime' => (int)$this->settings->max_bantime, @@ -78,29 +74,48 @@ class Fail2banSettings extends Component 'cidr_v6' => 'required|integer|min:8|max:128', ]); - // Einstellungen speichern - $this->settings->update([ - 'bantime' => $this->bantime, - 'max_bantime' => $this->max_bantime, - 'bantime_increment' => $this->bantime_increment, - 'bantime_factor' => $this->bantime_factor, - 'max_retry' => $this->max_retry, - 'findtime' => $this->findtime, - 'cidr_v4' => $this->cidr_v4, - 'cidr_v6' => $this->cidr_v6, - 'external_mode' => $this->external_mode, - ]); + try { + // Einstellungen speichern + $this->settings->update([ + 'bantime' => $this->bantime, + 'max_bantime' => $this->max_bantime, + 'bantime_increment' => $this->bantime_increment, + 'bantime_factor' => $this->bantime_factor, + 'max_retry' => $this->max_retry, + 'findtime' => $this->findtime, + 'cidr_v4' => $this->cidr_v4, + 'cidr_v6' => $this->cidr_v6, + 'external_mode' => $this->external_mode, + ]); - // Config-Dateien schreiben - $this->writeDefaultsConfig(); - $this->writeWhitelistConfig(); + // Config-Dateien schreiben + $this->writeDefaultsConfig(); + $this->writeWhitelistConfig(); - // Fail2Ban reload - $this->runCommand('sudo -n /usr/bin/fail2ban-client reload'); + // Fail2Ban reload + $this->runCommand('sudo -n /usr/bin/fail2ban-client reload'); - $this->dispatch('notify', message: 'Gespeichert & Fail2Ban neu geladen.'); + $this->dispatch('toast', + type: 'success', + badge: 'Fail2Ban', + title: 'Einstellungen gespeichert', + text: 'Die Fail2Ban-Konfiguration wurde erfolgreich übernommen und ist jetzt aktiv.', + duration: 6000, + ); + + } catch (\Throwable $e) { + $this->dispatch('toast', + type: 'error', + badge: 'Fail2Ban', + title: 'Fehler beim Anwenden', + text: 'Die neuen Einstellungen konnten nicht angewendet werden: ' . $e->getMessage(), + duration: 8000, + ); + } } + /* ---------------- Config-Dateien ---------------- */ + protected function writeDefaultsConfig(): void { $s = $this->settings; @@ -120,7 +135,8 @@ CONF; protected function writeWhitelistConfig(): void { - $ips = Fail2banIpList::where('type', 'whitelist')->pluck('ip')->toArray(); + // zieht System + User-Whitelist + $ips = Fail2banIpList::allWhitelistForConfig(); $ignore = implode(' ', array_unique(array_filter($ips))); $content = "[DEFAULT]\nignoreip = {$ignore}\n"; @@ -128,9 +144,8 @@ CONF; $this->writeRootFileViaTee('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content); } - /** - * Schreibt Root-Dateien sicher via `sudo tee` - */ + /* ---------------- Helper ---------------- */ + private function writeRootFileViaTee(string $target, string $content): void { if (!preg_match('#^/etc/fail2ban/jail\.d/[A-Za-z0-9._-]+\.local$#', $target)) { @@ -138,32 +153,28 @@ CONF; } $cmd = sprintf('sudo -n /usr/bin/tee %s >/dev/null', escapeshellarg($target)); - - $descriptorspec = [ + $desc = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; - $proc = proc_open($cmd, $descriptorspec, $pipes, null, null); + $proc = proc_open($cmd, $desc, $pipes); if (!is_resource($proc)) { - throw new \RuntimeException('Failed to start tee'); + throw new \RuntimeException('tee start fehlgeschlagen'); } fwrite($pipes[0], $content); fclose($pipes[0]); stream_get_contents($pipes[1]); stream_get_contents($pipes[2]); - $exitCode = proc_close($proc); + $code = proc_close($proc); - if ($exitCode !== 0) { + if ($code !== 0) { throw new \RuntimeException("tee failed writing to {$target}"); } } - /** - * Führt Systembefehle aus und wirft Exception bei Fehlern - */ private function runCommand(string $cmd): void { $output = []; @@ -186,6 +197,195 @@ CONF; } } +//namespace App\Livewire\Ui\Security; +// +//use Livewire\Attributes\On; +//use Livewire\Component; +//use App\Models\Fail2banSetting; +//use App\Models\Fail2banIpList; +// +//class Fail2banSettings extends Component +//{ +// // Formfelder +// public int $bantime; +// public int $max_bantime; +// public bool $bantime_increment; +// public float $bantime_factor; +// public int $max_retry; +// public int $findtime; +// public int $cidr_v4; +// public int $cidr_v6; +// public bool $external_mode; +// +// public array $whitelist = []; +// public array $blacklist = []; +// +// public Fail2banSetting $settings; +// +// #[On('f2b:refresh')] +// public function refreshLists(): void +// { +// $this->whitelist = Fail2banIpList::visibleWhitelist()->pluck('ip')->toArray(); +// $this->blacklist = Fail2banIpList::visibleBlacklist()->pluck('ip')->toArray(); +// } +// +// public function mount(): void +// { +// // Setting holen oder Defaults anlegen +// $this->settings = Fail2banSetting::first() ?? Fail2banSetting::create([ +// 'bantime' => 3600, +// 'max_bantime' => 43200, +// 'bantime_increment' => true, +// 'bantime_factor' => 1.5, +// 'max_retry' => 3, +// 'findtime' => 600, +// 'cidr_v4' => 32, +// 'cidr_v6' => 128, +// 'external_mode' => false, +// ]); +// +// // Properties befüllen +// $this->fill([ +// 'bantime' => (int)$this->settings->bantime, +// 'max_bantime' => (int)$this->settings->max_bantime, +// 'bantime_increment' => (bool)$this->settings->bantime_increment, +// 'bantime_factor' => (float)$this->settings->bantime_factor, +// 'max_retry' => (int)$this->settings->max_retry, +// 'findtime' => (int)$this->settings->findtime, +// 'cidr_v4' => (int)$this->settings->cidr_v4, +// 'cidr_v6' => (int)$this->settings->cidr_v6, +// 'external_mode' => (bool)$this->settings->external_mode, +// ]); +// +// $this->refreshLists(); +// } +// +// public function save(): void +// { +// $this->validate([ +// 'bantime' => 'required|integer|min:60', +// 'max_bantime' => 'required|integer|min:60', +// 'bantime_factor' => 'required|numeric|min:1', +// 'max_retry' => 'required|integer|min:1', +// 'findtime' => 'required|integer|min:60', +// 'cidr_v4' => 'required|integer|min:8|max:32', +// 'cidr_v6' => 'required|integer|min:8|max:128', +// ]); +// +// // Einstellungen speichern +// $this->settings->update([ +// 'bantime' => $this->bantime, +// 'max_bantime' => $this->max_bantime, +// 'bantime_increment' => $this->bantime_increment, +// 'bantime_factor' => $this->bantime_factor, +// 'max_retry' => $this->max_retry, +// 'findtime' => $this->findtime, +// 'cidr_v4' => $this->cidr_v4, +// 'cidr_v6' => $this->cidr_v6, +// 'external_mode' => $this->external_mode, +// ]); +// +// // Config-Dateien schreiben +// $this->writeDefaultsConfig(); +// $this->writeWhitelistConfig(); +// +// // Fail2Ban reload +// $this->runCommand('sudo -n /usr/bin/fail2ban-client reload'); +// +// $this->dispatch('toast', +// type: 'done', +// badge: 'Fail2Ban', +// title: 'Einstellungen gespeichert', +// text: 'Die Fail2Ban-Konfiguration wurde erfolgreich übernommen und ist jetzt aktiv.', +// duration: 6000, +// ); +// } +// +// protected function writeDefaultsConfig(): void +// { +// $s = $this->settings; +// +// $content = <<bantime} +//findtime = {$s->findtime} +//maxretry = {$s->max_retry} +//bantime.increment = {$this->boolToStr($s->bantime_increment)} +//bantime.factor = {$s->bantime_factor} +//bantime.maxtime = {$s->max_bantime} +//CONF; +// +// $this->writeRootFileViaTee('/etc/fail2ban/jail.d/00-mailwolt-defaults.local', $content); +// } +// +// protected function writeWhitelistConfig(): void +// { +// $ips = Fail2banIpList::where('type', 'whitelist')->pluck('ip')->toArray(); +// $ignore = implode(' ', array_unique(array_filter($ips))); +// +// $content = "[DEFAULT]\nignoreip = {$ignore}\n"; +// +// $this->writeRootFileViaTee('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content); +// } +// +// /** +// * Schreibt Root-Dateien sicher via `sudo tee` +// */ +// private function writeRootFileViaTee(string $target, string $content): void +// { +// if (!preg_match('#^/etc/fail2ban/jail\.d/[A-Za-z0-9._-]+\.local$#', $target)) { +// throw new \RuntimeException("Illegal path: $target"); +// } +// +// $cmd = sprintf('sudo -n /usr/bin/tee %s >/dev/null', escapeshellarg($target)); +// +// $descriptorspec = [ +// 0 => ['pipe', 'r'], +// 1 => ['pipe', 'w'], +// 2 => ['pipe', 'w'], +// ]; +// +// $proc = proc_open($cmd, $descriptorspec, $pipes, null, null); +// if (!is_resource($proc)) { +// throw new \RuntimeException('Failed to start tee'); +// } +// +// fwrite($pipes[0], $content); +// fclose($pipes[0]); +// stream_get_contents($pipes[1]); +// stream_get_contents($pipes[2]); +// $exitCode = proc_close($proc); +// +// if ($exitCode !== 0) { +// throw new \RuntimeException("tee failed writing to {$target}"); +// } +// } +// +// /** +// * Führt Systembefehle aus und wirft Exception bei Fehlern +// */ +// private function runCommand(string $cmd): void +// { +// $output = []; +// $return = 0; +// exec($cmd . ' 2>&1', $output, $return); +// +// if ($return !== 0) { +// throw new \RuntimeException("Command failed ($return): {$cmd}\n" . implode("\n", $output)); +// } +// } +// +// private function boolToStr(bool $v): string +// { +// return $v ? 'true' : 'false'; +// } +// +// public function render() +// { +// return view('livewire.ui.security.fail2ban-settings'); +// } +//} + // //namespace App\Livewire\Ui\Security; // diff --git a/app/Livewire/Ui/Security/Modal/Fail2banIpModal.php b/app/Livewire/Ui/Security/Modal/Fail2banIpModal.php index c56609f..66cf691 100644 --- a/app/Livewire/Ui/Security/Modal/Fail2banIpModal.php +++ b/app/Livewire/Ui/Security/Modal/Fail2banIpModal.php @@ -65,7 +65,7 @@ class Fail2banIpModal extends ModalComponent throw ValidationException::withMessages(['ip' => 'Loopback/localhost ist bereits systemseitig erlaubt und kann nicht geändert werden.']); } - // Duplikate abfangen (es gibt einen Unique-Index ip+type; trotzdem user-freundlich) + // Duplikate abfangen $exists = Fail2banIpList::where('ip', $ip)->where('type', $this->type)->exists(); if ($exists) { throw ValidationException::withMessages(['ip' => ucfirst($this->type) . ' enthält diese IP bereits.']); @@ -75,17 +75,36 @@ class Fail2banIpModal extends ModalComponent Fail2banIpList::create(['ip' => $ip, 'type' => $this->type]); if ($this->type === 'whitelist') { - $this->writeWhitelistConfig(); // schreibt /etc/fail2ban/jail.d/mailwolt-whitelist.local - $this->reloadFail2ban(); // f2b neu laden + // Whitelist-Datei aktualisieren + Fail2Ban reload + $this->writeWhitelistConfig(); + $this->reloadFail2ban(); + + // UI aktualisieren & Toast + $this->dispatch('f2b:refresh'); + $this->dispatch('toast', + type: 'success', + badge: 'Fail2Ban', + title: 'Whitelist aktualisiert', + text: 'Die IP wurde erfolgreich zur Whitelist hinzugefügt und ist nun freigegeben.', + duration: 6000, + ); } else { - // Blacklist = sofort bannen im dedizierten Jail + // Blacklist = sofort bannen $this->banIp($ip); + + // UI aktualisieren & Toast + $this->dispatch('f2b:refresh'); + $this->dispatch('toast', + type: 'warning', + badge: 'Fail2Ban', + title: 'Blacklist aktualisiert', + text: 'Die IP wurde zur Blacklist hinzugefügt und umgehend blockiert.', + duration: 6000, + ); } - $this->dispatch('f2b:refresh'); - $this->dispatch('notify', message: ucfirst($this->type) . ' aktualisiert.'); + // Modal bewusst am Ende schließen (Toast bleibt sichtbar) $this->closeModal(); - $this->dispatch('f2b:refresh'); } public function remove(): void @@ -105,14 +124,29 @@ class Fail2banIpModal extends ModalComponent if ($this->type === 'whitelist') { $this->writeWhitelistConfig(); $this->reloadFail2ban(); + + $this->dispatch('f2b:refresh'); + $this->dispatch('toast', + type: 'info', + badge: 'Fail2Ban', + title: 'Whitelist geändert', + text: 'Die IP wurde aus der Whitelist entfernt.', + duration: 6000, + ); } else { $this->unbanIp($ip); + + $this->dispatch('f2b:refresh'); + $this->dispatch('toast', + type: 'info', + badge: 'Fail2Ban', + title: 'Blacklist geändert', + text: 'Die IP wurde aus der Blacklist entfernt und ist wieder freigegeben.', + duration: 6000, + ); } - $this->dispatch('f2b:refresh'); - $this->dispatch('notify', message: ucfirst($this->type) . ' Eintrag entfernt.'); $this->closeModal(); - $this->dispatch('f2b:refresh'); } /* ---------------- helper ---------------- */ @@ -129,12 +163,11 @@ class Fail2banIpModal extends ModalComponent private function writeWhitelistConfig(): void { - // WICHTIG: inkl. System-IPs + // WICHTIG: inkl. System-IPs (unsichtbar in der UI) $ips = Fail2banIpList::allWhitelistForConfig(); $ignore = implode(' ', array_unique(array_filter($ips))); $content = "[DEFAULT]\nignoreip = {$ignore}\n"; - // sicher in Root-Pfad schreiben (sudo tee) $this->writeRootFileViaTee('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content); } @@ -154,12 +187,14 @@ class Fail2banIpModal extends ModalComponent if (!is_resource($proc)) { throw new \RuntimeException('tee start fehlgeschlagen'); } + fwrite($pipes[0], $content); fclose($pipes[0]); $stdout = stream_get_contents($pipes[1]); fclose($pipes[1]); $stderr = stream_get_contents($pipes[2]); fclose($pipes[2]); + $code = proc_close($proc); if ($code !== 0) { throw new \RuntimeException("tee failed (code $code): $stderr $stdout"); @@ -183,6 +218,194 @@ class Fail2banIpModal extends ModalComponent @shell_exec("sudo -n /usr/bin/fail2ban-client set mailwolt-blacklist unbanip {$ipEsc} 2>&1"); } } + +//namespace App\Livewire\Ui\Security\Modal; +// +//use LivewireUI\Modal\ModalComponent; +//use App\Models\Fail2banIpList; +//use Illuminate\Validation\ValidationException; +// +//class Fail2banIpModal extends ModalComponent +//{ +// /** 'whitelist' | 'blacklist' */ +// public string $type = 'whitelist'; +// +// /** 'add' | 'remove' */ +// public string $mode = 'add'; +// +// /** IP/CIDR im Formular */ +// public string $ip = ''; +// +// /** Für "remove" vorbefüllt */ +// public ?string $prefill = null; +// +// public static function modalMaxWidth(): string +// { +// return 'lg'; +// } +// +// public function mount(string $type = 'whitelist', string $mode = 'add', ?string $ip = null): void +// { +// $type = strtolower($type); +// $mode = strtolower($mode); +// +// if (!in_array($type, ['whitelist', 'blacklist'], true)) { +// throw new \InvalidArgumentException('Invalid type'); +// } +// if (!in_array($mode, ['add', 'remove'], true)) { +// throw new \InvalidArgumentException('Invalid mode'); +// } +// +// $this->type = $type; +// $this->mode = $mode; +// $this->ip = $ip ?? ''; +// $this->prefill = $ip; +// } +// +// public function render() +// { +// return view('livewire.ui.security.modal.fail2ban-ip-modal'); +// } +// +// /* ---------------- actions ---------------- */ +// +// public function save(): void +// { +// $this->assertAddMode(); +// $ip = trim($this->ip); +// +// if (!Fail2banIpList::isValidIpOrCidr($ip)) { +// throw ValidationException::withMessages(['ip' => 'Ungültige IP oder CIDR.']); +// } +// +// // Schutz: System-/Loopback-IPs darf der User nicht manuell pflegen +// if (Fail2banIpList::isLoopback($ip)) { +// throw ValidationException::withMessages(['ip' => 'Loopback/localhost ist bereits systemseitig erlaubt und kann nicht geändert werden.']); +// } +// +// // Duplikate abfangen (es gibt einen Unique-Index ip+type; trotzdem user-freundlich) +// $exists = Fail2banIpList::where('ip', $ip)->where('type', $this->type)->exists(); +// if ($exists) { +// throw ValidationException::withMessages(['ip' => ucfirst($this->type) . ' enthält diese IP bereits.']); +// } +// +// // DB schreiben +// Fail2banIpList::create(['ip' => $ip, 'type' => $this->type]); +// +// if ($this->type === 'whitelist') { +// $this->writeWhitelistConfig(); // schreibt /etc/fail2ban/jail.d/mailwolt-whitelist.local +// $this->reloadFail2ban(); // f2b neu laden +// } else { +// // Blacklist = sofort bannen im dedizierten Jail +// $this->banIp($ip); +// } +// +// $this->closeModal(); +// $this->dispatch('f2b:refresh'); +// } +// +// public function remove(): void +// { +// $this->assertRemoveMode(); +// $ip = trim($this->prefill ?? $this->ip); +// if ($ip === '') return; +// +// // System-Whitelist darf nicht entfernt werden +// $row = Fail2banIpList::where('type', $this->type)->where('ip', $ip)->first(); +// if ($row && $row->is_system) { +// throw ValidationException::withMessages(['ip' => 'Systemeintrag kann nicht entfernt werden.']); +// } +// +// Fail2banIpList::where('type', $this->type)->where('ip', $ip)->delete(); +// +// if ($this->type === 'whitelist') { +// $this->writeWhitelistConfig(); +// $this->reloadFail2ban(); +// } else { +// $this->unbanIp($ip); +// } +// +// $this->closeModal(); +// $this->dispatch('f2b:refresh'); +// $this->dispatch('toast', +// type: 'done', +// badge: 'Fail2Ban', +// title: 'Einstellungen gespeichert', +// text: 'Die Fail2Ban-Konfiguration wurde erfolgreich übernommen und ist jetzt aktiv.', +// duration: 6000, +// ); +// } +// +// /* ---------------- helper ---------------- */ +// +// private function assertAddMode(): void +// { +// if ($this->mode !== 'add') throw new \LogicException('Wrong mode'); +// } +// +// private function assertRemoveMode(): void +// { +// if ($this->mode !== 'remove') throw new \LogicException('Wrong mode'); +// } +// +// private function writeWhitelistConfig(): void +// { +// // WICHTIG: inkl. System-IPs +// $ips = Fail2banIpList::allWhitelistForConfig(); +// $ignore = implode(' ', array_unique(array_filter($ips))); +// $content = "[DEFAULT]\nignoreip = {$ignore}\n"; +// +// // sicher in Root-Pfad schreiben (sudo tee) +// $this->writeRootFileViaTee('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content); +// } +// +// private function writeRootFileViaTee(string $target, string $content): void +// { +// if (!preg_match('#^/etc/fail2ban/jail\.d/[A-Za-z0-9._-]+\.local$#', $target)) { +// throw new \RuntimeException("Illegal path: $target"); +// } +// +// $cmd = sprintf('sudo -n /usr/bin/tee %s >/dev/null', escapeshellarg($target)); +// $desc = [ +// 0 => ['pipe', 'r'], +// 1 => ['pipe', 'w'], +// 2 => ['pipe', 'w'], +// ]; +// $proc = proc_open($cmd, $desc, $pipes); +// if (!is_resource($proc)) { +// throw new \RuntimeException('tee start fehlgeschlagen'); +// } +// fwrite($pipes[0], $content); +// fclose($pipes[0]); +// $stdout = stream_get_contents($pipes[1]); +// fclose($pipes[1]); +// $stderr = stream_get_contents($pipes[2]); +// fclose($pipes[2]); +// $code = proc_close($proc); +// if ($code !== 0) { +// throw new \RuntimeException("tee failed (code $code): $stderr $stdout"); +// } +// } +// +// private function reloadFail2ban(): void +// { +// @shell_exec('sudo -n /usr/bin/fail2ban-client reload 2>&1'); +// } +// +// private function banIp(string $ip): void +// { +// $ipEsc = escapeshellarg($ip); +// @shell_exec("sudo -n /usr/bin/fail2ban-client set mailwolt-blacklist banip {$ipEsc} 2>&1"); +// } +// +// private function unbanIp(string $ip): void +// { +// $ipEsc = escapeshellarg($ip); +// @shell_exec("sudo -n /usr/bin/fail2ban-client set mailwolt-blacklist unbanip {$ipEsc} 2>&1"); +// } +//} + + // //namespace App\Livewire\Ui\Security\Modal; //