mailwolt/app/Livewire/Ui/Security/Modal/Fail2banIpModal.php

568 lines
18 KiB
PHP

<?php
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
$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') {
// 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
$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,
);
}
// Modal bewusst am Ende schließen (Toast bleibt sichtbar)
$this->closeModal();
}
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();
$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->closeModal();
}
/* ---------------- 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 (unsichtbar in der UI)
$ips = Fail2banIpList::allWhitelistForConfig();
$ignore = implode(' ', array_unique(array_filter($ips)));
$content = "[DEFAULT]\nignoreip = {$ignore}\n";
$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;
//
//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;
//
//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 (!$this->isValidIpOrCidr($ip)) {
// throw ValidationException::withMessages(['ip' => 'Ungültige IP oder CIDR.']);
// }
//
// // DB schreiben
// Fail2banIpList::firstOrCreate(['ip' => $ip, 'type' => $this->type]);
//
// if ($this->type === 'whitelist') {
// $this->writeWhitelistConfig();
// $this->reloadFail2ban();
// } else {
// // Blacklist = sofort bannen im dedizierten Jail
// $this->banIp($ip);
// }
//
// $this->dispatch('f2b:refresh');
// $this->dispatch('notify', message: ucfirst($this->type).' aktualisiert.');
// $this->closeModal();
// $this->dispatch('f2b:refresh'); // falls du eine Liste neu laden willst
// }
//
// public function remove(): void
// {
// $this->assertRemoveMode();
// $ip = trim($this->prefill ?? $this->ip);
//
// if ($ip === '') return;
//
// Fail2banIpList::where('type', $this->type)->where('ip', $ip)->delete();
//
// if ($this->type === 'whitelist') {
// $this->writeWhitelistConfig();
// $this->reloadFail2ban();
// } else {
// // aus Blacklist-Jail entbannen, falls noch aktiv
// $this->unbanIp($ip);
// }
//
// $this->dispatch('f2b:refresh');
// $this->dispatch('notify', message: ucfirst($this->type).' Eintrag entfernt.');
// $this->closeModal();
// $this->dispatch('f2b:refresh');
// }
//
// /* ---------------- 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 isValidIpOrCidr(string $s): bool
// {
// // IP
// if (filter_var($s, FILTER_VALIDATE_IP)) return true;
//
// // CIDR
// if (strpos($s, '/') !== false) {
// [$ip, $mask] = explode('/', $s, 2);
// if (!filter_var($ip, FILTER_VALIDATE_IP)) return false;
// if (strpos($ip, ':') !== false) {
// // IPv6
// return ctype_digit($mask) && (int)$mask >= 8 && (int)$mask <= 128;
// }
// // IPv4
// return ctype_digit($mask) && (int)$mask >= 8 && (int)$mask <= 32;
// }
// return false;
// }
//
// private function writeWhitelistConfig(): void
// {
// $ips = Fail2banIpList::where('type', 'whitelist')->pluck('ip')->toArray();
// $ignore = implode(' ', array_unique(array_filter($ips)));
// $content = "[DEFAULT]\nignoreip = {$ignore}\n";
//
// $file = '/etc/fail2ban/jail.d/mailwolt-whitelist.local';
// $tmp = $file.'.tmp';
// @file_put_contents($tmp, $content, LOCK_EX);
// @chmod($tmp, 0644);
// @rename($tmp, $file);
// }
//
// private function reloadFail2ban(): void
// {
// @shell_exec('sudo fail2ban-client reload 2>&1');
// }
//
// private function banIp(string $ip): void
// {
// $ipEsc = escapeshellarg($ip);
// @shell_exec("sudo fail2ban-client set mailwolt-blacklist banip {$ipEsc} 2>&1");
// // optional: in DB zusätzlich behalten, damit UI konsistent ist (bereits oben getan)
// }
//
// private function unbanIp(string $ip): void
// {
// $ipEsc = escapeshellarg($ip);
// @shell_exec("sudo fail2ban-client set mailwolt-blacklist unbanip {$ipEsc} 2>&1");
// }
//}