Domain Create Modal anpassen Fehler auf Null

main
boban 2025-10-19 11:13:38 +02:00
parent f1c4fbf9a1
commit 47cf6e95ae
4 changed files with 279 additions and 43 deletions

View File

@ -0,0 +1,93 @@
<?php
namespace App\Livewire\Ui\Domain;
use App\Models\Domain;
use App\Services\DkimService;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Process;
use Livewire\Component;
class DkimStatus extends Component
{
public Domain $domain;
public ?string $selector = null;
public function mount(Domain $domain, ?string $selector = null): void
{
$this->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 + stehts 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'));
}
}

View File

@ -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)) {

View File

@ -0,0 +1,19 @@
<div class="flex items-center gap-4 text-sm">
<div class="flex items-center gap-2">
<i class="ph-bold {{ $dkimOk ? 'ph-shield-check text-emerald-400' : 'ph-warning-octagon text-rose-400' }} text-[18px]"></i>
<span class="{{ $dkimOk ? 'text-emerald-400' : 'text-rose-400' }}">
{{ $dkimOk ? 'DKIM aktiv' : 'DKIM fehlt' }}
</span>
</div>
<button
wire:click="regenerate"
wire:loading.attr="disabled"
class="inline-flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs font-medium
border border-slate-600/40 bg-slate-800/40 hover:bg-slate-800
disabled:opacity-50">
<i class="ph-bold ph-arrows-counter-clockwise"></i>
<span wire:loading.remove>DKIM reparieren</span>
<span wire:loading>Erzeuge…</span>
</button>
</div>

View File

@ -38,6 +38,7 @@
@endif
</span>
{{ $systemDomain->domain }}
<livewire:ui.domain.dkim-status :domain="$systemDomain" class="ml-2"/>
</div>
@ -105,6 +106,7 @@
</span>
</span>
{{ $d->domain }}
<livewire:ui.domain.dkim-status :domain="$d" class="ml-2"/>
<div class="mt-1 flex items-center gap-1.5">
{{-- Zähler-Badges --}}