diff --git a/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php b/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php index daee5bd..cb24bfe 100644 --- a/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php +++ b/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php @@ -207,6 +207,7 @@ class DomainDnsModal extends ModalComponent $this->dynamic[$i]['actual'] = $actual; $this->dynamic[$i]['state'] = $state; $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 @@ -216,6 +217,7 @@ class DomainDnsModal extends ModalComponent $this->static[$i]['actual'] = $actual; $this->static[$i]['state'] = $state; $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 @@ -225,6 +227,7 @@ class DomainDnsModal extends ModalComponent $this->optional[$i]['actual'] = $actual; $this->optional[$i]['state'] = $state; $this->optional[$i]['boxClass'] = $this->stateColors[$state] ?? $this->stateColors['neutral']; + $this->optional[$i]['display_actual'] = $this->normActual($r['type'], $actual); } $this->checked = true; @@ -253,90 +256,201 @@ class DomainDnsModal extends ModalComponent /* ---------- 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 { $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); + $name = rtrim($name, '.'); - // Mehrzeiliges TXT zu einer Zeile squashen, Quotes weg - if ($type === 'TXT' && $out !== '') { + $out = @shell_exec( + '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))); - $joined = implode('', array_map(fn($l)=>trim($l,'"'), $lines)); + $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, '.'); - } + // TLSA: Kanonisieren (Leerzeichen/Zeilenumbrüche im Hash, Großbuchstaben, …) + if ($type === 'TLSA') { + // nimm die ganze Ausgabe (kann mehrzeilig sein) + return $this->canonicalizeTlsa(preg_replace('/\s+/', ' ', $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 +// { +// if ($actual === '') { +// return $optional ? 'neutral' : 'missing'; +// } +// +// $type = strtoupper($type); +// $exp = $this->normExpected($type, $expected); +// $act = $this->normActual($type, $actual); +// +// // Syntax plausibilisieren +// $syntaxOk = $this->validateSyntax($type, $act); +// 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') { +// $upperExp = strtoupper($exp); +// $upperAct = strtoupper($act); +// if (str_starts_with($upperExp, 'V=SPF1')) return str_starts_with($upperAct, 'V=SPF1') ? 'ok' : 'syntax'; +// if (str_starts_with($upperExp, 'V=DMARC1')) return str_starts_with($upperAct, 'V=DMARC1') ? 'ok' : 'syntax'; +// if (str_starts_with($upperExp, 'V=DKIM1')) return str_starts_with($upperAct, 'V=DKIM1') ? 'ok' : 'syntax'; +// return ($act !== '') ? 'ok' : ($optional ? 'neutral' : 'missing'); +// } +// +// // MX: „prio host“ – wir prüfen Host grob +// 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'; +// } +// +// // SRV: „prio weight port host“ – Port + Host grob +// if ($type === 'SRV') { +// $ap = preg_split('/\s+/', $act); +// $ep = preg_split('/\s+/', $exp); +// if (count($ap) >= 4 && count($ep) >= 4) { +// $aport = (int)$ap[2]; +// $eport = (int)$ep[2]; +// $ahost = strtolower(rtrim(end($ap), '.')); +// $ehost = strtolower(rtrim(end($ep), '.')); +// return ($aport === $eport && $ahost === $ehost) ? 'ok' : 'syntax'; +// } +// return 'syntax'; +// } +// +// // CNAME/A/AAAA/PTR/TLSA: Gleichheit nach Normalisierung +// return ($act === $exp) ? 'ok' : 'syntax'; +// } + +// 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 = preg_replace('/\s+/', ' ', $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 = preg_replace('/\s+/', '', $v); // Hash-Zeilen zusammenfügen +// $v = preg_replace('/^([0-3][\s]+[01][\s]+[123])/', '$1 ', $v); // spacing nach Header erzwingen +// } +// return $v; +// } + 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); $exp = $this->normExpected($type, $expected); $act = $this->normActual($type, $actual); - // Syntax plausibilisieren - $syntaxOk = $this->validateSyntax($type, $act); - if (!$syntaxOk) return 'syntax'; + // Syntaxcheck nach Normalisierung + if (!$this->validateSyntax($type, $act)) return 'syntax'; - // TXT-Policies: nur „Startet mit v=…“ prüfen → OK, - // selbst wenn Inhalt nicht 1:1 dem Vorschlag entspricht. + // TXT: nur „v=…“-Präfix grob prüfen if ($type === 'TXT') { - $upperExp = strtoupper($exp); - $upperAct = strtoupper($act); - if (str_starts_with($upperExp, 'V=SPF1')) return str_starts_with($upperAct, 'V=SPF1') ? 'ok' : 'syntax'; - if (str_starts_with($upperExp, 'V=DMARC1')) return str_starts_with($upperAct, 'V=DMARC1') ? 'ok' : 'syntax'; - if (str_starts_with($upperExp, 'V=DKIM1')) return str_starts_with($upperAct, 'V=DKIM1') ? 'ok' : 'syntax'; - return ($act !== '') ? 'ok' : ($optional ? 'neutral' : 'missing'); + $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'; } - // MX: „prio host“ – wir prüfen Host grob if ($type === 'MX') { - $parts = preg_split('/\s+/', $act); - $host = strtolower($parts[1] ?? $act); + $parts = preg_split('/\s+/', $act); + $host = strtolower($parts[1] ?? $act); $expHost = strtolower(preg_replace('/^\d+\s+/', '', $exp)); return ($host === $expHost) ? 'ok' : 'syntax'; } - // SRV: „prio weight port host“ – Port + Host grob if ($type === 'SRV') { $ap = preg_split('/\s+/', $act); $ep = preg_split('/\s+/', $exp); if (count($ap) >= 4 && count($ep) >= 4) { - $aport = (int)$ap[2]; - $eport = (int)$ep[2]; - $ahost = strtolower(rtrim(end($ap), '.')); - $ehost = strtolower(rtrim(end($ep), '.')); - return ($aport === $eport && $ahost === $ehost) ? 'ok' : 'syntax'; + return ((int)$ap[2] === (int)$ep[2] && strtolower(end($ap)) === strtolower(end($ep))) ? 'ok' : 'syntax'; } return 'syntax'; } - // CNAME/A/AAAA/PTR/TLSA: Gleichheit nach Normalisierung + // 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); $t = strtoupper($type); if (in_array($t, ['MX','CNAME','SRV'])) $v = rtrim($v, '.'); if ($t === 'PTR') $v = strtolower(rtrim($v, '.')); - if ($t === 'TLSA') $v = preg_replace('/\s+/', ' ', $v); + if ($t === 'TLSA') $v = $this->canonicalizeTlsa($v) ?? $v; return $v; } @@ -346,18 +460,38 @@ class DomainDnsModal extends ModalComponent $t = strtoupper($type); if (in_array($t, ['MX','CNAME','SRV'])) $v = rtrim($v, '.'); if ($t === 'PTR') $v = strtolower(rtrim($v, '.')); - if ($t === 'TLSA') { - $v = preg_replace('/\s+/', '', $v); // Hash-Zeilen zusammenfügen - $v = preg_replace('/^([0-3][\s]+[01][\s]+[123])/', '$1 ', $v); // spacing nach Header erzwingen - } + 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), @@ -365,7 +499,6 @@ class DomainDnsModal extends ModalComponent '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, }; diff --git a/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php b/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php index e8afe00..2d68bb9 100644 --- a/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php +++ b/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php @@ -45,20 +45,12 @@
{{ $r['value'] }}
- @if($checked && filled(trim($r['actual'] ?? '')) && ($r['state'] ?? '') !== 'ok')
+ @if($checked && ($r['state'] ?? 'neutral') !== 'ok' && !empty($r['display_actual']))
{{ $r['value'] }}
- @if($checked && filled(trim($r['actual'] ?? '')) && ($r['state'] ?? '') !== 'ok')
+ @if($checked && ($r['state'] ?? 'neutral') !== 'ok' && !empty($r['display_actual']))
{{ $r['value'] }}
-
- @if($checked && filled(trim($r['actual'] ?? '')) && ($r['state'] ?? '') !== 'ok')
+ @if($checked && ($r['state'] ?? 'neutral') !== 'ok' && !empty($r['display_actual']))