diff --git a/app/Livewire/Ui/Domain/DkimStatus.php b/app/Livewire/Ui/Domain/DkimStatus.php new file mode 100644 index 0000000..5051afb --- /dev/null +++ b/app/Livewire/Ui/Domain/DkimStatus.php @@ -0,0 +1,93 @@ +domain = $domain; + $this->selector = $selector + ?: optional($domain->dkimKeys()->where('is_active', true)->latest()->first())->selector + ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1'); + } + + /** interne Normalisierung nur fürs Dateisystem */ + protected function safeKey(string|Domain $value, int $len = 64): string + { + $val = $value instanceof Domain ? $value->domain : (string) $value; + return preg_replace('/[^a-zA-Z0-9_.-]+/', '_', substr($val, 0, $len)); + } + + /** Minimalcheck: existiert .private + steht’s in KeyTable & SigningTable? */ + protected function isDkimReady(string $domain, string $selector): bool + { + $hasFile = is_readable("/etc/opendkim/keys/{$domain}/{$selector}.private"); + + $keyTab = is_readable('/etc/opendkim/KeyTable') + ? (@file_get_contents('/etc/opendkim/KeyTable') ?: '') + : ''; + $signTab = is_readable('/etc/opendkim/SigningTable') + ? (@file_get_contents('/etc/opendkim/SigningTable') ?: '') + : ''; + + $inKey = (bool) preg_match( + "/^{$selector}\._domainkey\.{$domain}\s+{$domain}:{$selector}:/m", + $keyTab + ); + $inSign = (bool) preg_match( + "/^\*\@{$domain}\s+{$selector}\._domainkey\.{$domain}\s*$/m", + $signTab + ); + + return $hasFile && $inKey && $inSign; + } + + /** Button: (re)generieren + Helper einhängen + OpenDKIM reload */ + public function regenerate(?string $selector = null): void + { + $selector = $selector ?: ($this->selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1')); + + try { + /** @var DkimService $svc */ + $svc = app(DkimService::class); + + // 1) Key erzeugen/auffrischen – deine Funktion macht bereits alles Nötige: + // - schreibt .pem/.pub/.private/.txt in storage + // - pflegt DB (DkimKey) + // - ruft /usr/local/sbin/mailwolt-install-dkim auf + // - reloadet opendkim + $res = $svc->generateForDomain($this->domain, 2048, $selector); + + // 2) Sicherstellen, dass der Helper wirklich geladen hat (idempotent): + Process::run(['systemctl', 'reload', 'opendkim']); + + // 3) Status checken & Feedback + $ok = $this->isDkimReady($this->domain->domain, $selector); + $this->dispatch('toast', + type: $ok ? 'success' : 'warning', + message: $ok ? 'DKIM ist aktiv.' : 'DKIM generiert – OpenDKIM/Tabellen prüfen.'); + } catch (\Throwable $e) { + $this->dispatch('toast', type: 'error', message: 'DKIM Fehler: '.$e->getMessage()); + } + + // ggf. Selector für UI festhalten + $this->selector = $selector; + } + + public function render(): View + { + $dkimOk = $this->isDkimReady($this->domain->domain, $this->selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1')); + return view('livewire.ui.domain.dkim-status', compact('dkimOk')); + } +} diff --git a/app/Services/DkimService.php b/app/Services/DkimService.php index bc3b75b..19ae4a4 100644 --- a/app/Services/DkimService.php +++ b/app/Services/DkimService.php @@ -2,9 +2,11 @@ namespace App\Services; +use App\Models\DkimKey; use App\Models\Domain; use Illuminate\Contracts\Console\Kernel; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Storage; use RuntimeException; @@ -174,46 +176,132 @@ class DkimService // '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 { - // 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 + // Disk "local" zeigt bei dir auf storage/app/private (siehe Kommentar in deinem Code) + $disk = Storage::disk('local'); $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); + // unsere Dateien (alle Varianten, damit sowohl App als auch OpenDKIM glücklich sind) + $privPemRel = "{$baseRel}/{$selKey}.pem"; // PKCS#1/PKCS#8 (App-Backup) + $pubPemRel = "{$baseRel}/{$selKey}.pub"; // Public PEM (App-Backup) + $privOKRel = "{$baseRel}/{$selKey}.private"; // <-- OpenDKIM erwartet *genau das* + $dnsTxtRel = "{$baseRel}/{$selKey}.txt"; // TXT-String "v=DKIM1; k=rsa; p=..." + + // Absolute Pfade + $privPemAbs = method_exists($disk, 'path') ? $disk->path($privPemRel) : storage_path('app/private/'.$privPemRel); + $pubPemAbs = method_exists($disk, 'path') ? $disk->path($pubPemRel) : storage_path('app/private/'.$pubPemRel); + $privOKAbs = method_exists($disk, 'path') ? $disk->path($privOKRel) : storage_path('app/private/'.$privOKRel); + $dnsTxtAbs = method_exists($disk, 'path') ? $disk->path($dnsTxtRel) : storage_path('app/private/'.$dnsTxtRel); + + // Wenn schon vollständig vorhanden → direkt zurück + if ($disk->exists($privOKRel) && $disk->exists($dnsTxtRel)) { + $privatePem = $disk->exists($privPemRel) ? $disk->get($privPemRel) : $disk->get($privOKRel); + $publicPem = $disk->exists($pubPemRel) ? $disk->get($pubPemRel) : null; + $dnsTxt = $disk->get($dnsTxtRel); - // 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, + 'selector' => $selKey, + 'priv_path' => $privOKAbs, + 'pub_path' => $pubPemAbs, + 'private_pem' => $privatePem, + 'public_pem' => $publicPem, + 'dns_name' => "{$selKey}._domainkey", + 'dns_txt' => $dnsTxt, + 'bits' => $bits, ]; } - // 3) Neu generieren + // Neu generieren $disk->makeDirectory($baseRel); $res = openssl_pkey_new([ @@ -224,8 +312,8 @@ class DkimService throw new \RuntimeException('DKIM: openssl_pkey_new() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt')); } - $privateKey = ''; - if (!openssl_pkey_export($res, $privateKey)) { + $privatePem = ''; + if (!openssl_pkey_export($res, $privatePem)) { throw new \RuntimeException('DKIM: openssl_pkey_export() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt')); } @@ -233,31 +321,65 @@ class DkimService 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) { + $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.'); } + $dnsTxt = "v=DKIM1; k=rsa; p={$publicBase64}"; - 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}"); + // 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}"); + + // Dateirechte (best effort) + @chmod($privPemAbs, 0600); + @chmod($privOKAbs, 0600); + @chmod($pubPemAbs, 0640); + @chmod($dnsTxtAbs, 0644); + + // DB pflegen + DkimKey::updateOrCreate( + ['domain_id' => $domain->id, 'selector' => $selKey], + [ + 'private_key_pem' => $privatePem, + 'public_key_txt' => $publicBase64, + 'is_active' => true, + ] + ); + + // OpenDKIM einhängen (wenn Helper existiert) + if (is_executable('/usr/local/sbin/mailwolt-install-dkim')) { + $res = Process::run([ + '/usr/local/sbin/mailwolt-install-dkim', + $domain->domain, + $selKey, + $privOKAbs, // <-- .private (genau das will der Helper) + $dnsTxtAbs // optional: TXT-Datei, falls dein Helper das nutzt + ]); + + if ($res->failed()) { + throw new \RuntimeException("OpenDKIM-Install fehlgeschlagen: {$res->error()}"); + } + + // OpenDKIM neu laden + Process::run(['systemctl', 'reload', 'opendkim']); } 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, + 'selector' => $selKey, + 'priv_path' => $privOKAbs, + 'pub_path' => $pubPemAbs, + 'private_pem' => $privatePem, + 'public_pem' => $publicPem, + 'dns_name' => "{$selKey}._domainkey", + 'dns_txt' => $dnsTxt, + 'bits' => $bits, ]; } - + protected function safeKey($value, int $max = 64): string { if (is_object($value)) { diff --git a/resources/views/livewire/ui/domain/dkim-status.blade.php b/resources/views/livewire/ui/domain/dkim-status.blade.php new file mode 100644 index 0000000..0d4f056 --- /dev/null +++ b/resources/views/livewire/ui/domain/dkim-status.blade.php @@ -0,0 +1,19 @@ +
+
+ + + {{ $dkimOk ? 'DKIM aktiv' : 'DKIM fehlt' }} + +
+ + +
diff --git a/resources/views/livewire/ui/domain/domain-dns-list.blade.php b/resources/views/livewire/ui/domain/domain-dns-list.blade.php index 838707a..353dc87 100644 --- a/resources/views/livewire/ui/domain/domain-dns-list.blade.php +++ b/resources/views/livewire/ui/domain/domain-dns-list.blade.php @@ -38,6 +38,7 @@ @endif {{ $systemDomain->domain }} + @@ -105,6 +106,7 @@ {{ $d->domain }} +
{{-- Zähler-Badges --}}