From d99982416dc355b1be68936c735e85fb49ce983a Mon Sep 17 00:00:00 2001 From: master3395 Date: Sat, 14 Mar 2026 23:51:51 +0100 Subject: [PATCH] Subdomain fixes: creation (FQDN normalize), SSL (child webroot), CloudFlare delete (parent zone), acme-challenge dir --- plogical/customACME.py | 12 +++-- plogical/dnsUtilities.py | 94 ++++++++++++++++++++++++------------- plogical/sslUtilities.py | 39 +++++++++++---- plogical/vhost.py | 11 +++++ websiteFunctions/website.py | 22 ++++++--- 5 files changed, 125 insertions(+), 53 deletions(-) diff --git a/plogical/customACME.py b/plogical/customACME.py index 287f97793..264fec2eb 100644 --- a/plogical/customACME.py +++ b/plogical/customACME.py @@ -19,8 +19,8 @@ import socket class CustomACME: - def __init__(self, domain, admin_email, staging=False, provider='letsencrypt'): - """Initialize CustomACME""" + def __init__(self, domain, admin_email, staging=False, provider='letsencrypt', challenge_path=None): + """Initialize CustomACME. challenge_path: use domain docroot's .well-known/acme-challenge for child domains.""" logging.CyberCPLogFileWriter.writeToFile( f'Initializing CustomACME for domain: {domain}, email: {admin_email}, staging: {staging}, provider: {provider}') self.domain = domain @@ -53,9 +53,13 @@ class CustomACME: self.finalize_url = None self.certificate_url = None - # Initialize paths + # Initialize paths; use domain docroot for child domains so HTTP-01 validation succeeds self.cert_path = f'/etc/letsencrypt/live/{domain}' - self.challenge_path = '/usr/local/lsws/Example/html/.well-known/acme-challenge' + if challenge_path and isinstance(challenge_path, str) and challenge_path.strip(): + self.challenge_path = challenge_path.strip().rstrip('/') + logging.CyberCPLogFileWriter.writeToFile(f'Using domain webroot challenge path: {self.challenge_path}') + else: + self.challenge_path = '/usr/local/lsws/Example/html/.well-known/acme-challenge' self.account_key_path = f'/etc/letsencrypt/accounts/{domain}.key' logging.CyberCPLogFileWriter.writeToFile( f'Certificate path: {self.cert_path}, Challenge path: {self.challenge_path}') diff --git a/plogical/dnsUtilities.py b/plogical/dnsUtilities.py index 06a106fa8..014483dba 100644 --- a/plogical/dnsUtilities.py +++ b/plogical/dnsUtilities.py @@ -951,46 +951,74 @@ class DNS: cf = CloudFlare.CloudFlare(email=email, token=token) try: - # Find the zone for this domain + # Find the zone: for subdomains (e.g. status.newstargeted.com) the zone is the parent (newstargeted.com) + zone_id = None + zone_name = None + is_subdomain = False + + # Try zone = domainName first (main domain) params = {'name': domainName, 'per_page': 50} zones = cf.zones.get(params=params) + for z in sorted(zones, key=lambda v: v['name']): + if z['name'] == domainName: + zone_id = z['id'] + zone_name = z['name'] + break - for zone in sorted(zones, key=lambda v: v['name']): - if zone['name'] == domainName: - zone_id = zone['id'] - - # Get all DNS records for this zone - try: - dns_records = cf.zones.dns_records.get(zone_id) - - # Delete all DNS records - deleted_count = 0 - for record in dns_records: - try: - cf.zones.dns_records.delete(zone_id, record['id']) - deleted_count += 1 - except Exception as e: - logging.CyberCPLogFileWriter.writeToFile( - f'Error deleting CloudFlare DNS record {record["id"]} for {domainName}: {str(e)}') - - if deleted_count > 0: - logging.CyberCPLogFileWriter.writeToFile( - f'Deleted {deleted_count} CloudFlare DNS records for {domainName}') - return 1, f"Deleted {deleted_count} DNS records" - else: - return 1, "No DNS records found to delete" - - except CloudFlare.exceptions.CloudFlareAPIError as e: + # If not found, try parent zone (subdomain case: status.newstargeted.com -> newstargeted.com) + if not zone_id and '.' in domainName: + parent_domain = domainName.split('.', 1)[1] + params = {'name': parent_domain, 'per_page': 50} + zones = cf.zones.get(params=params) + for z in sorted(zones, key=lambda v: v['name']): + if z['name'] == parent_domain: + zone_id = z['id'] + zone_name = z['name'] + is_subdomain = True logging.CyberCPLogFileWriter.writeToFile( - f'CloudFlare API error deleting DNS records for {domainName}: {str(e)}') - return 0, str(e) + f'Subdomain {domainName}: using parent zone {zone_name}') + break + + if not zone_id: + return 1, "Domain not found in CloudFlare" + + # 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") + if is_subdomain: + subdomain_label = domainName.split('.')[0] + def record_matches(r): + n = (r.get('name') or '').rstrip('.') + return n == domainName or n == subdomain_label + to_delete = [r for r in dns_records if record_matches(r)] + else: + to_delete = list(dns_records) + + deleted_count = 0 + for record in to_delete: + try: + cf.zones.dns_records.delete(zone_id, record['id']) + deleted_count += 1 except Exception as e: logging.CyberCPLogFileWriter.writeToFile( - f'Error getting CloudFlare DNS records for {domainName}: {str(e)}') - return 0, str(e) + f'Error deleting CloudFlare DNS record {record["id"]} for {domainName}: {str(e)}') - # Zone not found in CloudFlare - return 1, "Domain not found in CloudFlare" + if deleted_count > 0: + logging.CyberCPLogFileWriter.writeToFile( + f'Deleted {deleted_count} CloudFlare DNS records for {domainName}') + return 1, f"Deleted {deleted_count} DNS records" + else: + return 1, "No DNS records found to delete" + + except CloudFlare.exceptions.CloudFlareAPIError as e: + logging.CyberCPLogFileWriter.writeToFile( + f'CloudFlare API error deleting DNS records for {domainName}: {str(e)}') + return 0, str(e) + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile( + f'Error getting CloudFlare DNS records for {domainName}: {str(e)}') + return 0, str(e) except CloudFlare.exceptions.CloudFlareAPIError as e: logging.CyberCPLogFileWriter.writeToFile( diff --git a/plogical/sslUtilities.py b/plogical/sslUtilities.py index 0aea6feb9..4e93c521e 100644 --- a/plogical/sslUtilities.py +++ b/plogical/sslUtilities.py @@ -711,11 +711,30 @@ context /.well-known/acme-challenge { sslUtilities.PatchVhostConf(virtualHostName) - if not os.path.exists('/usr/local/lsws/Example/html/.well-known/acme-challenge'): - command = f'mkdir -p /usr/local/lsws/Example/html/.well-known/acme-challenge' + default_webroot = '/usr/local/lsws/Example/html' + # Child domains: use their docroot so HTTP-01 challenge is served from the correct vhost + if sslpath and str(sslpath).strip(): + webroot = str(sslpath).strip().rstrip('/') + if webroot != default_webroot and os.path.isdir(webroot): + challenge_path = webroot + '/.well-known/acme-challenge' + if not os.path.exists(challenge_path): + os.makedirs(challenge_path, exist_ok=True) + command = f'chmod -R 755 {webroot}/.well-known' + ProcessUtilities.executioner(command) + logging.CyberCPLogFileWriter.writeToFile( + f"Using domain webroot for ACME challenge: {webroot}") + else: + webroot = default_webroot + challenge_path = None + else: + webroot = default_webroot + challenge_path = None + + if not os.path.exists(default_webroot + '/.well-known/acme-challenge'): + command = f'mkdir -p {default_webroot}/.well-known/acme-challenge' ProcessUtilities.normalExecutioner(command) - command = f'chmod -R 755 /usr/local/lsws/Example/html' + command = f'chmod -R 755 {default_webroot}' ProcessUtilities.executioner(command) # Try Let's Encrypt first @@ -752,7 +771,7 @@ context /.well-known/acme-challenge { except: pass - acme = CustomACME(virtualHostName, adminEmail, staging=False, provider='letsencrypt') + acme = CustomACME(virtualHostName, adminEmail, staging=False, provider='letsencrypt', challenge_path=challenge_path) if acme.issue_certificate(domains, use_dns=use_dns): logging.CyberCPLogFileWriter.writeToFile( f"Successfully obtained SSL using Let's Encrypt for: {virtualHostName}") @@ -797,7 +816,7 @@ context /.well-known/acme-challenge { logging.CyberCPLogFileWriter.writeToFile( f"www.{aliasDomain} has no DNS records, excluding from SSL request") - acme = CustomACME(virtualHostName, adminEmail, staging=False, provider='zerossl') + acme = CustomACME(virtualHostName, adminEmail, staging=False, provider='zerossl', challenge_path=challenge_path) if acme.issue_certificate(domains, use_dns=use_dns): logging.CyberCPLogFileWriter.writeToFile( f"Successfully obtained SSL using ZeroSSL for: {virtualHostName}") @@ -836,8 +855,8 @@ context /.well-known/acme-challenge { command = acmePath + " --issue" + domain_list \ + ' --cert-file ' + existingCertPath + '/cert.pem' + ' --key-file ' + existingCertPath + '/privkey.pem' \ - + ' --fullchain-file ' + existingCertPath + '/fullchain.pem' + ' -w /usr/local/lsws/Example/html -k ec-256 --force --staging' \ - + ' --webroot-path /usr/local/lsws/Example/html' + + ' --fullchain-file ' + existingCertPath + '/fullchain.pem' + ' -w ' + webroot + ' -k ec-256 --force --staging' \ + + ' --webroot-path ' + webroot try: result = subprocess.run(command, capture_output=True, universal_newlines=True, shell=True) @@ -849,8 +868,8 @@ context /.well-known/acme-challenge { if result.returncode == 0: command = acmePath + " --issue" + domain_list \ + ' --cert-file ' + existingCertPath + '/cert.pem' + ' --key-file ' + existingCertPath + '/privkey.pem' \ - + ' --fullchain-file ' + existingCertPath + '/fullchain.pem' + ' -w /usr/local/lsws/Example/html -k ec-256 --force --server letsencrypt' \ - + ' --webroot-path /usr/local/lsws/Example/html' + + ' --fullchain-file ' + existingCertPath + '/fullchain.pem' + ' -w ' + webroot + ' -k ec-256 --force --server letsencrypt' \ + + ' --webroot-path ' + webroot try: result = subprocess.run(command, capture_output=True, universal_newlines=True, shell=True) @@ -892,7 +911,7 @@ context /.well-known/acme-challenge { command = acmePath + " --issue" + domain_list \ + ' --cert-file ' + existingCertPath + '/cert.pem' + ' --key-file ' + existingCertPath + '/privkey.pem' \ - + ' --fullchain-file ' + existingCertPath + '/fullchain.pem' + ' -w /usr/local/lsws/Example/html -k ec-256 --force --server letsencrypt' + + ' --fullchain-file ' + existingCertPath + '/fullchain.pem' + ' -w ' + webroot + ' -k ec-256 --force --server letsencrypt' try: result = subprocess.run(command, capture_output=True, universal_newlines=True, shell=True) diff --git a/plogical/vhost.py b/plogical/vhost.py index 23b53e710..b67eb5080 100644 --- a/plogical/vhost.py +++ b/plogical/vhost.py @@ -1047,6 +1047,17 @@ class vhost: cmd = shlex.split(command) subprocess.call(cmd, stdout=FNULL, stderr=subprocess.STDOUT) + # Create .well-known/acme-challenge so LiteSpeed config validation does not fail (path must exist) + acme_path = path.rstrip('/') + '/.well-known/acme-challenge' + try: + command = 'sudo -u %s mkdir -p %s' % (virtualHostUser, acme_path) + ProcessUtilities.normalExecutioner(command) + command = "sudo -u %s chmod 755 %s" % (virtualHostUser, acme_path) + subprocess.call(shlex.split(command), stdout=FNULL, stderr=subprocess.STDOUT) + except Exception as acme_err: + logging.CyberCPLogFileWriter.writeToFile( + str(acme_err) + " [createDirectoryForDomain acme-challenge]") + except OSError as msg: logging.CyberCPLogFileWriter.writeToFile( str(msg) + "329 [Not able to create directories for virtual host [createDirectoryForDomain]]") diff --git a/websiteFunctions/website.py b/websiteFunctions/website.py index b2e0d2e0e..216918c5e 100644 --- a/websiteFunctions/website.py +++ b/websiteFunctions/website.py @@ -2401,13 +2401,22 @@ Require valid-user except: alias = 0 - masterDomain = data['masterDomain'] - domain = data['domainName'] + masterDomain = (data.get('masterDomain') or '').strip() + domain = (data.get('domainName') or '').strip() + # When user enters only the subdomain label (e.g. "ai"), build full FQDN (e.g. "ai.newstargeted.com") + # so validators.domain() passes; single-label "ai" is not a valid domain. + if domain and '.' not in domain and masterDomain: + domain = domain + '.' + masterDomain + + if not domain: + data_ret = {'status': 0, 'createWebSiteStatus': 0, 'error_message': "Invalid domain."} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) if alias == 0: phpSelection = data['phpSelection'] - path = data['path'] + path = (data.get('path') or '').strip() else: ### if master website have apache then create this sub-domain also as ols + apache @@ -2426,13 +2435,13 @@ Require valid-user json_data = json.dumps(data_ret) return HttpResponse(json_data) - if data['domainName'].find("cyberpanel.website") > -1: + if domain.find("cyberpanel.website") > -1: url = "https://platform.cyberpersons.com/CyberpanelAdOns/CreateDomain" domain_data = { "name": "test-domain", "IP": ACLManager.GetServerIP(), - "domain": data['domainName'] + "domain": domain } import requests @@ -2449,7 +2458,8 @@ Require valid-user else: return ACLManager.loadErrorJson('createWebSiteStatus', 0) - if data['path'].find('..') > -1: + path_from_data = (data.get('path') or '') if alias == 0 else '' + if path_from_data.find('..') > -1: return ACLManager.loadErrorJson('createWebSiteStatus', 0) if currentACL['admin'] != 1: