diff --git a/app/Livewire/Ui/Security/RblCard.php b/app/Livewire/Ui/Security/RblCard.php index 3529625..c04d3d5 100644 --- a/app/Livewire/Ui/Security/RblCard.php +++ b/app/Livewire/Ui/Security/RblCard.php @@ -1,4 +1,5 @@ load(); } - public function render() { return view('livewire.ui.security.rbl-card'); } - public function refresh(): void { $this->load(true); } + public ?string $ipv4 = null; + public ?string $ipv6 = null; - protected function load(bool $force=false): void + public function mount(): void { + $this->load(); + } + + public function render() + { + return view('livewire.ui.security.rbl-card'); + } + + public function refresh(): void + { + Cache::forget('dash.rbl'); + $this->load(true); + } + + protected function load(bool $force = false): void + { + // 1) IPv4/IPv6 bevorzugt aus /etc/mailwolt/installer.env + [$ip4, $ip6] = $this->resolvePublicIpsFromInstallerEnv(); + + // 2) Fallback auf .env + $this->ipv4 = $ip4 ?: trim((string) env('SERVER_PUBLIC_IPV4', '')) ?: '–'; + $this->ipv6 = $ip6 ?: trim((string) env('SERVER_PUBLIC_IPV6', '')) ?: '–'; + + // 3) RBL-Ermittlung (cached) $data = Cache::remember('dash.rbl', $force ? 1 : 21600, function () { - $ip = trim(@file_get_contents('/etc/mailwolt/public_ip') ?: ''); - if ($ip === '') $ip = trim(@shell_exec("curl -fsS --max-time 2 ifconfig.me 2>/dev/null") ?? ''); - if (!preg_match('/^\d+\.\d+\.\d+\.\d+$/', $ip)) $ip = '0.0.0.0'; + // bevorzugt eine valide IPv4 für den RBL-Check + $candidate = $this->validIPv4($this->ipv4 ?? '') ? $this->ipv4 : null; - $rev = implode('.', array_reverse(explode('.', $ip))); - $sources = [ - 'zen.spamhaus.org', - 'bl.spamcop.net', - 'dnsbl.sorbs.net', - 'b.barracudacentral.org', - ]; - - $lists = []; - foreach ($sources as $s) { - $q = "$rev.$s"; - $res = trim(@shell_exec("dig +short ".escapeshellarg($q)." A 2>/dev/null") ?? ''); - if ($res !== '') $lists[] = $s; + if (!$candidate) { + $fromFile = @file_get_contents('/etc/mailwolt/public_ip') ?: ''; + $fromFile = trim($fromFile); + if ($this->validIPv4($fromFile)) { + $candidate = $fromFile; + } } - return ['ip'=>$ip, 'hits'=>count($lists), 'lists'=>$lists]; + if (!$candidate) { + // letzter Fallback – kann auf Hardened-Systemen geblockt sein + $curl = @shell_exec("curl -fsS --max-time 2 ifconfig.me 2>/dev/null") ?: ''; + $curl = trim($curl); + if ($this->validIPv4($curl)) { + $candidate = $curl; + } + } + + $ip = $candidate ?: '0.0.0.0'; + $lists = $this->queryRblLists($ip); + + return ['ip' => $ip, 'hits' => count($lists), 'lists' => $lists]; }); - foreach ($data as $k=>$v) $this->$k = $v; + // 4) Werte ins Component-State + foreach ($data as $k => $v) { + $this->$k = $v; + } + } + + /** Bevorzugt Installer-ENV; gibt [ipv4, ipv6] zurück oder [null, null]. */ + private function resolvePublicIpsFromInstallerEnv(): array + { + $file = '/etc/mailwolt/installer.env'; + if (!is_readable($file)) { + return [null, null]; + } + + $ipv4 = null; + $ipv6 = null; + + $lines = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; + foreach ($lines as $line) { + // Kommentare überspringen + if (preg_match('/^\s*#/', $line)) { + continue; + } + // KEY=VALUE (VALUE evtl. in "..." oder '...') + if (!str_contains($line, '=')) { + continue; + } + [$k, $v] = array_map('trim', explode('=', $line, 2)); + $v = trim($v, " \t\n\r\0\x0B\"'"); + + if ($k === 'SERVER_PUBLIC_IPV4' && $this->validIPv4($v)) { + $ipv4 = $v; + } elseif ($k === 'SERVER_PUBLIC_IPV6' && $this->validIPv6($v)) { + $ipv6 = $v; + } + } + + return [$ipv4, $ipv6]; + } + + private function validIPv4(?string $ip): bool + { + return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); + } + + private function validIPv6(?string $ip): bool + { + return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); + } + + /** + * Prüft die IP gegen ein paar gängige RBLs. + * Nutzt PHP-DNS (checkdnsrr), keine externen Tools. + * + * @return array gelistete RBL-Zonen + */ + private function queryRblLists(string $ip): array + { + // Nur IPv4 prüfen (die meisten Listen hier sind v4) + if (!$this->validIPv4($ip)) { + return []; + } + + $rev = implode('.', array_reverse(explode('.', $ip))); + $sources = [ + 'zen.spamhaus.org', + 'bl.spamcop.net', + 'dnsbl.sorbs.net', + 'b.barracudacentral.org', + ]; + + $listed = []; + foreach ($sources as $zone) { + $qname = "{$rev}.{$zone}"; + // A-Record oder TXT deuten auf Listing hin + if (@checkdnsrr($qname . '.', 'A') || @checkdnsrr($qname . '.', 'TXT')) { + $listed[] = $zone; + } + } + + return $listed; } } +// +//namespace App\Livewire\Ui\Security; +// +//use Livewire\Component; +//use Illuminate\Support\Facades\Cache; +// +//class RblCard extends Component +//{ +// public string $ip = '–'; +// public int $hits = 0; +// public array $lists = []; +// +// public ?string $ipv4 = null; +// public ?string $ipv6 = null; +// +// +// public function mount(): void { $this->load(); } +// public function render() { return view('livewire.ui.security.rbl-card'); } +// public function refresh(): void { $this->load(true); } +// +// protected function load(bool $force=false): void +// { +// $this->ipv4 = trim(env('SERVER_PUBLIC_IPV4')) ?: '–'; +// $this->ipv6 = trim(env('SERVER_PUBLIC_IPV6')) ?: '–'; +// +// $data = Cache::remember('dash.rbl', $force ? 1 : 21600, function () { +// $ip = trim(@file_get_contents('/etc/mailwolt/public_ip') ?: ''); +// if ($ip === '') $ip = trim(@shell_exec("curl -fsS --max-time 2 ifconfig.me 2>/dev/null") ?? ''); +// if (!preg_match('/^\d+\.\d+\.\d+\.\d+$/', $ip)) $ip = '0.0.0.0'; +// +// $rev = implode('.', array_reverse(explode('.', $ip))); +// $sources = [ +// 'zen.spamhaus.org', +// 'bl.spamcop.net', +// 'dnsbl.sorbs.net', +// 'b.barracudacentral.org', +// ]; +// +// $lists = []; +// foreach ($sources as $s) { +// $q = "$rev.$s"; +// $res = trim(@shell_exec("dig +short ".escapeshellarg($q)." A 2>/dev/null") ?? ''); +// if ($res !== '') $lists[] = $s; +// } +// +// return ['ip'=>$ip, 'hits'=>count($lists), 'lists'=>$lists]; +// }); +// +// foreach ($data as $k=>$v) $this->$k = $v; +// } +//} diff --git a/app/Livewire/Ui/System/ServicesCard.php b/app/Livewire/Ui/System/ServicesCard.php index 4546e0b..6eedea1 100644 --- a/app/Livewire/Ui/System/ServicesCard.php +++ b/app/Livewire/Ui/System/ServicesCard.php @@ -1,44 +1,98 @@ ['label' => 'Postfix', 'hint' => 'MTA / Versand'], - 'dovecot' => ['label' => 'Dovecot', 'hint' => 'IMAP / POP3'], - 'rspamd' => ['label' => 'Rspamd', 'hint' => 'Spamfilter'], - 'clamav' => ['label' => 'ClamAV', 'hint' => 'Virenscanner'], + 'postfix' => [ + 'label' => 'Postfix', 'hint' => 'MTA / Versand', + 'sources' => ['systemd:postfix'], + ], + 'dovecot' => [ + 'label' => 'Dovecot', 'hint' => 'IMAP / POP3', + 'sources' => ['systemd:dovecot', 'tcp:127.0.0.1:993'], + ], + 'rspamd' => [ + 'label' => 'Rspamd', 'hint' => 'Spamfilter', + 'sources' => ['systemd:rspamd', 'tcp:127.0.0.1:11333', 'tcp:127.0.0.1:11334'], + ], + 'clamav' => [ + 'label' => 'ClamAV', 'hint' => 'Virenscanner', + // mehrere mögliche Units + Socket/PID + TCP + 'sources' => [ + 'systemd:clamav-daemon', + 'systemd:clamav-daemon@scan', + 'systemd:clamd', + 'socket:/run/clamav/clamd.ctl', + 'pid:/run/clamav/clamd.pid', + 'tcp:127.0.0.1:3310', + ], + ], // Daten & Cache - 'db' => ['label' => 'Datenbank', 'hint' => 'MySQL / MariaDB'], - '127.0.0.1:6379' => ['label' => 'Redis', 'hint' => 'Cache / Queue'], + 'db' => [ + 'label' => 'Datenbank', 'hint' => 'MySQL / MariaDB', + 'sources' => ['db'], + ], + 'redis' => [ + 'label' => 'Redis', 'hint' => 'Cache / Queue', + 'sources' => ['tcp:127.0.0.1:6379', 'systemd:redis-server', 'systemd:redis'], + ], // Web / PHP - 'php8.2-fpm' => ['label' => 'PHP-FPM', 'hint' => 'PHP Runtime'], - 'nginx' => ['label' => 'Nginx', 'hint' => 'Webserver'], + 'php-fpm' => [ + 'label' => 'PHP-FPM', 'hint' => 'PHP Runtime', + 'sources' => [ + 'systemd:php8.3-fpm', 'systemd:php8.2-fpm', 'systemd:php8.1-fpm', 'systemd:php-fpm', + 'socket:/run/php/php8.3-fpm.sock', 'socket:/run/php/php8.2-fpm.sock', + 'socket:/run/php/php8.1-fpm.sock', 'socket:/run/php/php-fpm.sock', + 'tcp:127.0.0.1:9000', + ], + ], + 'nginx' => [ + 'label' => 'Nginx', 'hint' => 'Webserver', + 'sources' => ['systemd:nginx', 'tcp:127.0.0.1:80'], + ], - // MailWolt - 'mailwolt-queue' => ['label' => 'MailWolt Queue', 'hint' => 'Job Worker'], - 'mailwolt-schedule'=> ['label' => 'MailWolt Schedule', 'hint' => 'Task Scheduler'], - 'mailwolt-ws' => ['label' => 'MailWolt WebSocket','hint' => 'Echtzeit Updates'], + // MailWolt (Units ODER laufende artisan-Prozesse) + 'mw-queue' => [ + 'label' => 'MailWolt Queue', 'hint' => 'Job Worker', + 'sources' => [ + 'systemd:mailwolt-queue', + 'proc:/php.*artisan(\.php)?\s+queue:work', + ], + ], + 'mw-schedule' => [ + 'label' => 'MailWolt Schedule', 'hint' => 'Task Scheduler', + 'sources' => [ + 'systemd:mailwolt-schedule', + 'proc:/php.*artisan(\.php)?\s+schedule:work', + ], + ], + 'mw-ws' => [ + 'label' => 'MailWolt WebSocket', 'hint' => 'Echtzeit Updates', + 'sources' => ['systemd:mailwolt-ws', 'tcp:127.0.0.1:8080'], + ], // Sonstiges - 'fail2ban' => ['label' => 'Fail2Ban', 'hint' => 'SSH / Mail Protection'], - 'systemd-journald'=> ['label' => 'System Logs', 'hint' => 'Journal'], - 'rsyslog' => ['label' => 'Rsyslog', 'hint' => 'Logging'], - - // WebSocket/TCP - '127.0.0.1:8080' => ['label' => 'Reverb', 'hint' => 'WebSocket Server'], + 'fail2ban' => [ + 'label' => 'Fail2Ban', 'hint' => 'SSH / Mail Protection', + 'sources' => ['systemd:fail2ban'], + ], + 'journal' => [ + 'label' => 'System Logs', 'hint' => 'Journal', + 'sources' => ['systemd:systemd-journald', 'systemd:rsyslog'], + ], ]; public function mount(): void @@ -46,36 +100,205 @@ class ServicesCard extends Component $this->load(); } + public function refresh(): void + { + Cache::forget('health:services.v2'); + $this->load(); + } + public function load(): void { - $this->services = Cache::get('health:services', []); - $meta = Cache::get('health:meta', []); - $updated = $meta['updated_at'] ?? null; + $data = Cache::remember('health:services.v2', 15, function () { + $out = []; + foreach ($this->cards as $key => $card) { + $ok = false; + foreach ($card['sources'] as $src) { + if ($this->check($src)) { + $ok = true; + break; + } + } + $out[$key] = ['label' => $card['label'], 'hint' => $card['hint'], 'ok' => $ok]; + } + return $out; + }); - $existing = collect($this->services)->keyBy('name'); - - $this->servicesCompact = collect($this->nameMap) - ->map(function ($meta, $key) use ($existing) { - $srv = $existing->get($key, []); - $ok = (bool)($srv['ok'] ?? false); - - return [ - 'label' => $meta['label'], - 'hint' => $meta['hint'], - 'ok' => $ok, - 'dotClass' => $ok ? 'bg-emerald-400' : 'bg-rose-400', - 'pillText' => $ok ? 'Aktiv' : 'Offline', - 'pillClass' => $ok - ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' - : 'text-rose-300 border-rose-400/30 bg-rose-500/10', - ]; - }) - ->values() - ->all(); + $this->servicesCompact = collect($data)->map(function ($row) { + $ok = (bool)$row['ok']; + return [ + 'label' => $row['label'], + 'hint' => $row['hint'], + 'ok' => $ok, + 'dotClass' => $ok ? 'bg-emerald-400' : 'bg-rose-400', + 'pillText' => $ok ? 'Online' : 'Offline', + 'pillClass' => $ok + ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' + : 'text-rose-300 border-rose-400/30 bg-rose-500/10', + ]; + })->values()->all(); } public function render() { return view('livewire.ui.system.services-card'); } + + // ───────────── Probes ───────────── + + protected function check(string $source): bool + { + if (str_starts_with($source, 'systemd:')) { + return $this->probeSystemd(substr($source, 8)); + } + if (str_starts_with($source, 'tcp:')) { + [$host, $port] = explode(':', substr($source, 4), 2); + return $this->probeTcp($host, (int)$port); + } + if (str_starts_with($source, 'socket:')) { + $path = substr($source, 7); + return is_string($path) && @file_exists($path); + } + if (str_starts_with($source, 'pid:')) { + $path = substr($source, 4); + if (!@is_file($path)) return false; + $pid = (int)trim(@file_get_contents($path) ?: ''); + return $pid > 1 && @posix_kill($pid, 0); + } + if (str_starts_with($source, 'proc:')) { + $regex = substr($source, 5); + return $this->probeProcessRegex($regex); + } + if ($source === 'db') { + return $this->probeDatabase(); + } + return false; + } + + protected function probeSystemd(string $unit): bool + { + // versuche /bin und /usr/bin + $bin = file_exists('/bin/systemctl') ? '/bin/systemctl' + : (file_exists('/usr/bin/systemctl') ? '/usr/bin/systemctl' : 'systemctl'); + + $cmd = sprintf('%s is-active --quiet %s 2>/dev/null', escapeshellcmd($bin), escapeshellarg($unit)); + $exit = null; + @exec($cmd, $_, $exit); + return $exit === 0; + } + + protected function probeTcp(string $host, int $port, int $timeout = 1): bool + { + $fp = @fsockopen($host, $port, $e1, $e2, $timeout); + if (is_resource($fp)) { + fclose($fp); + return true; + } + return false; + // Alternative: stream_socket_client("tcp://$host:$port", …) + } + + protected function probeProcessRegex(string $regex): bool + { + // /proc durchsuchen – leichtgewichtig und ohne ps-Depend + $regex = '#' . $regex . '#i'; + foreach (@scandir('/proc') ?: [] as $d) { + if (!ctype_digit($d)) continue; + $cmd = @file_get_contents("/proc/$d/cmdline"); + if ($cmd === false || $cmd === '') continue; + $cmd = str_replace("\0", ' ', $cmd); + if (preg_match($regex, $cmd)) return true; + } + return false; + } + + protected function probeDatabase(): bool + { + try { + DB::connection()->getPdo(); + return true; + } catch (\Throwable) { + return false; + } + } } + +// +//namespace App\Livewire\Ui\System; +// +//use Carbon\Carbon; +//use Illuminate\Support\Facades\Cache; +//use Livewire\Component; +// +//class ServicesCard extends Component +//{ +// public array $services = []; +// public array $servicesCompact = []; +// +// // Mapping für schöne Labels/Hints +// protected array $nameMap = [ +// // Mail +// 'postfix' => ['label' => 'Postfix', 'hint' => 'MTA / Versand'], +// 'dovecot' => ['label' => 'Dovecot', 'hint' => 'IMAP / POP3'], +// 'rspamd' => ['label' => 'Rspamd', 'hint' => 'Spamfilter'], +// 'clamav' => ['label' => 'ClamAV', 'hint' => 'Virenscanner'], +// +// // Daten & Cache +// 'db' => ['label' => 'Datenbank', 'hint' => 'MySQL / MariaDB'], +// '127.0.0.1:6379' => ['label' => 'Redis', 'hint' => 'Cache / Queue'], +// +// // Web / PHP +// 'php8.2-fpm' => ['label' => 'PHP-FPM', 'hint' => 'PHP Runtime'], +// 'nginx' => ['label' => 'Nginx', 'hint' => 'Webserver'], +// +// // MailWolt +// 'mailwolt-queue' => ['label' => 'MailWolt Queue', 'hint' => 'Job Worker'], +// 'mailwolt-schedule'=> ['label' => 'MailWolt Schedule', 'hint' => 'Task Scheduler'], +// 'mailwolt-ws' => ['label' => 'MailWolt WebSocket','hint' => 'Echtzeit Updates'], +// +// // Sonstiges +// 'fail2ban' => ['label' => 'Fail2Ban', 'hint' => 'SSH / Mail Protection'], +// 'systemd-journald'=> ['label' => 'System Logs', 'hint' => 'Journal'], +// 'rsyslog' => ['label' => 'Rsyslog', 'hint' => 'Logging'], +// +// // WebSocket/TCP +// '127.0.0.1:8080' => ['label' => 'Reverb', 'hint' => 'WebSocket Server'], +// ]; +// +// public function mount(): void +// { +// $this->load(); +// } +// +// public function load(): void +// { +// $this->services = Cache::get('health:services', []); +// $meta = Cache::get('health:meta', []); +// $updated = $meta['updated_at'] ?? null; +// +// $existing = collect($this->services)->keyBy('name'); +// +// $this->servicesCompact = collect($this->nameMap) +// ->map(function ($meta, $key) use ($existing) { +// $srv = $existing->get($key, []); +// $ok = (bool)($srv['ok'] ?? false); +// +// return [ +// 'label' => $meta['label'], +// 'hint' => $meta['hint'], +// 'ok' => $ok, +// 'dotClass' => $ok ? 'bg-emerald-400' : 'bg-rose-400', +// 'pillText' => $ok ? 'Aktiv' : 'Offline', +// 'pillClass' => $ok +// ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' +// : 'text-rose-300 border-rose-400/30 bg-rose-500/10', +// ]; +// }) +// ->values() +// ->all(); +// } +// +// public function render() +// { +// return view('livewire.ui.system.services-card'); +// } +//}