diff --git a/panel/lib/AccountManager.php b/panel/lib/AccountManager.php index 1f56c35..b20f0de 100644 --- a/panel/lib/AccountManager.php +++ b/panel/lib/AccountManager.php @@ -28,9 +28,9 @@ class AccountManager { // Create Linux user self::shell("useradd -m -d {$homeDir} -s /sbin/nologin -G www-data " . escapeshellarg($username)); self::shell("echo " . escapeshellarg("{$username}:{$password}") . " | chpasswd"); - self::shell("mkdir -p {$docRoot} {$homeDir}/logs {$homeDir}/tmp"); - self::shell("chown -R {$username}:www-data {$homeDir}"); - self::shell("chmod 750 {$homeDir}; chmod 755 {$docRoot}"); + self::shell("sudo mkdir -p {$docRoot} {$homeDir}/logs {$homeDir}/tmp"); + self::shell("sudo chown -R {$username}:www-data {$homeDir}"); + self::shell("sudo chmod 750 {$homeDir}"); self::shell("sudo chmod 775 {$docRoot}"); // Default index page file_put_contents("{$docRoot}/index.html", @@ -116,9 +116,9 @@ class AccountManager { public static function provisionEmailDNS(int $acctId, string $domain): void { // Generate DKIM keypair $keyDir = "/etc/opendkim/keys/{$domain}"; - self::shell("mkdir -p " . escapeshellarg($keyDir)); + self::shell("sudo mkdir -p " . escapeshellarg($keyDir)); self::shell("opendkim-genkey -b 2048 -s mail -d " . escapeshellarg($domain) . " -D " . escapeshellarg($keyDir)); - self::shell("chown -R opendkim:opendkim " . escapeshellarg($keyDir)); + self::shell("sudo chown -R opendkim:opendkim " . escapeshellarg($keyDir)); // Parse public key from .txt file $keyTxt = @file_get_contents("{$keyDir}/mail.txt") ?: ''; @@ -139,13 +139,17 @@ class AccountManager { ); // DKIM TXT record - DNSManager::addRecord($acctId, $domain, 'TXT', "mail._domainkey", "v=DKIM1; k=rsa; p={$pubKey}", 300); + $zoneRow = DB::getInstance()->fetchOne("SELECT id FROM dns_zones WHERE account_id=? AND domain=?", [$acctId, $domain]); + if ($zoneRow) DNSManager::addRecord((int)$zoneRow['id'], 'mail._domainkey', 'TXT', "v=DKIM1; k=rsa; p={$pubKey}", 300); } - // SPF - DNSManager::addRecord($acctId, $domain, 'TXT', '@', "v=spf1 mx a ~all", 300); - // DMARC - DNSManager::addRecord($acctId, $domain, 'TXT', '_dmarc', "v=DMARC1; p=quarantine; rua=mailto:dmarc@{$domain}", 300); + // SPF + DMARC — look up zone once + $db2 = DB::getInstance(); + $zoneRow = $zoneRow ?? $db2->fetchOne("SELECT id FROM dns_zones WHERE account_id=? AND domain=?", [$acctId, $domain]); + if ($zoneRow) { + DNSManager::addRecord((int)$zoneRow['id'], '@', 'TXT', "v=spf1 mx a ~all", 3600); + DNSManager::addRecord((int)$zoneRow['id'], '_dmarc', 'TXT', "v=DMARC1; p=quarantine; rua=mailto:dmarc@{$domain}", 3600); + } novacpx_log('info', "Email DNS provisioned for $domain"); } @@ -154,9 +158,9 @@ class AccountManager { $db = DB::getInstance(); $selector = 'mail' . date('Ym'); $keyDir = "/etc/opendkim/keys/{$domain}"; - self::shell("mkdir -p " . escapeshellarg($keyDir)); + self::shell("sudo mkdir -p " . escapeshellarg($keyDir)); self::shell("opendkim-genkey -b 2048 -s {$selector} -d " . escapeshellarg($domain) . " -D " . escapeshellarg($keyDir)); - self::shell("chown -R opendkim:opendkim " . escapeshellarg($keyDir)); + self::shell("sudo chown -R opendkim:opendkim " . escapeshellarg($keyDir)); $keyTxt = @file_get_contents("{$keyDir}/{$selector}.txt") ?: ''; preg_match('/p=([A-Za-z0-9+\/=]+)/', $keyTxt, $m); @@ -174,7 +178,8 @@ class AccountManager { ); // Add new TXT record, remove old mail._domainkey - DNSManager::addRecord($acctId, $domain, 'TXT', "{$selector}._domainkey", "v=DKIM1; k=rsa; p={$pubKey}", 300); + $zoneRow = $db->fetchOne("SELECT id FROM dns_zones WHERE account_id=? AND domain=?", [$acctId, $domain]); + if ($zoneRow) DNSManager::addRecord((int)$zoneRow['id'], "{$selector}._domainkey", 'TXT', "v=DKIM1; k=rsa; p={$pubKey}", 300); novacpx_log('info', "DKIM rotated for $domain, new selector: $selector"); return $selector; } @@ -185,6 +190,12 @@ class AccountManager { } private static function shell(string $cmd): string { + // Prefix privileged commands with sudo so www-data can run them + $privileged = ['useradd','userdel','usermod','chpasswd','a2ensite','a2dissite','apache2ctl','certbot','opendkim-genkey','rndc','named-checkzone','systemctl']; + $cmdBase = explode(' ', ltrim($cmd))[0]; + foreach ($privileged as $p) { + if (str_ends_with($cmdBase, $p) || $cmdBase === $p) { $cmd = 'sudo ' . $cmd; break; } + } $out = shell_exec($cmd . ' 2>&1'); novacpx_log('debug', "shell: $cmd"); return $out ?: ''; diff --git a/panel/lib/DNSManager.php b/panel/lib/DNSManager.php index 0694a7e..48e626f 100644 --- a/panel/lib/DNSManager.php +++ b/panel/lib/DNSManager.php @@ -26,7 +26,6 @@ class DNSManager { ['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( @@ -127,13 +126,14 @@ class DNSManager { // 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); + if (file_exists($mainConf) && !str_contains(file_get_contents($mainConf) ?: '', 'named.conf.novacpx')) { + $line = "\ninclude \"" . self::$namedConf . "\";\n"; + shell_exec("echo " . escapeshellarg($line) . " | sudo tee -a {$mainConf} > /dev/null 2>&1"); } } private static function reloadBind(): void { - shell_exec("rndc reload 2>/dev/null || systemctl reload named 2>/dev/null || true"); + shell_exec("sudo rndc reload 2>/dev/null || sudo systemctl reload named 2>/dev/null || sudo systemctl reload bind9 2>/dev/null || true"); } private static function serverIp(): string {