Files
novacpx/panel/lib/DNSManager.php
T
myron e3b166803a Add full API endpoint suite, lib managers, webmail (Roundcube :8883), and NovaCPX icon/branding assets
- 14 API endpoints: accounts, packages, domains, dns, email, databases, ftp, ssl, cron, php, files, stats, webmail, server_setup
- 8 lib managers: AccountManager, VhostManager, DNSManager, EmailManager, DatabaseManager, PHPManager, FTPManager, SSLManager
- Roundcube webmail on dedicated port 8883 (sequenced after 8880/8881/8882)
- Custom NovaCPX SVG icon sprite (30+ unique icons), logo, mark, favicon
- PORT_WEBMAIL=8883 wired into Core.php, install.sh, UFW, Fail2Ban, credentials file

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 05:50:50 +00:00

148 lines
6.4 KiB
PHP

<?php
/**
* DNSManager — BIND9 zone file generation and management
*/
class DNSManager {
private static string $zonesDir = '/etc/bind/novacpx-zones';
private static string $namedConf = '/etc/bind/named.conf.novacpx';
public static function createZone(int $accountId, string $domain): void {
$db = DB::getInstance();
$serial = (int)date('Ymd') * 100 + 1;
$ns1 = self::getSetting('default_nameserver1', 'ns1.localhost');
$ns2 = self::getSetting('default_nameserver2', 'ns2.localhost');
$email = 'hostmaster.' . $domain;
$ip = self::serverIp();
$zoneId = (int)$db->insert(
"INSERT INTO dns_zones (account_id, domain, serial, primary_ns, secondary_ns, admin_email) VALUES (?,?,?,?,?,?)",
[$accountId, $domain, $serial, $ns1, $ns2, $email]
);
// Default records
$defaults = [
['@', 'A', $ip, 3600, null],
['www', 'A', $ip, 3600, null],
['mail', 'A', $ip, 3600, null],
['@', 'MX', "mail.{$domain}.", 3600, 10],
['@', 'TXT', "v=spf1 a mx ~all", 3600, null],
];
foreach ($defaults as [$name, $type, $content, $ttl, $prio]) {
$db->execute(
"INSERT INTO dns_records (zone_id, name, type, content, ttl, priority) VALUES (?,?,?,?,?,?)",
[$zoneId, $name, $type, $content, $ttl, $prio]
);
}
self::writeZoneFile($zoneId);
self::reloadBind();
}
public static function removeZone(string $domain): void {
$db = DB::getInstance();
$zone = $db->fetchOne("SELECT id FROM dns_zones WHERE domain = ?", [$domain]);
if ($zone) {
$db->execute("DELETE FROM dns_zones WHERE id = ?", [$zone['id']]);
}
$file = self::$zonesDir . '/' . $domain . '.zone';
@unlink($file);
self::rebuildNamedConf();
self::reloadBind();
}
public static function addRecord(int $zoneId, string $name, string $type, string $content, int $ttl = 3600, ?int $priority = null): int {
$db = DB::getInstance();
$id = (int)$db->insert(
"INSERT INTO dns_records (zone_id, name, type, content, ttl, priority) VALUES (?,?,?,?,?,?)",
[$zoneId, $name, $type, $content, $ttl, $priority]
);
$db->execute("UPDATE dns_zones SET serial = serial + 1, updated_at = NOW() WHERE id = ?", [$zoneId]);
self::writeZoneFile($zoneId);
self::reloadBind();
return $id;
}
public static function updateRecord(int $recordId, array $data): void {
$db = DB::getInstance();
$rec = $db->fetchOne("SELECT zone_id FROM dns_records WHERE id = ?", [$recordId]);
if (!$rec) throw new RuntimeException("Record not found");
$db->execute(
"UPDATE dns_records SET name=?, type=?, content=?, ttl=?, priority=? WHERE id=?",
[$data['name'], $data['type'], $data['content'], $data['ttl'] ?? 3600, $data['priority'] ?? null, $recordId]
);
$db->execute("UPDATE dns_zones SET serial = serial + 1, updated_at = NOW() WHERE id = ?", [$rec['zone_id']]);
self::writeZoneFile($rec['zone_id']);
self::reloadBind();
}
public static function deleteRecord(int $recordId): void {
$db = DB::getInstance();
$rec = $db->fetchOne("SELECT zone_id FROM dns_records WHERE id = ?", [$recordId]);
if (!$rec) throw new RuntimeException("Record not found");
$db->execute("DELETE FROM dns_records WHERE id = ?", [$recordId]);
$db->execute("UPDATE dns_zones SET serial = serial + 1 WHERE id = ?", [$rec['zone_id']]);
self::writeZoneFile($rec['zone_id']);
self::reloadBind();
}
public static function writeZoneFile(int $zoneId): void {
$db = DB::getInstance();
$zone = $db->fetchOne("SELECT * FROM dns_zones WHERE id = ?", [$zoneId]);
if (!$zone) return;
$records = $db->fetchAll("SELECT * FROM dns_records WHERE zone_id = ? ORDER BY type, name", [$zoneId]);
@mkdir(self::$zonesDir, 0755, true);
$domain = $zone['domain'];
$content = "\$ORIGIN {$domain}.\n\$TTL {$zone['ttl']}\n\n";
$content .= "@ IN SOA {$zone['primary_ns']}. {$zone['admin_email']}. (\n";
$content .= " {$zone['serial']} ; serial\n";
$content .= " {$zone['refresh']} ; refresh\n";
$content .= " {$zone['retry']} ; retry\n";
$content .= " {$zone['expire']} ; expire\n";
$content .= " {$zone['minimum']} ; minimum\n)\n\n";
$content .= "@ IN NS {$zone['primary_ns']}.\n";
$content .= "@ IN NS {$zone['secondary_ns']}.\n\n";
foreach ($records as $r) {
$name = $r['name'] === '@' ? '@' : $r['name'];
$prio = $r['priority'] !== null ? "{$r['priority']} " : '';
$val = in_array($r['type'], ['TXT','SPF','DMARC','DKIM']) ? "\"{$r['content']}\"" : $r['content'];
$content .= "{$name} {$r['ttl']} IN {$r['type']} {$prio}{$val}\n";
}
file_put_contents(self::$zonesDir . '/' . $domain . '.zone', $content);
self::rebuildNamedConf();
}
private static function rebuildNamedConf(): void {
@mkdir(self::$zonesDir, 0755, true);
$zones = glob(self::$zonesDir . '/*.zone') ?: [];
$conf = "// NovaCPX auto-generated zone list\n";
foreach ($zones as $zf) {
$domain = basename($zf, '.zone');
$conf .= "zone \"{$domain}\" { type master; file \"" . self::$zonesDir . "/{$domain}.zone\"; };\n";
}
file_put_contents(self::$namedConf, $conf);
// Include in main named.conf if not already there
$mainConf = '/etc/bind/named.conf';
if (file_exists($mainConf) && !str_contains(file_get_contents($mainConf), 'named.conf.novacpx')) {
file_put_contents($mainConf, "\ninclude \"" . self::$namedConf . "\";\n", FILE_APPEND);
}
}
private static function reloadBind(): void {
shell_exec("rndc reload 2>/dev/null || systemctl reload named 2>/dev/null || true");
}
private static function serverIp(): string {
return trim(shell_exec("hostname -I | awk '{print $1}'") ?: '127.0.0.1');
}
private static function getSetting(string $key, string $default): string {
$row = DB::getInstance()->fetchOne("SELECT value FROM settings WHERE `key` = ?", [$key]);
return $row['value'] ?? $default;
}
}