From 2077692a7311d9d2c8f3457f7fc7b988fdc8eb7f Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 6 Apr 2026 00:57:06 +0200 Subject: [PATCH 1/3] fix(dns): delete all Cloudflare DNS records for child subdomains When removing a child domain, matching only the apex label (e.g. vscode) left mail.* and www.* (and MX/TXT/DMARC) records in the parent zone. Normalize record names to FQDN under the zone and delete the subdomain FQDN plus any names under it. --- plogical/dnsUtilities.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/plogical/dnsUtilities.py b/plogical/dnsUtilities.py index 014483dba..de5fe80b6 100644 --- a/plogical/dnsUtilities.py +++ b/plogical/dnsUtilities.py @@ -985,12 +985,25 @@ class DNS: # Get all DNS records for this zone try: dns_records = cf.zones.dns_records.get(zone_id) - # For subdomains, only delete records that match this subdomain (name can be "status" or "status.newstargeted.com") + # For subdomains, delete the FQDN and any deeper names (mail.sub, www.sub, _dmarc.sub, etc.) if is_subdomain: - subdomain_label = domainName.split('.')[0] + base_fqdn = domainName.rstrip('.').lower() + zone_fqdn = (zone_name or '').rstrip('.').lower() + + def record_to_fqdn(record_name): + n = (record_name or '').rstrip('.').lower() + if not zone_fqdn: + return n + if n == zone_fqdn or n.endswith('.' + zone_fqdn): + return n + return ('%s.%s' % (n, zone_fqdn)).rstrip('.') + def record_matches(r): - n = (r.get('name') or '').rstrip('.') - return n == domainName or n == subdomain_label + fqdn = record_to_fqdn(r.get('name') or '') + if fqdn == base_fqdn: + return True + return fqdn.endswith('.' + base_fqdn) + to_delete = [r for r in dns_records if record_matches(r)] else: to_delete = list(dns_records) From 385a1080145a459287ce00bca49dd8235d09d454 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 6 Apr 2026 01:48:35 +0200 Subject: [PATCH 2/3] Fix OLS+Apache child domains: DNS CF sync, ACME paths, subdomain creation UX - dnsUtilities: correct createDNSRecordCloudFlare argument order (priority, ttl) - vhostConfs/ApacheVhosts: OLSLBConf uses real docRoot and acme-challenge path for child vhosts (vhRoot is parent domain) - virtualHostUtilities: defer ChildDomains save until after SSL/Apache; cleanup ORM row on failure; createDomain CLI exits 0/1 with 1,/0, stdout - websiteFunctions: submitDomainCreation waits on subprocess and returns failure JSON on error --- ApachController/ApacheVhosts.py | 12 ++++++++++-- plogical/dnsUtilities.py | 2 +- plogical/vhostConfs.py | 4 ++-- plogical/virtualHostUtilities.py | 33 +++++++++++++++++++++----------- websiteFunctions/website.py | 31 ++++++++++++++++++++++++++---- 5 files changed, 62 insertions(+), 20 deletions(-) diff --git a/ApachController/ApacheVhosts.py b/ApachController/ApacheVhosts.py index 271f0f52a..3ac950ed8 100644 --- a/ApachController/ApacheVhosts.py +++ b/ApachController/ApacheVhosts.py @@ -396,8 +396,9 @@ class ApacheVhost: logging.writeToFile(str(msg)) @staticmethod - def perHostVirtualConfOLS(vhFile, administratorEmail): - # General Configurations tab + def perHostVirtualConfOLS(vhFile, administratorEmail, documentRoot=None): + # General Configurations tab. Child domains use vhRoot=master path in olsChildMainConf, so docRoot + # must be the real document root (not $VH_ROOT/public_html) for content and ACME HTTP-01. try: confFile = open(vhFile, "w+") virtualHostName = vhFile.split('/')[6] @@ -405,6 +406,13 @@ class ApacheVhost: currentConf = vhostConfs.OLSLBConf currentConf = currentConf.replace('{adminEmails}', administratorEmail) currentConf = currentConf.replace('{domain}', virtualHostName) + if documentRoot: + root = documentRoot.rstrip('/') + currentConf = currentConf.replace('{olsDocRoot}', root) + currentConf = currentConf.replace('{olsAcmeChallengeRoot}', root + '/.well-known/acme-challenge') + else: + currentConf = currentConf.replace('{olsDocRoot}', '$VH_ROOT/public_html') + currentConf = currentConf.replace('{olsAcmeChallengeRoot}', '$VH_ROOT/public_html/.well-known/acme-challenge') confFile.write(currentConf) confFile.close() diff --git a/plogical/dnsUtilities.py b/plogical/dnsUtilities.py index de5fe80b6..cae10c88b 100644 --- a/plogical/dnsUtilities.py +++ b/plogical/dnsUtilities.py @@ -889,7 +889,7 @@ class DNS: for zone in sorted(zones, key=lambda v: v['name']): zone = zone['id'] - DNS.createDNSRecordCloudFlare(cf, zone, name, type, value, ttl, priority) + DNS.createDNSRecordCloudFlare(cf, zone, name, type, value, priority, ttl) except CloudFlare.exceptions.CloudFlareAPIError as e: logging.CyberCPLogFileWriter.writeToFile(str(e)) diff --git a/plogical/vhostConfs.py b/plogical/vhostConfs.py index 1c1763597..55f593012 100644 --- a/plogical/vhostConfs.py +++ b/plogical/vhostConfs.py @@ -343,7 +343,7 @@ retryTimeout 0 respBuffer 0 } """ - OLSLBConf = """docRoot $VH_ROOT/public_html + OLSLBConf = """docRoot {olsDocRoot} vhDomain $VH_NAME vhAliases www.$VH_NAME adminEmails {adminEmails} @@ -371,7 +371,7 @@ accesslog $VH_ROOT/logs/$VH_NAME.access_log { } context /.well-known/acme-challenge { - location $VH_ROOT/public_html/.well-known/acme-challenge + location {olsAcmeChallengeRoot} allowBrowse 1 rewrite { diff --git a/plogical/virtualHostUtilities.py b/plogical/virtualHostUtilities.py index 029d827a5..5e2ad5843 100644 --- a/plogical/virtualHostUtilities.py +++ b/plogical/virtualHostUtilities.py @@ -1843,11 +1843,7 @@ local_name %s { raise BaseException(retValues[1]) ## Now restart litespeed after initial configurations are done - - if LimitsCheck: - website = ChildDomains(master=master, domain=virtualHostName, path=path, phpSelection=phpVersion, - ssl=ssl, alais=alias) - website.save() + ## ChildDomains row is saved only after SSL/Apache succeed (avoids orphan DB rows on failure) if ssl == 1: logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, 'Creating SSL..,50') @@ -1883,7 +1879,7 @@ local_name %s { if result[0] == 0: raise BaseException(result[1]) else: - ApacheVhost.perHostVirtualConfOLS(completePathToConfigFile, master.adminEmail) + ApacheVhost.perHostVirtualConfOLS(completePathToConfigFile, master.adminEmail, path) installUtilities.installUtilities.reStartLiteSpeed() php = PHPManager.getPHPString(phpVersion) @@ -1900,10 +1896,20 @@ local_name %s { if dkimCheck == 1: DNS.createDKIMRecords(virtualHostName) + if LimitsCheck: + website = ChildDomains(master=master, domain=virtualHostName, path=path, phpSelection=phpVersion, + ssl=ssl, alais=alias) + website.save() + logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, 'Domain successfully created. [200]') return 1, "None" except BaseException as msg: + try: + if ChildDomains.objects.filter(domain=virtualHostName).exists(): + ChildDomains.objects.filter(domain=virtualHostName).delete() + except BaseException: + pass if ACLManager.FindIfChild() == 0: numberOfWebsites = Websites.objects.count() + ChildDomains.objects.count() vhost.deleteCoreConf(virtualHostName, numberOfWebsites) @@ -1981,11 +1987,11 @@ local_name %s { if child: # Handle None values for child domains admin_email = website.master.adminEmail if website.master.adminEmail else website.master.admin.email - ApacheVhost.perHostVirtualConfOLS(completePathToConfigFile, admin_email) + ApacheVhost.perHostVirtualConfOLS(completePathToConfigFile, admin_email, website.path) else: # Handle None values for main domains admin_email = website.adminEmail if website.adminEmail else website.admin.email - ApacheVhost.perHostVirtualConfOLS(completePathToConfigFile, admin_email) + ApacheVhost.perHostVirtualConfOLS(completePathToConfigFile, admin_email, None) if child: # Handle None values for child domains @@ -2423,9 +2429,14 @@ def main(): except: aliasDomain = 0 - virtualHostUtilities.createDomain(args.masterDomain, args.virtualHostName, args.phpVersion, args.path, - int(args.ssl), dkimCheck, openBasedir, args.websiteOwner, apache, - tempStatusPath, 1, aliasDomain) + ret = virtualHostUtilities.createDomain(args.masterDomain, args.virtualHostName, args.phpVersion, args.path, + int(args.ssl), dkimCheck, openBasedir, args.websiteOwner, apache, + tempStatusPath, 1, aliasDomain) + if ret[0] == 1: + print("1," + str(ret[1])) + sys.exit(0) + print("0," + str(ret[1])) + sys.exit(1) elif args.function == "issueSSL": virtualHostUtilities.issueSSL(args.virtualHostName, args.path, args.administratorEmail) elif args.function == "issueSSLv2": diff --git a/websiteFunctions/website.py b/websiteFunctions/website.py index c976ba90e..419eacb15 100644 --- a/websiteFunctions/website.py +++ b/websiteFunctions/website.py @@ -2487,11 +2487,34 @@ Require valid-user + " --openBasedir " + str(data['openBasedir']) + ' --path ' + path + ' --websiteOwner ' \ + admin.userName + ' --tempStatusPath ' + tempStatusPath + " --apache " + apacheBackend + f' --aliasDomain {str(alias)}' - ProcessUtilities.popenExecutioner(execPath) - time.sleep(2) + create_result = subprocess.run(execPath, shell=True, capture_output=True, text=True, timeout=1800) + st = '' + try: + if os.path.isfile(tempStatusPath): + with open(tempStatusPath, 'r', encoding='utf-8', errors='replace') as sf: + st = sf.read().strip() + except BaseException: + st = '' + out = (create_result.stdout or '').strip() + last_line = out.split('\n')[-1] if out else '' + cli_ok = last_line.startswith('1,') + cli_fail = last_line.startswith('0,') + status_ok = ('Domain successfully created.' in st and '[200]' in st) + status_fail = ('[404]' in st) - data_ret = {'status': 1, 'createWebSiteStatus': 1, 'error_message': "None", - 'tempStatusPath': tempStatusPath} + if create_result.returncode == 0 and (cli_ok or status_ok) and not status_fail: + data_ret = {'status': 1, 'createWebSiteStatus': 1, 'error_message': "None", + 'tempStatusPath': tempStatusPath} + else: + err_msg = 'Child domain creation failed.' + if cli_fail and len(last_line) > 2: + err_msg = last_line.split(',', 1)[1].strip() or err_msg + elif st: + err_msg = st.replace('. [404]', '').strip() or err_msg + elif create_result.stderr and create_result.stderr.strip(): + err_msg = create_result.stderr.strip()[:500] + data_ret = {'status': 0, 'createWebSiteStatus': 0, 'error_message': err_msg[:2000], + 'tempStatusPath': tempStatusPath} json_data = json.dumps(data_ret) return HttpResponse(json_data) From f09f7b96b9b949e48a2d20359149e2bdfb1440e1 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 7 Apr 2026 18:50:16 +0200 Subject: [PATCH 3/3] DNS: stop auto-creating duplicate DMARC TXT records CyberPanel previously added _dmarc at the apex (p=none) in two code paths and _dmarc on every child subdomain, which conflicts with a single externally managed policy (e.g. Cloudflare) and violates RFC 7489 (one TXT RRset per name). Comment out automatic DMARC creation so operators set one record at _dmarc. only. --- plogical/dnsUtilities.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plogical/dnsUtilities.py b/plogical/dnsUtilities.py index cae10c88b..ea00d3c6f 100644 --- a/plogical/dnsUtilities.py +++ b/plogical/dnsUtilities.py @@ -326,7 +326,9 @@ class DNS: # auth=1) # record.save() - DNS.createDNSRecord(zone, "_dmarc." + topLevelDomain, "TXT", "v=DMARC1; p=none;", 0, 3600) + # Apex DMARC: do not auto-add p=none here — use one TXT at _dmarc. in Cloudflare/DNS + # to avoid conflicting duplicate DMARC records (invalid per RFC 7489). + # DNS.createDNSRecord(zone, "_dmarc." + topLevelDomain, "TXT", "v=DMARC1; p=none;", 0, 3600) # record = Records(domainOwner=zone, # domain_id=zone.id, @@ -489,7 +491,9 @@ class DNS: # auth=1) # record.save() - DNS.createDNSRecord(zone, "_dmarc." + topLevelDomain, "TXT", "v=DMARC1; p=none;", 0, 3600) + # Apex DMARC: do not auto-add p=none here — use one TXT at _dmarc. in Cloudflare/DNS + # to avoid conflicting duplicate DMARC records (invalid per RFC 7489). + # DNS.createDNSRecord(zone, "_dmarc." + topLevelDomain, "TXT", "v=DMARC1; p=none;", 0, 3600) # record = Records(domainOwner=zone, # domain_id=zone.id, @@ -585,7 +589,9 @@ class DNS: # auth=1) # record.save() - DNS.createDNSRecord(zone, "_dmarc." + actualSubDomain, "TXT", "v=DMARC1; p=none;", 0, 3600) + # Do not auto-create subdomain _dmarc: one organizational policy at _dmarc. is enough for + # typical setups; avoids dozens of p=none records and Cloudflare clutter. + # DNS.createDNSRecord(zone, "_dmarc." + actualSubDomain, "TXT", "v=DMARC1; p=none;", 0, 3600) # record = Records(domainOwner=zone, # domain_id=zone.id,