Refactor configuration modification methods for improved safety and validation

- Introduced a `safeModifyHttpdConfig` method in `installUtilities` to handle modifications to the OpenLiteSpeed configuration file with backup, validation, and rollback capabilities.
- Updated various modules (`modSec.py`, `sslUtilities.py`, `tuning.py`, `vhost.py`, etc.) to utilize the new safe modification method, enhancing reliability and preventing configuration corruption.
- Improved error handling and logging throughout the configuration modification processes to ensure better traceability and debugging.
This commit is contained in:
Master3395 2025-12-31 23:13:53 +01:00
parent b5d81eb68a
commit 18b1bad51f
8 changed files with 369 additions and 172 deletions

View File

@ -177,10 +177,18 @@ class PHPManager:
php_versions = [] php_versions = []
for entry in lsphp_lines: for entry in lsphp_lines:
# Find substring starting with 'php' and extract the version part # Find substring starting with 'php' and extract the version part
version = entry.split('php')[1] try:
# Format version as PHP X.Y if 'php' not in entry:
formatted_version = f"PHP {version[0]}.{version[1]}" continue
php_versions.append(formatted_version) parts = entry.split('php')
if len(parts) < 2 or len(parts[1]) < 2:
continue
version = parts[1]
# Format version as PHP X.Y
formatted_version = f"PHP {version[0]}.{version[1]}"
php_versions.append(formatted_version)
except (IndexError, ValueError):
continue
else: else:
lsphp_lines = [line for line in result.split('\n')] lsphp_lines = [line for line in result.split('\n')]

View File

@ -6,6 +6,7 @@ import pexpect
import os import os
import shlex import shlex
from plogical.processUtilities import ProcessUtilities from plogical.processUtilities import ProcessUtilities
from datetime import datetime
class installUtilities: class installUtilities:
@ -219,26 +220,147 @@ class installUtilities:
return 0 return 0
return 1 return 1
@staticmethod
def safeModifyHttpdConfig(config_modifier, description="config modification"):
"""
Safely modify httpd_config.conf with backup, validation, and rollback on failure.
Prevents corrupted configs that cause OpenLiteSpeed to fail binding ports 80/443.
Args:
config_modifier: A function that takes file content (list of lines) and returns modified content
description: Description of the modification for logging
Returns:
tuple: (success: bool, error_message: str or None)
Reference: https://github.com/usmannasir/cyberpanel/issues/1609
"""
config_file = "/usr/local/lsws/conf/httpd_config.conf"
if not os.path.exists(config_file):
error_msg = f"Config file not found: {config_file}"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
return False, error_msg
# Create backup with timestamp
try:
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
backup_file = f"{config_file}.backup-{timestamp}"
shutil.copy2(config_file, backup_file)
logging.writeToFile(f"[safeModifyHttpdConfig] Created backup: {backup_file} for {description}")
except Exception as e:
error_msg = f"Failed to create backup: {str(e)}"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
return False, error_msg
# Read current config
try:
with open(config_file, 'r') as f:
original_content = f.readlines()
except Exception as e:
error_msg = f"Failed to read config file: {str(e)}"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
return False, error_msg
# Modify config using callback
try:
modified_content = config_modifier(original_content)
if not isinstance(modified_content, list):
error_msg = "Config modifier must return a list of lines"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
return False, error_msg
except Exception as e:
error_msg = f"Config modifier function failed: {str(e)}"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
return False, error_msg
# Write modified config
try:
with open(config_file, 'w') as f:
f.writelines(modified_content)
except Exception as e:
error_msg = f"Failed to write modified config: {str(e)}"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
# Restore backup
try:
shutil.copy2(backup_file, config_file)
logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to write failure")
except:
pass
return False, error_msg
# Validate config using openlitespeed -t
try:
if ProcessUtilities.decideServer() == ProcessUtilities.OLS:
validate_cmd = ['/usr/local/lsws/bin/openlitespeed', '-t', '-f', config_file]
else:
# For LiteSpeed Enterprise, use lswsctrl
validate_cmd = ['/usr/local/lsws/bin/lswsctrl', '-t', '-f', config_file]
result = subprocess.run(validate_cmd, capture_output=True, text=True, timeout=30)
if result.returncode != 0:
error_msg = f"Config validation failed: {result.stderr}"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
# Restore backup
try:
shutil.copy2(backup_file, config_file)
logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation failure")
except Exception as restore_error:
logging.writeToFile(f"[safeModifyHttpdConfig] CRITICAL: Failed to restore backup: {str(restore_error)}")
return False, error_msg
except subprocess.TimeoutExpired:
error_msg = "Config validation timed out"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
# Restore backup
try:
shutil.copy2(backup_file, config_file)
logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation timeout")
except:
pass
return False, error_msg
except Exception as e:
error_msg = f"Config validation error: {str(e)}"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
# Restore backup
try:
shutil.copy2(backup_file, config_file)
logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation error")
except:
pass
return False, error_msg
logging.writeToFile(f"[safeModifyHttpdConfig] Successfully modified and validated config: {description}")
return True, None
@staticmethod @staticmethod
def changePortTo80(): def changePortTo80():
try: try:
data = open("/usr/local/lsws/conf/httpd_config.conf").readlines() def modify_config(lines):
writeDataToFile = open("/usr/local/lsws/conf/httpd_config.conf", 'w') modified = []
for line in lines:
for items in data: if "*:8088" in line:
if (items.find("*:8088") > -1): modified.append(line.replace("*:8088", "*:80"))
writeDataToFile.writelines(items.replace("*:8088","*:80")) else:
else: modified.append(line)
writeDataToFile.writelines(items) return modified
writeDataToFile.close() success, error = installUtilities.safeModifyHttpdConfig(
modify_config,
except IOError as msg: "Change port from 8088 to 80"
)
if not success:
error_msg = error if error else "Unknown error"
logging.writeToFile(f"[changePortTo80] Failed: {error_msg}")
return 0
return installUtilities.reStartLiteSpeed()
except Exception as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + " [changePortTo80]") logging.CyberCPLogFileWriter.writeToFile(str(msg) + " [changePortTo80]")
return 0 return 0
return installUtilities.reStartLiteSpeed()
@staticmethod @staticmethod
def installAllPHPVersion(): def installAllPHPVersion():

View File

@ -234,36 +234,49 @@ modsecurity_rules_file /usr/local/lsws/conf/modsec/rules.conf
if ProcessUtilities.decideServer() == ProcessUtilities.OLS: if ProcessUtilities.decideServer() == ProcessUtilities.OLS:
confFile = os.path.join(virtualHostUtilities.Server_root, "conf/httpd_config.conf") confFile = os.path.join(virtualHostUtilities.Server_root, "conf/httpd_config.conf")
confData = open(confFile).readlines()
conf = open(confFile, 'w') def modify_config(lines):
"""Update ModSecurity configuration parameters"""
for items in confData: modified = []
if items.find('modsecurity ') > -1: for line in lines:
conf.writelines(data[0]) if line.find('modsecurity ') > -1:
continue modified.append(data[0])
elif items.find('SecAuditEngine ') > -1: continue
conf.writelines(data[1]) elif line.find('SecAuditEngine ') > -1:
continue modified.append(data[1])
elif items.find('SecRuleEngine ') > -1: continue
conf.writelines(data[2]) elif line.find('SecRuleEngine ') > -1:
continue modified.append(data[2])
elif items.find('SecDebugLogLevel') > -1: continue
conf.writelines(data[3]) elif line.find('SecDebugLogLevel') > -1:
continue modified.append(data[3])
elif items.find('SecAuditLogRelevantStatus ') > -1: continue
conf.writelines(data[5]) elif line.find('SecAuditLogRelevantStatus ') > -1:
continue modified.append(data[5])
elif items.find('SecAuditLogParts ') > -1: continue
conf.writelines(data[4]) elif line.find('SecAuditLogParts ') > -1:
continue modified.append(data[4])
elif items.find('SecAuditLogType ') > -1: continue
conf.writelines(data[6]) elif line.find('SecAuditLogType ') > -1:
continue modified.append(data[6])
else: continue
conf.writelines(items) else:
modified.append(line)
conf.close()
return modified
# Use safe modification with backup and validation
success, error = installUtilities.installUtilities.safeModifyHttpdConfig(
modify_config,
"Update ModSecurity configuration parameters"
)
if not success:
error_msg = error if error else "Unknown error"
logging.writeToFile(f"[saveModSecConfigs] Failed: {error_msg}")
print(f"0,{error_msg}")
return
installUtilities.reStartLiteSpeed() installUtilities.reStartLiteSpeed()

View File

@ -518,22 +518,36 @@ context /.well-known/acme-challenge {
else: else:
if sslUtilities.checkIfSSLMap(virtualHostName) == 0: if sslUtilities.checkIfSSLMap(virtualHostName) == 0:
from plogical import installUtilities
data = open("/usr/local/lsws/conf/httpd_config.conf").readlines()
writeDataToFile = open("/usr/local/lsws/conf/httpd_config.conf", 'w') def modify_config(lines):
sslCheck = 0 """Add SSL map entry to existing SSL listener"""
modified = []
for items in data: sslCheck = 0
if items.find("listener") > -1 and items.find("SSL") > -1:
sslCheck = 1 for line in lines:
if line.find("listener") > -1 and line.find("SSL") > -1:
if (sslCheck == 1): sslCheck = 1
writeDataToFile.writelines(items)
writeDataToFile.writelines(map) if (sslCheck == 1):
sslCheck = 0 modified.append(line)
else: modified.append(map)
writeDataToFile.writelines(items) sslCheck = 0
writeDataToFile.close() else:
modified.append(line)
return modified
# Use safe modification with backup and validation
success, error = installUtilities.installUtilities.safeModifyHttpdConfig(
modify_config,
f"Add SSL map entry for {virtualHostName}"
)
if not success:
error_msg = error if error else "Unknown error"
logging.writeToFile(f"[sslUtilities] Failed to add SSL map entry: {error_msg}")
raise BaseException(f"Failed to add SSL map entry: {error_msg}")
###################### Write per host Configs for SSL ################### ###################### Write per host Configs for SSL ###################

View File

@ -81,49 +81,52 @@ class tuning:
def saveTuningDetails(maxConnections,maxSSLConnections,connectionTimeOut,keepAliveTimeOut,cacheSizeInMemory,gzipCompression): def saveTuningDetails(maxConnections,maxSSLConnections,connectionTimeOut,keepAliveTimeOut,cacheSizeInMemory,gzipCompression):
if ProcessUtilities.decideServer() == ProcessUtilities.OLS: if ProcessUtilities.decideServer() == ProcessUtilities.OLS:
try: try:
datas = open("/usr/local/lsws/conf/httpd_config.conf").readlines() from plogical import installUtilities
writeDataToFile = open("/usr/local/lsws/conf/httpd_config.conf","w")
def modify_config(lines):
if gzipCompression == "Enable": """Modify tuning parameters in config"""
gzip = 1 modified = []
else:
gzip = 0 if gzipCompression == "Enable":
gzip = 1
for items in datas:
if items.find("maxConnections") > -1:
data = " maxConnections "+str(maxConnections)+"\n"
writeDataToFile.writelines(data)
continue
elif items.find("maxSSLConnections") > -1:
data = " maxSSLConnections "+str(maxSSLConnections) + "\n"
writeDataToFile.writelines(data)
continue
elif items.find("connTimeout") > -1:
data =" connTimeout "+str(connectionTimeOut)+"\n"
writeDataToFile.writelines(data)
continue
elif items.find("keepAliveTimeout") > -1:
data = " keepAliveTimeout " + str(keepAliveTimeOut) + "\n"
writeDataToFile.writelines(data)
continue
elif items.find("totalInMemCacheSize") > -1:
data = " totalInMemCacheSize " + str(cacheSizeInMemory) + "\n"
writeDataToFile.writelines(data)
continue
elif items.find("enableGzipCompress") > -1:
data = " enableGzipCompress " + str(gzip) + "\n"
writeDataToFile.writelines(data)
continue
else: else:
writeDataToFile.writelines(items) gzip = 0
writeDataToFile.close() for line in lines:
if line.find("maxConnections") > -1:
modified.append(" maxConnections "+str(maxConnections)+"\n")
continue
elif line.find("maxSSLConnections") > -1:
modified.append(" maxSSLConnections "+str(maxSSLConnections) + "\n")
continue
elif line.find("connTimeout") > -1:
modified.append(" connTimeout "+str(connectionTimeOut)+"\n")
continue
elif line.find("keepAliveTimeout") > -1:
modified.append(" keepAliveTimeout " + str(keepAliveTimeOut) + "\n")
continue
elif line.find("totalInMemCacheSize") > -1:
modified.append(" totalInMemCacheSize " + str(cacheSizeInMemory) + "\n")
continue
elif line.find("enableGzipCompress") > -1:
modified.append(" enableGzipCompress " + str(gzip) + "\n")
continue
else:
modified.append(line)
return modified
# Use safe modification with backup and validation
success, error = installUtilities.installUtilities.safeModifyHttpdConfig(
modify_config,
"Update tuning parameters (maxConnections, maxSSLConnections, etc.)"
)
if not success:
error_msg = error if error else "Unknown error"
logging.writeToFile(f"[saveTuningDetails] Failed: {error_msg}")
print(f"0,{error_msg}")
return
print("1,None") print("1,None")
except BaseException as msg: except BaseException as msg:

View File

@ -10,6 +10,7 @@ import re
sys.path.append('/usr/local/CyberCP') sys.path.append('/usr/local/CyberCP')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings")
from plogical.errorSanitizer import ErrorSanitizer from plogical.errorSanitizer import ErrorSanitizer
from plogical.installUtilities import installUtilities
import shlex import shlex
import subprocess import subprocess
import shutil import shutil
@ -3481,6 +3482,7 @@ class Migration(migrations.Migration):
critical_files = [ critical_files = [
'/usr/local/CyberCP/CyberCP/settings.py', '/usr/local/CyberCP/CyberCP/settings.py',
'/usr/local/CyberCP/.git/config', # Git configuration '/usr/local/CyberCP/.git/config', # Git configuration
'/usr/local/lsws/conf/httpd_config.conf', # OpenLiteSpeed config - critical for preventing port binding failures
] ]
# Also backup any custom configurations # Also backup any custom configurations
@ -6397,15 +6399,15 @@ extprocessor proxyApacheBackendSSL {
lsws_config = "/usr/local/lsws/conf/httpd_config.conf" lsws_config = "/usr/local/lsws/conf/httpd_config.conf"
if os.path.exists(lsws_config): if os.path.exists(lsws_config):
with open(lsws_config, 'r') as f: def modify_apache_backends(lines):
lsws_content = f.read() """Modify config to add Apache proxy backends if missing"""
content = ''.join(lines)
modified = False modified_lines = lines[:]
# Check for apachebackend extprocessor # Check for apachebackend extprocessor
if 'extprocessor apachebackend' not in lsws_content: if 'extprocessor apachebackend' not in content:
# Add apachebackend configuration # Add apachebackend configuration
backend_config = ''' backend_config = '''
extprocessor apachebackend { extprocessor apachebackend {
type proxy type proxy
address 127.0.0.1:8082 address 127.0.0.1:8082
@ -6415,14 +6417,13 @@ extprocessor apachebackend {
respBuffer 0 respBuffer 0
} }
''' '''
lsws_content += backend_config modified_lines.append(backend_config)
modified = True print("Added apachebackend extprocessor configuration")
print("Added apachebackend extprocessor configuration")
# Check for proxyApacheBackendSSL extprocessor
# Check for proxyApacheBackendSSL extprocessor if 'extprocessor proxyApacheBackendSSL' not in content:
if 'extprocessor proxyApacheBackendSSL' not in lsws_content: # Add proxyApacheBackendSSL configuration
# Add proxyApacheBackendSSL configuration ssl_backend_config = '''
ssl_backend_config = '''
extprocessor proxyApacheBackendSSL { extprocessor proxyApacheBackendSSL {
type proxy type proxy
address https://127.0.0.1:8083 address https://127.0.0.1:8083
@ -6432,14 +6433,22 @@ extprocessor proxyApacheBackendSSL {
respBuffer 0 respBuffer 0
} }
''' '''
lsws_content += ssl_backend_config modified_lines.append(ssl_backend_config)
modified = True print("Added proxyApacheBackendSSL extprocessor configuration")
print("Added proxyApacheBackendSSL extprocessor configuration")
return modified_lines
if modified: # Use safe modification with backup and validation
with open(lsws_config, 'w') as f: success, error = installUtilities.safeModifyHttpdConfig(
f.write(lsws_content) modify_apache_backends,
"Add Apache proxy backend configurations"
)
if success:
print("Updated OpenLiteSpeed configuration with Apache proxy backends") print("Updated OpenLiteSpeed configuration with Apache proxy backends")
else:
print(f"WARNING: Failed to update OpenLiteSpeed configuration: {error}")
Upgrade.stdOut(f"Failed to update OpenLiteSpeed config: {error}", 0)
# Fix 3: Create/Update .htaccess files ONLY for domains actually using Apache # Fix 3: Create/Update .htaccess files ONLY for domains actually using Apache
print("Creating/Updating .htaccess files for Apache domains...") print("Creating/Updating .htaccess files for Apache domains...")

View File

@ -322,21 +322,31 @@ class vhost:
@staticmethod @staticmethod
def createNONSSLMapEntry(virtualHostName): def createNONSSLMapEntry(virtualHostName):
try: try:
data = open("/usr/local/lsws/conf/httpd_config.conf").readlines() def modify_config(lines):
writeDataToFile = open("/usr/local/lsws/conf/httpd_config.conf", 'w') map_entry = " map " + virtualHostName + " " + virtualHostName + "\n"
modified = []
map = " map " + virtualHostName + " " + virtualHostName + "\n" mapchecker = 1
mapchecker = 1 for line in lines:
if (mapchecker == 1 and (line.find("listener") > -1 and line.find("Default") > -1)):
for items in data: modified.append(line)
if (mapchecker == 1 and (items.find("listener") > -1 and items.find("Default") > -1)): modified.append(map_entry)
writeDataToFile.writelines(items) mapchecker = 0
writeDataToFile.writelines(map) else:
mapchecker = 0 modified.append(line)
else:
writeDataToFile.writelines(items) return modified
success, error = installUtilities.installUtilities.safeModifyHttpdConfig(
modify_config,
f"Add NON-SSL map entry for {virtualHostName}"
)
if not success:
error_msg = error if error else "Unknown error"
logging.writeToFile(f"[createNONSSLMapEntry] Failed: {error_msg}")
return 0
return 1 return 1
except BaseException as msg: except BaseException as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg)) logging.CyberCPLogFileWriter.writeToFile(str(msg))
@ -581,35 +591,47 @@ class vhost:
if os.path.exists(confPath): if os.path.exists(confPath):
shutil.rmtree(confPath) shutil.rmtree(confPath)
data = open("/usr/local/lsws/conf/httpd_config.conf").readlines() def modify_config(lines):
"""Remove virtual host entries from config"""
modified = []
check = 1
sslCheck = 1
writeDataToFile = open("/usr/local/lsws/conf/httpd_config.conf", 'w') for line in lines:
if numberOfSites == 1:
check = 1 if (line.find(' ' + virtualHostName) > -1 and line.find(" map " + virtualHostName) > -1):
sslCheck = 1 continue
if (line.find(' ' + virtualHostName) > -1 and (line.find("virtualHost") > -1 or line.find("virtualhost") > -1)):
for items in data: check = 0
if numberOfSites == 1: if line.find("listener") > -1 and line.find("SSL") > -1:
if (items.find(' ' + virtualHostName) > -1 and items.find(" map " + virtualHostName) > -1): sslCheck = 0
continue if (check == 1 and sslCheck == 1):
if (items.find(' ' + virtualHostName) > -1 and (items.find("virtualHost") > -1 or items.find("virtualhost") > -1)): modified.append(line)
check = 0 if (line.find("}") > -1 and (check == 0 or sslCheck == 0)):
if items.find("listener") > -1 and items.find("SSL") > -1: check = 1
sslCheck = 0 sslCheck = 1
if (check == 1 and sslCheck == 1): else:
writeDataToFile.writelines(items) if (line.find(' ' + virtualHostName) > -1 and line.find(" map " + virtualHostName) > -1):
if (items.find("}") > -1 and (check == 0 or sslCheck == 0)): continue
check = 1 if (line.find(' ' + virtualHostName) > -1 and (line.find("virtualHost") > -1 or line.find("virtualhost") > -1)):
sslCheck = 1 check = 0
else: if (check == 1):
if (items.find(' ' + virtualHostName) > -1 and items.find(" map " + virtualHostName) > -1): modified.append(line)
continue if (line.find("}") > -1 and check == 0):
if (items.find(' ' + virtualHostName) > -1 and (items.find("virtualHost") > -1 or items.find("virtualhost") > -1)): check = 1
check = 0
if (check == 1): return modified
writeDataToFile.writelines(items)
if (items.find("}") > -1 and check == 0): # Use safe modification with backup and validation
check = 1 success, error = installUtilities.installUtilities.safeModifyHttpdConfig(
modify_config,
f"Remove virtual host {virtualHostName} from config"
)
if not success:
error_msg = error if error else "Unknown error"
logging.writeToFile(f"[deleteCoreConf] Failed to remove vhost config: {error_msg}")
raise BaseException(f"Failed to remove vhost config: {error_msg}")
## Delete Apache Conf ## Delete Apache Conf

View File

@ -5737,6 +5737,12 @@ StrictHostKeyChecking no
if os.path.exists(finalConfPath): if os.path.exists(finalConfPath):
phpPath = ApacheVhost.whichPHPExists(self.domain) phpPath = ApacheVhost.whichPHPExists(self.domain)
if phpPath is None:
# If PHP path is not found, return error response
data_ret = {'status': 0, 'saveStatus': 0, 'error_message': 'PHP configuration file not found for this domain'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
command = 'sudo cat ' + phpPath command = 'sudo cat ' + phpPath
phpConf = ProcessUtilities.outputExecutioner(command).splitlines() phpConf = ProcessUtilities.outputExecutioner(command).splitlines()
pmMaxChildren = phpConf[8].split(' ')[2] pmMaxChildren = phpConf[8].split(' ')[2]