Subdomain fixes: creation (FQDN normalize), SSL (child webroot), CloudFlare delete (parent zone), acme-challenge dir

This commit is contained in:
master3395
2026-03-14 23:51:51 +01:00
parent 9b038badec
commit d99982416d
5 changed files with 125 additions and 53 deletions

View File

@@ -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}')

View File

@@ -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(

View File

@@ -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)

View File

@@ -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]]")

View File

@@ -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: