From 18b1bad51f89810cb81d2453043ed7b49d69256f Mon Sep 17 00:00:00 2001 From: Master3395 Date: Wed, 31 Dec 2025 23:13:53 +0100 Subject: [PATCH] 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. --- managePHP/phpManager.py | 16 +++- plogical/installUtilities.py | 150 +++++++++++++++++++++++++++++++---- plogical/modSec.py | 73 ++++++++++------- plogical/sslUtilities.py | 46 +++++++---- plogical/tuning.py | 87 ++++++++++---------- plogical/upgrade.py | 55 +++++++------ plogical/vhost.py | 108 +++++++++++++++---------- websiteFunctions/website.py | 6 ++ 8 files changed, 369 insertions(+), 172 deletions(-) diff --git a/managePHP/phpManager.py b/managePHP/phpManager.py index 74ebca609..71d74592e 100644 --- a/managePHP/phpManager.py +++ b/managePHP/phpManager.py @@ -177,10 +177,18 @@ class PHPManager: php_versions = [] for entry in lsphp_lines: # Find substring starting with 'php' and extract the version part - version = entry.split('php')[1] - # Format version as PHP X.Y - formatted_version = f"PHP {version[0]}.{version[1]}" - php_versions.append(formatted_version) + try: + if 'php' not in entry: + continue + 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: lsphp_lines = [line for line in result.split('\n')] diff --git a/plogical/installUtilities.py b/plogical/installUtilities.py index 982655ac9..fb3aaeba4 100644 --- a/plogical/installUtilities.py +++ b/plogical/installUtilities.py @@ -6,6 +6,7 @@ import pexpect import os import shlex from plogical.processUtilities import ProcessUtilities +from datetime import datetime class installUtilities: @@ -219,26 +220,147 @@ class installUtilities: return 0 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 def changePortTo80(): try: - data = open("/usr/local/lsws/conf/httpd_config.conf").readlines() - writeDataToFile = open("/usr/local/lsws/conf/httpd_config.conf", 'w') - - for items in data: - if (items.find("*:8088") > -1): - writeDataToFile.writelines(items.replace("*:8088","*:80")) - else: - writeDataToFile.writelines(items) - - writeDataToFile.close() - - except IOError as msg: + def modify_config(lines): + modified = [] + for line in lines: + if "*:8088" in line: + modified.append(line.replace("*:8088", "*:80")) + else: + modified.append(line) + return modified + + success, error = installUtilities.safeModifyHttpdConfig( + modify_config, + "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]") return 0 - return installUtilities.reStartLiteSpeed() - @staticmethod def installAllPHPVersion(): diff --git a/plogical/modSec.py b/plogical/modSec.py index 9aed99228..1e3c38f1f 100644 --- a/plogical/modSec.py +++ b/plogical/modSec.py @@ -234,36 +234,49 @@ modsecurity_rules_file /usr/local/lsws/conf/modsec/rules.conf if ProcessUtilities.decideServer() == ProcessUtilities.OLS: confFile = os.path.join(virtualHostUtilities.Server_root, "conf/httpd_config.conf") - confData = open(confFile).readlines() - conf = open(confFile, 'w') - - for items in confData: - - if items.find('modsecurity ') > -1: - conf.writelines(data[0]) - continue - elif items.find('SecAuditEngine ') > -1: - conf.writelines(data[1]) - continue - elif items.find('SecRuleEngine ') > -1: - conf.writelines(data[2]) - continue - elif items.find('SecDebugLogLevel') > -1: - conf.writelines(data[3]) - continue - elif items.find('SecAuditLogRelevantStatus ') > -1: - conf.writelines(data[5]) - continue - elif items.find('SecAuditLogParts ') > -1: - conf.writelines(data[4]) - continue - elif items.find('SecAuditLogType ') > -1: - conf.writelines(data[6]) - continue - else: - conf.writelines(items) - - conf.close() + + def modify_config(lines): + """Update ModSecurity configuration parameters""" + modified = [] + + for line in lines: + if line.find('modsecurity ') > -1: + modified.append(data[0]) + continue + elif line.find('SecAuditEngine ') > -1: + modified.append(data[1]) + continue + elif line.find('SecRuleEngine ') > -1: + modified.append(data[2]) + continue + elif line.find('SecDebugLogLevel') > -1: + modified.append(data[3]) + continue + elif line.find('SecAuditLogRelevantStatus ') > -1: + modified.append(data[5]) + continue + elif line.find('SecAuditLogParts ') > -1: + modified.append(data[4]) + continue + elif line.find('SecAuditLogType ') > -1: + modified.append(data[6]) + continue + else: + modified.append(line) + + 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() diff --git a/plogical/sslUtilities.py b/plogical/sslUtilities.py index 5355238fb..0aea6feb9 100644 --- a/plogical/sslUtilities.py +++ b/plogical/sslUtilities.py @@ -518,22 +518,36 @@ context /.well-known/acme-challenge { else: if sslUtilities.checkIfSSLMap(virtualHostName) == 0: - - data = open("/usr/local/lsws/conf/httpd_config.conf").readlines() - writeDataToFile = open("/usr/local/lsws/conf/httpd_config.conf", 'w') - sslCheck = 0 - - for items in data: - if items.find("listener") > -1 and items.find("SSL") > -1: - sslCheck = 1 - - if (sslCheck == 1): - writeDataToFile.writelines(items) - writeDataToFile.writelines(map) - sslCheck = 0 - else: - writeDataToFile.writelines(items) - writeDataToFile.close() + from plogical import installUtilities + + def modify_config(lines): + """Add SSL map entry to existing SSL listener""" + modified = [] + sslCheck = 0 + + for line in lines: + if line.find("listener") > -1 and line.find("SSL") > -1: + sslCheck = 1 + + if (sslCheck == 1): + modified.append(line) + modified.append(map) + sslCheck = 0 + 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 ################### diff --git a/plogical/tuning.py b/plogical/tuning.py index e146a0d28..d35487de2 100644 --- a/plogical/tuning.py +++ b/plogical/tuning.py @@ -81,49 +81,52 @@ class tuning: def saveTuningDetails(maxConnections,maxSSLConnections,connectionTimeOut,keepAliveTimeOut,cacheSizeInMemory,gzipCompression): if ProcessUtilities.decideServer() == ProcessUtilities.OLS: try: - datas = open("/usr/local/lsws/conf/httpd_config.conf").readlines() - writeDataToFile = open("/usr/local/lsws/conf/httpd_config.conf","w") - - if gzipCompression == "Enable": - gzip = 1 - else: - gzip = 0 - - - 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 + from plogical import installUtilities + + def modify_config(lines): + """Modify tuning parameters in config""" + modified = [] + + if gzipCompression == "Enable": + gzip = 1 else: - writeDataToFile.writelines(items) - - writeDataToFile.close() + gzip = 0 + + 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") except BaseException as msg: diff --git a/plogical/upgrade.py b/plogical/upgrade.py index b3cd256d7..cacfeacf5 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -10,6 +10,7 @@ import re sys.path.append('/usr/local/CyberCP') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings") from plogical.errorSanitizer import ErrorSanitizer +from plogical.installUtilities import installUtilities import shlex import subprocess import shutil @@ -3481,6 +3482,7 @@ class Migration(migrations.Migration): critical_files = [ '/usr/local/CyberCP/CyberCP/settings.py', '/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 @@ -6397,15 +6399,15 @@ extprocessor proxyApacheBackendSSL { lsws_config = "/usr/local/lsws/conf/httpd_config.conf" if os.path.exists(lsws_config): - with open(lsws_config, 'r') as f: - lsws_content = f.read() - - modified = False - - # Check for apachebackend extprocessor - if 'extprocessor apachebackend' not in lsws_content: - # Add apachebackend configuration - backend_config = ''' + def modify_apache_backends(lines): + """Modify config to add Apache proxy backends if missing""" + content = ''.join(lines) + modified_lines = lines[:] + + # Check for apachebackend extprocessor + if 'extprocessor apachebackend' not in content: + # Add apachebackend configuration + backend_config = ''' extprocessor apachebackend { type proxy address 127.0.0.1:8082 @@ -6415,14 +6417,13 @@ extprocessor apachebackend { respBuffer 0 } ''' - lsws_content += backend_config - modified = True - print("Added apachebackend extprocessor configuration") - - # Check for proxyApacheBackendSSL extprocessor - if 'extprocessor proxyApacheBackendSSL' not in lsws_content: - # Add proxyApacheBackendSSL configuration - ssl_backend_config = ''' + modified_lines.append(backend_config) + print("Added apachebackend extprocessor configuration") + + # Check for proxyApacheBackendSSL extprocessor + if 'extprocessor proxyApacheBackendSSL' not in content: + # Add proxyApacheBackendSSL configuration + ssl_backend_config = ''' extprocessor proxyApacheBackendSSL { type proxy address https://127.0.0.1:8083 @@ -6432,14 +6433,22 @@ extprocessor proxyApacheBackendSSL { respBuffer 0 } ''' - lsws_content += ssl_backend_config - modified = True - print("Added proxyApacheBackendSSL extprocessor configuration") + modified_lines.append(ssl_backend_config) + print("Added proxyApacheBackendSSL extprocessor configuration") + + return modified_lines - if modified: - with open(lsws_config, 'w') as f: - f.write(lsws_content) + # Use safe modification with backup and validation + success, error = installUtilities.safeModifyHttpdConfig( + modify_apache_backends, + "Add Apache proxy backend configurations" + ) + + if success: 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 print("Creating/Updating .htaccess files for Apache domains...") diff --git a/plogical/vhost.py b/plogical/vhost.py index 5d2aac093..7d952f590 100644 --- a/plogical/vhost.py +++ b/plogical/vhost.py @@ -322,21 +322,31 @@ class vhost: @staticmethod def createNONSSLMapEntry(virtualHostName): try: - data = open("/usr/local/lsws/conf/httpd_config.conf").readlines() - writeDataToFile = open("/usr/local/lsws/conf/httpd_config.conf", 'w') - - map = " map " + virtualHostName + " " + virtualHostName + "\n" - - mapchecker = 1 - - for items in data: - if (mapchecker == 1 and (items.find("listener") > -1 and items.find("Default") > -1)): - writeDataToFile.writelines(items) - writeDataToFile.writelines(map) - mapchecker = 0 - else: - writeDataToFile.writelines(items) - + def modify_config(lines): + map_entry = " map " + virtualHostName + " " + virtualHostName + "\n" + modified = [] + mapchecker = 1 + + for line in lines: + if (mapchecker == 1 and (line.find("listener") > -1 and line.find("Default") > -1)): + modified.append(line) + modified.append(map_entry) + mapchecker = 0 + else: + modified.append(line) + + 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 except BaseException as msg: logging.CyberCPLogFileWriter.writeToFile(str(msg)) @@ -581,35 +591,47 @@ class vhost: if os.path.exists(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') - - check = 1 - sslCheck = 1 - - for items in data: - if numberOfSites == 1: - if (items.find(' ' + virtualHostName) > -1 and items.find(" map " + virtualHostName) > -1): - continue - if (items.find(' ' + virtualHostName) > -1 and (items.find("virtualHost") > -1 or items.find("virtualhost") > -1)): - check = 0 - if items.find("listener") > -1 and items.find("SSL") > -1: - sslCheck = 0 - if (check == 1 and sslCheck == 1): - writeDataToFile.writelines(items) - if (items.find("}") > -1 and (check == 0 or sslCheck == 0)): - check = 1 - sslCheck = 1 - else: - if (items.find(' ' + virtualHostName) > -1 and items.find(" map " + virtualHostName) > -1): - continue - if (items.find(' ' + virtualHostName) > -1 and (items.find("virtualHost") > -1 or items.find("virtualhost") > -1)): - check = 0 - if (check == 1): - writeDataToFile.writelines(items) - if (items.find("}") > -1 and check == 0): - check = 1 + for line in lines: + if numberOfSites == 1: + if (line.find(' ' + virtualHostName) > -1 and line.find(" map " + virtualHostName) > -1): + continue + if (line.find(' ' + virtualHostName) > -1 and (line.find("virtualHost") > -1 or line.find("virtualhost") > -1)): + check = 0 + if line.find("listener") > -1 and line.find("SSL") > -1: + sslCheck = 0 + if (check == 1 and sslCheck == 1): + modified.append(line) + if (line.find("}") > -1 and (check == 0 or sslCheck == 0)): + check = 1 + sslCheck = 1 + else: + if (line.find(' ' + virtualHostName) > -1 and line.find(" map " + virtualHostName) > -1): + continue + if (line.find(' ' + virtualHostName) > -1 and (line.find("virtualHost") > -1 or line.find("virtualhost") > -1)): + check = 0 + if (check == 1): + modified.append(line) + if (line.find("}") > -1 and check == 0): + check = 1 + + return modified + + # Use safe modification with backup and validation + 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 diff --git a/websiteFunctions/website.py b/websiteFunctions/website.py index d028418d5..606b4f642 100644 --- a/websiteFunctions/website.py +++ b/websiteFunctions/website.py @@ -5737,6 +5737,12 @@ StrictHostKeyChecking no if os.path.exists(finalConfPath): 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 phpConf = ProcessUtilities.outputExecutioner(command).splitlines() pmMaxChildren = phpConf[8].split(' ')[2]