Files
jabali-panel/app/Observers/DomainObserver.php
2026-01-25 03:08:37 +02:00

199 lines
6.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Observers;
use App\Jobs\IssueSslCertificate;
use App\Models\DnsRecord;
use App\Models\DnsSetting;
use App\Models\Domain;
use App\Services\Agent\AgentClient;
use Exception;
use Illuminate\Support\Facades\Log;
class DomainObserver
{
public function created(Domain $domain): void
{
$this->createDefaultDnsRecords($domain);
$this->createDnsZone($domain);
$this->scheduleSSLIssuance($domain);
}
protected function scheduleSSLIssuance(Domain $domain): void
{
// Dispatch SSL issuance job with a 30 second delay
// This gives time for DNS to propagate and web server to be configured
IssueSslCertificate::dispatch($domain->id)->delay(now()->addSeconds(30));
Log::info("Scheduled SSL certificate issuance for {$domain->domain}");
}
public function deleted(Domain $domain): void
{
try {
$agent = new AgentClient;
$agent->send('dns.delete_zone', ['domain' => $domain->domain]);
} catch (Exception $e) {
Log::warning("Failed to delete DNS zone for {$domain->domain}: ".$e->getMessage());
}
}
protected function createDefaultDnsRecords(Domain $domain): void
{
$settings = DnsSetting::getAll();
$defaultIp = $domain->ip_address ?: ($settings['default_ip'] ?? $this->getServerIp());
$defaultIpv6 = $domain->ipv6_address ?: ($settings['default_ipv6'] ?? null);
$defaultTtl = (int) ($settings['default_ttl'] ?? 3600);
$hostname = $this->getServerHostname();
$ns1 = $settings['ns1'] ?? "ns1.{$hostname}";
$ns2 = $settings['ns2'] ?? "ns2.{$hostname}";
$defaultRecords = [
// NS records
['name' => '@', 'type' => 'NS', 'content' => $ns1, 'ttl' => $defaultTtl],
['name' => '@', 'type' => 'NS', 'content' => $ns2, 'ttl' => $defaultTtl],
// A records
['name' => '@', 'type' => 'A', 'content' => $defaultIp, 'ttl' => $defaultTtl],
['name' => 'www', 'type' => 'A', 'content' => $defaultIp, 'ttl' => $defaultTtl],
['name' => 'mail', 'type' => 'A', 'content' => $defaultIp, 'ttl' => $defaultTtl],
// MX record
['name' => '@', 'type' => 'MX', 'content' => "mail.{$domain->domain}", 'ttl' => $defaultTtl, 'priority' => 10],
// SPF record - allows mail from this server
['name' => '@', 'type' => 'TXT', 'content' => 'v=spf1 mx a ~all', 'ttl' => $defaultTtl],
// DMARC record - basic policy
['name' => '_dmarc', 'type' => 'TXT', 'content' => 'v=DMARC1; p=none; rua=mailto:postmaster@'.$domain->domain, 'ttl' => $defaultTtl],
];
if (! empty($defaultIpv6)) {
$defaultRecords[] = ['name' => '@', 'type' => 'AAAA', 'content' => $defaultIpv6, 'ttl' => $defaultTtl];
$defaultRecords[] = ['name' => 'www', 'type' => 'AAAA', 'content' => $defaultIpv6, 'ttl' => $defaultTtl];
$defaultRecords[] = ['name' => 'mail', 'type' => 'AAAA', 'content' => $defaultIpv6, 'ttl' => $defaultTtl];
}
$defaultRecords = $this->appendNameserverRecords(
$defaultRecords,
$domain->domain,
$ns1,
$ns2,
$defaultIp,
$defaultIpv6,
$defaultTtl
);
foreach ($defaultRecords as $record) {
DnsRecord::create(array_merge(['domain_id' => $domain->id], $record));
}
}
protected function createDnsZone(Domain $domain): void
{
try {
$settings = DnsSetting::getAll();
$records = DnsRecord::where('domain_id', $domain->id)->get()->toArray();
$hostname = $this->getServerHostname();
$serverIp = $this->getServerIp();
$serverIpv6 = $settings['default_ipv6'] ?? null;
$agent = new AgentClient;
$agent->send('dns.sync_zone', [
'domain' => $domain->domain,
'records' => $records,
'ns1' => $settings['ns1'] ?? "ns1.{$hostname}",
'ns2' => $settings['ns2'] ?? "ns2.{$hostname}",
'admin_email' => $settings['admin_email'] ?? "admin.{$hostname}",
'default_ip' => $settings['default_ip'] ?? $serverIp,
'default_ipv6' => $serverIpv6,
'default_ttl' => $settings['default_ttl'] ?? 3600,
]);
} catch (Exception $e) {
Log::warning("Failed to create DNS zone for {$domain->domain}: ".$e->getMessage());
}
}
protected function getServerHostname(): string
{
return gethostname() ?: 'localhost';
}
protected function getServerIp(): string
{
$ip = trim(shell_exec("hostname -I | awk '{print $1}'") ?? '');
return $ip ?: '127.0.0.1';
}
/**
* @param array<int, array<string, mixed>> $records
* @return array<int, array<string, mixed>>
*/
protected function appendNameserverRecords(
array $records,
string $domain,
string $ns1,
string $ns2,
string $ipv4,
?string $ipv6,
int $ttl
): array {
$labels = $this->getNameserverLabels($domain, [$ns1, $ns2]);
if ($labels === []) {
return $records;
}
$existingA = array_map(
fn (array $record): string => $record['name'] ?? '',
array_filter($records, fn (array $record): bool => ($record['type'] ?? '') === 'A')
);
$existingAAAA = array_map(
fn (array $record): string => $record['name'] ?? '',
array_filter($records, fn (array $record): bool => ($record['type'] ?? '') === 'AAAA')
);
foreach ($labels as $label) {
if (! in_array($label, $existingA, true)) {
$records[] = ['name' => $label, 'type' => 'A', 'content' => $ipv4, 'ttl' => $ttl];
}
if ($ipv6 && ! in_array($label, $existingAAAA, true)) {
$records[] = ['name' => $label, 'type' => 'AAAA', 'content' => $ipv6, 'ttl' => $ttl];
}
}
return $records;
}
/**
* @param array<int, string> $nameservers
* @return array<int, string>
*/
protected function getNameserverLabels(string $domain, array $nameservers): array
{
$domain = rtrim($domain, '.');
$labels = [];
foreach ($nameservers as $nameserver) {
$nameserver = rtrim($nameserver ?? '', '.');
if ($nameserver === '') {
continue;
}
if ($nameserver === $domain) {
$label = '@';
} elseif (str_ends_with($nameserver, '.'.$domain)) {
$label = substr($nameserver, 0, -strlen('.'.$domain));
} else {
continue;
}
if ($label !== '@') {
$labels[] = $label;
}
}
return array_values(array_unique($labels));
}
}