mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-05-06 19:47:00 +02:00
Merge pull request #1737 from master3395/v2.5.5-dev
Subdomain fixes: creation (FQDN normalize), SSL (child webroot), Clou…
This commit is contained in:
@@ -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}')
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]]")
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user