1280 lines
52 KiB
PHP
1280 lines
52 KiB
PHP
<?php
|
||
|
||
namespace App\Livewire\Ui\Domain\Modal;
|
||
|
||
use App\Models\Domain;
|
||
use App\Models\TlsaRecord;
|
||
use App\Support\NetProbe;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Livewire\Attributes\On;
|
||
use LivewireUI\Modal\ModalComponent;
|
||
|
||
class DomainDnsModal extends ModalComponent
|
||
{
|
||
public int $domainId;
|
||
public string $domainName = '';
|
||
public string $zone = '';
|
||
public string $ttl = '3600';
|
||
public array $recordColors = [];
|
||
|
||
/** @var array<int,array<string,string|int|null>> */
|
||
public array $static = [];
|
||
/** @var array<int,array<string,string|int|null>> */
|
||
public array $dynamic = [];
|
||
/** @var array<int,array<string,string|int|null>> */
|
||
public array $optional = [];
|
||
|
||
/** Farbklassen für den Status */
|
||
public array $stateColors = [
|
||
'ok' => 'border-emerald-400/30 bg-emerald-500/10',
|
||
'missing' => 'border-rose-400/30 bg-rose-500/10',
|
||
'syntax' => 'border-amber-400/30 bg-amber-500/10',
|
||
'neutral' => 'border-white/10 bg-white/5',
|
||
];
|
||
|
||
/** erst nach Klick färben */
|
||
public bool $checked = false;
|
||
|
||
public static function modalMaxWidth(): string { return '6xl'; }
|
||
|
||
public function mount(int $domainId): void
|
||
{
|
||
$this->recordColors = [
|
||
'A' => 'bg-cyan-500/20 text-cyan-300',
|
||
'AAAA' => 'bg-blue-500/20 text-blue-300',
|
||
'MX' => 'bg-emerald-500/20 text-emerald-300',
|
||
'CNAME' => 'bg-indigo-500/20 text-indigo-300',
|
||
'PTR' => 'bg-amber-500/20 text-amber-300',
|
||
'TXT' => 'bg-violet-500/20 text-violet-300',
|
||
'SRV' => 'bg-rose-500/20 text-rose-300',
|
||
'TLSA' => 'bg-red-500/20 text-red-300',
|
||
'OPTIONAL' => 'bg-gray-500/20 text-gray-300',
|
||
];
|
||
|
||
$d = Domain::findOrFail($domainId);
|
||
$this->domainId = $domainId;
|
||
$this->domainName = $d->domain;
|
||
|
||
$tlsa = TlsaRecord::forServer()->where('service', '_25._tcp')->latest('id')->first();
|
||
|
||
$ips = NetProbe::resolve();
|
||
$ipv4 = $ips['ipv4'];
|
||
$ipv6 = $ips['ipv6'];
|
||
|
||
$this->zone = $this->extractZone($d->domain);
|
||
$mta_sub = env('MTA_SUB');
|
||
$base = env('BASE_DOMAIN');
|
||
$mailServerFqdn = "{$mta_sub}.{$base}";
|
||
|
||
// --- Infrastruktur (global) ---
|
||
$this->static = [
|
||
['type' => 'A', 'name' => $mailServerFqdn, 'value' => $ipv4],
|
||
['type' => 'PTR', 'name' => $this->ptrFromIPv4($ipv4), 'value' => $mailServerFqdn],
|
||
];
|
||
if ($ipv6) {
|
||
$this->static[] = ['type' => 'AAAA','name' => $mailServerFqdn, 'value' => $ipv6];
|
||
$this->static[] = ['type' => 'PTR', 'name' => $this->ptrFromIPv6($ipv6), 'value' => $mailServerFqdn];
|
||
}
|
||
if ($tlsa?->dns_string) {
|
||
$this->static[] = [
|
||
'type' => 'TLSA',
|
||
'name' => "{$tlsa->service}.{$tlsa->host}",
|
||
'value' => "{$tlsa->usage} {$tlsa->selector} {$tlsa->matching} {$tlsa->hash}",
|
||
];
|
||
}
|
||
|
||
// --- Domain-spezifisch ---
|
||
$spf = "v=spf1 ip4:{$ipv4}" . ($ipv6 ? " ip6:{$ipv6}" : '') . " mx -all";
|
||
$dmarc = "v=DMARC1; p=none; rua=mailto:dmarc@{$this->domainName}; pct=100";
|
||
|
||
$dkim = DB::table('dkim_keys')->where('domain_id', $d->id)->where('is_active', 1)->orderByDesc('id')->first();
|
||
$selector = $dkim ? $dkim->selector : 'mwl1';
|
||
$dkimHost = "{$selector}._domainkey.{$this->domainName}";
|
||
$dkimTxt = $dkim && !str_starts_with(trim($dkim->public_key_txt), 'v=')
|
||
? 'v=DKIM1; k=rsa; p=' . trim($dkim->public_key_txt)
|
||
: (trim($dkim->public_key_txt ?? '') ?: 'v=DKIM1; k=rsa; p=');
|
||
|
||
$this->dynamic = [
|
||
['type' => 'MX', 'name' => $this->domainName, 'value' => "10 {$mailServerFqdn}."],
|
||
['type' => 'CNAME', 'name' => "autoconfig.{$this->domainName}", 'value' => "{$mailServerFqdn}."],
|
||
['type' => 'CNAME', 'name' => "autodiscover.{$this->domainName}", 'value' => "{$mailServerFqdn}."],
|
||
['type' => 'TXT', 'name' => $this->domainName, 'value' => $spf],
|
||
['type' => 'TXT', 'name' => "_dmarc.{$this->domainName}", 'value' => $dmarc],
|
||
['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 = [
|
||
// --- Service-Erkennung ---
|
||
[
|
||
'type' => 'SRV',
|
||
'name' => "_autodiscover._tcp.{$this->domainName}",
|
||
'value' => "0 1 443 {$mailServerFqdn}.",
|
||
'helpLabel' => 'Autodiscover SRV',
|
||
'helpUrl' => 'https://learn.microsoft.com/en-us/exchange/architecture/client-access/autodiscover',
|
||
'info' => 'Ermöglicht automatische Mailkonfiguration (v. a. für Outlook und Apple Mail).',
|
||
],
|
||
[
|
||
'type' => 'SRV',
|
||
'name' => "_imaps._tcp.{$this->domainName}",
|
||
'value' => "0 1 993 {$mailServerFqdn}.",
|
||
'helpLabel' => 'IMAPS SRV',
|
||
'helpUrl' => 'https://datatracker.ietf.org/doc/html/rfc6186',
|
||
'info' => 'Definiert den IMAP-Dienst (Port 993, SSL/TLS). Empfohlen für Clients.',
|
||
],
|
||
[
|
||
'type' => 'SRV',
|
||
'name' => "_pop3s._tcp.{$this->domainName}",
|
||
'value' => "0 1 995 {$mailServerFqdn}.",
|
||
'helpLabel' => 'POP3S SRV',
|
||
'helpUrl' => 'https://datatracker.ietf.org/doc/html/rfc6186',
|
||
'info' => 'Optional für POP3 (Port 995, SSL/TLS). Nur falls POP3 unterstützt wird.',
|
||
],
|
||
[
|
||
'type' => 'SRV',
|
||
'name' => "_submission._tcp.{$this->domainName}",
|
||
'value' => "0 1 587 {$mailServerFqdn}.",
|
||
'helpLabel' => 'Submission SRV',
|
||
'helpUrl' => 'https://datatracker.ietf.org/doc/html/rfc6409',
|
||
'info' => 'Definiert den SMTP-Submission-Dienst (Port 587, STARTTLS).',
|
||
],
|
||
|
||
// --- Sicherheit & Policies ---
|
||
[
|
||
'type' => 'TXT',
|
||
'name' => "_mta-sts.{$this->domainName}",
|
||
'value' => "v=STSv1; id=20250101T000000Z;",
|
||
'helpLabel' => 'MTA-STS Policy',
|
||
'helpUrl' => 'https://datatracker.ietf.org/doc/html/rfc8461',
|
||
'info' => 'Sichert SMTP-Verbindungen durch verpflichtendes TLS (Mail Transport Security).',
|
||
],
|
||
[
|
||
'type' => 'CNAME',
|
||
'name' => "mta-sts.{$this->domainName}",
|
||
'value' => "{$mailServerFqdn}.",
|
||
'helpLabel' => 'MTA-STS Host',
|
||
'helpUrl' => 'https://datatracker.ietf.org/doc/html/rfc8461',
|
||
'info' => 'Zeigt auf den Server, der die MTA-STS-Policy unter /.well-known/mta-sts.txt bereitstellt.',
|
||
],
|
||
[
|
||
'type' => 'TXT',
|
||
'name' => "_smtp._tls.{$this->domainName}",
|
||
'value' => "v=TLSRPTv1; rua=mailto:tlsrpt@{$this->domainName}",
|
||
'helpLabel' => 'TLS-RPT (Reporting)',
|
||
'helpUrl' => 'https://datatracker.ietf.org/doc/html/rfc8460',
|
||
'info' => 'Ermöglicht Berichte zu fehlerhaften TLS-Verbindungen beim Mailversand (TLS Reporting).',
|
||
],
|
||
|
||
// --- Komfort / Web ---
|
||
[
|
||
'type' => 'CNAME',
|
||
'name' => env('WEBMAIL_SUB') . ".{$this->domainName}",
|
||
'value' => "{$mailServerFqdn}.",
|
||
'helpLabel' => 'Webmail Alias',
|
||
'helpUrl' => 'https://en.wikipedia.org/wiki/CNAME_record',
|
||
'info' => 'Erlaubt Zugriff auf Webmail unter webmail.domain.tld.',
|
||
],
|
||
];
|
||
// wichtig: initial neutral einfärben (kein Check gelaufen)
|
||
$this->applyNeutralColors();
|
||
}
|
||
|
||
private function applyNeutralColors(): void
|
||
{
|
||
foreach (['static', 'dynamic', 'optional'] as $group) {
|
||
$this->{$group} = array_map(fn($r) => array_merge($r, [
|
||
'state' => 'neutral',
|
||
'boxClass' => $this->stateColors['neutral'],
|
||
'actual' => '',
|
||
]), $this->{$group});
|
||
}
|
||
}
|
||
|
||
/** Klick im Footer löst diesen Check aus */
|
||
#[On('domain:check-dns')]
|
||
public function checkDns(): void
|
||
{
|
||
// dynamische (Pflicht) Records prüfen
|
||
foreach ($this->dynamic as $i => $r) {
|
||
$actual = $this->dig($r['type'], $r['name']);
|
||
$state = $this->stateFor($r['type'], $r['value'], $actual, optional:false);
|
||
$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
|
||
foreach ($this->static as $i => $r) {
|
||
$actual = $this->dig($r['type'], $r['name']);
|
||
$state = $this->stateFor($r['type'], $r['value'], $actual, optional:false);
|
||
$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
|
||
foreach ($this->optional as $i => $r) {
|
||
$actual = $this->dig($r['type'], $r['name']);
|
||
$state = $this->stateFor($r['type'], $r['value'], $actual, optional:true);
|
||
$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;
|
||
}
|
||
|
||
/** Prüft eine Record-Liste und gibt die Liste mit 'state' zurück */
|
||
private function checkGroup(array $group, bool $optional = false): array
|
||
{
|
||
foreach ($group as &$r) {
|
||
$type = (string)$r['type'];
|
||
$name = (string)$r['name'];
|
||
$exp = (string)$r['value'];
|
||
|
||
$act = $this->dig($type, $name);
|
||
|
||
if ($act === '') {
|
||
$r['state'] = $optional ? 'missing' : 'missing';
|
||
continue;
|
||
}
|
||
|
||
$ok = $this->compareDns($type, $exp, $act);
|
||
$r['state'] = $ok ? 'ok' : 'mismatch';
|
||
}
|
||
return $group;
|
||
}
|
||
|
||
/* ---------- 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, '.');
|
||
|
||
$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));
|
||
return $joined;
|
||
}
|
||
|
||
// 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)) ?? '';
|
||
}
|
||
|
||
// 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';
|
||
|
||
$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);
|
||
$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) */
|
||
private function compareDns(string $type, string $expected, string $actual): bool
|
||
{
|
||
$type = strtoupper($type);
|
||
|
||
$norm = function (string $s): string {
|
||
$s = trim($s);
|
||
$s = trim($s, "\"'");
|
||
$s = preg_replace('/\s+/', ' ', $s);
|
||
$s = rtrim($s, '.');
|
||
return $s;
|
||
};
|
||
|
||
if ($type === 'TXT') {
|
||
$parts = array_map(
|
||
fn($line) => $norm($line),
|
||
preg_split('/\R+/', $actual)
|
||
);
|
||
$actualFlat = implode('', array_map(fn($p) => str_replace(['"',' '], '', $p), $parts));
|
||
$expectedFlat = str_replace(['"',' '], '', $norm($expected));
|
||
return stripos($actualFlat, $expectedFlat) !== false;
|
||
}
|
||
|
||
if ($type === 'MX') {
|
||
$ax = array_filter(array_map('trim', preg_split('/\R+/', $actual)));
|
||
foreach ($ax as $line) {
|
||
$line = $norm($line);
|
||
if (stripos($line, $norm($expected)) !== false) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
if (in_array($type, ['A','AAAA','CNAME','PTR','SRV','TLSA'], true)) {
|
||
$ax = array_filter(array_map('trim', preg_split('/\R+/', $actual)));
|
||
$exp = $norm($expected);
|
||
foreach ($ax as $line) {
|
||
if ($norm($line) === $exp) return true;
|
||
}
|
||
if (in_array($type, ['CNAME','SRV','TLSA'], true)) {
|
||
foreach ($ax as $line) {
|
||
if (stripos($norm($line), rtrim($exp, '.')) !== false) return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
return stripos($actual, $expected) !== false;
|
||
}
|
||
|
||
private function extractZone(string $fqdn): string
|
||
{
|
||
$fqdn = strtolower(trim($fqdn, "."));
|
||
$parts = explode('.', $fqdn);
|
||
$n = count($parts);
|
||
return $n >= 2 ? $parts[$n - 2] . '.' . $parts[$n - 1] : $fqdn;
|
||
}
|
||
|
||
private function ptrFromIPv4(string $ip): string
|
||
{
|
||
$p = array_reverse(explode('.', $ip));
|
||
return implode('.', $p) . '.in-addr.arpa';
|
||
}
|
||
|
||
private function ptrFromIPv6(string $ip): string
|
||
{
|
||
$bin = @inet_pton($ip);
|
||
if ($bin === false) return '';
|
||
$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()
|
||
{
|
||
return view('livewire.ui.domain.modal.domain-dns-modal');
|
||
}
|
||
}
|
||
//namespace App\Livewire\Ui\Domain\Modal;
|
||
//
|
||
//use App\Models\Domain;
|
||
//use App\Models\TlsaRecord;
|
||
//use App\Support\NetProbe;
|
||
//use Illuminate\Support\Facades\DB;
|
||
//use Livewire\Attributes\On;
|
||
//use LivewireUI\Modal\ModalComponent;
|
||
//
|
||
//class DomainDnsModal extends ModalComponent
|
||
//{
|
||
// public int $domainId;
|
||
// public string $domainName = '';
|
||
// public string $zone = '';
|
||
// public string $ttl = '3600';
|
||
// public array $recordColors = [];
|
||
//
|
||
// /** @var array<int,array<string,string|int|null>> */
|
||
// public array $static = [];
|
||
// /** @var array<int,array<string,string|int|null>> */
|
||
// public array $dynamic = [];
|
||
// /** @var array<int,array<string,string|int|null>> */
|
||
// public array $optional = [];
|
||
//
|
||
// /** Farbklassen für den Status */
|
||
// public array $stateColors = [
|
||
// 'ok' => 'border-emerald-400/30 bg-emerald-500/10',
|
||
// 'missing' => 'border-rose-400/30 bg-rose-500/10',
|
||
// 'syntax' => 'border-amber-400/30 bg-amber-500/10',
|
||
// 'neutral' => 'border-white/10 bg-white/5',
|
||
// ];
|
||
//
|
||
// /** erst nach Klick färben */
|
||
// public bool $checked = false;
|
||
//
|
||
// public static function modalMaxWidth(): string { return '6xl'; }
|
||
//
|
||
// public function mount(int $domainId): void
|
||
// {
|
||
// $this->recordColors = [
|
||
// 'A' => 'bg-cyan-500/20 text-cyan-300',
|
||
// 'AAAA' => 'bg-blue-500/20 text-blue-300',
|
||
// 'MX' => 'bg-emerald-500/20 text-emerald-300',
|
||
// 'CNAME' => 'bg-indigo-500/20 text-indigo-300',
|
||
// 'PTR' => 'bg-amber-500/20 text-amber-300',
|
||
// 'TXT' => 'bg-violet-500/20 text-violet-300',
|
||
// 'SRV' => 'bg-rose-500/20 text-rose-300',
|
||
// 'TLSA' => 'bg-red-500/20 text-red-300',
|
||
// 'OPTIONAL' => 'bg-gray-500/20 text-gray-300',
|
||
// ];
|
||
//
|
||
// $d = Domain::findOrFail($domainId);
|
||
// $this->domainId = $domainId;
|
||
// $this->domainName = $d->domain;
|
||
//
|
||
// $tlsa = TlsaRecord::forServer()->where('service', '_25._tcp')->latest('id')->first();
|
||
//
|
||
// $ips = NetProbe::resolve();
|
||
// $ipv4 = $ips['ipv4'];
|
||
// $ipv6 = $ips['ipv6'];
|
||
//
|
||
// $this->zone = $this->extractZone($d->domain);
|
||
// $mta_sub = env('MTA_SUB');
|
||
// $base = env('BASE_DOMAIN');
|
||
// $mailServerFqdn = "{$mta_sub}.{$base}";
|
||
//
|
||
// // --- Infrastruktur (global) ---
|
||
// $this->static = [
|
||
// ['type' => 'A', 'name' => $mailServerFqdn, 'value' => $ipv4],
|
||
// ['type' => 'PTR', 'name' => $this->ptrFromIPv4($ipv4), 'value' => $mailServerFqdn],
|
||
// ];
|
||
// if ($ipv6) {
|
||
// $this->static[] = ['type' => 'AAAA','name' => $mailServerFqdn, 'value' => $ipv6];
|
||
// $this->static[] = ['type' => 'PTR', 'name' => $this->ptrFromIPv6($ipv6), 'value' => $mailServerFqdn];
|
||
// }
|
||
// if ($tlsa?->dns_string) {
|
||
// $this->static[] = [
|
||
// 'type' => 'TLSA',
|
||
// 'name' => "{$tlsa->service}.{$tlsa->host}",
|
||
// 'value' => "{$tlsa->usage} {$tlsa->selector} {$tlsa->matching} {$tlsa->hash}",
|
||
// ];
|
||
// }
|
||
//
|
||
// // --- Domain-spezifisch ---
|
||
// $spf = "v=spf1 ip4:{$ipv4}" . ($ipv6 ? " ip6:{$ipv6}" : '') . " mx -all";
|
||
// $dmarc = "v=DMARC1; p=none; rua=mailto:dmarc@{$this->domainName}; pct=100";
|
||
//
|
||
// $dkim = DB::table('dkim_keys')->where('domain_id', $d->id)->where('is_active', 1)->orderByDesc('id')->first();
|
||
// $selector = $dkim ? $dkim->selector : 'mwl1';
|
||
// $dkimHost = "{$selector}._domainkey.{$this->domainName}";
|
||
// $dkimTxt = $dkim && !str_starts_with(trim($dkim->public_key_txt), 'v=')
|
||
// ? 'v=DKIM1; k=rsa; p=' . trim($dkim->public_key_txt)
|
||
// : (trim($dkim->public_key_txt ?? '') ?: 'v=DKIM1; k=rsa; p=');
|
||
//
|
||
// $this->dynamic = [
|
||
// ['type' => 'MX', 'name' => $this->domainName, 'value' => "10 {$mailServerFqdn}."],
|
||
// ['type' => 'CNAME', 'name' => "autoconfig.{$this->domainName}", 'value' => "{$mailServerFqdn}."],
|
||
// ['type' => 'CNAME', 'name' => "autodiscover.{$this->domainName}",'value' => "{$mailServerFqdn}."],
|
||
// ['type' => 'TXT', 'name' => $this->domainName, 'value' => $spf],
|
||
// ['type' => 'TXT', 'name' => "_dmarc.{$this->domainName}", 'value' => $dmarc],
|
||
// ['type' => 'TXT', 'name' => $dkimHost, 'value' => $dkimTxt],
|
||
// ];
|
||
//
|
||
// $this->optional = [
|
||
// ['type' => 'SRV', 'name' => "_autodiscover._tcp.{$this->domainName}", 'value' => "0 0 443 {$mailServerFqdn}."],
|
||
// ['type' => 'SRV', 'name' => "_imaps._tcp.{$this->domainName}", 'value' => "0 0 993 {$mailServerFqdn}."],
|
||
// ['type' => 'SRV', 'name' => "_pop3s._tcp.{$this->domainName}", 'value' => "0 0 995 {$mailServerFqdn}."],
|
||
// ['type' => 'SRV', 'name' => "_submission._tcp.{$this->domainName}", 'value' => "0 0 587 {$mailServerFqdn}."],
|
||
// ];
|
||
// }
|
||
//
|
||
// private function applyNeutralColors(): void
|
||
// {
|
||
// foreach (['static', 'dynamic', 'optional'] as $group) {
|
||
// $this->{$group} = array_map(fn($r) => array_merge($r, [
|
||
// 'state' => 'neutral',
|
||
// 'boxClass' => $this->stateColors['neutral'],
|
||
// 'actual' => '',
|
||
// ]), $this->{$group});
|
||
// }
|
||
// }
|
||
//
|
||
// /** Klick im Footer löst diesen Check aus */
|
||
// #[On('domain:check-dns')]
|
||
// public function checkDns(): void
|
||
// {
|
||
// // dynamische (pflicht) Records prüfen
|
||
// foreach ($this->dynamic as $i => $r) {
|
||
// $actual = $this->dig($r['type'], $r['name']);
|
||
// $state = $this->stateFor($r['type'], $r['value'], $actual, optional:false);
|
||
// $this->dynamic[$i]['actual'] = $actual;
|
||
// $this->dynamic[$i]['state'] = $state;
|
||
// }
|
||
//
|
||
// // statische (pflicht) Records prüfen (TLSA, PTR, A/AAAA …)
|
||
// foreach ($this->static as $i => $r) {
|
||
// $actual = $this->dig($r['type'], $r['name']);
|
||
// $state = $this->stateFor($r['type'], $r['value'], $actual, optional:false);
|
||
// $this->static[$i]['actual'] = $actual;
|
||
// $this->static[$i]['state'] = $state;
|
||
// }
|
||
//
|
||
// // optionale Records: nie „missing“, nur neutral|syntax|ok
|
||
// foreach ($this->optional as $i => $r) {
|
||
// $actual = $this->dig($r['type'], $r['name']);
|
||
// $state = $this->stateFor($r['type'], $r['value'], $actual, optional:true);
|
||
// $this->optional[$i]['actual'] = $actual;
|
||
// $this->optional[$i]['state'] = $state;
|
||
// }
|
||
//
|
||
// $this->checked = true;
|
||
// }
|
||
//
|
||
// /** Prüft eine Record-Liste und gibt die Liste mit 'state' zurück */
|
||
// private function checkGroup(array $group, bool $optional = false): array
|
||
// {
|
||
// foreach ($group as &$r) {
|
||
// $type = (string)$r['type'];
|
||
// $name = (string)$r['name'];
|
||
// $exp = (string)$r['value'];
|
||
//
|
||
// $act = $this->dig($type, $name);
|
||
//
|
||
// if ($act === '') {
|
||
// $r['state'] = $optional ? 'missing' : 'missing';
|
||
// continue;
|
||
// }
|
||
//
|
||
// $ok = $this->compareDns($type, $exp, $act);
|
||
// $r['state'] = $ok ? 'ok' : 'mismatch';
|
||
// }
|
||
// return $group;
|
||
// }
|
||
//
|
||
// /** dig wrapper */
|
||
// /* ---------- DNS & Bewertung ---------- */
|
||
//
|
||
// private function dig(string $type, string $name): string
|
||
// {
|
||
// $type = strtoupper($type);
|
||
// $name = rtrim($name, '.'); // wir arbeiten 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 (mehrere Antworten -> erste reicht)
|
||
// 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 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';
|
||
//
|
||
// // Bei TXT/Policies: inhaltlich locker matchen (Beginn mit v=…)
|
||
// if ($type === 'TXT') {
|
||
// // SPF
|
||
// if (str_starts_with(strtoupper($exp), 'V=SPF1')) {
|
||
// return str_starts_with(strtoupper($act), 'V=SPF1') ? 'ok' : 'syntax';
|
||
// }
|
||
// // DMARC
|
||
// if (str_starts_with(strtoupper($exp), 'V=DMARC1')) {
|
||
// return str_starts_with(strtoupper($act), 'V=DMARC1') ? 'ok' : 'syntax';
|
||
// }
|
||
// // DKIM
|
||
// if (str_starts_with(strtoupper($exp), 'V=DKIM1')) {
|
||
// return str_starts_with(strtoupper($act), 'V=DKIM1') ? 'ok' : 'syntax';
|
||
// }
|
||
// return ($act !== '') ? 'ok' : ($optional ? 'neutral' : 'missing');
|
||
// }
|
||
//
|
||
// // MX: wir erwarten „prio host“ – wir prüfen nur, ob host passt
|
||
// if ($type === 'MX') {
|
||
// // $act z.B. "10 mx.example.tld"
|
||
// $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“ – prüfen 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: einfacher Gleichheitsvergleich (normalisiert)
|
||
// 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);
|
||
// 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]{32,}$/i', $val),
|
||
// 'TXT' => strlen($val) > 0, // SPF/DMARC/DKIM oben extra geprüft
|
||
// default => true,
|
||
// };
|
||
// }
|
||
//
|
||
// /** Vergleich je Typ robust normalisieren */
|
||
// private function compareDns(string $type, string $expected, string $actual): bool
|
||
// {
|
||
// $type = strtoupper($type);
|
||
//
|
||
// // Normalisierer
|
||
// $norm = function (string $s): string {
|
||
// $s = trim($s);
|
||
// $s = trim($s, "\"'"); // TXT quotes
|
||
// $s = preg_replace('/\s+/', ' ', $s); // unify spaces
|
||
// $s = rtrim($s, '.'); // trailing dot
|
||
// return $s;
|
||
// };
|
||
//
|
||
// // TXT kann aus mehreren quoted Strings bestehen (mehrere Zeilen)
|
||
// if ($type === 'TXT') {
|
||
// // dig kann mehrere Zeilen liefern, jede in Quotes – zusammenfügen
|
||
// $parts = array_map(
|
||
// fn($line) => $norm($line),
|
||
// preg_split('/\R+/', $actual)
|
||
// );
|
||
// $actualFlat = implode('', array_map(fn($p) => str_replace(['"',' '], '', $p), $parts));
|
||
// $expectedFlat = str_replace(['"',' '], '', $norm($expected));
|
||
// return stripos($actualFlat, $expectedFlat) !== false;
|
||
// }
|
||
//
|
||
// if ($type === 'MX') {
|
||
// // Erwartet i.d.R. "10 host." – wir normalisieren beide Seiten
|
||
// $ax = array_filter(array_map('trim', preg_split('/\R+/', $actual)));
|
||
// foreach ($ax as $line) {
|
||
// $line = $norm($line);
|
||
// if (stripos($line, $norm($expected)) !== false) return true;
|
||
// }
|
||
// return false;
|
||
// }
|
||
//
|
||
// if (in_array($type, ['A','AAAA','CNAME','PTR','SRV','TLSA'], true)) {
|
||
// $ax = array_filter(array_map('trim', preg_split('/\R+/', $actual)));
|
||
// $exp = $norm($expected);
|
||
// foreach ($ax as $line) {
|
||
// if ($norm($line) === $exp) return true;
|
||
// }
|
||
// // bei CNAME/SRV/TLSA reicht oft „enthält denselben Zielhost“
|
||
// if (in_array($type, ['CNAME','SRV','TLSA'], true)) {
|
||
// foreach ($ax as $line) {
|
||
// if (stripos($norm($line), rtrim($exp, '.')) !== false) return true;
|
||
// }
|
||
// }
|
||
// return false;
|
||
// }
|
||
//
|
||
// // Fallback: substring
|
||
// return stripos($actual, $expected) !== false;
|
||
// }
|
||
//
|
||
// private function extractZone(string $fqdn): string
|
||
// {
|
||
// $fqdn = strtolower(trim($fqdn, "."));
|
||
// $parts = explode('.', $fqdn);
|
||
// $n = count($parts);
|
||
// return $n >= 2 ? $parts[$n - 2] . '.' . $parts[$n - 1] : $fqdn;
|
||
// }
|
||
//
|
||
// private function ptrFromIPv4(string $ip): string
|
||
// {
|
||
// $p = array_reverse(explode('.', $ip));
|
||
// return implode('.', $p) . '.in-addr.arpa';
|
||
// }
|
||
//
|
||
// private function ptrFromIPv6(string $ip): string
|
||
// {
|
||
// $expanded = strtolower(inet_ntop(inet_pton($ip)));
|
||
// $hex = str_replace(':', '', $expanded);
|
||
// return implode('.', array_reverse(str_split($hex))) . '.ip6.arpa';
|
||
// }
|
||
//
|
||
// public function render()
|
||
// {
|
||
// return view('livewire.ui.domain.modal.domain-dns-modal');
|
||
// }
|
||
//}
|
||
|
||
//namespace App\Livewire\Ui\Domain\Modal;
|
||
//
|
||
//use App\Models\Domain;
|
||
//use App\Models\TlsaRecord;
|
||
//use App\Support\NetProbe;
|
||
//use Illuminate\Support\Facades\DB;
|
||
//use Livewire\Attributes\On;
|
||
//use LivewireUI\Modal\ModalComponent;
|
||
//
|
||
//class DomainDnsModal extends ModalComponent
|
||
//{
|
||
// public int $domainId;
|
||
// public string $domainName = '';
|
||
// public string $zone = '';
|
||
// public string $ttl = '3600';
|
||
// public array $recordColors = [];
|
||
//
|
||
// /** @var array<int,array<string,string|int|null>> */
|
||
// public array $static = [];
|
||
// /** @var array<int,array<string,string|int|null>> */
|
||
// public array $dynamic = [];
|
||
// /** @var array<int,array<string,string|int|null>> */
|
||
// public array $optional = [];
|
||
//
|
||
// public array $stateColors = [
|
||
// 'ok' => 'text-emerald-300 bg-emerald-500/10 border-emerald-400/30',
|
||
// 'missing' => 'text-rose-300 bg-rose-500/10 border-rose-400/30',
|
||
// 'mismatch' => 'text-amber-200 bg-amber-500/10 border-amber-400/30',
|
||
// ];
|
||
//
|
||
// public static function modalMaxWidth(): string
|
||
// {
|
||
// return '6xl';
|
||
// }
|
||
//
|
||
// public function mount(int $domainId): void
|
||
// {
|
||
// $this->recordColors = [
|
||
// 'A' => 'bg-cyan-500/20 text-cyan-300',
|
||
// 'AAAA' => 'bg-blue-500/20 text-blue-300',
|
||
// 'MX' => 'bg-emerald-500/20 text-emerald-300',
|
||
// 'CNAME' => 'bg-indigo-500/20 text-indigo-300',
|
||
// 'PTR' => 'bg-amber-500/20 text-amber-300',
|
||
// 'TXT' => 'bg-violet-500/20 text-violet-300',
|
||
// 'SRV' => 'bg-rose-500/20 text-rose-300',
|
||
// 'TLSA' => 'bg-red-500/20 text-red-300',
|
||
// 'OPTIONAL' => 'bg-gray-500/20 text-gray-300',
|
||
// ];
|
||
//
|
||
// $d = Domain::findOrFail($domainId);
|
||
// $this->domainName = $d->domain;
|
||
//
|
||
// $tlsa = TlsaRecord::forServer()
|
||
// ->where('service', '_25._tcp')
|
||
// ->latest('id')
|
||
// ->first();
|
||
//
|
||
// $ips = NetProbe::resolve();
|
||
// $ipv4 = $ips['ipv4'];
|
||
// $ipv6 = $ips['ipv6'];
|
||
//
|
||
// $this->zone = $this->extractZone($d->domain);
|
||
// $mta_sub = env('MTA_SUB');
|
||
// $base = env('BASE_DOMAIN');
|
||
// $mailServerFqdn = $mta_sub . '.' . $base;
|
||
// $mta = $mta_sub . '.' . $this->zone;
|
||
//
|
||
// // --- Statische Infrastruktur (für alle Domains gleich) ---
|
||
// $this->static = [
|
||
// ['type' => 'A', 'name' => $mailServerFqdn, 'value' => $ipv4],
|
||
// ['type' => 'PTR', 'name' => $this->ptrFromIPv4($ipv4), 'value' => $tlsa->host],
|
||
// ];
|
||
// if ($ipv6) {
|
||
// $this->static[] = ['type' => 'AAAA', 'name' => $mailServerFqdn, 'value' => $ipv6];
|
||
// $this->static[] = ['type' => 'PTR', 'name' => $this->ptrFromIPv6($ipv6), 'value' => $mailServerFqdn];
|
||
// }
|
||
//
|
||
//
|
||
// if ($tlsa?->dns_string) {
|
||
// $this->static[] = [
|
||
// 'type' => 'TLSA',
|
||
// 'name' => "{$tlsa->service}.{$tlsa->host}",
|
||
// 'value' => "{$tlsa->usage} {$tlsa->selector} {$tlsa->matching} {$tlsa->hash}",
|
||
// ];
|
||
// }
|
||
//
|
||
// // --- Domain-spezifisch ---
|
||
// $spf = "v=spf1 ip4:{$ipv4} ip6:{$ipv6} mx -all";
|
||
// $dmarc = "v=DMARC1; p=none; rua=mailto:dmarc@{$this->domainName}; pct=100";
|
||
//
|
||
// $dkim = DB::table('dkim_keys')
|
||
// ->where('domain_id', $d->id)->where('is_active', 1)->orderByDesc('id')->first();
|
||
// $selector = $dkim ? $dkim->selector : 'mwl1';
|
||
// $dkimHost = "{$selector}._domainkey.{$this->domainName}";
|
||
// $dkimTxt = $dkim && !str_starts_with(trim($dkim->public_key_txt), 'v=')
|
||
// ? 'v=DKIM1; k=rsa; p=' . $dkim->public_key_txt
|
||
// : ($dkim->public_key_txt ?? 'v=DKIM1; k=rsa; p=');
|
||
//
|
||
// $this->dynamic = [
|
||
// ['type' => 'MX', 'name' => $this->domainName, 'value' => "10 {$mailServerFqdn}."],
|
||
//
|
||
// ['type' => 'CNAME', 'name' => "autoconfig.$this->domainName", 'value' => "$mailServerFqdn."],
|
||
// ['type' => 'CNAME', 'name' => "autodiscover.$this->domainName", 'value' => "$mailServerFqdn."],
|
||
//
|
||
// // TXT Records
|
||
// ['type' => 'TXT', 'name' => $this->domainName, 'value' => $spf, 'helpLabel' => 'SPF Record Syntax', 'helpUrl' => 'http://www.open-spf.org/SPF_Record_Syntax/'],
|
||
// ['type' => 'TXT', 'name' => "_dmarc.{$this->domainName}", 'value' => $dmarc, 'helpLabel' => 'DMARC Assistant', 'helpUrl' => 'https://www.kitterman.com/dmarc/assistant.html'],
|
||
// ['type' => 'TXT', 'name' => $dkimHost, 'value' => $dkimTxt, 'helpLabel' => 'DKIM Inspector', 'helpUrl' => 'https://dkimvalidator.com/'],
|
||
//
|
||
// ];
|
||
//
|
||
// $this->optional = [
|
||
// // SRV Records für Autodiscover und Maildienste
|
||
// ['type' => 'SRV', 'name' => "_autodiscover._tcp.$this->domainName", 'value' => "0 0 443 {$mailServerFqdn}."],
|
||
// ['type' => 'SRV', 'name' => "_imaps._tcp.$this->domainName", 'value' => "0 0 993 {$mailServerFqdn}."],
|
||
// ['type' => 'SRV', 'name' => "_pop3s._tcp.$this->domainName", 'value' => "0 0 995 {$mailServerFqdn}."],
|
||
// ['type' => 'SRV', 'name' => "_submission._tcp.$this->domainName", 'value' => "0 0 587 {$mailServerFqdn}."],
|
||
// ];
|
||
//
|
||
// }
|
||
//
|
||
// #[On('domain:check-dns')]
|
||
// public function checkDns(): void
|
||
// {
|
||
// $this->validateDnsRecords();
|
||
// }
|
||
//
|
||
//
|
||
// private function validateDnsRecords(): void
|
||
// {
|
||
// // alle Listen zusammenführen
|
||
// $records = array_merge($this->static, $this->dynamic);
|
||
//
|
||
// foreach ($records as &$record) {
|
||
// $type = $record['type'];
|
||
// $name = $record['name'];
|
||
// $expected = trim((string) $record['value']);
|
||
//
|
||
// $result = @shell_exec("dig +short {$type} {$name} 2>/dev/null");
|
||
// $result = trim((string) $result);
|
||
//
|
||
// if ($result === '') {
|
||
// $record['state'] = 'missing';
|
||
// } elseif (stripos($result, trim($expected, '.')) !== false) {
|
||
// $record['state'] = 'ok';
|
||
// } else {
|
||
// $record['state'] = 'mismatch';
|
||
// }
|
||
// }
|
||
//
|
||
// $this->static = array_values(array_filter($records, fn($r) => in_array($r['type'], ['A','AAAA','PTR','TLSA'])));
|
||
// $this->dynamic = array_values(array_filter($records, fn($r) => !in_array($r['type'], ['A','AAAA','PTR','TLSA'])));
|
||
// }
|
||
//
|
||
// private function extractZone(string $fqdn): string
|
||
// {
|
||
// $fqdn = strtolower(trim($fqdn, "."));
|
||
// $parts = explode('.', $fqdn);
|
||
// $n = count($parts);
|
||
// return $n >= 2 ? $parts[$n - 2] . '.' . $parts[$n - 1] : $fqdn; // nimmt die letzten 2 Labels
|
||
// }
|
||
//
|
||
// private function detectIPv4(): string
|
||
// {
|
||
// // robust & ohne env
|
||
// $out = @shell_exec("ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if(\$i==\"src\"){print \$(i+1); exit}}'");
|
||
// $ip = trim((string)$out);
|
||
// if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return $ip;
|
||
// $ip = trim($_SERVER['SERVER_ADDR'] ?? '');
|
||
// return $ip ?: '203.0.113.10'; // Fallback Demo
|
||
// }
|
||
//
|
||
// private function detectIPv6(): ?string
|
||
// {
|
||
// $out = @shell_exec("ip -6 route get 2001:4860:4860::8888 2>/dev/null | awk '{for(i=1;i<=NF;i++) if(\$i==\"src\"){print \$(i+1); exit}}'");
|
||
// $ip = trim((string)$out);
|
||
// return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? $ip : null;
|
||
// }
|
||
//
|
||
// private function ptrFromIPv4(string $ip): string
|
||
// {
|
||
// $p = array_reverse(explode('.', $ip));
|
||
// return implode('.', $p) . '.in-addr.arpa';
|
||
// }
|
||
//
|
||
// private function ptrFromIPv6(string $ip): string
|
||
// {
|
||
// $expanded = strtolower(inet_ntop(inet_pton($ip)));
|
||
// $hex = str_replace(':', '', $expanded);
|
||
// return implode('.', array_reverse(str_split($hex))) . '.ip6.arpa';
|
||
// }
|
||
//
|
||
// public function render()
|
||
// {
|
||
// return view('livewire.ui.domain.modal.domain-dns-modal');
|
||
// }
|
||
//
|
||
//// public int $domainId;
|
||
//// public Domain $domain;
|
||
////
|
||
//// public array $records = [];
|
||
////
|
||
//// public function mount(int $domainId): void
|
||
//// {
|
||
//// $this->domainId = $domainId;
|
||
//// $this->domain = Domain::findOrFail($domainId);
|
||
////
|
||
//// // Placeholder-Werte, sofern du sie anderswo speicherst gern ersetzen:
|
||
//// $serverIp = config('app.server_ip', 'DEINE.SERVER.IP');
|
||
//// $mxHost = config('mailwolt.mx_fqdn', 'mx.' . $this->domain->domain);
|
||
//// $selector = optional(
|
||
//// DkimKey::where('domain_id', $this->domain->id)->where('is_active', true)->first()
|
||
//// )->selector ?? 'mwl1';
|
||
////
|
||
//// $dkimTxt = optional(
|
||
//// DkimKey::where('domain_id', $this->domain->id)->where('is_active', true)->first()
|
||
//// )->public_key_txt ?? 'DKIM_PUBLIC_KEY';
|
||
////
|
||
//// $this->records = [
|
||
//// [
|
||
//// 'type' => 'A',
|
||
//// 'host' => $this->domain->domain,
|
||
//// 'value' => $serverIp,
|
||
//// 'ttl' => 3600,
|
||
//// ],
|
||
//// [
|
||
//// 'type' => 'MX',
|
||
//// 'host' => $this->domain->domain,
|
||
//// 'value' => "10 {$mxHost}.",
|
||
//// 'ttl' => 3600,
|
||
//// ],
|
||
//// [
|
||
//// 'type' => 'TXT',
|
||
//// 'host' => $this->domain->domain,
|
||
//// 'value' => 'v=spf1 mx a -all',
|
||
//// 'ttl' => 3600,
|
||
//// ],
|
||
//// [
|
||
//// 'type' => 'TXT',
|
||
//// 'host' => "{$selector}._domainkey." . $this->domain->domain,
|
||
//// // komplette, fertige TXT-Payload (aus deiner dkim_keys.public_key_txt Spalte)
|
||
//// 'value' => "v=DKIM1; k=rsa; p={$dkimTxt}",
|
||
//// 'ttl' => 3600,
|
||
//// ],
|
||
//// [
|
||
//// 'type' => 'TXT',
|
||
//// 'host' => "_dmarc." . $this->domain->domain,
|
||
//// 'value' => "v=DMARC1; p=none; rua=mailto:dmarc@" . $this->domain->domain . "; pct=100",
|
||
//// 'ttl' => 3600,
|
||
//// ],
|
||
//// ];
|
||
//// }
|
||
////
|
||
//// public static function modalMaxWidth(): string
|
||
//// {
|
||
//// return '4xl'; // schön breit
|
||
//// }
|
||
////
|
||
//// public function render()
|
||
//// {
|
||
//// return view('livewire.ui.domain.modal.domain-dns-modal');
|
||
//// }
|
||
//
|
||
//// public Domain $domain;
|
||
////
|
||
//// public static function modalMaxWidth(): string
|
||
//// {
|
||
//// return '3xl';
|
||
//// }
|
||
////
|
||
//// public function mount(int $domainId): void
|
||
//// {
|
||
//// $this->domain = Domain::with(['dkimKeys' /* falls Relationen existieren */])->findOrFail($domainId);
|
||
//// }
|
||
////
|
||
//// public function render()
|
||
//// {
|
||
//// // Hier kannst du die Records vorbereiten (SPF/DMARC/DKIM)
|
||
//// $records = [
|
||
//// [
|
||
//// 'type' => 'TXT',
|
||
//// 'host' => $this->domain->domain,
|
||
//// 'value'=> $this->domain->spf_record ?? 'v=spf1 mx a -all',
|
||
//// 'ttl' => 3600,
|
||
//// ],
|
||
//// [
|
||
//// 'type' => 'TXT',
|
||
//// 'host' => '_dmarc.' . $this->domain->domain,
|
||
//// 'value'=> $this->domain->dmarc_record ?? 'v=DMARC1; p=none; rua=mailto:dmarc@' . $this->domain->domain . '; pct=100',
|
||
//// 'ttl' => 3600,
|
||
//// ],
|
||
//// // DKIM (falls vorhanden)
|
||
//// // Beispiel: nimm den aktiven Key oder den ersten
|
||
//// ];
|
||
////
|
||
//// $dkim = optional($this->domain->dkimKeys()->where('is_active', true)->first() ?? $this->domain->dkimKeys()->first());
|
||
//// if ($dkim && $dkim->selector) {
|
||
//// $records[] = [
|
||
//// 'type' => 'TXT',
|
||
//// 'host' => "{$dkim->selector}._domainkey." . $this->domain->domain,
|
||
//// 'value'=> "v=DKIM1; k=rsa; p={$dkim->public_key_txt}",
|
||
//// 'ttl' => 3600,
|
||
//// ];
|
||
//// }
|
||
////
|
||
//// return view('livewire.ui.domain.modal.domain-dns-modal', [
|
||
//// 'records' => $records,
|
||
//// ]);
|
||
//// }
|
||
//
|
||
//// public function render()
|
||
//// {
|
||
//// return view('livewire.ui.domain.modal.domain-dns-modal');
|
||
//// }
|
||
//}
|