parent
aaae226c8d
commit
251f2d9c8f
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,29 +6,116 @@ use Livewire\Component;
|
||||||
|
|
||||||
class Fail2BanCard extends Component
|
class Fail2BanCard extends Component
|
||||||
{
|
{
|
||||||
public int $activeBans = 0;
|
public bool $available = true; // ob fail2ban vorhanden ist
|
||||||
public array $topIps = []; // [['ip'=>'1.2.3.4','count'=>12],...]
|
public int $activeBans = 0; // Summe über alle Jails
|
||||||
|
/** @var array<int,array{name:string,banned:int,ips:array<int,string>}> */
|
||||||
|
public array $jails = []; // je Jail: Name, Anzahl, IPs (gekürzt)
|
||||||
|
/** @var array<int,array{ip:string,count:int}> */
|
||||||
|
public array $topIps = []; // Top IPs aus Log/Journal (Ban-Events)
|
||||||
|
|
||||||
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)) {
|
public function refresh(): void
|
||||||
$rows[] = ['ip'=>$m[2],'count'=>(int)$m[1]];
|
{
|
||||||
}
|
$this->load(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function load(bool $force = false): void
|
||||||
|
{
|
||||||
|
// 0) vorhanden?
|
||||||
|
$bin = trim((string) @shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
|
||||||
|
if ($bin === '') {
|
||||||
|
$this->available = false;
|
||||||
|
$this->activeBans = 0;
|
||||||
|
$this->jails = [];
|
||||||
|
$this->topIps = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Jails ermitteln
|
||||||
|
$status = (string) (@shell_exec("timeout 2 $bin status 2>/dev/null") ?? '');
|
||||||
|
$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) {
|
||||||
|
$s = (string) (@shell_exec("timeout 2 $bin status ".escapeshellarg($j)." 2>/dev/null") ?? '');
|
||||||
|
$banned = (int) ($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||||
|
|
||||||
|
// „Banned IP list:“ kann fehlen → leeres Array
|
||||||
|
$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,
|
||||||
|
// nur die ersten 8 zur UI-Anzeige
|
||||||
|
'ips' => array_slice($ips, 0, 8),
|
||||||
|
];
|
||||||
|
$total += $banned;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->available = true;
|
||||||
|
$this->activeBans = $total;
|
||||||
|
$this->jails = $rows;
|
||||||
|
|
||||||
|
// 2) Top-IPs aus den letzten Ban-Events
|
||||||
|
$this->topIps = $this->collectTopIps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extrahiert erste Regex-Gruppe oder null */
|
||||||
|
private function firstMatch(string $pattern, string $haystack): ?string
|
||||||
|
{
|
||||||
|
return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zählt „Ban <IP>“ aus fail2ban.log (Fallback: journalctl) */
|
||||||
|
private function collectTopIps(): array
|
||||||
|
{
|
||||||
|
$lines = '';
|
||||||
|
if (is_readable('/var/log/fail2ban.log')) {
|
||||||
|
// letzte 500 Zeilen reichen völlig
|
||||||
|
$lines = (string) (@shell_exec('tail -n 500 /var/log/fail2ban.log 2>/dev/null') ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($lines === '') {
|
||||||
|
// Fallback: Journal (falls rsyslog die Datei nicht schreibt)
|
||||||
|
$lines = (string) (@shell_exec('timeout 2 journalctl -u fail2ban -n 500 --no-pager 2>/dev/null') ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($lines === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur Ban-Events, IP extrahieren
|
||||||
|
$ips = [];
|
||||||
|
foreach (preg_split('/\R+/', $lines) as $ln) {
|
||||||
|
if (stripos($ln, 'Ban ') === false) continue;
|
||||||
|
if (preg_match('/\b(\d{1,3}(?:\.\d{1,3}){3})\b/', $ln, $m)) {
|
||||||
|
$ip = $m[1];
|
||||||
|
$ips[$ip] = ($ips[$ip] ?? 0) + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$this->topIps = $rows;
|
|
||||||
|
if (!$ips) return [];
|
||||||
|
|
||||||
|
arsort($ips);
|
||||||
|
$top = array_slice($ips, 0, 5, true);
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
foreach ($top as $ip => $cnt) {
|
||||||
|
$out[] = ['ip' => $ip, 'count' => (int)$cnt];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button wire:click="refresh"
|
<button wire:click="refresh"
|
||||||
class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">
|
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>
|
||||||
Neu prüfen
|
Neu prüfen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -16,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 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'])
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,65 @@
|
||||||
<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">
|
@else
|
||||||
<span class="text-white/80">{{ $i['ip'] }}</span>
|
{{-- Jails --}}
|
||||||
<span class="text-white/60">{{ $i['count'] }}</span>
|
<div class="space-y-2">
|
||||||
</li>
|
@forelse($jails as $j)
|
||||||
@empty
|
<div class="rounded-xl border border-white/10 bg-white/5 px-3 py-2">
|
||||||
<li class="text-white/50">–</li>
|
<div class="flex items-center justify-between">
|
||||||
@endforelse
|
<div class="text-white/85 font-medium">{{ $j['name'] }}</div>
|
||||||
</ul>
|
<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">
|
||||||
|
@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"
|
||||||
|
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>
|
||||||
|
Neu prüfen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue