diff --git a/app/Livewire/Ui/Domain/DkimStatus.php b/app/Livewire/Ui/Domain/DkimStatus.php index 7a60de2..058de19 100644 --- a/app/Livewire/Ui/Domain/DkimStatus.php +++ b/app/Livewire/Ui/Domain/DkimStatus.php @@ -4,8 +4,8 @@ namespace App\Livewire\Ui\Domain; use App\Models\Domain; use App\Services\DkimService; +use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\Process; -use Illuminate\View\View; use Livewire\Component; class DkimStatus extends Component @@ -17,7 +17,7 @@ class DkimStatus extends Component { $this->domain = $domain; - // aktiven Selector aus DB, sonst Default aus Config + // aktiven Selector aus DB; sonst Default aus Config $this->selector = $selector ?: optional( $domain->dkimKeys()->where('is_active', true)->latest()->first() @@ -25,27 +25,38 @@ class DkimStatus extends Component ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1'); } - /** Prüft nur lokale Voraussetzungen (Keyfile + KeyTable + SigningTable) */ + /** + * Prüft NUR lokal: + * - Keyfile: /etc/opendkim/keys//.private + * - KeyTable & SigningTable enthalten passende Zeilen + */ protected function isDkimReady(string $domain, string $selector): bool { $d = preg_quote($domain, '/'); $s = preg_quote($selector, '/'); - // 1) Key-Datei unter /etc/opendkim/keys//.private + // 1) Key-Datei vorhanden & lesbar $keyFile = "/etc/opendkim/keys/{$domain}/{$selector}.private"; - $hasFile = is_readable($keyFile) && filesize($keyFile) > 0; + $hasFile = is_readable($keyFile) && (filesize($keyFile) > 0); - // 2) Einträge in KeyTable + SigningTable - $keyTab = is_readable('/etc/opendkim/KeyTable') ? (string) @file_get_contents('/etc/opendkim/KeyTable') : ''; - $signTab = is_readable('/etc/opendkim/SigningTable') ? (string) @file_get_contents('/etc/opendkim/SigningTable') : ''; + // 2) Tabellen-Inhalte lesen (leer wenn Datei fehlt) + $keyTab = is_readable('/etc/opendkim/KeyTable') + ? (string) @file_get_contents('/etc/opendkim/KeyTable') + : ''; + $signTab = is_readable('/etc/opendkim/SigningTable') + ? (string) @file_get_contents('/etc/opendkim/SigningTable') + : ''; - // Beispiele: + // Beispiel-Zeilen: // KeyTable: mwl1._domainkey.sysmail.toastra.com sysmail.toastra.com:mwl1:/etc/opendkim/keys/sysmail.toastra.com/mwl1.private // SigningTable:*@sysmail.toastra.com mwl1._domainkey.sysmail.toastra.com - $inKey = (bool) preg_match( + + // Robust gegen Mehrfach-Spaces/Tabs: + $inKey = (bool) preg_match( "/^{$s}\._domainkey\.{$d}\s+{$d}:{$s}:/m", $keyTab ); + $inSign = (bool) preg_match( "/^\*\@{$d}\s+{$s}\._domainkey\.{$d}\s*$/m", $signTab @@ -54,36 +65,42 @@ class DkimStatus extends Component return $hasFile && $inKey && $inSign; } - /** Reparieren/Neu erzeugen */ + /** Button: (Re)generieren via Service (der ruft sudo-Helper auf) */ public function regenerate(?string $selector = null): void { - $selector = $selector ?: ($this->selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1')); + $selector = $selector + ?: ($this->selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1')); try { /** @var DkimService $svc */ $svc = app(DkimService::class); - // erzeugt/aktualisiert Keys in storage, pflegt DB, ruft Helper auf (falls vorhanden) + // erzeugt/aktualisiert Keys in storage, pflegt DB, + // ruft /usr/local/sbin/mailwolt-install-dkim via sudo -n auf, + // lädt opendkim neu (im Service) $svc->generateForDomain($this->domain, 2048, $selector); - // OpenDKIM neu laden (idempotent) - Process::run(['systemctl', 'reload', 'opendkim']); - // Status neu prüfen $ok = $this->isDkimReady($this->domain->domain, $selector); - $this->dispatch('toast', type: $ok ? 'success' : 'warning', - message: $ok ? 'DKIM ist aktiv.' : 'DKIM generiert – OpenDKIM prüfen.'); + + $this->dispatch('toast', + type: $ok ? 'success' : 'warning', + message: $ok ? 'DKIM ist aktiv.' : 'DKIM generiert – OpenDKIM prüfen.' + ); } catch (\Throwable $e) { $this->dispatch('toast', type: 'error', message: 'DKIM Fehler: '.$e->getMessage()); } - $this->selector = $selector; // für Folge-Checks + // aktuellen Selector merken (falls der Fallback zuvor anders war) + $this->selector = $selector; } public function render(): View { - $dkimOk = $this->isDkimReady($this->domain->domain, $this->selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1')); + $sel = $this->selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1'); + $dkimOk = $this->isDkimReady($this->domain->domain, $sel); + return view('livewire.ui.domain.dkim-status', compact('dkimOk')); } } diff --git a/app/Services/DkimService.php b/app/Services/DkimService.php index 19ae4a4..794eaa2 100644 --- a/app/Services/DkimService.php +++ b/app/Services/DkimService.php @@ -13,253 +13,6 @@ use RuntimeException; class DkimService { /** Erzeugt Keypair & gibt den TXT-Record (ohne Host) zurück. */ -// public function generateForDomain(Domain $domain, int $bits = 2048, string $selector = null): array -// { -// // 1) Selector zentral aus der Config (Fallback 'mwl1') -// $selector = $selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1'); -// -// $dirKey = $this->safeKey($domain); -// $selKey = $this->safeKey($selector, 32); -// -// $disk = Storage::disk('local'); -// $baseRel = "dkim/{$dirKey}"; -// $privRel = "{$baseRel}/{$selKey}.pem"; -// $pubRel = "{$baseRel}/{$selKey}.pub"; -// -// // 2) Idempotent: existiert das Paar schon? -> nur lesen & zurückgeben -// if ($disk->exists($privRel) && $disk->exists($pubRel)) { -// $privateKey = $disk->get($privRel); -// $publicKeyPem = $disk->get($pubRel); -// $publicKeyBase = self::extractPublicKeyBase64($publicKeyPem); -// if (strlen($publicKeyBase) < 300) { -// throw new \RuntimeException('DKIM: Public Key zu kurz – vermutlich Parsing-Fehler.'); -// } -// return [ -// 'selector' => $selKey, -// 'priv_path' => storage_path("app/{$privRel}"), -// 'pub_path' => storage_path("app/{$pubRel}"), -// 'public_pem' => $publicKeyPem, -// 'private_pem' => $privateKey, -// 'dns_name' => "{$selKey}._domainkey", -// 'dns_txt' => "v=DKIM1; k=rsa; p={$publicKeyBase}", -// 'bits' => $bits, -// ]; -// } -// -// // 3) Sonst neu generieren -// $disk->makeDirectory($baseRel); -// -// $res = openssl_pkey_new([ -// 'private_key_type' => OPENSSL_KEYTYPE_RSA, -// 'private_key_bits' => $bits, -// ]); -// if ($res === false) { -// throw new \RuntimeException('DKIM: openssl_pkey_new() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt')); -// } -// -// $privateKey = ''; -// if (!openssl_pkey_export($res, $privateKey)) { -// throw new \RuntimeException('DKIM: openssl_pkey_export() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt')); -// } -// -// $details = openssl_pkey_get_details($res); -// if ($details === false || empty($details['key'])) { -// throw new \RuntimeException('DKIM: Public Key konnte nicht gelesen werden.'); -// } -// $publicKeyPem = $details['key']; -// $publicKeyBase = self::extractPublicKeyBase64($publicKeyPem); -// if (strlen($publicKeyBase) < 300) { -// throw new \RuntimeException('DKIM: Public Key zu kurz – vermutlich Parsing-Fehler.'); -// } -// -// if (!$disk->put($privRel, $privateKey)) { -// throw new \RuntimeException("DKIM: Private-Key schreiben fehlgeschlagen: {$privRel}"); -// } -// if (!$disk->put($pubRel, $publicKeyPem)) { -// throw new \RuntimeException("DKIM: Public-Key schreiben fehlgeschlagen: {$pubRel}"); -// } -// -// // 4) Rückgabe -// return [ -// 'selector' => $selKey, -// 'priv_path' => storage_path("app/{$privRel}"), -// 'pub_path' => storage_path("app/{$pubRel}"), -// 'public_pem' => $publicKeyPem, -// 'private_pem' => $privateKey, -// 'dns_name' => "{$selKey}._domainkey", -// 'dns_txt' => "v=DKIM1; k=rsa; p={$publicKeyBase}", -// 'bits' => $bits, -// ]; -// } - -// public function generateForDomain(Domain $domain, int $bits = 2048, string $selector = null): array -// { -// // 1) Selector zentral aus der Config (Fallback 'mwl1') -// $selector = $selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1'); -// -// $dirKey = $this->safeKey($domain); -// $selKey = $this->safeKey($selector, 32); -// -// $disk = Storage::disk('local'); -// $baseRel = "dkim/{$dirKey}"; -// $privRel = "{$baseRel}/{$selKey}.pem"; -// $pubRel = "{$baseRel}/{$selKey}.pub"; -// -// $privAbs = method_exists($disk, 'path') -// ? $disk->path($privRel) // -> /var/www/mailwolt/storage/app/private/dkim//.pem -// : storage_path('app/private/'.$privRel); // Fallback falls 'path' nicht existiert -// -// $pubAbs = method_exists($disk, 'path') -// ? $disk->path($pubRel) -// : storage_path('app/private/'.$pubRel); -// -// // 2) Idempotent: existiert das Paar schon? -// if ($disk->exists($privRel) && $disk->exists($pubRel)) { -// $privateKey = $disk->get($privRel); -// $publicKeyPem = $disk->get($pubRel); -// $publicKeyBase = self::extractPublicKeyBase64($publicKeyPem); -// if (strlen($publicKeyBase) < 300) { -// throw new \RuntimeException('DKIM: Public Key zu kurz – vermutlich Parsing-Fehler.'); -// } -// return [ -// 'selector' => $selKey, -// 'priv_path' => $privAbs, -// 'pub_path' => $pubAbs, -// 'public_pem' => $publicKeyPem, -// 'private_pem' => $privateKey, -// 'dns_name' => "{$selKey}._domainkey", -// 'dns_txt' => "v=DKIM1; k=rsa; p={$publicKeyBase}", -// 'bits' => $bits, -// ]; -// } -// -// // 3) Neu generieren -// $disk->makeDirectory($baseRel); -// -// $res = openssl_pkey_new([ -// 'private_key_type' => OPENSSL_KEYTYPE_RSA, -// 'private_key_bits' => $bits, -// ]); -// if ($res === false) { -// throw new \RuntimeException('DKIM: openssl_pkey_new() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt')); -// } -// -// $privateKey = ''; -// if (!openssl_pkey_export($res, $privateKey)) { -// throw new \RuntimeException('DKIM: openssl_pkey_export() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt')); -// } -// -// $details = openssl_pkey_get_details($res); -// if ($details === false || empty($details['key'])) { -// throw new \RuntimeException('DKIM: Public Key konnte nicht gelesen werden.'); -// } -// $publicKeyPem = $details['key']; -// $publicKeyBase = self::extractPublicKeyBase64($publicKeyPem); -// if (strlen($publicKeyBase) < 300) { -// throw new \RuntimeException('DKIM: Public Key zu kurz – vermutlich Parsing-Fehler.'); -// } -// -// if (!$disk->put($privRel, $privateKey)) { -// throw new \RuntimeException("DKIM: Private-Key schreiben fehlgeschlagen: {$privRel}"); -// } -// if (!$disk->put($pubRel, $publicKeyPem)) { -// throw new \RuntimeException("DKIM: Public-Key schreiben fehlgeschlagen: {$pubRel}"); -// } -// -// return [ -// 'selector' => $selKey, -// 'priv_path' => $privAbs, -// 'pub_path' => $pubAbs, -// 'public_pem' => $publicKeyPem, -// 'private_pem' => $privateKey, -// 'dns_name' => "{$selKey}._domainkey", -// 'dns_txt' => "v=DKIM1; k=rsa; p={$publicKeyBase}", -// 'bits' => $bits, -// ]; -// } - -// public function generateForDomain(Domain $domain, int $bits = 2048, string $selector = null): array -// { -// // 1) Selector (Fallback mwl1) -// $selector = $selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1'); -// -// $dirKey = $this->safeKey($domain); -// $selKey = $this->safeKey($selector, 32); -// -// $disk = Storage::disk('local'); // root: /var/www/mailwolt/storage/app/private -// $baseRel = "dkim/{$dirKey}"; -// $privRel = "{$baseRel}/{$selKey}.pem"; -// $pubRel = "{$baseRel}/{$selKey}.pub"; -// -// // Absolute Pfade (robust gegen geändertes Disk-Root) -// $privAbs = method_exists($disk, 'path') ? $disk->path($privRel) : storage_path('app/private/'.$privRel); -// $pubAbs = method_exists($disk, 'path') ? $disk->path($pubRel) : storage_path('app/private/'.$pubRel); -// -// // 2) Idempotent: existiert das Paar schon? -// if ($disk->exists($privRel) && $disk->exists($pubRel)) { -// $privateKey = $disk->get($privRel); // ← Inhalte laden, nicht Pfade! -// $publicKeyPem = $disk->get($pubRel); -// $publicKeyBase = self::extractPublicKeyBase64($publicKeyPem); -// if (strlen($publicKeyBase) < 300) { -// throw new \RuntimeException('DKIM: Public Key zu kurz – vermutlich Parsing-Fehler.'); -// } -// return [ -// 'selector' => $selKey, -// 'priv_path' => $privAbs, -// 'pub_path' => $pubAbs, -// 'public_pem' => $publicKeyPem, -// 'private_pem' => $privateKey, -// 'dns_name' => "{$selKey}._domainkey", -// 'dns_txt' => "v=DKIM1; k=rsa; p={$publicKeyBase}", -// 'bits' => $bits, -// ]; -// } -// -// // 3) Neu generieren -// $disk->makeDirectory($baseRel); -// -// $res = openssl_pkey_new([ -// 'private_key_type' => OPENSSL_KEYTYPE_RSA, -// 'private_key_bits' => $bits, -// ]); -// if ($res === false) { -// throw new \RuntimeException('DKIM: openssl_pkey_new() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt')); -// } -// -// $privateKey = ''; -// if (!openssl_pkey_export($res, $privateKey)) { -// throw new \RuntimeException('DKIM: openssl_pkey_export() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt')); -// } -// -// $details = openssl_pkey_get_details($res); -// if ($details === false || empty($details['key'])) { -// throw new \RuntimeException('DKIM: Public Key konnte nicht gelesen werden.'); -// } -// $publicKeyPem = $details['key']; -// $publicKeyBase = self::extractPublicKeyBase64($publicKeyPem); -// if (strlen($publicKeyBase) < 300) { -// throw new \RuntimeException('DKIM: Public Key zu kurz – vermutlich Parsing-Fehler.'); -// } -// -// if (!$disk->put($privRel, $privateKey)) { -// throw new \RuntimeException("DKIM: Private-Key schreiben fehlgeschlagen: {$privRel}"); -// } -// if (!$disk->put($pubRel, $publicKeyPem)) { -// throw new \RuntimeException("DKIM: Public-Key schreiben fehlgeschlagen: {$pubRel}"); -// } -// -// return [ -// 'selector' => $selKey, -// 'priv_path' => $privAbs, -// 'pub_path' => $pubAbs, -// 'public_pem' => $publicKeyPem, -// 'private_pem' => $privateKey, -// 'dns_name' => "{$selKey}._domainkey", -// 'dns_txt' => "v=DKIM1; k=rsa; p={$publicKeyBase}", -// 'bits' => $bits, -// ]; -// } - public function generateForDomain(Domain $domain, int $bits = 2048, string $selector = null): array { $selector = $selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1'); @@ -309,30 +62,30 @@ class DkimService 'private_key_bits' => $bits, ]); if ($res === false) { - throw new \RuntimeException('DKIM: openssl_pkey_new() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt')); + throw new RuntimeException('DKIM: openssl_pkey_new() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt')); } $privatePem = ''; if (!openssl_pkey_export($res, $privatePem)) { - throw new \RuntimeException('DKIM: openssl_pkey_export() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt')); + throw new RuntimeException('DKIM: openssl_pkey_export() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt')); } $details = openssl_pkey_get_details($res); if ($details === false || empty($details['key'])) { - throw new \RuntimeException('DKIM: Public Key konnte nicht gelesen werden.'); + throw new RuntimeException('DKIM: Public Key konnte nicht gelesen werden.'); } $publicPem = $details['key']; $publicBase64 = trim(preg_replace('/-----(BEGIN|END)\s+PUBLIC KEY-----|\s+/', '', $publicPem)); if (strlen($publicBase64) < 300) { - throw new \RuntimeException('DKIM: Public Key zu kurz – vermutlich Parsing-Fehler.'); + throw new RuntimeException('DKIM: Public Key zu kurz – vermutlich Parsing-Fehler.'); } $dnsTxt = "v=DKIM1; k=rsa; p={$publicBase64}"; // Dateien schreiben (App + OpenDKIM) - if (!$disk->put($privPemRel, $privatePem)) throw new \RuntimeException("DKIM: Private-Key schreiben fehlgeschlagen: {$privPemRel}"); - if (!$disk->put($pubPemRel, $publicPem)) throw new \RuntimeException("DKIM: Public-Key schreiben fehlgeschlagen: {$pubPemRel}"); - if (!$disk->put($privOKRel, $privatePem)) throw new \RuntimeException("DKIM: OpenDKIM-Key schreiben fehlgeschlagen: {$privOKRel}"); - if (!$disk->put($dnsTxtRel, $dnsTxt)) throw new \RuntimeException("DKIM: DNS-TXT schreiben fehlgeschlagen: {$dnsTxtRel}"); + if (!$disk->put($privPemRel, $privatePem)) throw new RuntimeException("DKIM: Private-Key schreiben fehlgeschlagen: {$privPemRel}"); + if (!$disk->put($pubPemRel, $publicPem)) throw new RuntimeException("DKIM: Public-Key schreiben fehlgeschlagen: {$pubPemRel}"); + if (!$disk->put($privOKRel, $privatePem)) throw new RuntimeException("DKIM: OpenDKIM-Key schreiben fehlgeschlagen: {$privOKRel}"); + if (!$disk->put($dnsTxtRel, $dnsTxt)) throw new RuntimeException("DKIM: DNS-TXT schreiben fehlgeschlagen: {$dnsTxtRel}"); // Dateirechte (best effort) @chmod($privPemAbs, 0600); @@ -351,21 +104,33 @@ class DkimService ); // OpenDKIM einhängen (wenn Helper existiert) - if (is_executable('/usr/local/sbin/mailwolt-install-dkim')) { - $res = Process::run([ - '/usr/local/sbin/mailwolt-install-dkim', + $helper = '/usr/local/sbin/mailwolt-install-dkim'; + + if (is_executable($helper)) { + $cmd = [ + 'sudo','-n', $helper, $domain->domain, $selKey, - $privOKAbs, // <-- .private (genau das will der Helper) - $dnsTxtAbs // optional: TXT-Datei, falls dein Helper das nutzt - ]); + $privOKAbs, // …/storage/app/private/dkim//.private + $dnsTxtAbs // …/storage/app/private/dkim//.txt + ]; + + $res = Process::timeout(30)->run($cmd); if ($res->failed()) { - throw new \RuntimeException("OpenDKIM-Install fehlgeschlagen: {$res->error()}"); + Log::error('DKIM install failed', [ + 'cmd' => implode(' ', $cmd), + 'exit' => $res->exitCode(), + 'out' => $res->output(), + 'err' => $res->errorOutput(), + ]); + throw new RuntimeException( + 'OpenDKIM-Install fehlgeschlagen: '.$res->errorOutput() + ); } - // OpenDKIM neu laden - Process::run(['systemctl', 'reload', 'opendkim']); + // OpenDKIM neu laden (falls der Helper das nicht selbst tut) + Process::run(['sudo','-n','systemctl','reload','opendkim']); } return [ diff --git a/resources/views/livewire/ui/domain/dkim-status.blade.php b/resources/views/livewire/ui/domain/dkim-status.blade.php index ca69361..b9469c3 100644 --- a/resources/views/livewire/ui/domain/dkim-status.blade.php +++ b/resources/views/livewire/ui/domain/dkim-status.blade.php @@ -1,16 +1,16 @@
+ {{ $dkimOk ? 'bg-green-500/10 text-green-500 ring-1 ring-green-500/30' : 'bg-red-500/10 text-red-500 ring-1 ring-red-500/30' }}"> - {{ $dkimOk ? 'DKIM aktiv' : 'DKIM fehlt' }} + {{ $dkimOk ? 'DKIM aktiv' : 'DKIM fehlt' }}
@if(!$dkimOk)