Compare commits
45 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
af369acbf6 | |
|
|
d81c3bc07c | |
|
|
821a2bde33 | |
|
|
8e68051fde | |
|
|
afb8d09db3 | |
|
|
fc04ef44d0 | |
|
|
a7d84899fb | |
|
|
e3dc81ef73 | |
|
|
9acea7b89b | |
|
|
6c3cde5f65 | |
|
|
77f22518c8 | |
|
|
9aa9475387 | |
|
|
d4255b08fa | |
|
|
94aec78d4c | |
|
|
d3783e1717 | |
|
|
dcf9a8d3e9 | |
|
|
595828c5f6 | |
|
|
834f173bb9 | |
|
|
a3a4ec4d06 | |
|
|
2f390af9ed | |
|
|
530faf6b45 | |
|
|
8058f9b814 | |
|
|
c4b906223c | |
|
|
81860d1851 | |
|
|
792f0e3528 | |
|
|
46591669d6 | |
|
|
8690067d9c | |
|
|
beb2f863a3 | |
|
|
e833033074 | |
|
|
02e558bf4b | |
|
|
d9867db546 | |
|
|
e77d9f64bb | |
|
|
ee44ff3def | |
|
|
4d1fd64158 | |
|
|
6b0dd7d176 | |
|
|
67b6e1fa02 | |
|
|
8b4f2d9fe8 | |
|
|
e3c7e8de33 | |
|
|
385b67c3c5 | |
|
|
251f2d9c8f | |
|
|
aaae226c8d | |
|
|
3c1093311c | |
|
|
0cb7212d4b | |
|
|
ab13bab984 | |
|
|
47bca4c8de |
|
|
@ -1,9 +0,0 @@
|
||||||
[90m= [39m[34;4mApp\Models\Setting[39;24m {#6409
|
|
||||||
[34mid[39m: [35m13[39m,
|
|
||||||
[34mgroup[39m: "[32mwoltguard[39m",
|
|
||||||
[34mkey[39m: "[32mservices[39m",
|
|
||||||
[34mvalue[39m: "[32m{"ts":1761504019,"rows":[{"name":"postfix","ok":true},{"name":"dovecot","ok":true},{"name":"rspamd","ok":true},{"name":"clamav","ok":true},{"name":"db","ok":true},{"name":"redis","ok":true},{"name":"php-fpm","ok":true},{"name":"nginx","ok":true},{"name":"mw-queue","ok":true},{"name":"mw-schedule","ok":true},{"name":"mw-ws","ok":true},{"name":"fail2ban","ok":true},{"name":"journal","ok":true}]}[39m",
|
|
||||||
[34mcreated_at[39m: "[32m2025-10-26 19:40:19[39m",
|
|
||||||
[34mupdated_at[39m: "[32m2025-10-26 19:40:19[39m",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||||
|
use Illuminate\Session\TokenMismatchException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class Handler extends ExceptionHandler
|
||||||
|
{
|
||||||
|
public function render($request, \Throwable $e)
|
||||||
|
{
|
||||||
|
if ($e instanceof TokenMismatchException) {
|
||||||
|
if ($request->expectsJson()) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'session_expired',
|
||||||
|
'redirect' => route('login'),
|
||||||
|
], 419);
|
||||||
|
}
|
||||||
|
return redirect()
|
||||||
|
->route('login')
|
||||||
|
->with('warning', 'Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
||||||
|
}
|
||||||
|
return parent::render($request, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,7 +31,6 @@ if (!function_exists('webmail_host')) {
|
||||||
if (!function_exists('mta_host')) {
|
if (!function_exists('mta_host')) {
|
||||||
function mta_host(?int $domainId = null): string
|
function mta_host(?int $domainId = null): string
|
||||||
{
|
{
|
||||||
// 1️⃣ Vorrang: Datenbankwert (z. B. aus der domains-Tabelle)
|
|
||||||
if ($domainId) {
|
if ($domainId) {
|
||||||
try {
|
try {
|
||||||
$domain = \App\Models\Domain::find($domainId);
|
$domain = \App\Models\Domain::find($domainId);
|
||||||
|
|
@ -39,17 +38,25 @@ if (!function_exists('mta_host')) {
|
||||||
return $domain->mta_host;
|
return $domain->mta_host;
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// DB evtl. noch nicht migriert — fallback auf env
|
// DB evtl. noch nicht migriert — fallback auf env
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2️⃣ ENV-Variante (z. B. MTA_SUB=mail01)
|
|
||||||
$sub = env('MTA_SUB');
|
$sub = env('MTA_SUB');
|
||||||
if ($sub) {
|
if ($sub) {
|
||||||
return domain_host($sub);
|
return domain_host($sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3️⃣ Notfall: statischer Fallback
|
|
||||||
return domain_host('mx');
|
return domain_host('mx');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! function_exists('countryFlag')) {
|
||||||
|
function countryFlag(string $code): string
|
||||||
|
{
|
||||||
|
$code = strtoupper($code);
|
||||||
|
return implode('', array_map(
|
||||||
|
fn($char) => mb_chr(ord($char) + 127397, 'UTF-8'),
|
||||||
|
str_split($code)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class GuestOnlyMiddleware
|
||||||
{
|
{
|
||||||
if (Auth::check()) {
|
if (Auth::check()) {
|
||||||
// Eingeloggt → z. B. Dashboard weiterleiten
|
// Eingeloggt → z. B. Dashboard weiterleiten
|
||||||
return redirect()->route('dashboard');
|
return redirect()->route('ui.dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
|
|
|
||||||
|
|
@ -103,13 +103,6 @@ class DomainDnsModal extends ModalComponent
|
||||||
['type' => 'TXT', 'name' => $dkimHost, 'value' => $dkimTxt],
|
['type' => 'TXT', 'name' => $dkimHost, 'value' => $dkimTxt],
|
||||||
];
|
];
|
||||||
|
|
||||||
// $this->optional = [
|
|
||||||
// ['type' => 'SRV', 'name' => "_autodiscover._tcp.{$this->domainName}", 'value' => "0 1 443 {$mailServerFqdn}."],
|
|
||||||
// ['type' => 'SRV', 'name' => "_imaps._tcp.{$this->domainName}", 'value' => "0 1 993 {$mailServerFqdn}."],
|
|
||||||
// ['type' => 'SRV', 'name' => "_pop3s._tcp.{$this->domainName}", 'value' => "0 1 995 {$mailServerFqdn}."],
|
|
||||||
// ['type' => 'SRV', 'name' => "_submission._tcp.{$this->domainName}", 'value' => "0 1 587 {$mailServerFqdn}."],
|
|
||||||
// ];
|
|
||||||
|
|
||||||
$this->optional = [
|
$this->optional = [
|
||||||
// --- Service-Erkennung ---
|
// --- Service-Erkennung ---
|
||||||
[
|
[
|
||||||
|
|
@ -207,6 +200,7 @@ class DomainDnsModal extends ModalComponent
|
||||||
$this->dynamic[$i]['actual'] = $actual;
|
$this->dynamic[$i]['actual'] = $actual;
|
||||||
$this->dynamic[$i]['state'] = $state;
|
$this->dynamic[$i]['state'] = $state;
|
||||||
$this->dynamic[$i]['boxClass'] = $this->stateColors[$state] ?? $this->stateColors['neutral'];
|
$this->dynamic[$i]['boxClass'] = $this->stateColors[$state] ?? $this->stateColors['neutral'];
|
||||||
|
$this->dynamic[$i]['display_actual'] = $this->normActual($r['type'], $actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
// statische (Pflicht) Records prüfen
|
// statische (Pflicht) Records prüfen
|
||||||
|
|
@ -216,6 +210,7 @@ class DomainDnsModal extends ModalComponent
|
||||||
$this->static[$i]['actual'] = $actual;
|
$this->static[$i]['actual'] = $actual;
|
||||||
$this->static[$i]['state'] = $state;
|
$this->static[$i]['state'] = $state;
|
||||||
$this->static[$i]['boxClass'] = $this->stateColors[$state] ?? $this->stateColors['neutral'];
|
$this->static[$i]['boxClass'] = $this->stateColors[$state] ?? $this->stateColors['neutral'];
|
||||||
|
$this->static[$i]['display_actual'] = $this->normActual($r['type'], $actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
// optionale Records: nie „missing“, nur neutral|syntax|ok
|
// optionale Records: nie „missing“, nur neutral|syntax|ok
|
||||||
|
|
@ -225,6 +220,7 @@ class DomainDnsModal extends ModalComponent
|
||||||
$this->optional[$i]['actual'] = $actual;
|
$this->optional[$i]['actual'] = $actual;
|
||||||
$this->optional[$i]['state'] = $state;
|
$this->optional[$i]['state'] = $state;
|
||||||
$this->optional[$i]['boxClass'] = $this->stateColors[$state] ?? $this->stateColors['neutral'];
|
$this->optional[$i]['boxClass'] = $this->stateColors[$state] ?? $this->stateColors['neutral'];
|
||||||
|
$this->optional[$i]['display_actual'] = $this->normActual($r['type'], $actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->checked = true;
|
$this->checked = true;
|
||||||
|
|
@ -253,124 +249,260 @@ class DomainDnsModal extends ModalComponent
|
||||||
|
|
||||||
/* ---------- DNS & Bewertung ---------- */
|
/* ---------- DNS & Bewertung ---------- */
|
||||||
|
|
||||||
|
// private function dig(string $type, string $name): string
|
||||||
|
// {
|
||||||
|
// $type = strtoupper($type);
|
||||||
|
// $name = rtrim($name, '.'); // intern ohne trailing dot
|
||||||
|
// $out = @shell_exec('dig +timeout=2 +tries=1 +short '
|
||||||
|
// . escapeshellarg($name) . ' ' . escapeshellarg($type) . ' 2>/dev/null') ?? '';
|
||||||
|
// $out = trim($out);
|
||||||
|
//
|
||||||
|
// // Mehrzeiliges TXT zu einer Zeile squashen, Quotes weg
|
||||||
|
// if ($type === 'TXT' && $out !== '') {
|
||||||
|
// $lines = array_filter(array_map('trim', explode("\n", $out)));
|
||||||
|
// $joined = implode('', array_map(fn($l)=>trim($l,'"'), $lines));
|
||||||
|
// return $joined;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Nur erste Zeile vergleichen
|
||||||
|
// if ($out !== '') {
|
||||||
|
// $out = trim(explode("\n", $out)[0]);
|
||||||
|
// // MX/SRV/CNAME Ziele ohne trailing dot vergleichen
|
||||||
|
// if (in_array($type, ['MX','CNAME','SRV'])) {
|
||||||
|
// $out = rtrim($out, '.');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return $out;
|
||||||
|
// }
|
||||||
|
|
||||||
private function dig(string $type, string $name): string
|
private function dig(string $type, string $name): string
|
||||||
{
|
{
|
||||||
$type = strtoupper($type);
|
$type = strtoupper($type);
|
||||||
$name = rtrim($name, '.'); // intern ohne trailing dot
|
$name = rtrim($name, '.');
|
||||||
$out = @shell_exec('dig +timeout=2 +tries=1 +short '
|
|
||||||
. escapeshellarg($name) . ' ' . escapeshellarg($type) . ' 2>/dev/null') ?? '';
|
|
||||||
$out = trim($out);
|
|
||||||
|
|
||||||
// Mehrzeiliges TXT zu einer Zeile squashen, Quotes weg
|
$out = @shell_exec(
|
||||||
if ($type === 'TXT' && $out !== '') {
|
'dig +timeout=2 +tries=1 +short ' . escapeshellarg($name) . ' ' . escapeshellarg($type) . ' 2>/dev/null'
|
||||||
|
) ?? '';
|
||||||
|
$out = trim($out);
|
||||||
|
if ($out === '') return '';
|
||||||
|
|
||||||
|
// TXT: mehrere Zeilen / Quotes zu einer Zeile squashen
|
||||||
|
if ($type === 'TXT') {
|
||||||
$lines = array_filter(array_map('trim', explode("\n", $out)));
|
$lines = array_filter(array_map('trim', explode("\n", $out)));
|
||||||
$joined = implode('', array_map(fn($l)=>trim($l,'"'), $lines));
|
$joined = implode('', array_map(fn($l) => trim($l, '"'), $lines));
|
||||||
return $joined;
|
return $joined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nur erste Zeile vergleichen
|
// TLSA: Kanonisieren (Leerzeichen/Zeilenumbrüche im Hash, Großbuchstaben, …)
|
||||||
if ($out !== '') {
|
if ($type === 'TLSA') {
|
||||||
$out = trim(explode("\n", $out)[0]);
|
// nimm die ganze Ausgabe (kann mehrzeilig sein)
|
||||||
// MX/SRV/CNAME Ziele ohne trailing dot vergleichen
|
return $this->canonicalizeTlsa(preg_replace('/\s+/', ' ', $out)) ?? '';
|
||||||
if (in_array($type, ['MX','CNAME','SRV'])) {
|
|
||||||
$out = rtrim($out, '.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return $out;
|
|
||||||
|
// Für alles andere: erste Zeile reicht, trailing dots weg wo sinnvoll
|
||||||
|
$line = trim(strtok($out, "\n"));
|
||||||
|
if (in_array($type, ['MX','CNAME','SRV'])) $line = rtrim($line, '.');
|
||||||
|
return $line;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function stateFor(string $type, string $expected, string $actual, bool $optional): string
|
private function stateFor(string $type, string $expected, string $actual, bool $optional): string {
|
||||||
{
|
if ($actual === '') return $optional ? 'neutral' : 'missing';
|
||||||
if ($actual === '') {
|
|
||||||
return $optional ? 'neutral' : 'missing';
|
|
||||||
}
|
|
||||||
|
|
||||||
$type = strtoupper($type);
|
$type = strtoupper($type);
|
||||||
$exp = $this->normExpected($type, $expected);
|
$exp = $this->normExpected($type, $expected);
|
||||||
$act = $this->normActual($type, $actual);
|
$act = $this->normActual($type, $actual);
|
||||||
|
|
||||||
// Syntax plausibilisieren
|
// Syntaxcheck auf dem **actual**
|
||||||
$syntaxOk = $this->validateSyntax($type, $act);
|
if (!$this->validateSyntax($type, $act)) return 'syntax';
|
||||||
if (!$syntaxOk) return 'syntax';
|
|
||||||
|
|
||||||
// TXT-Policies: nur „Startet mit v=…“ prüfen → OK,
|
|
||||||
// selbst wenn Inhalt nicht 1:1 dem Vorschlag entspricht.
|
|
||||||
if ($type === 'TXT') {
|
if ($type === 'TXT') {
|
||||||
$upperExp = strtoupper($exp);
|
$ue = strtoupper($exp);
|
||||||
$upperAct = strtoupper($act);
|
$ua = strtoupper($act);
|
||||||
if (str_starts_with($upperExp, 'V=SPF1')) return str_starts_with($upperAct, 'V=SPF1') ? 'ok' : 'syntax';
|
if (str_starts_with($ue,'V=SPF1')) return str_starts_with($ua,'V=SPF1') ? 'ok' : 'syntax';
|
||||||
if (str_starts_with($upperExp, 'V=DMARC1')) return str_starts_with($upperAct, 'V=DMARC1') ? 'ok' : 'syntax';
|
if (str_starts_with($ue,'V=DMARC1')) return str_starts_with($ua,'V=DMARC1') ? 'ok' : 'syntax';
|
||||||
if (str_starts_with($upperExp, 'V=DKIM1')) return str_starts_with($upperAct, 'V=DKIM1') ? 'ok' : 'syntax';
|
if (str_starts_with($ue,'V=DKIM1')) return str_starts_with($ua,'V=DKIM1') ? 'ok' : 'syntax';
|
||||||
return ($act !== '') ? 'ok' : ($optional ? 'neutral' : 'missing');
|
return 'ok';
|
||||||
}
|
}
|
||||||
|
|
||||||
// MX: „prio host“ – wir prüfen Host grob
|
|
||||||
if ($type === 'MX') {
|
if ($type === 'MX') {
|
||||||
$parts = preg_split('/\s+/', $act);
|
$parts = preg_split('/\s+/', $act);
|
||||||
$host = strtolower($parts[1] ?? $act);
|
$host = strtolower($parts[1] ?? $act);
|
||||||
$expHost = strtolower(preg_replace('/^\d+\s+/', '', $exp));
|
$expHost = strtolower(preg_replace('/^\d+\s+/', '', $exp));
|
||||||
return ($host === $expHost) ? 'ok' : 'syntax';
|
return ($host === $expHost) ? 'ok' : 'syntax';
|
||||||
}
|
}
|
||||||
|
|
||||||
// SRV: „prio weight port host“ – Port + Host grob
|
|
||||||
if ($type === 'SRV') {
|
if ($type === 'SRV') {
|
||||||
$ap = preg_split('/\s+/', $act);
|
$ap = preg_split('/\s+/', $act);
|
||||||
$ep = preg_split('/\s+/', $exp);
|
$ep = preg_split('/\s+/', $exp);
|
||||||
if (count($ap) >= 4 && count($ep) >= 4) {
|
if (count($ap) >= 4 && count($ep) >= 4) {
|
||||||
$aport = (int)$ap[2];
|
return ((int)$ap[2] === (int)$ep[2] && strtolower(end($ap)) === strtolower(end($ep))) ? 'ok' : 'syntax';
|
||||||
$eport = (int)$ep[2];
|
|
||||||
$ahost = strtolower(rtrim(end($ap), '.'));
|
|
||||||
$ehost = strtolower(rtrim(end($ep), '.'));
|
|
||||||
return ($aport === $eport && $ahost === $ehost) ? 'ok' : 'syntax';
|
|
||||||
}
|
}
|
||||||
return 'syntax';
|
return 'syntax';
|
||||||
}
|
}
|
||||||
|
|
||||||
// CNAME/A/AAAA/PTR/TLSA: Gleichheit nach Normalisierung
|
// A/AAAA/CNAME/PTR/TLSA
|
||||||
return ($act === $exp) ? 'ok' : 'syntax';
|
return ($act === $exp) ? 'ok' : 'syntax';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normExpected(string $type, string $v): string
|
// private function stateFor(string $type, string $expected, string $actual, bool $optional): string
|
||||||
|
// {
|
||||||
|
// if ($actual === '') return $optional ? 'neutral' : 'missing';
|
||||||
|
//
|
||||||
|
// $type = strtoupper($type);
|
||||||
|
// $exp = $this->normExpected($type, $expected);
|
||||||
|
// $act = $this->normActual($type, $actual);
|
||||||
|
//
|
||||||
|
// // Syntaxcheck nach Normalisierung
|
||||||
|
// if (!$this->validateSyntax($type, $act)) return 'syntax';
|
||||||
|
//
|
||||||
|
// // TXT: nur „v=…“-Präfix grob prüfen
|
||||||
|
// if ($type === 'TXT') {
|
||||||
|
// $E = strtoupper($exp);
|
||||||
|
// $A = strtoupper($act);
|
||||||
|
// if (str_starts_with($E, 'V=SPF1')) return str_starts_with($A, 'V=SPF1') ? 'ok' : 'syntax';
|
||||||
|
// if (str_starts_with($E, 'V=DMARC1')) return str_starts_with($A, 'V=DMARC1') ? 'ok' : 'syntax';
|
||||||
|
// if (str_starts_with($E, 'V=DKIM1')) return str_starts_with($A, 'V=DKIM1') ? 'ok' : 'syntax';
|
||||||
|
// return 'ok';
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if ($type === 'MX') {
|
||||||
|
// $parts = preg_split('/\s+/', $act);
|
||||||
|
// $host = strtolower($parts[1] ?? $act);
|
||||||
|
// $expHost = strtolower(preg_replace('/^\d+\s+/', '', $exp));
|
||||||
|
// return ($host === $expHost) ? 'ok' : 'syntax';
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if ($type === 'SRV') {
|
||||||
|
// $ap = preg_split('/\s+/', $act);
|
||||||
|
// $ep = preg_split('/\s+/', $exp);
|
||||||
|
// if (count($ap) >= 4 && count($ep) >= 4) {
|
||||||
|
// return ((int)$ap[2] === (int)$ep[2] && strtolower(end($ap)) === strtolower(end($ep))) ? 'ok' : 'syntax';
|
||||||
|
// }
|
||||||
|
// return 'syntax';
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // TLSA / A / AAAA / CNAME / PTR: exakter Vergleich nach Norm.
|
||||||
|
// return ($act === $exp) ? 'ok' : 'syntax';
|
||||||
|
// }
|
||||||
|
|
||||||
|
private function canonicalizeTlsa(?string $v): ?string
|
||||||
{
|
{
|
||||||
|
if (!$v) return null;
|
||||||
|
$v = trim($v);
|
||||||
|
|
||||||
|
// tokenisiere: u s m [hash...]
|
||||||
|
$parts = preg_split('/\s+/', $v);
|
||||||
|
if (count($parts) < 4) return null;
|
||||||
|
|
||||||
|
$u = $parts[0]; $s = $parts[1]; $m = $parts[2];
|
||||||
|
// restliche Teile gehören zum Hash – zusammenfügen, Non-hex entfernen, kleinschreiben
|
||||||
|
$hash = strtolower(preg_replace('/[^0-9a-f]/i', '', implode('', array_slice($parts, 3))));
|
||||||
|
|
||||||
|
if ($hash === '') return null;
|
||||||
|
return sprintf('%s %s %s %s', $u, $s, $m, $hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normExpected(string $type, string $v): string {
|
||||||
$v = trim($v);
|
$v = trim($v);
|
||||||
$t = strtoupper($type);
|
$t = strtoupper($type);
|
||||||
if (in_array($t, ['MX','CNAME','SRV'])) $v = rtrim($v, '.');
|
if (in_array($t, ['MX','CNAME','SRV'])) $v = rtrim($v, '.');
|
||||||
if ($t === 'PTR') $v = strtolower(rtrim($v, '.'));
|
if ($t === 'PTR') $v = strtolower(rtrim($v, '.')); // Hostname als Value
|
||||||
if ($t === 'TLSA') $v = preg_replace('/\s+/', ' ', $v);
|
if ($t === 'TLSA') $v = strtolower(preg_replace('/\s+/', '', $v)); // hex compact, lc
|
||||||
|
if ($t === 'TXT') $v = trim($v, "\"'");
|
||||||
return $v;
|
return $v;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normActual(string $type, string $v): string
|
private function normActual(string $type, string $v): string {
|
||||||
{
|
|
||||||
$v = trim($v);
|
$v = trim($v);
|
||||||
$t = strtoupper($type);
|
$t = strtoupper($type);
|
||||||
|
// nur 1. Antwortzeile
|
||||||
|
if ($v !== '') $v = trim(explode("\n", $v)[0]);
|
||||||
if (in_array($t, ['MX','CNAME','SRV'])) $v = rtrim($v, '.');
|
if (in_array($t, ['MX','CNAME','SRV'])) $v = rtrim($v, '.');
|
||||||
if ($t === 'PTR') $v = strtolower(rtrim($v, '.'));
|
if ($t === 'PTR') $v = strtolower(rtrim($v, '.')); // Hostname als Value
|
||||||
if ($t === 'TLSA') {
|
if ($t === 'TLSA') $v = strtolower(preg_replace('/\s+/', '', $v)); // hex compact, lc
|
||||||
$v = preg_replace('/\s+/', '', $v); // Hash-Zeilen zusammenfügen
|
if ($t === 'TXT') $v = trim($v, "\"'");
|
||||||
$v = preg_replace('/^([0-3][\s]+[01][\s]+[123])/', '$1 ', $v); // spacing nach Header erzwingen
|
|
||||||
}
|
|
||||||
return $v;
|
return $v;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateSyntax(string $type, string $val): bool
|
private function validateSyntax(string $type, string $val): bool {
|
||||||
{
|
|
||||||
$t = strtoupper($type);
|
$t = strtoupper($type);
|
||||||
if ($val === '') return false;
|
if ($val === '') return false;
|
||||||
|
|
||||||
return match ($t) {
|
return match ($t) {
|
||||||
'A' => (bool)filter_var($val, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4),
|
'A' => (bool)filter_var($val, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4),
|
||||||
'AAAA' => (bool)filter_var($val, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6),
|
'AAAA' => (bool)filter_var($val, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6),
|
||||||
'CNAME' => (bool)preg_match('/^[a-z0-9._-]+$/i', $val),
|
'CNAME' => (bool)preg_match('/^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i', $val),
|
||||||
'PTR' => (bool)preg_match('/\.(in-addr|ip6)\.arpa$/i', $val),
|
// PTR: Value ist ein Hostname (rdns zeigt auf FQDN)
|
||||||
|
'PTR' => (bool)preg_match('/^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i', $val),
|
||||||
'MX' => (bool)preg_match('/^\d+\s+[a-z0-9._-]+$/i', $val),
|
'MX' => (bool)preg_match('/^\d+\s+[a-z0-9._-]+$/i', $val),
|
||||||
'SRV' => (bool)preg_match('/^\d+\s+\d+\s+\d+\s+[a-z0-9._-]+$/i', $val),
|
'SRV' => (bool)preg_match('/^\d+\s+\d+\s+\d+\s+[a-z0-9._-]+$/i', $val),
|
||||||
'TLSA' => (bool)preg_match('/^[0-3]\s+[01]\s+[123]\s+[0-9a-f\s]{32,}$/i', $val),
|
// TLSA: „usage selector matching hex…“ (hex jetzt schon lc/kompakt)
|
||||||
|
'TLSA' => (bool)preg_match('/^[0-3][01][123][0-9a-f]{32,}$/', str_replace(' ','',$val)),
|
||||||
'TXT' => strlen($val) > 0,
|
'TXT' => strlen($val) > 0,
|
||||||
default => true,
|
default => true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// private function normExpected(string $type, string $v): string
|
||||||
|
// {
|
||||||
|
// $v = trim($v);
|
||||||
|
// $t = strtoupper($type);
|
||||||
|
// if (in_array($t, ['MX','CNAME','SRV'])) $v = rtrim($v, '.');
|
||||||
|
// if ($t === 'PTR') $v = strtolower(rtrim($v, '.'));
|
||||||
|
// if ($t === 'TLSA') $v = $this->canonicalizeTlsa($v) ?? $v;
|
||||||
|
// return $v;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private function normActual(string $type, string $v): string
|
||||||
|
// {
|
||||||
|
// $v = trim($v);
|
||||||
|
// $t = strtoupper($type);
|
||||||
|
// if (in_array($t, ['MX','CNAME','SRV'])) $v = rtrim($v, '.');
|
||||||
|
// if ($t === 'PTR') $v = strtolower(rtrim($v, '.'));
|
||||||
|
// if ($t === 'TLSA') $v = $this->canonicalizeTlsa($v) ?? $v;
|
||||||
|
// return $v;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// private function validateSyntax(string $type, string $val): bool
|
||||||
|
// {
|
||||||
|
// $t = strtoupper($type);
|
||||||
|
// if ($val === '') return false;
|
||||||
|
//
|
||||||
|
// return match ($t) {
|
||||||
|
// 'A' => (bool)filter_var($val, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4),
|
||||||
|
// 'AAAA' => (bool)filter_var($val, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6),
|
||||||
|
// 'CNAME' => (bool)preg_match('/^[a-z0-9._-]+$/i', $val),
|
||||||
|
// 'PTR' => (bool)preg_match('/\.(in-addr|ip6)\.arpa$/i', $val),
|
||||||
|
// 'MX' => (bool)preg_match('/^\d+\s+[a-z0-9._-]+$/i', $val),
|
||||||
|
// 'SRV' => (bool)preg_match('/^\d+\s+\d+\s+\d+\s+[a-z0-9._-]+$/i', $val),
|
||||||
|
// 'TLSA' => (bool)preg_match('/^[0-3]\s+[01]\s+[123]\s+[0-9a-f\s]{32,}$/i', $val),
|
||||||
|
// 'TXT' => strlen($val) > 0,
|
||||||
|
// default => true,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// private function validateSyntax(string $type, string $val): bool
|
||||||
|
// {
|
||||||
|
// $t = strtoupper($type);
|
||||||
|
// if ($val === '') return false;
|
||||||
|
//
|
||||||
|
// if ($t === 'TLSA') {
|
||||||
|
// $canon = $this->canonicalizeTlsa($val);
|
||||||
|
// return is_string($canon) && (bool)preg_match('/^[0-3]\s+[01]\s+[123]\s+[0-9a-f]{32,}$/', $canon);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return match ($t) {
|
||||||
|
// 'A' => (bool)filter_var($val, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4),
|
||||||
|
// 'AAAA' => (bool)filter_var($val, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6),
|
||||||
|
// 'CNAME' => (bool)preg_match('/^[a-z0-9._-]+$/i', $val),
|
||||||
|
// 'PTR' => (bool)preg_match('/\.(in-addr|ip6)\.arpa$/i', $val),
|
||||||
|
// 'MX' => (bool)preg_match('/^\d+\s+[a-z0-9._-]+$/i', $val),
|
||||||
|
// 'SRV' => (bool)preg_match('/^\d+\s+\d+\s+\d+\s+[a-z0-9._-]+$/i', $val),
|
||||||
|
// 'TXT' => strlen($val) > 0,
|
||||||
|
// default => true,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
/** Vergleich je Typ robust normalisieren (nicht genutzt, bleibt aber da) */
|
/** Vergleich je Typ robust normalisieren (nicht genutzt, bleibt aber da) */
|
||||||
private function compareDns(string $type, string $expected, string $actual): bool
|
private function compareDns(string $type, string $expected, string $actual): bool
|
||||||
{
|
{
|
||||||
|
|
@ -436,9 +568,11 @@ class DomainDnsModal extends ModalComponent
|
||||||
|
|
||||||
private function ptrFromIPv6(string $ip): string
|
private function ptrFromIPv6(string $ip): string
|
||||||
{
|
{
|
||||||
$expanded = strtolower(inet_ntop(inet_pton($ip)));
|
$bin = @inet_pton($ip);
|
||||||
$hex = str_replace(':', '', $expanded);
|
if ($bin === false) return '';
|
||||||
return implode('.', array_reverse(str_split($hex))) . '.ip6.arpa';
|
$hex = bin2hex($bin); // exakt 32 Hex-Zeichen, lowercase
|
||||||
|
$nibbles = str_split($hex, 1); // 32 Nibbles
|
||||||
|
return implode('.', array_reverse($nibbles)) . '.ip6.arpa';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
|
|
@ -446,6 +580,8 @@ class DomainDnsModal extends ModalComponent
|
||||||
return view('livewire.ui.domain.modal.domain-dns-modal');
|
return view('livewire.ui.domain.modal.domain-dns-modal');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//namespace App\Livewire\Ui\Domain\Modal;
|
//namespace App\Livewire\Ui\Domain\Modal;
|
||||||
//
|
//
|
||||||
//use App\Models\Domain;
|
//use App\Models\Domain;
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,16 @@
|
||||||
|
|
||||||
namespace App\Livewire\Ui\Mail;
|
namespace App\Livewire\Ui\Mail;
|
||||||
|
|
||||||
use Livewire\Attributes\On;
|
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class DnsHealthCard extends Component
|
class DnsHealthCard extends Component
|
||||||
{
|
{
|
||||||
public array $rows = []; // [{id,name,ok,missing:[...]}]
|
public array $rows = []; // [{id,name,ok,missing:[...]}]
|
||||||
public string $mtaHost = ''; // z.B. mx.nexlab.at
|
public string $mtaHost = '';
|
||||||
public bool $tlsa = false; // hostweit
|
public bool $tlsa = false;
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
|
|
@ -25,7 +25,7 @@ class DnsHealthCard extends Component
|
||||||
|
|
||||||
public function refresh(): void
|
public function refresh(): void
|
||||||
{
|
{
|
||||||
$this->load(true);
|
$this->load();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function openDnsModal(int $domainId): void
|
public function openDnsModal(int $domainId): void
|
||||||
|
|
@ -33,53 +33,114 @@ class DnsHealthCard extends Component
|
||||||
$this->dispatch('openModal', component: 'ui.domain.modal.domain-dns-modal', arguments: ['domainId' => $domainId]);
|
$this->dispatch('openModal', component: 'ui.domain.modal.domain-dns-modal', arguments: ['domainId' => $domainId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function load(bool $force = false): void
|
// protected function load(bool $force = false): void
|
||||||
|
// {
|
||||||
|
// [$this->mtaHost, $this->tlsa, $this->rows] = Cache::remember('dash.dnshealth.v2', $force ? 1 : 600, function () {
|
||||||
|
//
|
||||||
|
// $base = trim((string) env('BASE_DOMAIN', ''));
|
||||||
|
// $mtaSub = trim((string) env('MTA_SUB', 'mx'));
|
||||||
|
// $mtaHost = $base !== '' ? "{$mtaSub}.{$base}" : $mtaSub; // z.B. mx.nexlab.at
|
||||||
|
//
|
||||||
|
// // ▼ gewünschter Filter:
|
||||||
|
// $domains = Domain::query()
|
||||||
|
// ->where('is_active', true)
|
||||||
|
// ->where('is_server', false) // <<< Server-Domain sauber ausschließen
|
||||||
|
// ->orderBy('domain')
|
||||||
|
// ->get(['id', 'domain']);
|
||||||
|
//
|
||||||
|
// $rows = [];
|
||||||
|
// foreach ($domains as $d) {
|
||||||
|
// $dom = $d->domain;
|
||||||
|
//
|
||||||
|
// // DKIM-Selector ermitteln: .env > DB > Fallback null
|
||||||
|
// $selector = trim((string) env('DKIM_SELECTOR', ''));
|
||||||
|
// if ($selector === '') {
|
||||||
|
// $selector = (string) DB::table('dkim_keys')
|
||||||
|
// ->where('domain_id', $d->id)
|
||||||
|
// ->where('is_active', 1)
|
||||||
|
// ->orderByDesc('id')
|
||||||
|
// ->value('selector') ?? '';
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $missing = [];
|
||||||
|
//
|
||||||
|
// if (!$this->mxPointsTo($dom, [$mtaHost])) $missing[] = 'MX';
|
||||||
|
// if (!$this->hasSpf($dom)) $missing[] = 'SPF';
|
||||||
|
// if (!$this->hasDkim($dom, $selector)) $missing[] = 'DKIM';
|
||||||
|
// if (!$this->hasTxt("_dmarc.$dom")) $missing[] = 'DMARC';
|
||||||
|
//
|
||||||
|
// $rows[] = [
|
||||||
|
// 'id' => (int) $d->id,
|
||||||
|
// 'name' => $dom,
|
||||||
|
// 'ok' => empty($missing),
|
||||||
|
// 'missing' => $missing,
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Hostweites TLSA (nur Hinweis)
|
||||||
|
// $tlsa = $this->hasTlsa("_25._tcp.$mtaHost") || $this->hasTlsa("_465._tcp.$mtaHost") || $this->hasTlsa("_587._tcp.$mtaHost");
|
||||||
|
//
|
||||||
|
// return [$mtaHost, $tlsa, $rows];
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
protected function load(): void
|
||||||
{
|
{
|
||||||
[$this->mtaHost, $this->tlsa, $this->rows] = Cache::remember('dash.dnshealth.v1', $force ? 1 : 600, function () {
|
$base = trim((string) env('BASE_DOMAIN', ''));
|
||||||
|
$mtaSub = trim((string) env('MTA_SUB', 'mx'));
|
||||||
|
$mtaHost = $base !== '' ? "{$mtaSub}.{$base}" : $mtaSub; // z.B. mx.nexlab.at
|
||||||
|
|
||||||
$base = trim((string)env('BASE_DOMAIN', ''));
|
// nur aktive, NICHT-Server-Domains (System + Custom, solange is_server = false)
|
||||||
$mtaSub = trim((string)env('MTA_SUB', 'mx'));
|
$domains = Domain::query()
|
||||||
$mtaHost = $base !== '' ? "{$mtaSub}.{$base}" : $mtaSub;
|
->where('is_active', true)
|
||||||
|
->where('is_server', false)
|
||||||
|
->orderBy('domain')
|
||||||
|
->get(['id','domain']);
|
||||||
|
|
||||||
$rows = [];
|
$rows = [];
|
||||||
$domains = Domain::query()
|
|
||||||
->where('is_system', false)
|
|
||||||
->where('is_active', true)
|
|
||||||
->orderBy('domain')
|
|
||||||
->get(['id', 'domain']);
|
|
||||||
|
|
||||||
foreach ($domains as $d) {
|
foreach ($domains as $d) {
|
||||||
$dom = $d->domain;
|
$dom = $d->domain;
|
||||||
|
|
||||||
$missing = [];
|
// DKIM-Selector: .env > DB > leer
|
||||||
|
$selector = trim((string) env('DKIM_SELECTOR', ''));
|
||||||
// Pflicht-Checks
|
if ($selector === '') {
|
||||||
if (!$this->mxPointsTo($dom, [$mtaHost])) $missing[] = 'MX';
|
$selector = (string) DB::table('dkim_keys')
|
||||||
if (!$this->hasSpf($dom)) $missing[] = 'SPF';
|
->where('domain_id', $d->id)
|
||||||
if (!$this->hasDkim($dom)) $missing[] = 'DKIM';
|
->where('is_active', 1)
|
||||||
if (!$this->hasTxt("_dmarc.$dom")) $missing[] = 'DMARC';
|
->orderByDesc('id')
|
||||||
|
->value('selector') ?? '';
|
||||||
$rows[] = [
|
|
||||||
'id' => (int)$d->id,
|
|
||||||
'name' => $dom,
|
|
||||||
'ok' => empty($missing),
|
|
||||||
'missing' => $missing,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLSA (hostweit, nur Info)
|
$missing = [];
|
||||||
$tlsa = $this->hasTlsa("_25._tcp.$mtaHost") || $this->hasTlsa("_465._tcp.$mtaHost") || $this->hasTlsa("_587._tcp.$mtaHost");
|
if (!$this->mxPointsTo($dom, [$mtaHost])) $missing[] = 'MX';
|
||||||
|
if (!$this->hasSpf($dom)) $missing[] = 'SPF';
|
||||||
|
if (!$this->hasDkim($dom, $selector)) $missing[] = 'DKIM';
|
||||||
|
if (!$this->hasTxt("_dmarc.$dom")) $missing[] = 'DMARC';
|
||||||
|
|
||||||
return [$mtaHost, $tlsa, $rows];
|
$rows[] = [
|
||||||
});
|
'id' => (int) $d->id,
|
||||||
|
'name' => $dom,
|
||||||
|
'ok' => empty($missing),
|
||||||
|
'missing' => $missing,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hostweites TLSA (nur Info)
|
||||||
|
$tlsa = $this->hasTlsa("_25._tcp.$mtaHost")
|
||||||
|
|| $this->hasTlsa("_465._tcp.$mtaHost")
|
||||||
|
|| $this->hasTlsa("_587._tcp.$mtaHost");
|
||||||
|
|
||||||
|
$this->mtaHost = $mtaHost;
|
||||||
|
$this->tlsa = $tlsa;
|
||||||
|
$this->rows = $rows;
|
||||||
}
|
}
|
||||||
|
/* ── DNS Helpers ───────────────────────────────────────────────────── */
|
||||||
/* ── DNS Helpers (mit Timeout, damit UI nicht hängt) ───────────────── */
|
|
||||||
|
|
||||||
protected function digShort(string $type, string $name): string
|
protected function digShort(string $type, string $name): string
|
||||||
{
|
{
|
||||||
$cmd = "timeout 2 dig +short " . escapeshellarg($name) . " " . escapeshellarg(strtoupper($type)) . " 2>/dev/null";
|
$cmd = "timeout 2 dig +short " . escapeshellarg($name) . ' ' . escapeshellarg(strtoupper($type)) . " 2>/dev/null";
|
||||||
return (string)@shell_exec($cmd) ?: '';
|
return (string) @shell_exec($cmd) ?: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function hasTxt(string $name): bool
|
protected function hasTxt(string $name): bool
|
||||||
|
|
@ -101,14 +162,25 @@ class DnsHealthCard extends Component
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DKIM: wenn spezifischer Selector vorhanden → prüfe den, sonst akzeptiere _domainkey-Policy als “vorhanden”
|
// ▼ DKIM: bevorzugt konkreten Selector prüfen; wenn leer, versuche Policy (_domainkey)
|
||||||
protected function hasDkim(string $domain): bool
|
protected function hasDkim(string $domain, string $selector = ''): bool
|
||||||
{
|
{
|
||||||
$sel = trim((string)env('DKIM_SELECTOR', ''));
|
if ($selector !== '' && $this->hasTxt("{$selector}._domainkey.$domain")) {
|
||||||
if ($sel !== '' && $this->hasTxt("{$sel}._domainkey.$domain")) return true;
|
return true;
|
||||||
|
}
|
||||||
|
// Fallback: irgendein _domainkey-TXT vorhanden
|
||||||
return $this->hasTxt("_domainkey.$domain");
|
return $this->hasTxt("_domainkey.$domain");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// protected function hasDkim(string $domain, string $selector = ''): bool
|
||||||
|
// {
|
||||||
|
// if ($selector !== '') {
|
||||||
|
// return $this->hasTxt("{$selector}._domainkey.$domain");
|
||||||
|
// }
|
||||||
|
// // Manche Betreiber veröffentlichen eine Policy auf _domainkey.<dom>
|
||||||
|
// return $this->hasTxt("_domainkey.$domain");
|
||||||
|
// }
|
||||||
|
|
||||||
protected function mxPointsTo(string $domain, array $allowedHosts): bool
|
protected function mxPointsTo(string $domain, array $allowedHosts): bool
|
||||||
{
|
{
|
||||||
$out = $this->digShort('MX', $domain);
|
$out = $this->digShort('MX', $domain);
|
||||||
|
|
@ -116,21 +188,153 @@ class DnsHealthCard extends Component
|
||||||
|
|
||||||
$targets = [];
|
$targets = [];
|
||||||
foreach (preg_split('/\R+/', trim($out)) as $line) {
|
foreach (preg_split('/\R+/', trim($out)) as $line) {
|
||||||
// Format: "10 mx.example.com."
|
|
||||||
if (preg_match('~\s+([A-Za-z0-9\.\-]+)\.?$~', trim($line), $m)) {
|
if (preg_match('~\s+([A-Za-z0-9\.\-]+)\.?$~', trim($line), $m)) {
|
||||||
$targets[] = strtolower($m[1]);
|
$targets[] = strtolower($m[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!$targets) return false;
|
if (!$targets) return false;
|
||||||
|
|
||||||
$allowed = array_map('strtolower', $allowedHosts);
|
$allowed = array_map(fn ($h) => strtolower(rtrim($h, '.')), $allowedHosts);
|
||||||
foreach ($targets as $t) {
|
foreach ($targets as $t) {
|
||||||
|
$t = rtrim($t, '.');
|
||||||
if (in_array($t, $allowed, true)) return true;
|
if (in_array($t, $allowed, true)) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//namespace App\Livewire\Ui\Mail;
|
||||||
|
//
|
||||||
|
//use Livewire\Attributes\On;
|
||||||
|
//use Livewire\Component;
|
||||||
|
//use App\Models\Domain;
|
||||||
|
//use Illuminate\Support\Facades\Cache;
|
||||||
|
//
|
||||||
|
//class DnsHealthCard extends Component
|
||||||
|
//{
|
||||||
|
// public array $rows = []; // [{id,name,ok,missing:[...]}]
|
||||||
|
// public string $mtaHost = ''; // z.B. mx.nexlab.at
|
||||||
|
// public bool $tlsa = false; // hostweit
|
||||||
|
//
|
||||||
|
// public function mount(): void
|
||||||
|
// {
|
||||||
|
// $this->load();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public function render()
|
||||||
|
// {
|
||||||
|
// return view('livewire.ui.mail.dns-health-card');
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public function refresh(): void
|
||||||
|
// {
|
||||||
|
// $this->load(true);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public function openDnsModal(int $domainId): void
|
||||||
|
// {
|
||||||
|
// $this->dispatch('openModal', component: 'ui.domain.modal.domain-dns-modal', arguments: ['domainId' => $domainId]);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// protected function load(bool $force = false): void
|
||||||
|
// {
|
||||||
|
// [$this->mtaHost, $this->tlsa, $this->rows] = Cache::remember('dash.dnshealth.v1', $force ? 1 : 600, function () {
|
||||||
|
//
|
||||||
|
// $base = trim((string)env('BASE_DOMAIN', ''));
|
||||||
|
// $mtaSub = trim((string)env('MTA_SUB', 'mx'));
|
||||||
|
// $mtaHost = $base !== '' ? "{$mtaSub}.{$base}" : $mtaSub;
|
||||||
|
//
|
||||||
|
// $rows = [];
|
||||||
|
// $domains = Domain::query()
|
||||||
|
// ->where('is_system', false)
|
||||||
|
// ->where('is_active', true)
|
||||||
|
// ->orderBy('domain')
|
||||||
|
// ->get(['id', 'domain']);
|
||||||
|
//
|
||||||
|
// foreach ($domains as $d) {
|
||||||
|
// $dom = $d->domain;
|
||||||
|
//
|
||||||
|
// $missing = [];
|
||||||
|
//
|
||||||
|
// // Pflicht-Checks
|
||||||
|
// if (!$this->mxPointsTo($dom, [$mtaHost])) $missing[] = 'MX';
|
||||||
|
// if (!$this->hasSpf($dom)) $missing[] = 'SPF';
|
||||||
|
// if (!$this->hasDkim($dom)) $missing[] = 'DKIM';
|
||||||
|
// if (!$this->hasTxt("_dmarc.$dom")) $missing[] = 'DMARC';
|
||||||
|
//
|
||||||
|
// $rows[] = [
|
||||||
|
// 'id' => (int)$d->id,
|
||||||
|
// 'name' => $dom,
|
||||||
|
// 'ok' => empty($missing),
|
||||||
|
// 'missing' => $missing,
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // TLSA (hostweit, nur Info)
|
||||||
|
// $tlsa = $this->hasTlsa("_25._tcp.$mtaHost") || $this->hasTlsa("_465._tcp.$mtaHost") || $this->hasTlsa("_587._tcp.$mtaHost");
|
||||||
|
//
|
||||||
|
// return [$mtaHost, $tlsa, $rows];
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /* ── DNS Helpers (mit Timeout, damit UI nicht hängt) ───────────────── */
|
||||||
|
//
|
||||||
|
// protected function digShort(string $type, string $name): string
|
||||||
|
// {
|
||||||
|
// $cmd = "timeout 2 dig +short " . escapeshellarg($name) . " " . escapeshellarg(strtoupper($type)) . " 2>/dev/null";
|
||||||
|
// return (string)@shell_exec($cmd) ?: '';
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// protected function hasTxt(string $name): bool
|
||||||
|
// {
|
||||||
|
// return trim($this->digShort('TXT', $name)) !== '';
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// protected function hasTlsa(string $name): bool
|
||||||
|
// {
|
||||||
|
// return trim($this->digShort('TLSA', $name)) !== '';
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// protected function hasSpf(string $domain): bool
|
||||||
|
// {
|
||||||
|
// $out = $this->digShort('TXT', $domain);
|
||||||
|
// foreach (preg_split('/\R+/', trim($out)) as $line) {
|
||||||
|
// if (stripos($line, 'v=spf1') !== false) return true;
|
||||||
|
// }
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // DKIM: wenn spezifischer Selector vorhanden → prüfe den, sonst akzeptiere _domainkey-Policy als “vorhanden”
|
||||||
|
// protected function hasDkim(string $domain): bool
|
||||||
|
// {
|
||||||
|
// $sel = trim((string)env('DKIM_SELECTOR', ''));
|
||||||
|
// if ($sel !== '' && $this->hasTxt("{$sel}._domainkey.$domain")) return true;
|
||||||
|
// return $this->hasTxt("_domainkey.$domain");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// protected function mxPointsTo(string $domain, array $allowedHosts): bool
|
||||||
|
// {
|
||||||
|
// $out = $this->digShort('MX', $domain);
|
||||||
|
// if ($out === '') return false;
|
||||||
|
//
|
||||||
|
// $targets = [];
|
||||||
|
// foreach (preg_split('/\R+/', trim($out)) as $line) {
|
||||||
|
// // Format: "10 mx.example.com."
|
||||||
|
// if (preg_match('~\s+([A-Za-z0-9\.\-]+)\.?$~', trim($line), $m)) {
|
||||||
|
// $targets[] = strtolower($m[1]);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (!$targets) return false;
|
||||||
|
//
|
||||||
|
// $allowed = array_map('strtolower', $allowedHosts);
|
||||||
|
// foreach ($targets as $t) {
|
||||||
|
// if (in_array($t, $allowed, true)) return true;
|
||||||
|
// }
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
//namespace App\Livewire\Ui\Mail;
|
//namespace App\Livewire\Ui\Mail;
|
||||||
//
|
//
|
||||||
//use Livewire\Component;
|
//use Livewire\Component;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -92,7 +92,7 @@ class MailboxCreateModal extends ModalComponent
|
||||||
{
|
{
|
||||||
// alle Nicht-System-Domains in Select
|
// alle Nicht-System-Domains in Select
|
||||||
$this->domains = Domain::query()
|
$this->domains = Domain::query()
|
||||||
->where('is_system', false)
|
->where('is_system', false)->where('is_server', false)
|
||||||
->orderBy('domain')->get(['id', 'domain'])->toArray();
|
->orderBy('domain')->get(['id', 'domain'])->toArray();
|
||||||
|
|
||||||
// vorselektieren falls mitgegeben, sonst 1. Domain (falls vorhanden)
|
// vorselektieren falls mitgegeben, sonst 1. Domain (falls vorhanden)
|
||||||
|
|
@ -291,251 +291,3 @@ class MailboxCreateModal extends ModalComponent
|
||||||
return view('livewire.ui.mail.modal.mailbox-create-modal');
|
return view('livewire.ui.mail.modal.mailbox-create-modal');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//namespace App\Livewire\Ui\Mail\Modal;
|
|
||||||
//
|
|
||||||
//use App\Models\Domain;
|
|
||||||
//use App\Models\MailUser;
|
|
||||||
//use Illuminate\Database\QueryException;
|
|
||||||
//use Illuminate\Support\Facades\Hash;
|
|
||||||
//use Illuminate\Validation\Rule;
|
|
||||||
//use Livewire\Attributes\On;
|
|
||||||
//use LivewireUI\Modal\ModalComponent;
|
|
||||||
//
|
|
||||||
//class MailboxCreateModal extends ModalComponent
|
|
||||||
//{
|
|
||||||
// // optional vorselektierte Domain
|
|
||||||
// public ?int $domain_id = null;
|
|
||||||
//
|
|
||||||
// // Anzeige
|
|
||||||
// public string $domain_name = '';
|
|
||||||
// /** @var array<int,array{id:int,domain:string}> */
|
|
||||||
// public array $domains = [];
|
|
||||||
// public string $email_preview = '';
|
|
||||||
//
|
|
||||||
// public string $localpart = '';
|
|
||||||
// public ?string $display_name = null;
|
|
||||||
// public ?string $password = null;
|
|
||||||
// public int $quota_mb = 0;
|
|
||||||
// public ?int $rate_limit_per_hour = null;
|
|
||||||
// public bool $is_active = true;
|
|
||||||
// public bool $must_change_pw = true;
|
|
||||||
//
|
|
||||||
// // Limits / Status
|
|
||||||
// public ?int $limit_max_mailboxes = null;
|
|
||||||
// public ?int $limit_default_quota_mb = null;
|
|
||||||
// public ?int $limit_max_quota_per_mb = null;
|
|
||||||
// public ?int $limit_total_quota_mb = null; // 0 = unlimitiert
|
|
||||||
// public ?int $limit_domain_rate_per_hour = null;
|
|
||||||
// public bool $allow_rate_limit_override = false;
|
|
||||||
//
|
|
||||||
// public int $mailbox_count_used = 0;
|
|
||||||
// public int $domain_storage_used_mb = 0;
|
|
||||||
//
|
|
||||||
// // Hints/Flags
|
|
||||||
// public string $quota_hint = '';
|
|
||||||
// public bool $rate_limit_readonly = false;
|
|
||||||
// public bool $no_mailbox_slots = false;
|
|
||||||
// public bool $no_storage_left = false;
|
|
||||||
// public bool $can_create = true;
|
|
||||||
// public string $block_reason = '';
|
|
||||||
//
|
|
||||||
// /* ---------- Validation ---------- */
|
|
||||||
// protected function rules(): array
|
|
||||||
// {
|
|
||||||
// $maxPerMailbox = $this->limit_max_quota_per_mb ?? PHP_INT_MAX;
|
|
||||||
// $remainingByTotal = (is_null($this->limit_total_quota_mb) || (int)$this->limit_total_quota_mb === 0)
|
|
||||||
// ? PHP_INT_MAX
|
|
||||||
// : max(0, (int)$this->limit_total_quota_mb - (int)$this->domain_storage_used_mb);
|
|
||||||
// $cap = min($maxPerMailbox, $remainingByTotal);
|
|
||||||
//
|
|
||||||
// return [
|
|
||||||
// 'domain_id' => ['required', Rule::exists('domains', 'id')],
|
|
||||||
// 'localpart' => [
|
|
||||||
// 'required', 'max:191', 'regex:/^[A-Za-z0-9._%+-]+$/',
|
|
||||||
// Rule::unique('mail_users', 'localpart')->where(fn($q) => $q->where('domain_id', $this->domain_id)),
|
|
||||||
// ],
|
|
||||||
// 'display_name' => ['nullable', 'max:191'],
|
|
||||||
// 'password' => ['nullable', 'min:8'],
|
|
||||||
// 'quota_mb' => ['required', 'integer', 'min:0', 'max:' . $cap],
|
|
||||||
// 'rate_limit_per_hour' => ['nullable', 'integer', 'min:1'],
|
|
||||||
// 'is_active' => ['boolean'],
|
|
||||||
// 'must_change_pw' => ['boolean'],
|
|
||||||
// ];
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /* ---------- Lifecycle ---------- */
|
|
||||||
// public function mount(?int $domainId = null): void
|
|
||||||
// {
|
|
||||||
// // alle Nicht-System-Domains in Select
|
|
||||||
// $this->domains = Domain::query()
|
|
||||||
// ->where('is_system', false)
|
|
||||||
// ->orderBy('domain')->get(['id', 'domain'])->toArray();
|
|
||||||
//
|
|
||||||
// // vorselektieren falls mitgegeben, sonst 1. Domain (falls vorhanden)
|
|
||||||
// $this->domain_id = $domainId ?: ($this->domains[0]['id'] ?? null);
|
|
||||||
//
|
|
||||||
// // Limits + Anzeige laden
|
|
||||||
// $this->syncDomainContext();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public function updatedDomainId(): void
|
|
||||||
// {
|
|
||||||
// $this->resetErrorBag(); // scoped unique etc.
|
|
||||||
// $this->syncDomainContext();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public function updatedLocalpart(): void
|
|
||||||
// {
|
|
||||||
// $this->localpart = strtolower(trim($this->localpart));
|
|
||||||
// $this->rebuildEmailPreview();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public function updatedQuotaMb(): void
|
|
||||||
// {
|
|
||||||
// $this->recomputeQuotaHints();
|
|
||||||
// $this->recomputeBlockers();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /* ---------- Helpers ---------- */
|
|
||||||
// private function syncDomainContext(): void
|
|
||||||
// {
|
|
||||||
// if (!$this->domain_id) return;
|
|
||||||
//
|
|
||||||
// $d = Domain::query()
|
|
||||||
// ->withCount('mailUsers')
|
|
||||||
// ->withSum('mailUsers as used_storage_mb', 'quota_mb')
|
|
||||||
// ->findOrFail($this->domain_id);
|
|
||||||
//
|
|
||||||
// $this->domain_name = $d->domain;
|
|
||||||
// $this->limit_max_mailboxes = (int)$d->max_mailboxes;
|
|
||||||
// $this->limit_default_quota_mb = (int)$d->default_quota_mb;
|
|
||||||
// $this->limit_max_quota_per_mb = $d->max_quota_per_mailbox_mb !== null ? (int)$d->max_quota_per_mailbox_mb : null;
|
|
||||||
// $this->limit_total_quota_mb = (int)$d->total_quota_mb; // 0 = unlimitiert
|
|
||||||
// $this->limit_domain_rate_per_hour = $d->rate_limit_per_hour !== null ? (int)$d->rate_limit_per_hour : null;
|
|
||||||
// $this->allow_rate_limit_override = (bool)$d->rate_limit_override;
|
|
||||||
//
|
|
||||||
// $this->mailbox_count_used = (int)$d->mail_users_count;
|
|
||||||
// $this->domain_storage_used_mb = (int)($d->used_storage_mb ?? 0);
|
|
||||||
//
|
|
||||||
// // Defaults
|
|
||||||
// $this->quota_mb = $this->limit_default_quota_mb ?? 0;
|
|
||||||
// if (!$this->allow_rate_limit_override) {
|
|
||||||
// $this->rate_limit_per_hour = $this->limit_domain_rate_per_hour;
|
|
||||||
// $this->rate_limit_readonly = true;
|
|
||||||
// } else {
|
|
||||||
// $this->rate_limit_per_hour = $this->limit_domain_rate_per_hour;
|
|
||||||
// $this->rate_limit_readonly = false;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// $this->rebuildEmailPreview();
|
|
||||||
// $this->recomputeQuotaHints();
|
|
||||||
// $this->recomputeBlockers();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private function rebuildEmailPreview(): void
|
|
||||||
// {
|
|
||||||
// $this->email_preview = $this->localpart && $this->domain_name
|
|
||||||
// ? ($this->localpart . '@' . $this->domain_name) : '';
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private function recomputeQuotaHints(): void
|
|
||||||
// {
|
|
||||||
// $parts = [];
|
|
||||||
//
|
|
||||||
// if (!is_null($this->limit_total_quota_mb) && (int)$this->limit_total_quota_mb > 0) {
|
|
||||||
// $remainingNow = max(0, (int)$this->limit_total_quota_mb - (int)$this->domain_storage_used_mb);
|
|
||||||
// $remainingAfter = max(0, $remainingNow - max(0, (int)$this->quota_mb));
|
|
||||||
// $parts[] = "Verbleibend jetzt: {$remainingNow} MiB";
|
|
||||||
// $parts[] = "nach Speichern: {$remainingAfter} MiB";
|
|
||||||
// }
|
|
||||||
// if (!is_null($this->limit_max_quota_per_mb)) $parts[] = "Max {$this->limit_max_quota_per_mb} MiB pro Postfach";
|
|
||||||
// if (!is_null($this->limit_default_quota_mb)) $parts[] = "Standard: {$this->limit_default_quota_mb} MiB";
|
|
||||||
//
|
|
||||||
// $this->quota_hint = implode(' · ', $parts);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private function recomputeBlockers(): void
|
|
||||||
// {
|
|
||||||
// // Slots
|
|
||||||
// $this->no_mailbox_slots = false;
|
|
||||||
// if (!is_null($this->limit_max_mailboxes)) {
|
|
||||||
// $free = (int)$this->limit_max_mailboxes - (int)$this->mailbox_count_used;
|
|
||||||
// if ($free <= 0) $this->no_mailbox_slots = true;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Speicher
|
|
||||||
// $this->no_storage_left = false;
|
|
||||||
// if (!is_null($this->limit_total_quota_mb) && (int)$this->limit_total_quota_mb > 0) {
|
|
||||||
// $remaining = (int)$this->limit_total_quota_mb - (int)$this->domain_storage_used_mb;
|
|
||||||
// if ($remaining <= 0) $this->no_storage_left = true;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// $reasons = [];
|
|
||||||
// if ($this->no_mailbox_slots) $reasons[] = 'Keine freien Postfach-Slots in dieser Domain.';
|
|
||||||
// if ($this->no_storage_left) $reasons[] = 'Kein Domain-Speicher mehr verfügbar.';
|
|
||||||
// $this->block_reason = implode(' ', $reasons);
|
|
||||||
// $this->can_create = !($this->no_mailbox_slots || $this->no_storage_left);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /* ---------- Save ---------- */
|
|
||||||
// #[On('mailbox:create')]
|
|
||||||
// public function save(): void
|
|
||||||
// {
|
|
||||||
// $this->recomputeBlockers();
|
|
||||||
// if (!$this->can_create) {
|
|
||||||
// $this->addError('domain_id', $this->block_reason ?: 'Erstellung aktuell nicht möglich.');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// $data = $this->validate();
|
|
||||||
// $email = $data['localpart'] . '@' . $this->domain_name;
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
// $u = new MailUser();
|
|
||||||
// $u->domain_id = $data['domain_id'];
|
|
||||||
// $u->localpart = $data['localpart'];
|
|
||||||
// $u->email = $email;
|
|
||||||
// $u->display_name = $this->display_name ?: null;
|
|
||||||
// $u->password_hash = $this->password ? Hash::make($this->password) : null;
|
|
||||||
// $u->is_system = false;
|
|
||||||
// $u->is_active = (bool)$data['is_active'];
|
|
||||||
// $u->must_change_pw = (bool)$data['must_change_pw'];
|
|
||||||
// $u->quota_mb = (int)$data['quota_mb'];
|
|
||||||
// $u->rate_limit_per_hour = $data['rate_limit_per_hour'];
|
|
||||||
// $u->save();
|
|
||||||
// } catch (QueryException $e) {
|
|
||||||
// $msg = strtolower($e->getMessage());
|
|
||||||
// if (str_contains($msg, 'mail_users_domain_localpart_unique')) {
|
|
||||||
// $this->addError('localpart', 'Dieses Postfach existiert in dieser Domain bereits.');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// if (str_contains($msg, 'mail_users_email_unique')) {
|
|
||||||
// $this->addError('localpart', 'Diese E-Mail-Adresse ist bereits vergeben.');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// throw $e;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// $this->dispatch('mailbox:created');
|
|
||||||
// $this->dispatch('closeModal');
|
|
||||||
// $this->dispatch('toast',
|
|
||||||
// type: 'done',
|
|
||||||
// badge: 'Postfach',
|
|
||||||
// title: 'Postfach angelegt',
|
|
||||||
// text: 'Das Postfach <b>' . e($email) . '</b> wurde erfolgreich angelegt.',
|
|
||||||
// duration: 6000
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public static function modalMaxWidth(): string
|
|
||||||
// {
|
|
||||||
// return '3xl';
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public function render()
|
|
||||||
// {
|
|
||||||
// return view('livewire.ui.mail.modal.mailbox-create-modal');
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,964 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace App\Livewire\Ui\Security;
|
namespace App\Livewire\Ui\Security;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class Fail2BanCard extends Component
|
class Fail2BanCard extends Component
|
||||||
{
|
{
|
||||||
|
public bool $available = true;
|
||||||
|
public bool $permDenied = false;
|
||||||
|
public bool $error = false;
|
||||||
public int $activeBans = 0;
|
public int $activeBans = 0;
|
||||||
public array $topIps = []; // [['ip'=>'1.2.3.4','count'=>12],...]
|
public array $jails = [];
|
||||||
|
|
||||||
public function mount(): void { $this->load(); }
|
public function mount(): void
|
||||||
public function render() { return view('livewire.ui.security.fail2-ban-card'); }
|
|
||||||
public function refresh(): void { $this->load(true); }
|
|
||||||
|
|
||||||
protected function load(bool $force=false): void
|
|
||||||
{
|
{
|
||||||
$status = @shell_exec('fail2ban-client status 2>/dev/null') ?? '';
|
$this->load();
|
||||||
$bans = preg_match('/Currently banned:\s+(\d+)/i', $status, $m) ? (int)$m[1] : 0;
|
}
|
||||||
$this->activeBans = $bans;
|
|
||||||
|
|
||||||
// quick & rough: last 1000 lines auth/mail logs → top IPs
|
public function render()
|
||||||
$log = @shell_exec('tail -n 1000 /var/log/auth.log /var/log/mail.log 2>/dev/null | grep -Eo "([0-9]{1,3}\.){3}[0-9]{1,3}" | sort | uniq -c | sort -nr | head -5');
|
{
|
||||||
$rows = [];
|
return view('livewire.ui.security.fail2-ban-card');
|
||||||
if ($log) {
|
}
|
||||||
foreach (preg_split('/\R+/', trim($log)) as $l) {
|
|
||||||
if (preg_match('/^\s*(\d+)\s+(\d+\.\d+\.\d+\.\d+)/', $l, $m)) {
|
#[On('f2b:refresh-banlist')]
|
||||||
$rows[] = ['ip'=>$m[2],'count'=>(int)$m[1]];
|
public function refresh(): void
|
||||||
}
|
{
|
||||||
}
|
$this->load(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function openDetails(string $jail): void
|
||||||
|
{
|
||||||
|
$this->dispatch('openModal', component: 'ui.security.modal.fail2-ban-jail-modal', arguments: ['jail' => $jail]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- intern ---------------- */
|
||||||
|
|
||||||
|
protected function load(bool $force = false): void
|
||||||
|
{
|
||||||
|
$this->available = $this->permDenied = $this->error = false;
|
||||||
|
$this->activeBans = 0;
|
||||||
|
$this->jails = [];
|
||||||
|
|
||||||
|
$bin = trim((string)@shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
|
||||||
|
if ($bin === '') {
|
||||||
|
$this->available = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
$this->topIps = $rows;
|
$this->available = true;
|
||||||
|
|
||||||
|
[, $ping] = $this->f2b('ping');
|
||||||
|
if ($this->looksDenied($ping)) {
|
||||||
|
$this->permDenied = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[, $status] = $this->f2b('status');
|
||||||
|
if ($this->looksDenied($status)) {
|
||||||
|
$this->permDenied = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!preg_match('/Jail list:\s*(.+)$/mi', $status, $mm)) {
|
||||||
|
$this->error = true;
|
||||||
|
Log::warning('Fail2BanCard: unexpected status output', ['status' => $status]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$jails = array_filter(array_map('trim', preg_split('/\s*,\s*/', $mm[1] ?? '')));
|
||||||
|
$sum = 0;
|
||||||
|
$rows = [];
|
||||||
|
|
||||||
|
foreach ($jails as $j) {
|
||||||
|
$jEsc = escapeshellarg($j);
|
||||||
|
[, $s] = $this->f2b("status {$jEsc}");
|
||||||
|
if ($this->looksDenied($s)) {
|
||||||
|
$this->permDenied = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||||
|
$bantime = $this->getBantime($j);
|
||||||
|
|
||||||
|
$rows[] = ['name' => $j, 'banned' => $banned, 'bantime' => $bantime, 'ips' => []];
|
||||||
|
$sum += $banned;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->activeBans = $sum;
|
||||||
|
$this->jails = $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function f2b(string $args): array
|
||||||
|
{
|
||||||
|
$sudo = $this->bin('sudo');
|
||||||
|
$f2b = $this->bin('fail2ban-client');
|
||||||
|
$cmd = "timeout 3 $sudo -n $f2b $args 2>&1";
|
||||||
|
$out = (string)@shell_exec($cmd);
|
||||||
|
|
||||||
|
$ok = stripos($out, 'Status') !== false
|
||||||
|
|| stripos($out, 'Jail list') !== false
|
||||||
|
|| stripos($out, 'pong') !== false;
|
||||||
|
|
||||||
|
return [$ok, $out];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getBantime(string $jail): int
|
||||||
|
{
|
||||||
|
[, $out] = $this->f2b('get ' . escapeshellarg($jail) . ' bantime');
|
||||||
|
if ($this->looksDenied($out)) {
|
||||||
|
$this->permDenied = true;
|
||||||
|
return 600;
|
||||||
|
}
|
||||||
|
if (preg_match('/-?\d+/', trim($out), $m)) return (int)$m[0];
|
||||||
|
return 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function looksDenied(string $out): bool
|
||||||
|
{
|
||||||
|
return (bool)preg_match('/(permission denied|not allowed to execute|a password is required)/i', $out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function firstMatch(string $pattern, string $haystack): ?string
|
||||||
|
{
|
||||||
|
return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bin(string $name): string
|
||||||
|
{
|
||||||
|
$p = trim((string)@shell_exec("command -v " . escapeshellarg($name) . " 2>/dev/null"));
|
||||||
|
return $p !== '' ? $p : $name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//namespace App\Livewire\Ui\Security;
|
||||||
|
//
|
||||||
|
//use Illuminate\Support\Facades\Log;
|
||||||
|
//use Livewire\Attributes\On;
|
||||||
|
//use Livewire\Component;
|
||||||
|
//
|
||||||
|
//class Fail2BanCard extends Component
|
||||||
|
//{
|
||||||
|
// public bool $available = true; // fail2ban-client vorhanden?
|
||||||
|
// public bool $permDenied = false; // sudo / Socket-Rechte fehlen?
|
||||||
|
// public bool $error = false; // anderer Fehler (Output unerwartet)
|
||||||
|
// public int $activeBans = 0;
|
||||||
|
// public array $jails = []; // [['name','banned','bantime','ips'=>[...]]]
|
||||||
|
//
|
||||||
|
// public function mount(): void
|
||||||
|
// {
|
||||||
|
// $this->load();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public function render()
|
||||||
|
// {
|
||||||
|
// return view('livewire.ui.security.fail2-ban-card');
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[On('f2b:refresh-banlist')]
|
||||||
|
// public function refresh(): void
|
||||||
|
// {
|
||||||
|
// $this->load(true);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public function openDetails(string $jail): void
|
||||||
|
// {
|
||||||
|
// // wire-elements/modal (v2): Event-Namen + Component + Params
|
||||||
|
// $this->dispatch('openModal', component: 'ui.security.modal.fail2-ban-jail-modal', arguments: ['jail' => $jail]);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /* ------------------- intern ------------------- */
|
||||||
|
//
|
||||||
|
// protected function load(bool $force = false): void
|
||||||
|
// {
|
||||||
|
// $this->available = true;
|
||||||
|
// $this->permDenied = false;
|
||||||
|
// $this->error = false;
|
||||||
|
// $this->activeBans = 0;
|
||||||
|
// $this->jails = [];
|
||||||
|
//
|
||||||
|
// // existiert fail2ban-client?
|
||||||
|
// $bin = trim((string)@shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
|
||||||
|
// if ($bin === '') {
|
||||||
|
// $this->available = false;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Rechte / Erreichbarkeit
|
||||||
|
// [, $ping] = $this->f2b('ping');
|
||||||
|
// if ($this->looksDenied($ping)) {
|
||||||
|
// $this->permDenied = true;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Jails lesen
|
||||||
|
// [, $status] = $this->f2b('status');
|
||||||
|
// if ($this->looksDenied($status)) {
|
||||||
|
// $this->permDenied = true;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (!preg_match('/Jail list:\s*(.+)$/mi', $status, $mm)) {
|
||||||
|
// // etwas stimmt nicht – loggen und „error“ zeigen
|
||||||
|
// $this->error = true;
|
||||||
|
// Log::warning('Fail2BanCard: unexpected status output', ['status' => $status]);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $jails = array_filter(array_map('trim', preg_split('/\s*,\s*/', $mm[1] ?? '')));
|
||||||
|
// $sum = 0;
|
||||||
|
// $rows = [];
|
||||||
|
//
|
||||||
|
// foreach ($jails as $j) {
|
||||||
|
// $jEsc = escapeshellarg($j);
|
||||||
|
// [, $s] = $this->f2b("status {$jEsc}");
|
||||||
|
// if ($this->looksDenied($s)) {
|
||||||
|
// $this->permDenied = true;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||||
|
// $bantime = $this->getBantime($j);
|
||||||
|
// $ipLine = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
|
||||||
|
// $ips = $ipLine !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipLine)))) : [];
|
||||||
|
//
|
||||||
|
// $rows[] = [
|
||||||
|
// 'name' => $j,
|
||||||
|
// 'banned' => $banned,
|
||||||
|
// 'bantime' => $bantime,
|
||||||
|
// // wir zeigen IPs NICHT mehr in der Card; Details sind im Modal
|
||||||
|
// 'ips' => [],
|
||||||
|
// ];
|
||||||
|
// $sum += $banned;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $this->activeBans = $sum;
|
||||||
|
// $this->jails = $rows;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private function f2b(string $args): array
|
||||||
|
// {
|
||||||
|
// $sudo = '/usr/bin/sudo';
|
||||||
|
// $f2b = '/usr/bin/fail2ban-client';
|
||||||
|
// $cmd = "timeout 3 $sudo -n $f2b $args 2>&1";
|
||||||
|
// $out = (string)@shell_exec($cmd);
|
||||||
|
//
|
||||||
|
// $ok = stripos($out, 'Status') !== false
|
||||||
|
// || stripos($out, 'Jail list') !== false
|
||||||
|
// || stripos($out, 'pong') !== false;
|
||||||
|
//
|
||||||
|
// return [$ok, $out];
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private function getBantime(string $jail): int
|
||||||
|
// {
|
||||||
|
// [, $out] = $this->f2b('get ' . escapeshellarg($jail) . ' bantime');
|
||||||
|
// if ($this->looksDenied($out)) {
|
||||||
|
// $this->permDenied = true;
|
||||||
|
// return 600;
|
||||||
|
// }
|
||||||
|
// $val = trim($out);
|
||||||
|
// if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0];
|
||||||
|
// return 600;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private function looksDenied(string $out): bool
|
||||||
|
// {
|
||||||
|
// return preg_match('/(permission denied|not allowed to execute|a password is required)/i', $out) === 1;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private function firstMatch(string $pattern, string $haystack): ?string
|
||||||
|
// {
|
||||||
|
// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
//namespace App\Livewire\Ui\Security;
|
||||||
|
//
|
||||||
|
//use Livewire\Attributes\On;
|
||||||
|
//use Livewire\Component;
|
||||||
|
//
|
||||||
|
//class Fail2BanCard extends Component
|
||||||
|
//{
|
||||||
|
// public bool $available = true;
|
||||||
|
// public bool $permDenied = false;
|
||||||
|
// public int $activeBans = 0;
|
||||||
|
// public array $jails = [];
|
||||||
|
//
|
||||||
|
// public function mount(): void
|
||||||
|
// {
|
||||||
|
// $this->load();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public function render()
|
||||||
|
// {
|
||||||
|
// return view('livewire.ui.security.fail2-ban-card');
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[On('f2b:refresh-banlist')]
|
||||||
|
// public function refresh(): void
|
||||||
|
// {
|
||||||
|
// $this->load(true);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public function openDetails(string $jail): void
|
||||||
|
// {
|
||||||
|
// // KORREKTER DISPATCH für wire-elements/modal
|
||||||
|
// $this->dispatch('openModal', component: 'ui.security.modal.fail2-ban-jail-modal', arguments: ['jail' => $jail]);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /* ------------------- intern ------------------- */
|
||||||
|
//
|
||||||
|
// protected function load(bool $force = false): void
|
||||||
|
// {
|
||||||
|
// $bin = trim((string)@shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
|
||||||
|
// if ($bin === '') {
|
||||||
|
// $this->available = false;
|
||||||
|
// $this->permDenied = false;
|
||||||
|
// $this->activeBans = 0;
|
||||||
|
// $this->jails = [];
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Rechte prüfen
|
||||||
|
// [$ok, $raw] = $this->f2b('ping');
|
||||||
|
// if (!$ok && stripos($raw, 'permission denied') !== false) {
|
||||||
|
// $this->available = true;
|
||||||
|
// $this->permDenied = true;
|
||||||
|
// $this->activeBans = 0;
|
||||||
|
// $this->jails = [];
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Jail-Liste
|
||||||
|
// [, $status] = $this->f2b('status');
|
||||||
|
// $jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status);
|
||||||
|
// $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : [];
|
||||||
|
//
|
||||||
|
// $rows = [];
|
||||||
|
// $sum = 0;
|
||||||
|
//
|
||||||
|
// foreach ($jails as $j) {
|
||||||
|
// $jEsc = escapeshellarg($j);
|
||||||
|
// [, $s] = $this->f2b("status {$jEsc}");
|
||||||
|
// $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||||
|
// $bantime = $this->getBantime($j);
|
||||||
|
// $ipListLine = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
|
||||||
|
// $ips = $ipListLine !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipListLine)))) : [];
|
||||||
|
//
|
||||||
|
// // Details inkl. Restzeit je IP
|
||||||
|
// $ipDetails = $this->buildIpDetails($j, $ips, $bantime);
|
||||||
|
//
|
||||||
|
// $rows[] = [
|
||||||
|
// 'name' => $j,
|
||||||
|
// 'banned' => $banned,
|
||||||
|
// 'bantime' => $bantime, // Sek. (-1 = permanent)
|
||||||
|
// 'ips' => $ipDetails, // [['ip'=>..., 'remaining'=>..., 'until'=>...], ...]
|
||||||
|
// ];
|
||||||
|
// $sum += $banned;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $this->available = true;
|
||||||
|
// $this->permDenied = false;
|
||||||
|
// $this->activeBans = $sum;
|
||||||
|
// $this->jails = $rows;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private function f2b(string $args): array
|
||||||
|
// {
|
||||||
|
// $sudo = '/usr/bin/sudo';
|
||||||
|
// $f2b = '/usr/bin/fail2ban-client';
|
||||||
|
// $cmd = "timeout 3 $sudo -n $f2b $args 2>&1";
|
||||||
|
// $out = (string)@shell_exec($cmd);
|
||||||
|
//
|
||||||
|
// $ok = stripos($out, 'Status') !== false
|
||||||
|
// || stripos($out, 'Jail list') !== false
|
||||||
|
// || stripos($out, 'pong') !== false;
|
||||||
|
//
|
||||||
|
// return [$ok, $out];
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /** konfig. Bantime des Jails in Sekunden (-1 = permanent) */
|
||||||
|
// private function getBantime(string $jail): int
|
||||||
|
// {
|
||||||
|
// [, $out] = $this->f2b('get '.escapeshellarg($jail).' bantime');
|
||||||
|
// $val = trim($out);
|
||||||
|
// if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0];
|
||||||
|
// return 600; // konservativer Fallback
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /** Letzten Ban-Zeitpunkt (Unix-Timestamp) aus /var/log/fail2ban.log ermitteln. */
|
||||||
|
// private function lastBanTimestamp(string $jail, string $ip): ?int
|
||||||
|
// {
|
||||||
|
// $file = '/var/log/fail2ban.log';
|
||||||
|
// if (!is_readable($file)) return null;
|
||||||
|
//
|
||||||
|
// // nur das Ende der Datei lesen (Performance, auch bei Rotation groß genug wählen)
|
||||||
|
// $tailBytes = 400000; // 400 KB
|
||||||
|
// $size = @filesize($file) ?: 0;
|
||||||
|
// $seek = max(0, $size - $tailBytes);
|
||||||
|
//
|
||||||
|
// $fh = @fopen($file, 'rb');
|
||||||
|
// if (!$fh) return null;
|
||||||
|
// if ($seek > 0) fseek($fh, $seek);
|
||||||
|
// $data = stream_get_contents($fh) ?: '';
|
||||||
|
// fclose($fh);
|
||||||
|
//
|
||||||
|
// // Beispielzeile:
|
||||||
|
// // 2025-10-30 22:34:20,797 fail2ban.actions [...] NOTICE [sshd] Ban 193.46.255.244
|
||||||
|
// $j = preg_quote($jail, '/');
|
||||||
|
// $p = preg_quote($ip, '/');
|
||||||
|
// $pattern = '/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2}),\d+.*\['.$j.'\]\s+Ban\s+'.$p.'\s*$/m';
|
||||||
|
//
|
||||||
|
// if (preg_match_all($pattern, $data, $m) && !empty($m[1])) {
|
||||||
|
// $date = end($m[1]); // YYYY-MM-DD
|
||||||
|
// $time = end($m[2]); // HH:MM:SS
|
||||||
|
// $dt = \DateTime::createFromFormat('Y-m-d H:i:s', "$date $time", new \DateTimeZone(date_default_timezone_get()));
|
||||||
|
// return $dt ? $dt->getTimestamp() : null;
|
||||||
|
// }
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /** Baut Details inkl. Restzeit (Sekunden; -1 = permanent). */
|
||||||
|
// private function buildIpDetails(string $jail, array $ips, int $bantime): array
|
||||||
|
// {
|
||||||
|
// $now = time();
|
||||||
|
// $out = [];
|
||||||
|
//
|
||||||
|
// foreach ($ips as $ip) {
|
||||||
|
// $banAt = $this->lastBanTimestamp($jail, $ip);
|
||||||
|
// $remaining = null;
|
||||||
|
// $until = null;
|
||||||
|
//
|
||||||
|
// if ($bantime === -1) {
|
||||||
|
// $remaining = -1; // permanent
|
||||||
|
// } elseif ($banAt !== null) {
|
||||||
|
// $remaining = max(0, $bantime - ($now - $banAt));
|
||||||
|
// $until = $remaining > 0 ? ($banAt + $bantime) : null;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $out[] = [
|
||||||
|
// 'ip' => $ip,
|
||||||
|
// 'remaining' => $remaining, // -1 = permanent, null = Ban-Zeitpunkt nicht gefunden, >=0 = Sekunden
|
||||||
|
// 'until' => $until, // Unix-Timestamp oder null
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
// return $out;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// private function firstMatch(string $pattern, string $haystack): ?string
|
||||||
|
// {
|
||||||
|
// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
//namespace App\Livewire\Ui\Security;
|
||||||
|
//
|
||||||
|
//use Livewire\Component;
|
||||||
|
//
|
||||||
|
//class Fail2BanCard extends Component
|
||||||
|
//{
|
||||||
|
// public bool $available = true; // fail2ban-client vorhanden?
|
||||||
|
// public bool $permDenied = false; // Socket/Root-Rechte fehlen?
|
||||||
|
// public int $activeBans = 0; // Summe gebannter IPs
|
||||||
|
// /** @var array<int,array{name:string,banned:int,bantime:int}> */
|
||||||
|
// public array $jails = [];
|
||||||
|
//
|
||||||
|
// public function mount(): void
|
||||||
|
// {
|
||||||
|
// $this->load();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public function render()
|
||||||
|
// {
|
||||||
|
// return view('livewire.ui.security.fail2-ban-card');
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public function refresh(): void
|
||||||
|
// {
|
||||||
|
// $this->load(true);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Optional: öffnet später dein Detail-Modal/Tab
|
||||||
|
// public function openDetails(string $jail): void
|
||||||
|
// {
|
||||||
|
// $this->dispatch('openModal', 'ui.security.modal.fail2-ban-jail-modal', ['jail' => $jail]);
|
||||||
|
// }
|
||||||
|
// /* ---------------- intern ---------------- */
|
||||||
|
//
|
||||||
|
// protected function load(bool $force = false): void
|
||||||
|
// {
|
||||||
|
// $bin = trim((string)@shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
|
||||||
|
// if ($bin === '') {
|
||||||
|
// $this->available = false;
|
||||||
|
// $this->permDenied = false;
|
||||||
|
// $this->activeBans = 0;
|
||||||
|
// $this->jails = [];
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Rechtecheck
|
||||||
|
// [$ok, $raw] = $this->f2b('ping');
|
||||||
|
// if (!$ok && stripos($raw, 'permission denied') !== false) {
|
||||||
|
// $this->available = true;
|
||||||
|
// $this->permDenied = true;
|
||||||
|
// $this->activeBans = 0;
|
||||||
|
// $this->jails = [];
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Jails laden
|
||||||
|
// [, $status] = $this->f2b('status');
|
||||||
|
// $jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status);
|
||||||
|
// $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : [];
|
||||||
|
//
|
||||||
|
// $rows = [];
|
||||||
|
// $sum = 0;
|
||||||
|
//
|
||||||
|
// foreach ($jails as $j) {
|
||||||
|
// [, $s] = $this->f2b('status ' . escapeshellarg($j));
|
||||||
|
// $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||||
|
// $bantime = $this->getBantime($j); // Sek.; -1 = permanent
|
||||||
|
// $rows[] = ['name' => $j, 'banned' => $banned, 'bantime' => $bantime];
|
||||||
|
// $sum += $banned;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $this->available = true;
|
||||||
|
// $this->permDenied = false;
|
||||||
|
// $this->activeBans = $sum;
|
||||||
|
// $this->jails = $rows;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /** sudo + fail2ban-client ausführen; [ok, output] */
|
||||||
|
// private function f2b(string $args): array
|
||||||
|
// {
|
||||||
|
// $sudo = '/usr/bin/sudo';
|
||||||
|
// $f2b = '/usr/bin/fail2ban-client';
|
||||||
|
// $out = (string)@shell_exec("timeout 2 $sudo -n $f2b $args 2>&1");
|
||||||
|
// $ok = stripos($out, 'Status') !== false
|
||||||
|
// || stripos($out, 'Jail list') !== false
|
||||||
|
// || stripos($out, 'pong') !== false;
|
||||||
|
// return [$ok, $out];
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private function getBantime(string $jail): int
|
||||||
|
// {
|
||||||
|
// [, $out] = $this->f2b('get ' . escapeshellarg($jail) . ' bantime');
|
||||||
|
// $val = trim($out);
|
||||||
|
// if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0];
|
||||||
|
// return 600; // defensiver Default
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private function firstMatch(string $pattern, string $haystack): ?string
|
||||||
|
// {
|
||||||
|
// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
//namespace App\Livewire\Ui\Security;
|
||||||
|
//
|
||||||
|
//use Livewire\Component;
|
||||||
|
//
|
||||||
|
//class Fail2BanCard extends Component
|
||||||
|
//{
|
||||||
|
// public bool $available = true; // fail2ban-client vorhanden?
|
||||||
|
// public bool $permDenied = false; // Socket/Root-Rechte fehlen?
|
||||||
|
// public int $activeBans = 0; // Summe gebannter IPs über alle Jails
|
||||||
|
// public array $jails = []; // [['name','banned','bantime','ips'=>[['ip','remaining','until'],...]],...]
|
||||||
|
// public array $topIps = []; // [['ip'=>'x.x.x.x','count'=>N], ...]
|
||||||
|
//
|
||||||
|
// public function mount(): void
|
||||||
|
// {
|
||||||
|
// $this->load();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public function render()
|
||||||
|
// {
|
||||||
|
// return view('livewire.ui.security.fail2-ban-card');
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /** Button „Neu prüfen“ */
|
||||||
|
// public function refresh(): void
|
||||||
|
// {
|
||||||
|
// $this->load(true);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /* --------------------- intern --------------------- */
|
||||||
|
//
|
||||||
|
// protected function load(bool $force = false): void
|
||||||
|
// {
|
||||||
|
// // existiert fail2ban-client?
|
||||||
|
// $bin = trim((string)@shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
|
||||||
|
// if ($bin === '') {
|
||||||
|
// $this->available = false;
|
||||||
|
// $this->permDenied = false;
|
||||||
|
// $this->activeBans = 0;
|
||||||
|
// $this->jails = [];
|
||||||
|
// $this->topIps = [];
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // ping → prüft zugleich Rechte (bei Permission-Fehler kommt Klartext)
|
||||||
|
// [$ok, $raw] = $this->f2b('ping'); // ok == "pong" erkannt
|
||||||
|
// if (!$ok && stripos($raw, 'permission denied') !== false) {
|
||||||
|
// $this->available = true;
|
||||||
|
// $this->permDenied = true;
|
||||||
|
// $this->activeBans = 0;
|
||||||
|
// $this->jails = [];
|
||||||
|
// $this->topIps = $this->collectTopIps();
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Jails auflisten
|
||||||
|
// [, $status] = $this->f2b('status');
|
||||||
|
// $jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status);
|
||||||
|
// $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : [];
|
||||||
|
//
|
||||||
|
// $total = 0;
|
||||||
|
// $rows = [];
|
||||||
|
//
|
||||||
|
// foreach ($jails as $j) {
|
||||||
|
// $bantimeSecs = $this->getBantime($j); // Sek., -1 = permanent
|
||||||
|
//
|
||||||
|
// [, $s] = $this->f2b('status ' . escapeshellarg($j));
|
||||||
|
// $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||||
|
// $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
|
||||||
|
// $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : [];
|
||||||
|
//
|
||||||
|
// // Restzeiten je IP bestimmen (aus /var/log/fail2ban.log)
|
||||||
|
// $ipDetails = [];
|
||||||
|
// foreach (array_slice($ips, 0, 50) as $ip) {
|
||||||
|
// $banAt = $this->lastBanTimestamp($j, $ip); // Unix-Timestamp oder null
|
||||||
|
// $remaining = null;
|
||||||
|
// $until = null;
|
||||||
|
//
|
||||||
|
// if ($banAt !== null) {
|
||||||
|
// if ((int)$bantimeSecs === -1) {
|
||||||
|
// $remaining = -1; // permanent
|
||||||
|
// } else {
|
||||||
|
// $remaining = max(0, $bantimeSecs - (time() - $banAt));
|
||||||
|
// $until = $remaining > 0 ? ($banAt + $bantimeSecs) : null;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $ipDetails[] = [
|
||||||
|
// 'ip' => $ip,
|
||||||
|
// 'remaining' => $remaining, // -1 = permanent, 0 = abgelaufen, >0 Sek.
|
||||||
|
// 'until' => $until, // Unix-Timestamp oder null
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $rows[] = [
|
||||||
|
// 'name' => $j,
|
||||||
|
// 'banned' => $banned,
|
||||||
|
// 'ips' => $ipDetails,
|
||||||
|
// 'bantime' => (int)$bantimeSecs,
|
||||||
|
// ];
|
||||||
|
// $total += $banned;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $this->available = true;
|
||||||
|
// $this->permDenied = false;
|
||||||
|
// $this->activeBans = $total;
|
||||||
|
// $this->jails = $rows;
|
||||||
|
// $this->topIps = $this->collectTopIps();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /** führt fail2ban-client via sudo aus; gibt [ok, output] zurück */
|
||||||
|
// private function f2b(string $args): array
|
||||||
|
// {
|
||||||
|
// $sudo = '/usr/bin/sudo';
|
||||||
|
// $f2b = '/usr/bin/fail2ban-client';
|
||||||
|
// $cmd = "timeout 2 $sudo -n $f2b $args 2>&1";
|
||||||
|
// $out = (string)@shell_exec($cmd);
|
||||||
|
//
|
||||||
|
// $ok = stripos($out, 'Status') !== false
|
||||||
|
// || stripos($out, 'Jail list') !== false
|
||||||
|
// || stripos($out, 'pong') !== false;
|
||||||
|
//
|
||||||
|
// return [$ok, $out];
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private function getBantime(string $jail): int
|
||||||
|
// {
|
||||||
|
// [, $out] = $this->f2b('get ' . escapeshellarg($jail) . ' bantime');
|
||||||
|
// $val = trim($out);
|
||||||
|
// if (preg_match('/-?\d+/', $val, $m)) {
|
||||||
|
// return (int)$m[0];
|
||||||
|
// }
|
||||||
|
// return 600; // defensiver Default
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /** letzte Ban-Zeile aus /var/log/fail2ban.log → Unix-Timestamp */
|
||||||
|
// private function lastBanTimestamp(string $jail, string $ip): ?int
|
||||||
|
// {
|
||||||
|
// $cmd = "grep -F \"[{$jail}] Ban {$ip}\" /var/log/fail2ban.log 2>/dev/null | tail -n 1";
|
||||||
|
// $line = trim((string)@shell_exec($cmd));
|
||||||
|
// if ($line === '') return null;
|
||||||
|
//
|
||||||
|
// // "YYYY-MM-DD HH:MM:SS,mmm ..."
|
||||||
|
// if (preg_match('/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})/', $line, $m)) {
|
||||||
|
// $ts = strtotime($m[1] . ' ' . $m[2]);
|
||||||
|
// return $ts ?: null;
|
||||||
|
// }
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private function firstMatch(string $pattern, string $haystack): ?string
|
||||||
|
// {
|
||||||
|
// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /** Top-IPs grob zählen (aus der aktuellen Jail-Liste; Fallback: Log) */
|
||||||
|
// private function collectTopIps(): array
|
||||||
|
// {
|
||||||
|
// $map = [];
|
||||||
|
// foreach ($this->jails as $jail) {
|
||||||
|
// foreach ($jail['ips'] as $row) {
|
||||||
|
// $ip = $row['ip'] ?? null;
|
||||||
|
// if (!$ip) continue;
|
||||||
|
// $map[$ip] = ($map[$ip] ?? 0) + 1;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (!empty($map)) {
|
||||||
|
// arsort($map);
|
||||||
|
// $out = [];
|
||||||
|
// foreach (array_slice($map, 0, 5, true) as $ip => $count) {
|
||||||
|
// $out[] = ['ip' => $ip, 'count' => $count];
|
||||||
|
// }
|
||||||
|
// return $out;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Fallback: aus fail2ban.log
|
||||||
|
// $cmd = 'grep -Eo "([0-9]{1,3}\.){3}[0-9]{1,3}" /var/log/fail2ban.log 2>/dev/null'
|
||||||
|
// . ' | sort | uniq -c | sort -nr | head -5';
|
||||||
|
// $log = (string)@shell_exec($cmd);
|
||||||
|
// $rows = [];
|
||||||
|
// if ($log !== '') {
|
||||||
|
// foreach (preg_split('/\R+/', trim($log)) as $l) {
|
||||||
|
// if (preg_match('/^\s*(\d+)\s+(\d+\.\d+\.\d+\.\d+)/', $l, $m)) {
|
||||||
|
// $rows[] = ['ip' => $m[2], 'count' => (int)$m[1]];
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return $rows;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
//
|
||||||
|
//namespace App\Livewire\Ui\Security;
|
||||||
|
//
|
||||||
|
//use Livewire\Component;
|
||||||
|
//
|
||||||
|
//class Fail2BanCard extends Component
|
||||||
|
//{
|
||||||
|
// public bool $available = true; // fail2ban-client vorhanden?
|
||||||
|
// public bool $permDenied = false; // Socket/Root-Rechte fehlen?
|
||||||
|
// public int $activeBans = 0; // Summe gebannter IPs über alle Jails
|
||||||
|
// public array $jails = []; // [['name'=>'sshd','banned'=>2,'ips'=>['1.2.3.4',...]], ...]
|
||||||
|
// public array $topIps = []; // [['ip'=>'x.x.x.x','count'=>N], ...]
|
||||||
|
//
|
||||||
|
// public function mount(): void
|
||||||
|
// {
|
||||||
|
// $this->load();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public function render()
|
||||||
|
// {
|
||||||
|
// return view('livewire.ui.security.fail2-ban-card');
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Wird vom Button "Neu prüfen" genutzt
|
||||||
|
// public function refresh(): void
|
||||||
|
// {
|
||||||
|
// $this->load(true);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /* --------------------- intern --------------------- */
|
||||||
|
//
|
||||||
|
// protected function load(bool $force = false): void
|
||||||
|
// {
|
||||||
|
// // existiert fail2ban-client?
|
||||||
|
// $bin = trim((string) @shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
|
||||||
|
// if ($bin === '') {
|
||||||
|
// $this->available = false;
|
||||||
|
// $this->permDenied = false;
|
||||||
|
// $this->activeBans = 0;
|
||||||
|
// $this->jails = [];
|
||||||
|
// $this->topIps = [];
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // ping → prüft zugleich Rechte (bei Permission-Fehler kommt Klartext)
|
||||||
|
// [$ok, $raw] = $this->f2b('ping'); // ok == "pong" erkannt
|
||||||
|
// if (!$ok && stripos($raw, 'permission denied') !== false) {
|
||||||
|
// $this->available = true;
|
||||||
|
// $this->permDenied = true;
|
||||||
|
// $this->activeBans = 0;
|
||||||
|
// $this->jails = [];
|
||||||
|
// $this->topIps = $this->collectTopIps();
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Jails auflisten
|
||||||
|
// [, $status] = $this->f2b('status');
|
||||||
|
// $jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status);
|
||||||
|
// $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : [];
|
||||||
|
//
|
||||||
|
// $total = 0; $rows = [];
|
||||||
|
//// ... in load() NACH dem Einlesen der Jail-Liste:
|
||||||
|
// $rows = [];
|
||||||
|
// foreach ($jails as $j) {
|
||||||
|
// $bantimeSecs = $this->getBantime($j); // konfigurierter Wert (Sekunden, -1 = permanent)
|
||||||
|
//
|
||||||
|
// [, $s] = $this->f2b('status '.escapeshellarg($j));
|
||||||
|
// $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||||
|
// $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
|
||||||
|
// $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : [];
|
||||||
|
//
|
||||||
|
// // Restzeiten je IP bestimmen (aus /var/log/fail2ban.log)
|
||||||
|
// $ipDetails = [];
|
||||||
|
// foreach (array_slice($ips, 0, 50) as $ip) {
|
||||||
|
// $banAt = $this->lastBanTimestamp($j, $ip); // Unix-Timestamp oder null
|
||||||
|
// $remaining = null;
|
||||||
|
// $until = null;
|
||||||
|
//
|
||||||
|
// if ($banAt !== null) {
|
||||||
|
// if ((int)$bantimeSecs === -1) {
|
||||||
|
// $remaining = -1; // permanent
|
||||||
|
// } else {
|
||||||
|
// $remaining = max(0, $bantimeSecs - (time() - $banAt));
|
||||||
|
// $until = $remaining > 0 ? ($banAt + $bantimeSecs) : null;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $ipDetails[] = [
|
||||||
|
// 'ip' => $ip,
|
||||||
|
// 'remaining' => $remaining, // -1 = permanent, 0 = abgelaufen, >0 Sek.
|
||||||
|
// 'until' => $until, // Unix-Timestamp oder null
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $rows[] = [
|
||||||
|
// 'name' => $j,
|
||||||
|
// 'banned' => $banned,
|
||||||
|
// 'ips' => $ipDetails, // jetzt mit Details
|
||||||
|
// 'bantime' => (int)$bantimeSecs,
|
||||||
|
// ];
|
||||||
|
// $total += $banned;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // foreach ($jails as $j) {
|
||||||
|
//// [, $s] = $this->f2b('status '.escapeshellarg($j));
|
||||||
|
//// $banned = (int) ($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||||
|
//// $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
|
||||||
|
//// $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : [];
|
||||||
|
//// $rows[] = ['name'=>$j,'banned'=>$banned,'ips'=>array_slice($ips, 0, 8)];
|
||||||
|
//// $total += $banned;
|
||||||
|
//// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// $this->available = true;
|
||||||
|
// $this->permDenied = false;
|
||||||
|
// $this->activeBans = $total;
|
||||||
|
// $this->jails = $rows;
|
||||||
|
// $this->topIps = $this->collectTopIps();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /** führt fail2ban-client via sudo aus; gibt [ok, output] zurück */
|
||||||
|
// private function f2b(string $args): array
|
||||||
|
// {
|
||||||
|
// $sudo = '/usr/bin/sudo';
|
||||||
|
// $f2b = '/usr/bin/fail2ban-client';
|
||||||
|
// $cmd = "timeout 2 $sudo -n $f2b $args 2>&1";
|
||||||
|
// $out = (string) @shell_exec($cmd);
|
||||||
|
//
|
||||||
|
// $ok = stripos($out, 'Status') !== false
|
||||||
|
// || stripos($out, 'Jail list') !== false
|
||||||
|
// || stripos($out, 'pong') !== false;
|
||||||
|
//
|
||||||
|
// return [$ok, $out];
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private function getBantime(string $jail): int
|
||||||
|
// {
|
||||||
|
// [, $out] = $this->f2b('get '.escapeshellarg($jail).' bantime');
|
||||||
|
// // fail2ban liefert Seconds als Zahl (oder mit Newline)
|
||||||
|
// $val = trim($out);
|
||||||
|
// // Fallback: manche Versionen geben nur Zahl ohne Kontext zurück,
|
||||||
|
// // sonst aus jail.local ermitteln wäre overkill -> einfache Zahl extrahieren:
|
||||||
|
// if (preg_match('/-?\d+/', $val, $m)) {
|
||||||
|
// return (int)$m[0];
|
||||||
|
// }
|
||||||
|
// // wenn nicht ermittelbar: 600 Sekunden als conservative default
|
||||||
|
// return 600;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /** Sucht die letzte "Ban <IP>"-Zeile für Jail in /var/log/fail2ban.log und gibt Unix-Timestamp zurück. */
|
||||||
|
// private function lastBanTimestamp(string $jail, string $ip): ?int
|
||||||
|
// {
|
||||||
|
// // Beispiel-Logzeilen:
|
||||||
|
// // 2025-10-29 18:07:11,436 fail2ban.actions [12345]: NOTICE [sshd] Ban 1.2.3.4
|
||||||
|
// // Wir holen die letzte passende Zeile (tail mit grep), dann parsen Datum.
|
||||||
|
// $pattern = escapeshellarg(sprintf('\\[%s\\] Ban %s', $jail, $ip));
|
||||||
|
// $cmd = "grep -F \"[{$jail}] Ban {$ip}\" /var/log/fail2ban.log 2>/dev/null | tail -n 1";
|
||||||
|
// $line = (string)@shell_exec($cmd);
|
||||||
|
// $line = trim($line);
|
||||||
|
// if ($line === '') {
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// // Datumsformat am Anfang: "YYYY-MM-DD HH:MM:SS,mmm"
|
||||||
|
// if (preg_match('/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})/', $line, $m)) {
|
||||||
|
// $ts = strtotime($m[1].' '.$m[2]);
|
||||||
|
// return $ts ?: null;
|
||||||
|
// }
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private function firstMatch(string $pattern, string $haystack): ?string
|
||||||
|
// {
|
||||||
|
// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /** Zählt die häufigsten IPs aus den letzten Fail2Ban-Logs (ban/unban Events) */
|
||||||
|
// private function collectTopIps(): array
|
||||||
|
// {
|
||||||
|
// // 1. Versuch: IPs direkt aus den Jails
|
||||||
|
// $rows = [];
|
||||||
|
// foreach ($this->jails as $jail) {
|
||||||
|
// foreach ($jail['ips'] as $ip) {
|
||||||
|
// $rows[$ip] = ($rows[$ip] ?? 0) + 1;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (!empty($rows)) {
|
||||||
|
// arsort($rows);
|
||||||
|
// return collect($rows)
|
||||||
|
// ->map(fn($count, $ip) => ['ip' => $ip, 'count' => $count])
|
||||||
|
// ->values()
|
||||||
|
// ->take(5)
|
||||||
|
// ->toArray();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 2. Fallback: Falls keine Jails/IPs → Logdatei
|
||||||
|
// $cmd = 'grep -Eo "([0-9]{1,3}\.){3}[0-9]{1,3}" /var/log/fail2ban.log 2>/dev/null'
|
||||||
|
// . ' | sort | uniq -c | sort -nr | head -5';
|
||||||
|
// $log = (string) @shell_exec($cmd);
|
||||||
|
//
|
||||||
|
// $rows = [];
|
||||||
|
// if ($log !== '') {
|
||||||
|
// foreach (preg_split('/\R+/', trim($log)) as $l) {
|
||||||
|
// if (preg_match('/^\s*(\d+)\s+(\d+\.\d+\.\d+\.\d+)/', $l, $m)) {
|
||||||
|
// $rows[] = ['ip'=>$m[2],'count'=>(int)$m[1]];
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return $rows;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Ui\Security;
|
||||||
|
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class Fail2banBanlist extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* null oder '*' => alle Jails
|
||||||
|
* 'recidive' => nur dieses Jail
|
||||||
|
* 'mailwolt-blacklist' etc.
|
||||||
|
*/
|
||||||
|
public ?string $jail = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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->loadBanned();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(?string $jail = null): void
|
||||||
|
{
|
||||||
|
$this->jail = $jail;
|
||||||
|
$this->loadBanned();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.ui.security.fail2ban-banlist');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= core ================= */
|
||||||
|
|
||||||
|
private function loadBanned(): void
|
||||||
|
{
|
||||||
|
$jails = $this->jailList();
|
||||||
|
|
||||||
|
// ggf. nur ein bestimmtes Jail
|
||||||
|
if (is_string($this->jail) && $this->jail !== '' && $this->jail !== '*') {
|
||||||
|
$jails = in_array($this->jail, $jails, true) ? [$this->jail] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
foreach ($jails as $j) {
|
||||||
|
$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);
|
||||||
|
|
||||||
|
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';
|
||||||
|
$style = 'permanent';
|
||||||
|
$dot = 'bg-rose-500';
|
||||||
|
} else {
|
||||||
|
$box = 'border-amber-400/20 bg-white/3';
|
||||||
|
$badge = 'border-amber-400/30 bg-amber-500/10 text-amber-200';
|
||||||
|
$label = 'Temporär';
|
||||||
|
$style = 'temporary';
|
||||||
|
$dot = 'bg-amber-400';
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows[] = [
|
||||||
|
'ip' => $ip,
|
||||||
|
'jail' => $j,
|
||||||
|
'permanent' => $permanent,
|
||||||
|
'style' => $style,
|
||||||
|
'label' => $label,
|
||||||
|
'box' => $box,
|
||||||
|
'badge' => $badge,
|
||||||
|
'dot' => $dot,
|
||||||
|
'btn' => 'border-rose-400/30 bg-rose-500/10 text-rose-200 hover:border-rose-400/50',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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;
|
||||||
|
|
||||||
|
$cmd = sprintf(
|
||||||
|
'sudo -n /usr/bin/fail2ban-client set %s unbanip %s 2>&1',
|
||||||
|
escapeshellarg($jail),
|
||||||
|
escapeshellarg($ip)
|
||||||
|
);
|
||||||
|
@shell_exec($cmd);
|
||||||
|
|
||||||
|
$this->loadBanned();
|
||||||
|
|
||||||
|
$this->dispatch('toast',
|
||||||
|
type: 'done',
|
||||||
|
badge: 'Fail2Ban',
|
||||||
|
title: 'IP entbannt',
|
||||||
|
text: "IP {$ip} in Jail „{$jail}“ entbannt.",
|
||||||
|
duration: 5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= helpers ================= */
|
||||||
|
|
||||||
|
/** 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');
|
||||||
|
if (preg_match('/Jail list:\s*(.+)$/mi', $out, $m)) {
|
||||||
|
$jails = array_map('trim', preg_split('/\s*,\s*/', trim($m[1])));
|
||||||
|
return array_values(array_filter($jails, fn($v) => $v !== ''));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,543 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
// 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
|
||||||
|
{
|
||||||
|
$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,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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',
|
||||||
|
]);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Fail2Ban reload
|
||||||
|
$this->runCommand('sudo -n /usr/bin/fail2ban-client reload');
|
||||||
|
|
||||||
|
$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;
|
||||||
|
|
||||||
|
$content = <<<CONF
|
||||||
|
[DEFAULT]
|
||||||
|
bantime = {$s->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
|
||||||
|
{
|
||||||
|
// zieht System + User-Whitelist
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- Helper ---------------- */
|
||||||
|
|
||||||
|
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]);
|
||||||
|
stream_get_contents($pipes[1]);
|
||||||
|
stream_get_contents($pipes[2]);
|
||||||
|
$code = proc_close($proc);
|
||||||
|
|
||||||
|
if ($code !== 0) {
|
||||||
|
throw new \RuntimeException("tee failed writing to {$target}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
//
|
||||||
|
//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 = <<<CONF
|
||||||
|
//[DEFAULT]
|
||||||
|
//bantime = {$s->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;
|
||||||
|
//
|
||||||
|
//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::where('type', 'whitelist')->pluck('ip')->toArray();
|
||||||
|
// $this->blacklist = Fail2banIpList::where('type', 'blacklist')->pluck('ip')->toArray();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public function mount(): void
|
||||||
|
// {
|
||||||
|
// // Setting holen oder mit 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 füllen (KEINE Mixed-Objekte in Inputs binden)
|
||||||
|
// $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->whitelist = Fail2banIpList::where('type','whitelist')->pluck('ip')->toArray();
|
||||||
|
// $this->blacklist = Fail2banIpList::where('type','blacklist')->pluck('ip')->toArray();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 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',
|
||||||
|
// ]);
|
||||||
|
//
|
||||||
|
// $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,
|
||||||
|
// ]);
|
||||||
|
//
|
||||||
|
// $this->writeDefaultsConfig();
|
||||||
|
// $this->writeWhitelistConfig();
|
||||||
|
//
|
||||||
|
// @shell_exec('sudo fail2ban-client reload');
|
||||||
|
// $this->dispatch('notify', message: 'Gespeichert & Fail2Ban neu geladen.');
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// protected function writeDefaultsConfig(): void
|
||||||
|
// {
|
||||||
|
// $s = $this->settings;
|
||||||
|
// $content = <<<CONF
|
||||||
|
//[DEFAULT]
|
||||||
|
//bantime = {$s->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;
|
||||||
|
// file_put_contents('/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";
|
||||||
|
// file_put_contents('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private function writeRootFileViaTee(string $target, string $content): void
|
||||||
|
// {
|
||||||
|
// // Nur erlaubte Pfade (Hardening)
|
||||||
|
// 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'], // stdin -> tee
|
||||||
|
// 1 => ['pipe', 'w'], // stdout
|
||||||
|
// 2 => ['pipe', 'w'], // stderr
|
||||||
|
// ];
|
||||||
|
//
|
||||||
|
// $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]);
|
||||||
|
// $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 boolToStr(bool $v): string
|
||||||
|
// {
|
||||||
|
// return $v ? 'true' : 'false';
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public function render()
|
||||||
|
// {
|
||||||
|
// return view('livewire.ui.security.fail2ban-settings');
|
||||||
|
// }
|
||||||
|
//}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Ui\Security\Modal\Fail2BanJailModal;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class Php extends Component
|
||||||
|
{
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.ui.security.modal.fail2-ban-jail-modal.php');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,567 @@
|
||||||
|
<?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");
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Ui\System\Form;
|
||||||
|
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class DomainsSslForm extends Component
|
||||||
|
{
|
||||||
|
// fix / readonly aus ENV oder config
|
||||||
|
public string $mail_domain_readonly = '';
|
||||||
|
|
||||||
|
// editierbar
|
||||||
|
public string $ui_domain = '';
|
||||||
|
public string $webmail_domain = '';
|
||||||
|
|
||||||
|
protected function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ui_domain' => 'nullable|string|max:190',
|
||||||
|
'webmail_domain' => 'nullable|string|max:190',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->mail_domain_readonly = (string) config('mailwolt.domain.mail', 'mx');
|
||||||
|
$this->ui_domain = Setting::get('ui_domain', $this->ui_domain);
|
||||||
|
$this->webmail_domain = Setting::get('webmail_domain', $this->webmail_domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(): void
|
||||||
|
{
|
||||||
|
$this->validate();
|
||||||
|
|
||||||
|
Setting::put('ui_domain', $this->ui_domain);
|
||||||
|
Setting::put('webmail_domain', $this->webmail_domain);
|
||||||
|
|
||||||
|
$this->dispatch('toast',
|
||||||
|
type: 'done',
|
||||||
|
badge: 'System',
|
||||||
|
title: 'Domains gespeichert',
|
||||||
|
text: 'UI- und Webmail-Domain wurden übernommen.',
|
||||||
|
duration: 5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render() { return view('livewire.ui.system.form.domains-ssl-form'); }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Ui\System\Form;
|
||||||
|
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class GeneralForm extends Component
|
||||||
|
{
|
||||||
|
public string $locale = 'de';
|
||||||
|
public string $timezone = 'Europe/Berlin';
|
||||||
|
|
||||||
|
protected function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'locale' => 'required|string|max:10',
|
||||||
|
'timezone' => 'required|string|max:64',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
// Defaults aus ENV nur für den allerersten Seed in Settings (Redis/DB)
|
||||||
|
$envLocale = env('APP_LOCALE') ?? env('APP_FALLBACK_LOCALE') ?? $this->locale;
|
||||||
|
$envTimezone = env('APP_TIMEZONE') ?? $this->timezone;
|
||||||
|
|
||||||
|
// Wenn (noch) nichts in Settings liegt, einmalig mit ENV-Werten befüllen
|
||||||
|
if (Setting::get('locale', null) === null) {
|
||||||
|
Setting::set('locale', $envLocale);
|
||||||
|
}
|
||||||
|
if (Setting::get('timezone', null) === null) {
|
||||||
|
Setting::set('timezone', $envTimezone);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ab hier ausschließlich aus Settings lesen (Redis → DB Fallback)
|
||||||
|
$this->locale = (string) Setting::get('locale', $envLocale);
|
||||||
|
$this->timezone = (string) Setting::get('timezone', $envTimezone);
|
||||||
|
|
||||||
|
// Sofort für die aktuelle Request anwenden
|
||||||
|
app()->setLocale($this->locale);
|
||||||
|
@date_default_timezone_set($this->timezone);
|
||||||
|
config([
|
||||||
|
'app.locale' => $this->locale,
|
||||||
|
'app.fallback_locale' => $this->locale,
|
||||||
|
'app.timezone' => $this->timezone,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(): void
|
||||||
|
{
|
||||||
|
$this->validate();
|
||||||
|
|
||||||
|
// Persistieren: DB → Redis (siehe Setting::set)
|
||||||
|
Setting::set('locale', $this->locale);
|
||||||
|
Setting::set('timezone', $this->timezone);
|
||||||
|
|
||||||
|
// Direkt in der laufenden Request aktivieren
|
||||||
|
app()->setLocale($this->locale);
|
||||||
|
@date_default_timezone_set($this->timezone);
|
||||||
|
config([
|
||||||
|
'app.locale' => $this->locale,
|
||||||
|
'app.fallback_locale' => $this->locale, // optional
|
||||||
|
'app.timezone' => $this->timezone,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->dispatch('toast',
|
||||||
|
type: 'done',
|
||||||
|
badge: 'System',
|
||||||
|
title: 'Allgemein gespeichert',
|
||||||
|
text: 'Sprache und Zeitzone wurden übernommen.',
|
||||||
|
duration: 5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.ui.system.form.general-form');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Ui\System\Form;
|
||||||
|
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class SecurityForm extends Component
|
||||||
|
{
|
||||||
|
public bool $twofa_enabled = false;
|
||||||
|
public ?int $rate_limit = 5;
|
||||||
|
public ?int $password_min = 10;
|
||||||
|
|
||||||
|
protected function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'twofa_enabled' => 'boolean',
|
||||||
|
'rate_limit' => 'nullable|integer|min:1|max:100',
|
||||||
|
'password_min' => 'nullable|integer|min:6|max:128',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->twofa_enabled = (bool) Setting::get('twofa_enabled', $this->twofa_enabled);
|
||||||
|
$this->rate_limit = (int) Setting::get('rate_limit', $this->rate_limit);
|
||||||
|
$this->password_min = (int) Setting::get('password_min', $this->password_min);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(): void
|
||||||
|
{
|
||||||
|
$this->validate();
|
||||||
|
|
||||||
|
Setting::put('twofa_enabled', $this->twofa_enabled);
|
||||||
|
Setting::put('rate_limit', $this->rate_limit);
|
||||||
|
Setting::put('password_min', $this->password_min);
|
||||||
|
|
||||||
|
$this->dispatch('toast',
|
||||||
|
type: 'done',
|
||||||
|
badge: 'Sicherheit',
|
||||||
|
title: 'Sicherheit gespeichert',
|
||||||
|
text: '2FA/Rate-Limits/Passwortregeln wurden übernommen.',
|
||||||
|
duration: 5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render() { return view('livewire.ui.system.form.security-form'); }
|
||||||
|
}
|
||||||
|
|
@ -111,6 +111,7 @@ class UpdateCard extends Component
|
||||||
$this->recompute();
|
$this->recompute();
|
||||||
|
|
||||||
if ($this->rc === 0 && !$this->postActionsDone) {
|
if ($this->rc === 0 && !$this->postActionsDone) {
|
||||||
|
@shell_exec('nohup php /var/www/mailwolt/artisan optimize:clear >/dev/null 2>&1 &');
|
||||||
@shell_exec('nohup php /var/www/mailwolt/artisan health:collect >/dev/null 2>&1 &');
|
@shell_exec('nohup php /var/www/mailwolt/artisan health:collect >/dev/null 2>&1 &');
|
||||||
@shell_exec('nohup php /var/www/mailwolt/artisan settings:sync >/dev/null 2>&1 &');
|
@shell_exec('nohup php /var/www/mailwolt/artisan settings:sync >/dev/null 2>&1 &');
|
||||||
@shell_exec('nohup php /var/www/mailwolt/artisan spamav:collect >/dev/null 2>&1 &');
|
@shell_exec('nohup php /var/www/mailwolt/artisan spamav:collect >/dev/null 2>&1 &');
|
||||||
|
|
@ -119,7 +120,6 @@ class UpdateCard extends Component
|
||||||
|
|
||||||
$ver = $this->displayCurrent ?? 'aktuelle Version';
|
$ver = $this->displayCurrent ?? 'aktuelle Version';
|
||||||
$this->progressLine = 'Update abgeschlossen: ' . $ver;
|
$this->progressLine = 'Update abgeschlossen: ' . $ver;
|
||||||
|
|
||||||
// Optional: NICHT sofort reloaden – Nutzer entscheidet
|
// Optional: NICHT sofort reloaden – Nutzer entscheidet
|
||||||
// $this->dispatch('reload-page', delay: 6000);
|
// $this->dispatch('reload-page', delay: 6000);
|
||||||
} elseif ($this->rc !== null && $this->rc !== 0 && !$this->postActionsDone) {
|
} elseif ($this->rc !== null && $this->rc !== 0 && !$this->postActionsDone) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,278 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Fail2banIpList extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'fail2ban_ip_lists';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'ip',
|
||||||
|
'type',
|
||||||
|
'is_system',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'ip' => 'string',
|
||||||
|
'type' => 'string',
|
||||||
|
'is_system' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
public const TYPE_WHITELIST = 'whitelist';
|
||||||
|
public const TYPE_BLACKLIST = 'blacklist';
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
Boot-Hooks (Schutz & Normalisierung)
|
||||||
|
=========================== */
|
||||||
|
protected static function booted()
|
||||||
|
{
|
||||||
|
// Normalisierung & Loopback-Flag setzen
|
||||||
|
static::saving(function (self $m) {
|
||||||
|
$m->ip = trim($m->ip);
|
||||||
|
|
||||||
|
if (!self::isValidIpOrCidr($m->ip)) {
|
||||||
|
throw new \InvalidArgumentException("Ungültige IP/CIDR: {$m->ip}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loopback immer als System markieren
|
||||||
|
if (self::isLoopback($m->ip)) {
|
||||||
|
$m->is_system = true;
|
||||||
|
$m->type = self::TYPE_WHITELIST; // Loopback gehört auf die Whitelist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Systemeinträge dürfen nicht in die Blacklist
|
||||||
|
if ($m->is_system && $m->type === self::TYPE_BLACKLIST) {
|
||||||
|
throw new \InvalidArgumentException("Systemeinträge dürfen nicht auf die Blacklist.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Systemeinträge sind unveränderlich (bis auf interne Seeds/Maintenance – dann per DB direkt ändern)
|
||||||
|
static::updating(function (self $m) {
|
||||||
|
if ($m->getOriginal('is_system')) {
|
||||||
|
// Erlaube nur no-op Updates (z. B. Timestamps), aber blocke ip/type Änderungen
|
||||||
|
$blocked = $m->isDirty('ip') || $m->isDirty('type') || $m->isDirty('is_system');
|
||||||
|
if ($blocked) {
|
||||||
|
throw new \RuntimeException("Systemeinträge können nicht geändert werden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
static::deleting(function (self $m) {
|
||||||
|
if ($m->is_system) {
|
||||||
|
throw new \RuntimeException("Systemeintrag kann nicht gelöscht werden.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
Scopes
|
||||||
|
=========================== */
|
||||||
|
public function scopeWhitelist($q)
|
||||||
|
{
|
||||||
|
return $q->where('type', self::TYPE_WHITELIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeBlacklist($q)
|
||||||
|
{
|
||||||
|
return $q->where('type', self::TYPE_BLACKLIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Für UI: blende Systemeinträge aus
|
||||||
|
public function scopeVisible($q)
|
||||||
|
{
|
||||||
|
return $q->where('is_system', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kombiniert: z. B. Fail2banIpList::visible()->whitelist()->get();
|
||||||
|
public function scopeVisibleWhitelist($q)
|
||||||
|
{
|
||||||
|
return $q->visible()->whitelist();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeVisibleBlacklist($q)
|
||||||
|
{
|
||||||
|
return $q->visible()->blacklist();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
Helper-Listen
|
||||||
|
=========================== */
|
||||||
|
// Für UI-Listen (ohne System)
|
||||||
|
public static function whitelistArray(): array
|
||||||
|
{
|
||||||
|
return static::where('type', self::TYPE_WHITELIST)
|
||||||
|
->where('is_system', false)
|
||||||
|
->pluck('ip')->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function blacklistArray(): array
|
||||||
|
{
|
||||||
|
return static::where('type', self::TYPE_BLACKLIST)
|
||||||
|
->where('is_system', false)
|
||||||
|
->pluck('ip')->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Für das Schreiben der Fail2ban-Whitelist-Datei (inkl. System!)
|
||||||
|
public static function allWhitelistForConfig(): array
|
||||||
|
{
|
||||||
|
return static::where('type', self::TYPE_WHITELIST)
|
||||||
|
->pluck('ip')->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
Validierung
|
||||||
|
=========================== */
|
||||||
|
// Erlaubt IPv4/IPv6, optional mit CIDR (/0..32 bzw. /0..128)
|
||||||
|
public static function isValidIpOrCidr(string $value): bool
|
||||||
|
{
|
||||||
|
$value = trim($value);
|
||||||
|
|
||||||
|
// IP ohne CIDR
|
||||||
|
if (filter_var($value, FILTER_VALIDATE_IP)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP/CIDR
|
||||||
|
if (strpos($value, '/') !== false) {
|
||||||
|
[$ip, $prefix] = explode('/', $value, 2);
|
||||||
|
if (!ctype_digit($prefix)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$prefix = (int)$prefix;
|
||||||
|
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
return $prefix >= 0 && $prefix <= 32;
|
||||||
|
}
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
return $prefix >= 0 && $prefix <= 128;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loopback-Erkennung (IPv4 127.0.0.0/8, IPv6 ::1/128)
|
||||||
|
public static function isLoopback(string $value): bool
|
||||||
|
{
|
||||||
|
$value = trim($value);
|
||||||
|
|
||||||
|
// Klartext-Fälle
|
||||||
|
if (in_array($value, ['127.0.0.1', '127.0.0.1/8', '::1', '::1/128'], true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv4 Loopback Bereich
|
||||||
|
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
return str_starts_with($value, '127.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv4-CIDR Loopback
|
||||||
|
if (strpos($value, '/') !== false) {
|
||||||
|
[$ip, $prefix] = explode('/', $value, 2);
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && ctype_digit($prefix)) {
|
||||||
|
$prefix = (int)$prefix;
|
||||||
|
// Prüfe, ob Netz 127.0.0.0/8 überlappt
|
||||||
|
return self::cidrOverlaps($ip, $prefix, '127.0.0.0', 8);
|
||||||
|
}
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && ctype_digit($prefix)) {
|
||||||
|
$prefix = (int)$prefix;
|
||||||
|
// Prüfe, ob ::1/128 überlappt (nur exakt ::1)
|
||||||
|
return self::cidrOverlaps($ip, $prefix, '::1', 128);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv6 single
|
||||||
|
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
return $value === '::1';
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple Overlap-Check für IPv4/IPv6 Netze
|
||||||
|
private static function cidrOverlaps(string $ip, int $prefix, string $netIp, int $netPrefix): bool
|
||||||
|
{
|
||||||
|
$a = inet_pton($ip);
|
||||||
|
$b = inet_pton($netIp);
|
||||||
|
if ($a === false || $b === false || strlen($a) !== strlen($b)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$len = strlen($a);
|
||||||
|
$bytes = intdiv(max($prefix, $netPrefix), 8);
|
||||||
|
$bits = max($prefix, $netPrefix) % 8;
|
||||||
|
|
||||||
|
// Netzmaske anwenden (auf die längere Präfixlänge)
|
||||||
|
for ($i = 0; $i < $bytes; $i++) {
|
||||||
|
if ($a[$i] !== $b[$i]) return false;
|
||||||
|
}
|
||||||
|
if ($bits > 0) {
|
||||||
|
$mask = chr(0xFF << (8 - $bits));
|
||||||
|
if ((ord($a[$bytes]) & ord($mask)) !== (ord($b[$bytes]) & ord($mask))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
//namespace App\Models;
|
||||||
|
//
|
||||||
|
//use Illuminate\Database\Eloquent\Model;
|
||||||
|
//
|
||||||
|
//class Fail2banIpList extends Model
|
||||||
|
//{
|
||||||
|
// protected $fillable = [
|
||||||
|
// 'ip',
|
||||||
|
// 'type',
|
||||||
|
// ];
|
||||||
|
//
|
||||||
|
// protected $casts = [
|
||||||
|
// 'ip' => 'string',
|
||||||
|
// 'type' => 'string',
|
||||||
|
// ];
|
||||||
|
//
|
||||||
|
// const TYPE_WHITELIST = 'whitelist';
|
||||||
|
// const TYPE_BLACKLIST = 'blacklist';
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * Scopes
|
||||||
|
// */
|
||||||
|
// public function scopeWhitelist($query)
|
||||||
|
// {
|
||||||
|
// return $query->where('type', self::TYPE_WHITELIST);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public function scopeBlacklist($query)
|
||||||
|
// {
|
||||||
|
// return $query->where('type', self::TYPE_BLACKLIST);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * Validiert grob die IP.
|
||||||
|
// */
|
||||||
|
// public function isValidIp(): bool
|
||||||
|
// {
|
||||||
|
// return filter_var($this->ip, FILTER_VALIDATE_IP) !== false;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * Gibt Liste aller Whitelist-IPs als Array zurück.
|
||||||
|
// */
|
||||||
|
// public static function whitelistArray(): array
|
||||||
|
// {
|
||||||
|
// return static::where('type', self::TYPE_WHITELIST)->pluck('ip')->all();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * Gibt Liste aller Blacklist-IPs als Array zurück.
|
||||||
|
// */
|
||||||
|
// public static function blacklistArray(): array
|
||||||
|
// {
|
||||||
|
// return static::where('type', self::TYPE_BLACKLIST)->pluck('ip')->all();
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Fail2banSetting extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'fail2ban_settings';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'bantime','max_bantime','bantime_increment','bantime_factor',
|
||||||
|
'max_retry','findtime','cidr_v4','cidr_v6','external_mode',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'bantime' => 'integer',
|
||||||
|
'max_bantime' => 'integer',
|
||||||
|
'bantime_increment' => 'boolean',
|
||||||
|
'bantime_factor' => 'float',
|
||||||
|
'max_retry' => 'integer',
|
||||||
|
'findtime' => 'integer',
|
||||||
|
'cidr_v4' => 'integer',
|
||||||
|
'cidr_v6' => 'integer',
|
||||||
|
'external_mode' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Gibt die erste Konfiguration oder Default-Werte zurück.
|
||||||
|
// */
|
||||||
|
// public static function current(): self
|
||||||
|
// {
|
||||||
|
// return static::first() ?? new static([
|
||||||
|
// 'bantime' => 3600,
|
||||||
|
// 'max_bantime' => 43200,
|
||||||
|
// 'bantime_increment' => true,
|
||||||
|
// 'bantime_factor' => 1.5,
|
||||||
|
// 'max_retry' => 5,
|
||||||
|
// 'findtime' => 600,
|
||||||
|
// 'cidr_v4' => 32,
|
||||||
|
// 'cidr_v6' => 128,
|
||||||
|
// 'external_mode' => false,
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konvertiert bool zu "true"/"false" (für Config-Dateien).
|
||||||
|
*/
|
||||||
|
public function boolToString(bool $val): string
|
||||||
|
{
|
||||||
|
return $val ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,30 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'domain' => [
|
||||||
|
'base' => env('BASE_DOMAIN'),
|
||||||
|
'mail' => env('MTA_SUB'),
|
||||||
|
'ui' => env('UI_SUB'),
|
||||||
|
'webmail' => env('WEBMAIL_SUB'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'language' => [
|
||||||
|
|
||||||
|
'de' => [
|
||||||
|
'label' => 'Deutsch',
|
||||||
|
'locale' => 'de',
|
||||||
|
'fallback_locale' => 'de',
|
||||||
|
'flag' => 'de',
|
||||||
|
],
|
||||||
|
|
||||||
|
'en' => [
|
||||||
|
'label' => 'English',
|
||||||
|
'locale' => 'en',
|
||||||
|
'fallback_locale' => 'en',
|
||||||
|
'flag' => 'gb',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
'units' => [
|
'units' => [
|
||||||
['name' => 'nginx', 'action' => 'reload'],
|
['name' => 'nginx', 'action' => 'reload'],
|
||||||
['name' => 'postfix', 'action' => 'try-reload-or-restart'],
|
['name' => 'postfix', 'action' => 'try-reload-or-restart'],
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ return [
|
||||||
'label' => 'Mail', 'icon' => 'ph-envelope', 'items' => [
|
'label' => 'Mail', 'icon' => 'ph-envelope', 'items' => [
|
||||||
['label' => 'Postfächer', 'route' => 'ui.mail.mailboxes.index'],
|
['label' => 'Postfächer', 'route' => 'ui.mail.mailboxes.index'],
|
||||||
['label' => 'Aliasse', 'route' => 'ui.mail.aliases.index'],
|
['label' => 'Aliasse', 'route' => 'ui.mail.aliases.index'],
|
||||||
['label' => 'Gruppen', 'route' => 'ui.mail.groups.index'],
|
// ['label' => 'Gruppen', 'route' => 'ui.mail.groups.index'],
|
||||||
['label' => 'Filter', 'route' => 'ui.mail.filters.index'],
|
// ['label' => 'Filter', 'route' => 'ui.mail.filters.index'],
|
||||||
['label' => 'Quarantäne', 'route' => 'ui.mail.quarantine.index'],
|
['label' => 'Quarantäne', 'route' => 'ui.mail.quarantine.index'],
|
||||||
['label' => 'Queues', 'route' => 'ui.mail.queues.index'],
|
['label' => 'Queues', 'route' => 'ui.mail.queues.index'],
|
||||||
],
|
],
|
||||||
|
|
@ -19,45 +19,59 @@ return [
|
||||||
[
|
[
|
||||||
'label' => 'Domains', 'icon' => 'ph-globe', 'items' => [
|
'label' => 'Domains', 'icon' => 'ph-globe', 'items' => [
|
||||||
['label' => 'Übersicht', 'route' => 'ui.domains.index'],
|
['label' => 'Übersicht', 'route' => 'ui.domains.index'],
|
||||||
['label' => 'DNS-Assistent', 'route' => 'ui.domains.dns'],
|
// ['label' => 'DNS-Assistent', 'route' => 'ui.domains.dns'],
|
||||||
['label' => 'Zertifikate', 'route' => 'ui.domains.certificates'],
|
// ['label' => 'Zertifikate', 'route' => 'ui.domains.certificates'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => 'Webmail', 'icon' => 'ph-browser', 'items' => [
|
'label' => 'Webmail', 'icon' => 'ph-browser', 'items' => [
|
||||||
['label' => 'Allgemein', 'route' => 'ui.webmail.settings'],
|
['label' => 'Allgemein', 'route' => 'ui.logout'],
|
||||||
['label' => 'Plugins', 'route' => 'ui.webmail.plugins'],
|
['label' => 'Plugins', 'route' => 'ui.logout'],
|
||||||
|
// ['label' => 'Allgemein', 'route' => 'ui.webmail.settings'],
|
||||||
|
// ['label' => 'Plugins', 'route' => 'ui.webmail.plugins'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => 'Benutzer', 'icon' => 'ph-users', 'items' => [
|
'label' => 'Benutzer', 'icon' => 'ph-users', 'items' => [
|
||||||
['label' => 'Benutzer', 'route' => 'ui.users.index'],
|
['label' => 'Benutzer', 'route' => 'ui.logout'],
|
||||||
['label' => 'Rollen & Rechte', 'route' => 'ui.users.roles'],
|
['label' => 'Rollen & Rechte', 'route' => 'ui.logout'],
|
||||||
['label' => 'Anmeldesicherheit', 'route' => 'ui.users.security'],
|
['label' => 'Anmeldesicherheit', 'route' => 'ui.logout'],
|
||||||
|
// ['label' => 'Benutzer', 'route' => 'ui.users.index'],
|
||||||
|
// ['label' => 'Rollen & Rechte', 'route' => 'ui.users.roles'],
|
||||||
|
// ['label' => 'Anmeldesicherheit', 'route' => 'ui.users.security'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => 'Sicherheit', 'icon' => 'ph-shield', 'items' => [
|
'label' => 'Sicherheit', 'icon' => 'ph-shield', 'items' => [
|
||||||
['label' => 'TLS & Ciphers', 'route' => 'ui.security.tls'],
|
['label' => 'TLS & Ciphers', 'route' => 'ui.security.tls'],
|
||||||
['label' => 'Ratelimits', 'route' => 'ui.security.abuse'],
|
['label' => 'Fail2Ban', 'route' => 'ui.security.fail2ban'],
|
||||||
['label' => 'Audit-Logs', 'route' => 'ui.security.audit'],
|
['label' => 'Audit-Logs', 'route' => 'ui.security.audit'],
|
||||||
|
['label' => 'Rspamd', 'route' => 'ui.security.rspamd'],
|
||||||
|
['label' => 'SSL', 'route' => 'ui.security.ssl'],
|
||||||
|
// ['label' => 'Ratelimits', 'route' => 'ui.security.audit'],
|
||||||
|
// ['label' => 'TLS & Ciphers', 'route' => 'ui.security.tls'],
|
||||||
|
// ['label' => 'Ratelimits', 'route' => 'ui.security.abuse'],
|
||||||
|
// ['label' => 'Audit-Logs', 'route' => 'ui.security.audit'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => 'System', 'icon' => 'ph-gear-six', 'items' => [
|
'label' => 'System', 'icon' => 'ph-gear-six', 'items' => [
|
||||||
['label' => 'Einstellungen', 'route' => 'ui.system.settings'],
|
['label' => 'Einstellungen', 'route' => 'ui.system.settings'],
|
||||||
['label' => 'Dienste & Status', 'route' => 'ui.system.services'],
|
// ['label' => 'Dienste & Status', 'route' => 'ui.system.services'],
|
||||||
['label' => 'Jobs & Queues', 'route' => 'ui.system.jobs'],
|
// ['label' => 'Jobs & Queues', 'route' => 'ui.system.jobs'],
|
||||||
['label' => 'Logs', 'route' => 'ui.system.logs'],
|
// ['label' => 'Logs', 'route' => 'ui.system.logs'],
|
||||||
['label' => 'Speicher', 'route' => 'ui.system.storage'],
|
// ['label' => 'Speicher', 'route' => 'ui.system.storage'],
|
||||||
['label' => 'Über', 'route' => 'ui.system.about'],
|
// ['label' => 'Über', 'route' => 'ui.system.about'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => 'Entwickler', 'icon' => 'ph-brackets-curly', 'items' => [
|
'label' => 'Entwickler', 'icon' => 'ph-brackets-curly', 'items' => [
|
||||||
['label' => 'API-Schlüssel', 'route' => 'ui.dev.tokens'],
|
['label' => 'API-Schlüssel', 'route' => 'ui.logout'],
|
||||||
['label' => 'Webhooks', 'route' => 'ui.dev.webhooks'],
|
['label' => 'Webhooks', 'route' => 'ui.logout'],
|
||||||
['label' => 'Sandbox', 'route' => 'ui.dev.sandbox'],
|
['label' => 'Sandbox', 'route' => 'ui.logout'],
|
||||||
|
// ['label' => 'API-Schlüssel', 'route' => 'ui.dev.tokens'],
|
||||||
|
// ['label' => 'Webhooks', 'route' => 'ui.dev.webhooks'],
|
||||||
|
// ['label' => 'Sandbox', 'route' => 'ui.dev.sandbox'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('fail2ban_settings', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->integer('bantime')->default(3600);
|
||||||
|
$table->integer('max_bantime')->default(43200);
|
||||||
|
$table->boolean('bantime_increment')->default(true);
|
||||||
|
$table->float('bantime_factor')->default(1.5);
|
||||||
|
$table->integer('max_retry')->default(3);
|
||||||
|
$table->integer('findtime')->default(600);
|
||||||
|
$table->integer('cidr_v4')->default(32);
|
||||||
|
$table->integer('cidr_v6')->default(128);
|
||||||
|
$table->boolean('external_mode')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('fail2ban_settings');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('fail2ban_ip_lists', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('ip');
|
||||||
|
$table->enum('type', ['whitelist', 'blacklist']);
|
||||||
|
$table->boolean('is_system')->default(false)->index();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['ip', 'type'], 'fail2ban_ip_lists_ip_type_unique');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('fail2ban_ip_lists');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use App\Models\Fail2banSetting;
|
||||||
|
use App\Models\Fail2banIpList;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class Fail2banSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$this->command->info('⚙️ Fail2ban Defaults werden initialisiert …');
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// 1) Standardwerte für Fail2ban Settings
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
$settings = Fail2banSetting::firstOrCreate([], [
|
||||||
|
'bantime' => 3600, // 1h
|
||||||
|
'max_bantime' => 43200, // 12h
|
||||||
|
'bantime_increment' => true,
|
||||||
|
'bantime_factor' => 1.5,
|
||||||
|
'max_retry' => 5,
|
||||||
|
'findtime' => 600, // 10m
|
||||||
|
'cidr_v4' => 32,
|
||||||
|
'cidr_v6' => 128,
|
||||||
|
'external_mode' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// 2) Standard-IPs für Whitelist
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
$defaultWhitelist = [
|
||||||
|
'127.0.0.1/8',
|
||||||
|
'::1',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($defaultWhitelist as $ip) {
|
||||||
|
Fail2banIpList::firstOrCreate([
|
||||||
|
'ip' => $ip,
|
||||||
|
'type' => Fail2banIpList::TYPE_WHITELIST,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// 3) Fail2ban Config-Dateien erzeugen
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
$this->writeDefaultsConfig($settings);
|
||||||
|
$this->writeWhitelistConfig();
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// 4) Fail2ban reload (optional, falls Dienst läuft)
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
$out = shell_exec('sudo -n fail2ban-client reload 2>&1') ?? '';
|
||||||
|
if (stripos($out, 'OK') === false && stripos($out, 'Reloaded') === false) {
|
||||||
|
Log::warning('Fail2ban reload output', ['out' => $out]);
|
||||||
|
$this->command->warn('⚠️ Fail2ban reload möglicherweise nicht erfolgreich.');
|
||||||
|
} else {
|
||||||
|
$this->command->info('✅ Fail2ban reload erfolgreich.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// interne Hilfsfunktionen
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
|
||||||
|
private function writeDefaultsConfig(Fail2banSetting $s): void
|
||||||
|
{
|
||||||
|
$content = <<<CONF
|
||||||
|
[DEFAULT]
|
||||||
|
bantime = {$s->bantime}
|
||||||
|
findtime = {$s->findtime}
|
||||||
|
maxretry = {$s->max_retry}
|
||||||
|
bantime.increment = {$this->boolToString($s->bantime_increment)}
|
||||||
|
bantime.factor = {$s->bantime_factor}
|
||||||
|
bantime.maxtime = {$s->max_bantime}
|
||||||
|
CONF;
|
||||||
|
|
||||||
|
$this->atomicWrite('/etc/fail2ban/jail.d/00-mailwolt-defaults.local', $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeWhitelistConfig(): void
|
||||||
|
{
|
||||||
|
$ips = Fail2banIpList::where('type', Fail2banIpList::TYPE_WHITELIST)
|
||||||
|
->pluck('ip')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$ignore = implode(' ', array_unique(array_filter($ips)));
|
||||||
|
$content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
||||||
|
|
||||||
|
$this->atomicWrite('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function atomicWrite(string $path, string $content): void
|
||||||
|
{
|
||||||
|
$tmp = $path . '.tmp';
|
||||||
|
file_put_contents($tmp, $content);
|
||||||
|
rename($tmp, $path);
|
||||||
|
@chown($path, 'root');
|
||||||
|
@chgrp($path, 'root');
|
||||||
|
@chmod($path, 0644);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function boolToString(bool $v): string
|
||||||
|
{
|
||||||
|
return $v ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
Livewire.on('reload-page', e => setTimeout(() => window.location.reload(), e.delay || 0));
|
Livewire.on('reload-page', e => setTimeout(() => window.location.reload(), e.delay || 0));
|
||||||
|
|
||||||
|
// document.addEventListener('livewire:error', e => {
|
||||||
|
// if (e.detail.status === 419) {
|
||||||
|
// console.warn('Session expired – refreshing...');
|
||||||
|
// window.location.reload();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
// // import { showToast } from '../ui/toast.js'
|
// // import { showToast } from '../ui/toast.js'
|
||||||
//
|
//
|
||||||
// // — Livewire-Hooks (global)
|
// // — Livewire-Hooks (global)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{{-- resources/views/auth/login.blade.php --}}
|
{{-- resources/views/auth/login.blade.php --}}
|
||||||
@extends('layouts.app')
|
@extends('layouts.blank')
|
||||||
|
|
||||||
@section('title', 'Login')
|
@section('title', 'Login')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
@extends('layouts.app')
|
@extends('layouts.blank')
|
||||||
|
|
||||||
@section('title', 'Konto erstellen')
|
@section('title', 'Konto erstellen')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,8 @@
|
||||||
$itemIcon = $item['icon'] ?? 'ph-dot-outline';
|
$itemIcon = $item['icon'] ?? 'ph-dot-outline';
|
||||||
@endphp
|
@endphp
|
||||||
<li>
|
<li>
|
||||||
{{-- <a href="{{ isset($item['route']) ? route($item['route']) : '#' }}"--}}
|
<a href="{{ isset($item['route']) ? route($item['route']) : '#' }}"
|
||||||
<a href="#"
|
{{-- <a href="#"--}}
|
||||||
class="sidebar-link group relative flex items-center gap-2 px-2 py-2 rounded-lg
|
class="sidebar-link group relative flex items-center gap-2 px-2 py-2 rounded-lg
|
||||||
border border-transparent transition-colors
|
border border-transparent transition-colors
|
||||||
hover:bg-gradient-to-t hover:from-white/10 hover:to-transparent
|
hover:bg-gradient-to-t hover:from-white/10 hover:to-transparent
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,20 @@
|
||||||
@vite(['resources/js/app.js'])
|
@vite(['resources/js/app.js'])
|
||||||
@livewireScripts
|
@livewireScripts
|
||||||
@livewire('wire-elements-modal')
|
@livewire('wire-elements-modal')
|
||||||
|
<script>
|
||||||
|
// Registriere den Hook, sobald Livewire bereit ist.
|
||||||
|
document.addEventListener('livewire:init', () => {
|
||||||
|
Livewire.hook('request', ({ fail }) => {
|
||||||
|
fail(({ status, preventDefault, json }) => {
|
||||||
|
if (status === 419) {
|
||||||
|
preventDefault();
|
||||||
|
const to = (json && json.redirect) ? json.redirect : '{{ route('login') }}';
|
||||||
|
window.location.replace(to);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{--<!DOCTYPE html>--}}
|
{{--<!DOCTYPE html>--}}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_','-',app()->getLocale()) }}" class="h-full">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<title>@yield('title', config('app.name'))</title>
|
||||||
|
<script>document.documentElement.setAttribute('data-ui', 'booting');</script>
|
||||||
|
@vite(['resources/css/app.css'])
|
||||||
|
@livewireStyles
|
||||||
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
<body class="text-slate-100 font-bai">
|
||||||
|
<div class="app-backdrop"></div>
|
||||||
|
<div id="main" class="main content group/side main-shell rail">
|
||||||
|
<div class="px-2.5 pb-2.5 pt-0.5">
|
||||||
|
<div class="mt-5">
|
||||||
|
@yield('content')
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@vite(['resources/js/app.js'])
|
||||||
|
@livewireScripts
|
||||||
|
@livewire('wire-elements-modal')
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -45,20 +45,12 @@
|
||||||
<div class="px-4 pb-3 space-y-2">
|
<div class="px-4 pb-3 space-y-2">
|
||||||
<pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 opacity-80 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>
|
<pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 opacity-80 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>
|
||||||
|
|
||||||
@if($checked && filled(trim($r['actual'] ?? '')) && ($r['state'] ?? '') !== 'ok')
|
@if($checked && ($r['state'] ?? 'neutral') !== 'ok' && !empty($r['display_actual']))
|
||||||
<div class="text-[11px] text-white/60 break-words">
|
<div class="text-[11px] text-white/60 break-words">
|
||||||
<span class="opacity-70">Ist:</span>
|
<span class="opacity-70">Ist:</span>
|
||||||
<span class="font-mono break-all">
|
<span class="font-mono break-words">{{ $r['display_actual'] }}</span>
|
||||||
{{ str_replace('"', '', preg_replace('/\s+/', ' ', trim($r['actual']))) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
{{-- @if($checked && !empty($r['actual']))--}}
|
|
||||||
{{-- <div class="text-[11px] text-white/60">--}}
|
|
||||||
{{-- <span class="opacity-70">Ist:</span>--}}
|
|
||||||
{{-- <span class="font-mono break-words">{{ $r['actual'] }}</span>--}}
|
|
||||||
{{-- </div>--}}
|
|
||||||
{{-- @endif--}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
@ -76,22 +68,36 @@
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
@foreach ($static as $r)
|
@foreach ($static as $r)
|
||||||
<div class="rounded-xl border {{ $r['boxClass'] ?? $stateColors['neutral'] }}">
|
<div class="rounded-xl border {{ $r['boxClass'] ?? $stateColors['neutral'] }}">
|
||||||
<div class="flex items-center justify-between px-4 py-2 text-[12px]">
|
{{-- <div class="flex items-center justify-between px-4 py-2 text-[12px]">--}}
|
||||||
<div class="flex items-center gap-2">
|
{{-- <div class="flex items-center gap-2">--}}
|
||||||
<span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">{{ $r['type'] }}</span>
|
{{-- <span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">{{ $r['type'] }}</span>--}}
|
||||||
<span class="text-slate-200">{{ $r['name'] }}</span>
|
{{-- <span class="text-slate-200">{{ $r['name'] }}</span>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- <div class="flex items-center gap-2 text-slate-300/70">--}}
|
||||||
|
{{-- <x-button.copy-btn :text="$r['value']" />--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
<div class="flex items-start justify-between gap-2 px-4 py-2 text-[12px]">
|
||||||
|
<div class="min-w-0 flex items-start gap-2">
|
||||||
|
<span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">
|
||||||
|
{{ $r['type'] }}
|
||||||
|
</span>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<span
|
||||||
|
class="text-[12px] leading-5 text-slate-200 break-all"
|
||||||
|
title="{{ $r['name'] }}">{{ $r['name'] }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 text-slate-300/70">
|
<div class="shrink-0">
|
||||||
<x-button.copy-btn :text="$r['value']" />
|
<x-button.copy-btn :text="$r['value']" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-4 pb-3 space-y-2">
|
<div class="px-4 pb-3 space-y-2">
|
||||||
<pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 opacity-80 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>
|
<pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 opacity-80 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>
|
||||||
@if($checked && filled(trim($r['actual'] ?? '')) && ($r['state'] ?? '') !== 'ok')
|
@if($checked && ($r['state'] ?? 'neutral') !== 'ok' && !empty($r['display_actual']))
|
||||||
<div class="text-[11px] text-white/60 break-words">
|
<div class="text-[11px] text-white/60 break-words">
|
||||||
<span class="opacity-70">Ist:</span>
|
<span class="opacity-70">Ist:</span>
|
||||||
<span class="font-mono break-all">{{ $r['actual'] }}</span>
|
<span class="font-mono break-words">{{ $r['display_actual'] }}</span>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -129,13 +135,10 @@
|
||||||
|
|
||||||
<div class="px-4 pb-3 space-y-2">
|
<div class="px-4 pb-3 space-y-2">
|
||||||
<pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 opacity-80 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>
|
<pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 opacity-80 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>
|
||||||
|
@if($checked && ($r['state'] ?? 'neutral') !== 'ok' && !empty($r['display_actual']))
|
||||||
@if($checked && filled(trim($r['actual'] ?? '')) && ($r['state'] ?? '') !== 'ok')
|
|
||||||
<div class="text-[11px] text-white/60 break-words">
|
<div class="text-[11px] text-white/60 break-words">
|
||||||
<span class="opacity-70">Ist:</span>
|
<span class="opacity-70">Ist:</span>
|
||||||
<span class="font-mono break-all">
|
<span class="font-mono break-words">{{ $r['display_actual'] }}</span>
|
||||||
{{ str_replace('"', '', preg_replace('/\s+/', ' ', trim($r['actual']))) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if(!empty($r['helpUrl']))
|
@if(!empty($r['helpUrl']))
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
<div wire:poll.60s="refresh" class="glass-card p-5 rounded-2xl border border-white/10 bg-white/5">
|
<div class="glass-card p-5 rounded-2xl border border-white/10 bg-white/5">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">
|
<div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">
|
||||||
<i class="ph ph-globe-stand text-white/70 text-[13px]"></i>
|
<i class="ph ph-globe-stand text-white/70 text-[13px]"></i>
|
||||||
<span class="text-[11px] uppercase text-white/70 tracking-wide">Mail-DNS Health</span>
|
<span class="text-[11px] uppercase text-white/70 tracking-wide">Mail-DNS Health</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-white/60">
|
<div class="flex items-center gap-3">
|
||||||
<span class="opacity-70">TLSA:</span>
|
<button wire:click="refresh"
|
||||||
<span class="{{ $tlsa ? 'text-emerald-300' : 'text-rose-300' }}">
|
class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">
|
||||||
{{ $tlsa ? 'ok' : 'fehlend' }}
|
<i class="ph ph-arrows-clockwise text-[13px]"></i>
|
||||||
</span>
|
Neu prüfen
|
||||||
<span class="opacity-50">({{ $mtaHost }})</span>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
@forelse($rows as $r)
|
@forelse($rows as $r)
|
||||||
<button type="button"
|
<button type="button"
|
||||||
wire:click="openDnsModal({{ $r['id'] }})"
|
wire:click="openDnsModal({{ $r['id'] }})"
|
||||||
class="w-full text-left py-3 flex items-center justify-between hover:bg-white/5/20 rounded-lg px-2 hover:bg-white/5">
|
class="w-full text-left py-3 flex items-center justify-between rounded-lg px-2 hover:bg-white/5 text-sm">
|
||||||
<div class="text-white/85">{{ $r['name'] }}</div>
|
<div class="text-white/85">{{ $r['name'] }}</div>
|
||||||
|
|
||||||
@if($r['ok'])
|
@if($r['ok'])
|
||||||
|
|
@ -25,14 +25,9 @@
|
||||||
OK
|
OK
|
||||||
</span>
|
</span>
|
||||||
@else
|
@else
|
||||||
<div class="flex items-center gap-2">
|
<span class="px-2 py-0.5 rounded-full border text-amber-200 border-amber-400/30 bg-amber-500/10 text-xs">
|
||||||
<span class="px-2 py-0.5 rounded-full border text-amber-200 border-amber-400/30 bg-amber-500/10 text-xs">
|
Fertig konfigurieren
|
||||||
Fertig konfigurieren
|
</span>
|
||||||
</span>
|
|
||||||
{{-- <span class="text-[11px] text-white/45">--}}
|
|
||||||
{{-- fehlt: {{ implode(', ', $r['missing']) }}--}}
|
|
||||||
{{-- </span>--}}
|
|
||||||
</div>
|
|
||||||
@endif
|
@endif
|
||||||
</button>
|
</button>
|
||||||
@empty
|
@empty
|
||||||
|
|
@ -41,6 +36,49 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{--<div wire:poll.60s="refresh" class="glass-card p-5 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||||
|
{{-- <div class="flex items-center justify-between mb-4">--}}
|
||||||
|
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||||
|
{{-- <i class="ph ph-globe-stand text-white/70 text-[13px]"></i>--}}
|
||||||
|
{{-- <span class="text-[11px] uppercase text-white/70 tracking-wide">Mail-DNS Health</span>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- <div class="text-xs text-white/60">--}}
|
||||||
|
{{-- <span class="opacity-70">TLSA:</span>--}}
|
||||||
|
{{-- <span class="{{ $tlsa ? 'text-emerald-300' : 'text-rose-300' }}">--}}
|
||||||
|
{{-- {{ $tlsa ? 'ok' : 'fehlend' }}--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- <span class="opacity-50">({{ $mtaHost }})</span>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- <div class="divide-y divide-white/5">--}}
|
||||||
|
{{-- @forelse($rows as $r)--}}
|
||||||
|
{{-- <button type="button"--}}
|
||||||
|
{{-- wire:click="openDnsModal({{ $r['id'] }})"--}}
|
||||||
|
{{-- class="w-full text-left py-3 flex items-center justify-between hover:bg-white/5/20 rounded-lg px-2 hover:bg-white/5">--}}
|
||||||
|
{{-- <div class="text-white/85">{{ $r['name'] }}</div>--}}
|
||||||
|
|
||||||
|
{{-- @if($r['ok'])--}}
|
||||||
|
{{-- <span class="px-2 py-0.5 rounded-full border text-emerald-300 border-emerald-400/30 bg-emerald-500/10 text-xs">--}}
|
||||||
|
{{-- OK--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- @else--}}
|
||||||
|
{{-- <div class="flex items-center gap-2">--}}
|
||||||
|
{{-- <span class="px-2 py-0.5 rounded-full border text-amber-200 border-amber-400/30 bg-amber-500/10 text-xs">--}}
|
||||||
|
{{-- Fertig konfigurieren--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- <span class="text-[11px] text-white/45">--}}
|
||||||
|
{{-- fehlt: {{ implode(', ', $r['missing']) }}--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
{{-- </button>--}}
|
||||||
|
{{-- @empty--}}
|
||||||
|
{{-- <div class="py-4 text-sm text-white/60">Keine Domains.</div>--}}
|
||||||
|
{{-- @endforelse--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{--</div>--}}
|
||||||
|
|
||||||
{{--<div wire:poll.60s="refresh" class="glass-card p-5 rounded-2xl border border-white/10 bg-white/5">--}}
|
{{--<div wire:poll.60s="refresh" class="glass-card p-5 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||||
{{-- <div class="flex items-center justify-between mb-4">--}}
|
{{-- <div class="flex items-center justify-between mb-4">--}}
|
||||||
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@
|
||||||
|
|
||||||
{{-- Row 1: Domain + Typ --}}
|
{{-- Row 1: Domain + Typ --}}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
|
||||||
{{-- DOMAIN (TailwindPlus Elements) --}}
|
{{-- DOMAIN (TailwindPlus Elements) --}}
|
||||||
<div class="relative" wire:ignore id="domain-select-{{ $this->getId() }}">
|
<div class="relative" wire:ignore id="domain-select-{{ $this->getId() }}">
|
||||||
<label class="block text-xs text-white/60 mb-1">Domain</label>
|
<label class="block text-xs text-white/60 mb-1">Domain</label>
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,374 @@
|
||||||
<i class="ph ph-shield-checkered text-white/70 text-[13px]"></i>
|
<i class="ph ph-shield-checkered text-white/70 text-[13px]"></i>
|
||||||
<span class="text-[11px] uppercase text-white/70">Fail2Ban</span>
|
<span class="text-[11px] uppercase text-white/70">Fail2Ban</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="px-2 py-0.5 rounded-full border text-xs
|
|
||||||
{{ $activeBans>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' }}">
|
@if($available)
|
||||||
{{ $activeBans }} aktuell
|
<span class="px-2 py-0.5 rounded-full border text-xs
|
||||||
</span>
|
{{ $activeBans>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' }}">
|
||||||
|
{{ $activeBans }} aktuell
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="px-2 py-0.5 rounded-full border text-xs text-rose-300 border-rose-400/30 bg-rose-500/10">
|
||||||
|
nicht installiert
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-white/70">Top IPs (letzte Logs):</div>
|
|
||||||
<ul class="mt-2 space-y-1 text-sm">
|
@if(!$available)
|
||||||
@forelse($topIps as $i)
|
<div class="text-sm text-white/60">fail2ban-client wurde nicht gefunden.</div>
|
||||||
<li class="flex justify-between">
|
|
||||||
<span class="text-white/80">{{ $i['ip'] }}</span>
|
@elseif($permDenied)
|
||||||
<span class="text-white/60">{{ $i['count'] }}</span>
|
<div class="text-sm text-amber-200">
|
||||||
</li>
|
Keine Berechtigung (sudo) auf <code class="font-mono">fail2ban-client</code>/<code class="font-mono">journalctl</code>/<code class="font-mono">zgrep</code>.
|
||||||
@empty
|
<span class="opacity-80">Sudo-Regel prüfen.</span>
|
||||||
<li class="text-white/50">–</li>
|
</div>
|
||||||
@endforelse
|
|
||||||
</ul>
|
@elseif($error)
|
||||||
|
<div class="text-sm text-amber-200">
|
||||||
|
Unerwartete Ausgabe von <code class="font-mono">fail2ban-client status</code>.
|
||||||
|
<span class="opacity-80">Details in <code>storage/logs/laravel.log</code>.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@else
|
||||||
|
<div class="space-y-2">
|
||||||
|
@forelse($jails as $j)
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/5 px-3 py-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="text-white/85 font-medium">{{ $j['name'] }}</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-[11px] text-white/60">
|
||||||
|
Bannzeit: {{ $j['bantime'] === -1 ? 'permanent' : ($j['bantime'].'s') }}
|
||||||
|
</span>
|
||||||
|
<span class="px-2 py-0.5 rounded-full border text-[11px]
|
||||||
|
{{ $j['banned']>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-white/60 border-white/20 bg-white/5' }}">
|
||||||
|
{{ $j['banned'] }} gebannt
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button wire:click.stop="openDetails('{{ $j['name'] }}')"
|
||||||
|
class="text-[11px] px-2 py-0.5 rounded border border-white/15 bg-white/5 hover:bg-white/10">
|
||||||
|
Details
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="text-sm text-white/60">Keine Jails gefunden.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex justify-end">
|
||||||
|
<button wire:click="refresh" wire:loading.attr="disabled"
|
||||||
|
class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">
|
||||||
|
<i class="ph ph-arrows-clockwise text-[13px]"></i>
|
||||||
|
<span wire:loading.remove>Neu prüfen</span>
|
||||||
|
<span wire:loading>prüfe…</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{--<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||||
|
{{-- <div class="flex items-center justify-between mb-3">--}}
|
||||||
|
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||||
|
{{-- <i class="ph ph-shield-checkered text-white/70 text-[13px]"></i>--}}
|
||||||
|
{{-- <span class="text-[11px] uppercase text-white/70">Fail2Ban</span>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- @if($available)--}}
|
||||||
|
{{-- <span class="px-2 py-0.5 rounded-full border text-xs--}}
|
||||||
|
{{-- {{ $activeBans>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' }}">--}}
|
||||||
|
{{-- {{ $activeBans }} aktuell--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- @else--}}
|
||||||
|
{{-- <span class="px-2 py-0.5 rounded-full border text-xs text-rose-300 border-rose-400/30 bg-rose-500/10">--}}
|
||||||
|
{{-- nicht installiert--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- @if(!$available)--}}
|
||||||
|
{{-- <div class="text-sm text-white/60">fail2ban-client wurde nicht gefunden.</div>--}}
|
||||||
|
{{-- @elseif($permDenied)--}}
|
||||||
|
{{-- <div class="text-sm text-amber-200">--}}
|
||||||
|
{{-- Keine Berechtigung auf <code class="font-mono">/var/run/fail2ban/fail2ban.sock</code>.--}}
|
||||||
|
{{-- <span class="opacity-80">Sudo-Regel prüfen.</span>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @else--}}
|
||||||
|
{{-- <div class="space-y-2">--}}
|
||||||
|
{{-- @forelse($jails as $j)--}}
|
||||||
|
{{-- <div class="rounded-xl border border-white/10 bg-white/5 px-3 py-2">--}}
|
||||||
|
{{-- <div class="flex items-center justify-between">--}}
|
||||||
|
{{-- <div class="text-white/85 font-medium">{{ $j['name'] }}</div>--}}
|
||||||
|
{{-- <div class="flex items-center gap-2">--}}
|
||||||
|
{{-- <span class="text-[11px] text-white/60">--}}
|
||||||
|
{{-- Bannzeit:--}}
|
||||||
|
{{-- @if($j['bantime'] === -1)--}}
|
||||||
|
{{-- permanent--}}
|
||||||
|
{{-- @else--}}
|
||||||
|
{{-- {{ $j['bantime'] }}s--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- <span class="px-2 py-0.5 rounded-full border text-[11px]--}}
|
||||||
|
{{-- {{ $j['banned']>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-white/60 border-white/20 bg-white/5' }}">--}}
|
||||||
|
{{-- {{ $j['banned'] }} gebannt--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
|
||||||
|
{{-- --}}{{-- fix: stop event bubbling --}}
|
||||||
|
{{-- <button wire:click.stop="openDetails('{{ $j['name'] }}')"--}}
|
||||||
|
{{-- class="text-[11px] px-2 py-0.5 rounded border border-white/15 bg-white/5 hover:bg-white/10">--}}
|
||||||
|
{{-- Details--}}
|
||||||
|
{{-- </button>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @empty--}}
|
||||||
|
{{-- <div class="text-sm text-white/60">Keine Jails gefunden.</div>--}}
|
||||||
|
{{-- @endforelse--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- <div class="mt-4 flex justify-end">--}}
|
||||||
|
{{-- <button wire:click="refresh" wire:loading.attr="disabled"--}}
|
||||||
|
{{-- class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">--}}
|
||||||
|
{{-- <i class="ph ph-arrows-clockwise text-[13px]"></i>--}}
|
||||||
|
{{-- <span wire:loading.remove>Neu prüfen</span>--}}
|
||||||
|
{{-- <span wire:loading>prüfe…</span>--}}
|
||||||
|
{{-- </button>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
{{--</div>--}}
|
||||||
|
|
||||||
|
{{--<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||||
|
{{-- <div class="flex items-center justify-between mb-3">--}}
|
||||||
|
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||||
|
{{-- <i class="ph ph-shield-checkered text-white/70 text-[13px]"></i>--}}
|
||||||
|
{{-- <span class="text-[11px] uppercase text-white/70">Fail2Ban</span>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- @if($available)--}}
|
||||||
|
{{-- <span class="px-2 py-0.5 rounded-full border text-xs--}}
|
||||||
|
{{-- {{ $activeBans>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' }}">--}}
|
||||||
|
{{-- {{ $activeBans }} aktuell--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- @else--}}
|
||||||
|
{{-- <span class="px-2 py-0.5 rounded-full border text-xs text-rose-300 border-rose-400/30 bg-rose-500/10">--}}
|
||||||
|
{{-- nicht installiert--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- @if(!$available)--}}
|
||||||
|
{{-- <div class="text-sm text-white/60">fail2ban-client wurde nicht gefunden.</div>--}}
|
||||||
|
{{-- @elseif($permDenied)--}}
|
||||||
|
{{-- <div class="text-sm text-amber-200">--}}
|
||||||
|
{{-- Keine Berechtigung auf <code class="font-mono">/var/run/fail2ban/fail2ban.sock</code>.--}}
|
||||||
|
{{-- <span class="opacity-80">Sudo-Regel prüfen.</span>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @else--}}
|
||||||
|
{{-- <div class="space-y-2">--}}
|
||||||
|
{{-- @forelse($jails as $j)--}}
|
||||||
|
{{-- <div class="rounded-xl border border-white/10 bg-white/5 px-3 py-2">--}}
|
||||||
|
{{-- <div class="flex items-center justify-between">--}}
|
||||||
|
{{-- <div class="text-white/85 font-medium">{{ $j['name'] }}</div>--}}
|
||||||
|
|
||||||
|
{{-- <div class="flex items-center gap-2">--}}
|
||||||
|
{{-- <span class="px-2 py-0.5 rounded-full border text-[11px]--}}
|
||||||
|
{{-- {{ $j['banned']>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-white/60 border-white/20 bg-white/5' }}">--}}
|
||||||
|
{{-- {{ $j['banned'] }} gebannt--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- --}}{{-- Optional: Details öffnen (Tab/Modal) --}}
|
||||||
|
{{-- <button wire:click="openDetails('{{ $j['name'] }}')"--}}
|
||||||
|
{{-- class="text-[11px] px-2 py-0.5 rounded border border-white/15 bg-white/5 hover:bg-white/10">--}}
|
||||||
|
{{-- Details--}}
|
||||||
|
{{-- </button>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @empty--}}
|
||||||
|
{{-- <div class="text-sm text-white/60">Keine Jails gefunden.</div>--}}
|
||||||
|
{{-- @endforelse--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- <div class="mt-4 flex justify-end">--}}
|
||||||
|
{{-- <button wire:click="refresh" wire:loading.attr="disabled"--}}
|
||||||
|
{{-- class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">--}}
|
||||||
|
{{-- <i class="ph ph-arrows-clockwise text-[13px]"></i>--}}
|
||||||
|
{{-- <span wire:loading.remove>Neu prüfen</span>--}}
|
||||||
|
{{-- <span wire:loading>prüfe…</span>--}}
|
||||||
|
{{-- </button>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
{{--</div>--}}
|
||||||
|
|
||||||
|
{{--<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||||
|
{{-- <div class="flex items-center justify-between mb-3">--}}
|
||||||
|
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||||
|
{{-- <i class="ph ph-shield-checkered text-white/70 text-[13px]"></i>--}}
|
||||||
|
{{-- <span class="text-[11px] uppercase text-white/70">Fail2Ban</span>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- @if($available)--}}
|
||||||
|
{{-- <span class="px-2 py-0.5 rounded-full border text-xs--}}
|
||||||
|
{{-- {{ $activeBans>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' }}">--}}
|
||||||
|
{{-- {{ $activeBans }} aktuell--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- @else--}}
|
||||||
|
{{-- <span class="px-2 py-0.5 rounded-full border text-xs text-rose-300 border-rose-400/30 bg-rose-500/10">--}}
|
||||||
|
{{-- nicht installiert--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- @if(!$available)--}}
|
||||||
|
{{-- <div class="text-sm text-white/60">fail2ban-client wurde nicht gefunden.</div>--}}
|
||||||
|
{{-- @else--}}
|
||||||
|
{{-- --}}{{-- Jails --}}
|
||||||
|
{{-- <div class="space-y-2">--}}
|
||||||
|
{{-- @forelse($jails as $j)--}}
|
||||||
|
{{-- <div class="rounded-xl border border-white/10 bg-white/5 px-3 py-2">--}}
|
||||||
|
{{-- <div class="flex items-center justify-between">--}}
|
||||||
|
{{-- <div class="text-white/85 font-medium">{{ $j['name'] }}</div>--}}
|
||||||
|
{{-- <div class="flex items-center gap-2">--}}
|
||||||
|
{{-- <span class="text-[11px] text-white/50">--}}
|
||||||
|
{{-- Bannzeit:--}}
|
||||||
|
{{-- @if($j['bantime'] === -1) permanent--}}
|
||||||
|
{{-- @else {{ $j['bantime'] }}s--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- <span class="px-2 py-0.5 rounded-full border text-[11px]--}}
|
||||||
|
{{-- {{ $j['banned']>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-white/60 border-white/20 bg-white/5' }}">--}}
|
||||||
|
{{-- {{ $j['banned'] }} gebannt--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- @if(!empty($j['ips']))--}}
|
||||||
|
{{-- <div class="mt-2 grid gap-1">--}}
|
||||||
|
{{-- @foreach($j['ips'] as $ip)--}}
|
||||||
|
{{-- <div class="flex items-center gap-2 text-[12px] text-white/80 font-mono">--}}
|
||||||
|
{{-- <span>{{ $ip['ip'] }}</span>--}}
|
||||||
|
{{-- @if($ip['remaining'] === -1)--}}
|
||||||
|
{{-- <span class="px-1.5 py-0.5 rounded border border-rose-400/30 bg-rose-500/10 text-rose-200">--}}
|
||||||
|
{{-- permanent--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- @elseif(is_int($ip['remaining']) && $ip['remaining'] > 0)--}}
|
||||||
|
{{-- <span class="px-1.5 py-0.5 rounded border border-amber-400/30 bg-amber-500/10 text-amber-100">--}}
|
||||||
|
{{-- {{ gmdate('H\h i\m s\s', $ip['remaining']) }}--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @endforeach--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @empty--}}
|
||||||
|
{{-- <div class="text-sm text-white/60">Keine Jails gefunden.</div>--}}
|
||||||
|
{{-- @endforelse--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- --}}{{-- Top IPs aus Ban-Events --}}
|
||||||
|
{{-- <div class="mt-4">--}}
|
||||||
|
{{-- <div class="text-sm text-white/70">Top IPs (letzte Fail2Ban-Logs):</div>--}}
|
||||||
|
{{-- <ul class="mt-2 space-y-1 text-sm">--}}
|
||||||
|
{{-- @forelse($topIps as $i)--}}
|
||||||
|
{{-- <li class="flex justify-between">--}}
|
||||||
|
{{-- <span class="text-white/80 font-mono">{{ $i['ip'] }}</span>--}}
|
||||||
|
{{-- <span class="text-white/60">{{ $i['count'] }}</span>--}}
|
||||||
|
{{-- </li>--}}
|
||||||
|
{{-- @empty--}}
|
||||||
|
{{-- <li class="text-white/50">–</li>--}}
|
||||||
|
{{-- @endforelse--}}
|
||||||
|
{{-- </ul>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- <div class="mt-4 flex justify-end">--}}
|
||||||
|
{{-- <button wire:click="refresh" wire:loading.attr="disabled"--}}
|
||||||
|
{{-- class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">--}}
|
||||||
|
{{-- <i class="ph ph-arrows-clockwise text-[13px]"></i>--}}
|
||||||
|
{{-- <span wire:loading.remove>Neu prüfen</span>--}}
|
||||||
|
{{-- <span wire:loading>prüfe…</span>--}}
|
||||||
|
{{-- </button>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
{{--</div>--}}
|
||||||
|
|
||||||
|
{{--<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||||
|
{{-- <div class="flex items-center justify-between mb-3">--}}
|
||||||
|
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||||
|
{{-- <i class="ph ph-shield-checkered text-white/70 text-[13px]"></i>--}}
|
||||||
|
{{-- <span class="text-[11px] uppercase text-white/70">Fail2Ban</span>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- @if($available)--}}
|
||||||
|
{{-- <span class="px-2 py-0.5 rounded-full border text-xs--}}
|
||||||
|
{{-- {{ $activeBans>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' }}">--}}
|
||||||
|
{{-- {{ $activeBans }} aktuell--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- @else--}}
|
||||||
|
{{-- <span class="px-2 py-0.5 rounded-full border text-xs text-rose-300 border-rose-400/30 bg-rose-500/10">--}}
|
||||||
|
{{-- nicht installiert--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- @if(!$available)--}}
|
||||||
|
{{-- <div class="text-sm text-white/60">fail2ban-client wurde nicht gefunden.</div>--}}
|
||||||
|
{{-- @else--}}
|
||||||
|
{{-- --}}{{-- Jails --}}
|
||||||
|
{{-- <div class="space-y-2">--}}
|
||||||
|
{{-- @forelse($jails as $j)--}}
|
||||||
|
{{-- <div class="rounded-xl border border-white/10 bg-white/5 px-3 py-2">--}}
|
||||||
|
{{-- <div class="flex items-center justify-between">--}}
|
||||||
|
{{-- <div class="text-white/85 font-medium">{{ $j['name'] }}</div>--}}
|
||||||
|
{{-- <span class="px-2 py-0.5 rounded-full border text-[11px]--}}
|
||||||
|
{{-- {{ $j['banned']>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-white/60 border-white/20 bg-white/5' }}">--}}
|
||||||
|
{{-- {{ $j['banned'] }} gebannt--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @if(!empty($j['ips']))--}}
|
||||||
|
{{-- <div class="mt-1 text-[12px] text-white/65 font-mono break-words">--}}
|
||||||
|
{{-- {{ implode(', ', $j['ips']) }}--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @empty--}}
|
||||||
|
{{-- <div class="text-sm text-white/60">Keine Jails gefunden.</div>--}}
|
||||||
|
{{-- @endforelse--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- --}}{{-- Top IPs aus Ban-Events --}}
|
||||||
|
{{-- <div class="mt-3">--}}
|
||||||
|
{{-- <div class="text-sm text-white/70">Top IPs (letzte Fail2Ban-Logs):</div>--}}
|
||||||
|
{{-- <ul class="mt-2 space-y-1 text-sm">--}}
|
||||||
|
{{-- @foreach($j['ips'] as $ip)--}}
|
||||||
|
{{-- <div class="flex items-center gap-2 text-[12px] text-white/80 font-mono">--}}
|
||||||
|
{{-- <span>{{ $ip['ip'] }}</span>--}}
|
||||||
|
{{-- @if($ip['remaining'] === -1)--}}
|
||||||
|
{{-- <span class="px-1.5 py-0.5 rounded border border-rose-400/30 bg-rose-500/10 text-rose-200">permanent</span>--}}
|
||||||
|
{{-- @elseif(is_int($ip['remaining']) && $ip['remaining'] > 0)--}}
|
||||||
|
{{-- <span class="px-1.5 py-0.5 rounded border border-amber-400/30 bg-amber-500/10 text-amber-100">--}}
|
||||||
|
{{-- {{ gmdate('H\h i\m s\s', $ip['remaining']) }}--}}
|
||||||
|
{{-- </span>--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @endforeach--}}
|
||||||
|
{{-- @forelse($topIps as $i)--}}
|
||||||
|
{{-- <li class="flex justify-between">--}}
|
||||||
|
{{-- <span class="text-white/80 font-mono">{{ $i['ip'] }}</span>--}}
|
||||||
|
{{-- <span class="text-white/60">{{ $i['count'] }}</span>--}}
|
||||||
|
{{-- </li>--}}
|
||||||
|
{{-- @empty--}}
|
||||||
|
{{-- <li class="text-white/50">–</li>--}}
|
||||||
|
{{-- @endforelse--}}
|
||||||
|
{{-- </ul>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- <div class="mt-4 flex justify-end">--}}
|
||||||
|
{{-- <button wire:click="refresh" wire:loading.attr="disabled"--}}
|
||||||
|
{{-- class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">--}}
|
||||||
|
{{-- <i class="ph ph-arrows-clockwise text-[13px]"></i>--}}
|
||||||
|
{{-- <span wire:loading.remove>Neu prüfen</span>--}}
|
||||||
|
{{-- <span wire:loading>prüfe…</span>--}}
|
||||||
|
{{-- </button>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
{{--</div>--}}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-white/90">Aktuell gebannte IPs</h3>
|
||||||
|
<button wire:click="refreshList"
|
||||||
|
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($rows))
|
||||||
|
<div class="text-white/50 text-sm">Keine aktiven Banns vorhanden.</div>
|
||||||
|
@else
|
||||||
|
<div class="space-y-3">
|
||||||
|
@foreach ($rows as $r)
|
||||||
|
<div class="flex items-center justify-between rounded-2xl border px-4 py-2.5 {{ $r['box'] }}">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
{{-- Statuspunkt: rot=permanent, gelb=temporär --}}
|
||||||
|
<span class="inline-block w-2.5 h-2.5 rounded-full {{ $r['dot'] }}"></span>
|
||||||
|
|
||||||
|
{{-- IP klein + monospace, ohne Jail-Text --}}
|
||||||
|
<span class="font-mono text-[13px] md:text-[14px] text-white/85 tracking-normal">
|
||||||
|
{{ $r['ip'] }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
wire:click="unban('{{ $r['ip'] }}','{{ $r['jail'] }}')"
|
||||||
|
class="text-[12px] px-3 py-1.5 rounded-xl border {{ $r['btn'] }}">
|
||||||
|
Entbannen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
<div class="grid grid-cols-1 xl:grid-cols-3 gap-5">
|
||||||
|
{{-- LEFT 2/3 --}}
|
||||||
|
<div class="xl:col-span-2 space-y-5">
|
||||||
|
<div class="glass-card p-5">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
|
||||||
|
<i class="ph ph-shield text-white/70 text-[13px]"></i>
|
||||||
|
<span class="text-[11px] uppercase tracking-wide text-white/70">Fail2Ban Konfiguration</span>
|
||||||
|
</div>
|
||||||
|
<button wire:click="save"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-white/60 text-sm mb-1">Bantime (Sekunden)</label>
|
||||||
|
<input type="number" wire:model.defer="bantime"
|
||||||
|
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||||
|
<p class="mt-1 text-xs text-white/45">Standard-Sperrzeit.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-white/60 text-sm mb-1">Max. Bantime (Sekunden)</label>
|
||||||
|
<input type="number" wire:model.defer="max_bantime"
|
||||||
|
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||||
|
<p class="mt-1 text-xs text-white/45">Obergrenze bei dynamischer Erhöhung.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-white/60 text-sm mb-1">Findtime (Sekunden)</label>
|
||||||
|
<input type="number" wire:model.defer="findtime"
|
||||||
|
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||||
|
<p class="mt-1 text-xs text-white/45">Zeitraum für Wiederholungen.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-white/60 text-sm mb-1">Max. Retry</label>
|
||||||
|
<input type="number" wire:model.defer="max_retry"
|
||||||
|
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||||
|
<p class="mt-1 text-xs text-white/45">Fehlversuche bis Bann.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="block text-white/60 text-sm mb-1">Erhöhungs-Faktor</label>
|
||||||
|
<input type="number" step="0.1" wire:model.defer="bantime_factor"
|
||||||
|
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||||
|
<p class="mt-1 text-xs text-white/45">Multiplikator (z. B. 1.5).</p>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
<span class="text-[11px] uppercase tracking-wide text-white/70">Hinweise</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="list-disc list-inside text-sm text-white/60 space-y-1">
|
||||||
|
<li><strong>bantime.increment</strong> = true bedeutet, dass sich die Sperrzeit bei wiederholten
|
||||||
|
Angriffen erhöht (z. B. 1 h → 1.5 h → 2.25 h …).
|
||||||
|
</li>
|
||||||
|
<li>Die SQLite-Datenbank befindet sich unter <code>/var/lib/fail2ban/fail2ban.sqlite3</code>.</li>
|
||||||
|
<li>Alle Änderungen hier werden nach Klick auf <em>„Speichern & Reload“</em> sofort aktiv.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- RIGHT 1/3 --}}
|
||||||
|
<div class="space-y-5">
|
||||||
|
<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-3">
|
||||||
|
<i class="ph ph-list text-white/70 text-[13px]"></i>
|
||||||
|
<span class="text-[11px] uppercase tracking-wide text-white/70">Whitelist</span>
|
||||||
|
</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">
|
||||||
|
<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 }}'}})">
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="text-sm text-white/50">Keine Einträge.</div>
|
||||||
|
@endforelse
|
||||||
|
|
||||||
|
<button class="primary-btn w-full justify-center mt-2"
|
||||||
|
wire:click="$dispatch('openModal',{component:'ui.security.modal.fail2ban-ip-modal',arguments:{type:'whitelist'}})">
|
||||||
|
IP hinzufügen
|
||||||
|
</button>
|
||||||
|
</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">
|
||||||
|
<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">
|
||||||
|
<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 }}'}})">
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<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'}})">
|
||||||
|
IP hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
@push('modal.header')
|
||||||
|
<div class="px-5 pt-5 pb-3 border-b border-white/10 backdrop-blur rounded-t-2xl">
|
||||||
|
<h2 class="text-[18px] font-semibold text-slate-100">Fail2Ban – {{ $jail }}</h2>
|
||||||
|
<p class="text-[13px] text-slate-300/80">Aktuell gebannte IPs und Restlaufzeiten.</p>
|
||||||
|
</div>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
<div class="p-5 space-y-3">
|
||||||
|
@forelse($rows as $r)
|
||||||
|
<div class="rounded-xl border px-4 py-3 {{ $r['box_class'] }}">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="text-white/90 font-mono text-[14px]">{{ $r['ip'] }}</div>
|
||||||
|
<div class="text-[12px] text-white/80">{{ $r['time_text'] }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-[12px] text-white/55">{{ $r['meta_text'] }}</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white/70">
|
||||||
|
Keine gebannten IPs in diesem Jail.
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@push('modal.footer')
|
||||||
|
<div class="px-5 py-3 border-t border-white/10 backdrop-blur rounded-b-2xl">
|
||||||
|
<div class="flex items-center gap-2 justify-end">
|
||||||
|
{{-- WICHTIG: refresht NUR das Modal --}}
|
||||||
|
<button wire:click="$dispatch('f2b:refresh-banlist')" wire:loading.attr="disabled"
|
||||||
|
class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">
|
||||||
|
<i class="ph ph-arrows-clockwise text-[13px]"></i>
|
||||||
|
<span wire:loading.remove>Neu prüfen</span>
|
||||||
|
<span wire:loading>prüfe…</span>
|
||||||
|
</button>
|
||||||
|
<button wire:click="$dispatch('closeModal')"
|
||||||
|
class="px-3 py-1.5 rounded-lg text-sm bg-emerald-500/20 text-emerald-300 border border-emerald-400/30 hover:bg-emerald-500/30">
|
||||||
|
Fertig
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
{{--@push('modal.header')--}}
|
||||||
|
{{-- <div class="px-5 pt-5 pb-3 border-b border-white/10 backdrop-blur rounded-t-2xl">--}}
|
||||||
|
{{-- <h2 class="text-[18px] font-semibold text-slate-100">--}}
|
||||||
|
{{-- Fail2Ban – {{ $jail }}--}}
|
||||||
|
{{-- </h2>--}}
|
||||||
|
{{-- <p class="text-[13px] text-slate-300/80">--}}
|
||||||
|
{{-- Aktuell gebannte IPs und Restlaufzeiten.--}}
|
||||||
|
{{-- </p>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{--@endpush--}}
|
||||||
|
|
||||||
|
{{--<div class="p-5 space-y-3">--}}
|
||||||
|
{{-- @forelse($rows as $r)--}}
|
||||||
|
{{-- <div class="rounded-xl border px-4 py-3 {{ $r['box_class'] }}">--}}
|
||||||
|
{{-- <div class="flex items-center justify-between">--}}
|
||||||
|
{{-- <div class="text-white/90 font-mono text-[14px]">{{ $r['ip'] }}</div>--}}
|
||||||
|
{{-- <div class="text-[12px] text-white/80">--}}
|
||||||
|
{{-- {{ $r['time_text'] }}--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- <div class="mt-1 text-[12px] text-white/55">--}}
|
||||||
|
{{-- {{ $r['meta_text'] }}--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @empty--}}
|
||||||
|
{{-- <div class="rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white/70">--}}
|
||||||
|
{{-- Keine gebannten IPs in diesem Jail.--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @endforelse--}}
|
||||||
|
{{--</div>--}}
|
||||||
|
|
||||||
|
{{--@push('modal.footer')--}}
|
||||||
|
{{-- <div class="px-5 py-3 border-t border-white/10 backdrop-blur rounded-b-2xl">--}}
|
||||||
|
{{-- <div class="flex items-center gap-2 justify-end">--}}
|
||||||
|
{{-- <button wire:click="$dispatch('f2b:refresh-banlist')" wire:loading.attr="disabled"--}}
|
||||||
|
{{-- class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">--}}
|
||||||
|
{{-- <i class="ph ph-arrows-clockwise text-[13px]"></i>--}}
|
||||||
|
{{-- <span wire:loading.remove>Neu prüfen</span>--}}
|
||||||
|
{{-- <span wire:loading>prüfe…</span>--}}
|
||||||
|
{{-- </button>--}}
|
||||||
|
{{-- <button wire:click="$dispatch('closeModal')"--}}
|
||||||
|
{{-- class="px-3 py-1.5 rounded-lg text-sm bg-emerald-500/20 text-emerald-300 border border-emerald-400/30 hover:bg-emerald-500/30">--}}
|
||||||
|
{{-- Fertig--}}
|
||||||
|
{{-- </button>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{--@endpush--}}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<div class="p-5">
|
||||||
|
{{-- Header --}}
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div class="inline-flex items-center gap-2 rounded-full
|
||||||
|
{{ $type === 'blacklist' ? 'bg-rose-500/10 border border-rose-400/30' : 'bg-white/5 border border-white/10' }}
|
||||||
|
px-2.5 py-1">
|
||||||
|
<i class="ph {{ $type === 'blacklist' ? 'ph-hand text-rose-300' : 'ph-list text-white/70' }} text-[13px]"></i>
|
||||||
|
<span class="text-[11px] uppercase tracking-wide
|
||||||
|
{{ $type === 'blacklist' ? 'text-rose-300' : 'text-white/70' }}">
|
||||||
|
{{ strtoupper($type) }} – {{ $mode === 'add' ? 'hinzufügen' : 'entfernen' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" wire:click="$dispatch('closeModal')"
|
||||||
|
class="rounded-lg border border-white/10 bg-white/5 px-2.5 py-1 text-white/70 hover:text-white">
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Body --}}
|
||||||
|
<div class="space-y-3">
|
||||||
|
@if($mode === 'add')
|
||||||
|
<div>
|
||||||
|
<label class="block text-white/60 text-sm mb-1">IP oder CIDR</label>
|
||||||
|
<input type="text" wire:model.defer="ip" placeholder="z. B. 203.0.113.4 oder 203.0.113.0/24 oder 2001:db8::/32"
|
||||||
|
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||||
|
@error('ip') <p class="text-sm text-rose-400 mt-1">{{ $message }}</p> @enderror
|
||||||
|
</div>
|
||||||
|
<button wire:click="save"
|
||||||
|
class="primary-btn w-full justify-center">
|
||||||
|
{{ $type === 'blacklist' ? 'Zur Blacklist hinzufügen & bannen' : 'Zur Whitelist hinzufügen' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if($type === 'blacklist')
|
||||||
|
<p class="text-xs text-white/50 mt-2">
|
||||||
|
Wird sofort im Jail <code>mailwolt-blacklist</code> gebannt (bantime = permanent).
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/[0.04] px-3 py-2">
|
||||||
|
<div class="text-white/80 text-sm">IP: {{ $prefill ?? $ip }}</div>
|
||||||
|
<div class="text-white/50 text-xs">Wird aus der {{ $type }} entfernt
|
||||||
|
@if($type === 'blacklist') und im Blacklist-Jail entbannt @endif.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button wire:click="remove"
|
||||||
|
class="text-[13px] w-full px-3 py-2 rounded-xl border
|
||||||
|
{{ $type === 'blacklist'
|
||||||
|
? 'border-rose-400/40 bg-rose-500/10 text-rose-200 hover:border-rose-400/70'
|
||||||
|
: 'border-white/20 bg-white/5 text-white/80 hover:border-white/40' }}">
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -54,9 +54,7 @@
|
||||||
|
|
||||||
<div class="mt-5 flex justify-center">
|
<div class="mt-5 flex justify-center">
|
||||||
<button wire:click="refresh"
|
<button wire:click="refresh"
|
||||||
class="inline-flex items-center gap-1.5 rounded-full text-[12px] px-4 py-1.5
|
class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">
|
||||||
text-white/80 bg-white/10 border border-white/15
|
|
||||||
hover:bg-white/15 hover:text-white transition">
|
|
||||||
<i class="ph ph-arrows-clockwise text-[13px]"></i>
|
<i class="ph ph-arrows-clockwise text-[13px]"></i>
|
||||||
Neu prüfen
|
Neu prüfen
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5"
|
<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5"
|
||||||
wire:poll.2s="refresh">
|
@if($running) wire:poll.2s="refresh" @endif>
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">
|
<div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-white/60 text-sm mb-1">Mailserver-Domain (fix)</label>
|
||||||
|
<input type="text" value="{{ $mail_domain_readonly }}" disabled
|
||||||
|
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.06] px-3 text-white/60 cursor-not-allowed">
|
||||||
|
<p class="mt-1 text-xs text-white/45">Wird aus ENV/Config gelesen und ist nicht änderbar.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-white/60 text-sm mb-1">UI-Domain</label>
|
||||||
|
<input type="text" wire:model.defer="ui_domain" placeholder="z. B. ui.deinedomain.tld"
|
||||||
|
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||||
|
@error('ui_domain') <p class="text-xs text-rose-400 mt-1">{{ $message }}</p> @enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-white/60 text-sm mb-1">Webmail-Domain</label>
|
||||||
|
<input type="text" wire:model.defer="webmail_domain" placeholder="z. B. mail.deinedomain.tld"
|
||||||
|
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||||
|
@error('webmail_domain') <p class="text-xs text-rose-400 mt-1">{{ $message }}</p> @enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<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">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 text-xs text-white/45">
|
||||||
|
TLS/Redirect ist systemweit immer erzwungen (HTTPS). ACME/Zertifikate haben ihren eigenen Reiter.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{{-- Sprache --}}
|
||||||
|
<div>
|
||||||
|
<label class="block text-white/60 text-sm mb-1">Sprache</label>
|
||||||
|
<select wire:model.defer="locale"
|
||||||
|
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||||
|
@foreach (config('mailwolt.language') as $key => $lang)
|
||||||
|
<option value="{{ $lang['locale'] }}">{{ $lang['label'] }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('locale') <p class="text-xs text-rose-400 mt-1">{{ $message }}</p> @enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Zeitzone --}}
|
||||||
|
<div>
|
||||||
|
<label class="block text-white/60 text-sm mb-1">Zeitzone</label>
|
||||||
|
<select wire:model.defer="timezone"
|
||||||
|
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||||
|
@foreach (DateTimeZone::listIdentifiers() as $tz)
|
||||||
|
<option value="{{ $tz }}">{{ $tz }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('timezone') <p class="text-xs text-rose-400 mt-1">{{ $message }}</p> @enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Actions: immer unten rechts, volle Breite, rechts ausgerichtet --}}
|
||||||
|
<div class="md:col-span-2 flex justify-end">
|
||||||
|
<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">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<div class="space-y-4">
|
||||||
|
<label class="flex items-center gap-3">
|
||||||
|
<input type="checkbox" wire:model.defer="twofa_enabled" class="h-4 w-4">
|
||||||
|
<span class="text-white/80">Zwei-Faktor-Authentifizierung aktivieren</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-white/60 text-sm mb-1">Login-Rate-Limit (Versuche/Minute)</label>
|
||||||
|
<input type="number" min="1" max="100" wire:model.defer="rate_limit"
|
||||||
|
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||||
|
@error('rate_limit') <p class="text-xs text-rose-400 mt-1">{{ $message }}</p> @enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-white/60 text-sm mb-1">Minimale Passwortlänge</label>
|
||||||
|
<input type="number" min="6" max="128" wire:model.defer="password_min"
|
||||||
|
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||||
|
@error('password_min') <p class="text-xs text-rose-400 mt-1">{{ $message }}</p> @enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<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">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -1 +1,10 @@
|
||||||
<?php
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Fail2Ban')
|
||||||
|
@section('header_title', 'Fail2Ban')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<livewire:ui.security.fail2ban-settings />
|
||||||
|
|
||||||
|
@endsection
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,3 @@
|
||||||
{{-- resources/views/ui/system/settings.blade.php --}}
|
|
||||||
{{--@extends('layouts.app')--}}
|
|
||||||
|
|
||||||
{{--@section('title', 'System · Einstellungen')--}}
|
|
||||||
{{--@section('header_title', 'System · Einstellungen')--}}
|
|
||||||
|
|
||||||
{{--@section('content')--}}
|
|
||||||
{{-- <div class="glass-card p-5">--}}
|
|
||||||
{{-- <livewire:ui.system.settings-form />--}}
|
|
||||||
{{-- </div>--}}
|
|
||||||
{{--@endsection--}}
|
|
||||||
{{-- resources/views/ui/system/settings/index.blade.php --}}
|
{{-- resources/views/ui/system/settings/index.blade.php --}}
|
||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
|
@ -55,7 +44,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Livewire-Form (Allgemein) --}}
|
{{-- Livewire-Form (Allgemein) --}}
|
||||||
<livewire:ui.system.general-form />
|
<livewire:ui.system.form.general-form />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,14 +37,16 @@ Route::middleware('auth.user')->name('ui.')->group(function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
#DOMAIN ROUTES
|
#DOMAIN ROUTES
|
||||||
Route::name('domain.')->group(function () {
|
Route::name('domains.')->group(function () {
|
||||||
Route::get('/domains', [DomainDnsController::class, 'index'])->name('index');
|
Route::get('/domains', [DomainDnsController::class, 'index'])->name('index');
|
||||||
});
|
});
|
||||||
|
|
||||||
#MAIL ROUTES
|
#MAIL ROUTES
|
||||||
Route::name('mail.')->group(function () {
|
Route::name('mail.')->group(function () {
|
||||||
Route::get('/mailboxes', [MailboxController::class, 'index'])->name('mailbox.index');
|
Route::get('/mailboxes', [MailboxController::class, 'index'])->name('mailboxes.index');
|
||||||
Route::get('/aliases', [AliasController::class, 'index'])->name('aliases.index');
|
Route::get('/aliases', [AliasController::class, 'index'])->name('aliases.index');
|
||||||
|
Route::get('/quarantine', function () {return 'Quarantäne';})->name('quarantine.index');
|
||||||
|
Route::get('/queues', function () {return 'Queues';})->name('queues.index');
|
||||||
});
|
});
|
||||||
|
|
||||||
#LOGOUT ROUTE
|
#LOGOUT ROUTE
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue