From 1264f1a680c2f13ddbe20395316b9b39a8ef9b27 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Fri, 10 Oct 2025 00:09:42 +0500 Subject: [PATCH 001/129] bug fix: alma8 install --- install/installCyberPanel.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/install/installCyberPanel.py b/install/installCyberPanel.py index 6f39a6788..fcc9538fa 100644 --- a/install/installCyberPanel.py +++ b/install/installCyberPanel.py @@ -996,9 +996,11 @@ def Main(cwd, mysql, distro, ent, serial=None, port="8090", ftp=None, dns=None, except: pass - if distro == centos: + # For RHEL-based systems (CentOS, AlmaLinux, Rocky, etc.), generate a separate password + if distro in [centos, cent8, openeuler]: InstallCyberPanel.mysqlPassword = install_utils.generate_pass() else: + # For Ubuntu/Debian, use the same password as root InstallCyberPanel.mysqlPassword = InstallCyberPanel.mysql_Root_password installer = InstallCyberPanel("/usr/local/lsws/", cwd, distro, ent, serial, port, ftp, dns, publicip, remotemysql, From 93b9fa4c3a032b1595e045fccb4555b5206a1df4 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Fri, 10 Oct 2025 01:00:22 +0500 Subject: [PATCH 002/129] Fix AlmaLinux 8 installation: Add python-dotenv to requirements (v2.4.4) - Install python-dotenv in virtual environment during CyberPanel setup - Fixes Django's inability to load .env file on AlmaLinux 8 - Resolves "Access denied for user 'cyberpanel'@'localhost'" errors - Added to all installation paths (normal, DEV, and after_install) This ensures Django can properly load database credentials from .env file on AlmaLinux 8 systems where python-dotenv was missing. --- install/venvsetup.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/install/venvsetup.sh b/install/venvsetup.sh index f26476fdb..2a36599a7 100644 --- a/install/venvsetup.sh +++ b/install/venvsetup.sh @@ -901,6 +901,8 @@ source /usr/local/CyberPanel/bin/activate rm -rf requirements.txt wget -O requirements.txt https://raw.githubusercontent.com/usmannasir/cyberpanel/1.8.0/requirments.txt pip install --ignore-installed -r requirements.txt +# Install python-dotenv for loading .env file (critical for AlmaLinux 8) +pip install python-dotenv fi if [[ $DEV == "ON" ]] ; then @@ -911,6 +913,8 @@ if [[ $DEV == "ON" ]] ; then source /usr/local/CyberPanel/bin/activate wget -O requirements.txt https://raw.githubusercontent.com/usmannasir/cyberpanel/$BRANCH_NAME/requirments.txt pip3.6 install --ignore-installed -r requirements.txt + # Install python-dotenv for loading .env file (critical for AlmaLinux 8) + pip3.6 install python-dotenv fi if [ -f requirements.txt ] && [ -d cyberpanel ] ; then @@ -965,6 +969,8 @@ python3.6 -m venv /usr/local/CyberCP source /usr/local/CyberCP/bin/activate wget -O requirements.txt https://raw.githubusercontent.com/usmannasir/cyberpanel/$BRANCH_NAME/requirments.txt pip3.6 install --ignore-installed -r requirements.txt +# Install python-dotenv for loading .env file (critical for AlmaLinux 8) +pip3.6 install python-dotenv systemctl restart lscpd fi From 1daa70a9ead7f47d9c893f9cb8fe6a376e675362 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Fri, 10 Oct 2025 17:26:50 +0500 Subject: [PATCH 003/129] bug fix: python 3.6 compatibility --- install/install.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install/install.py b/install/install.py index 31267817b..c3ced5ecd 100644 --- a/install/install.py +++ b/install/install.py @@ -619,7 +619,8 @@ password="%s" logging.InstallLog.writeToFile("Generating secure environment configuration!") # Generate secure environment file instead of hardcoding passwords - self.generate_secure_env_file(mysqlPassword, password) + # Note: password = MySQL root password, mysqlPassword = CyberPanel DB password + self.generate_secure_env_file(password, mysqlPassword) logging.InstallLog.writeToFile("Environment configuration generated successfully!") From af728de58063d5741847a343579a3e8915ce989f Mon Sep 17 00:00:00 2001 From: usmannasir Date: Fri, 10 Oct 2025 17:59:01 +0500 Subject: [PATCH 004/129] bug fix: python 3.6 compatibility --- requirments-old.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirments-old.txt b/requirments-old.txt index 89018350d..d629b171f 100644 --- a/requirments-old.txt +++ b/requirments-old.txt @@ -36,4 +36,5 @@ uvicorn asyncssh python-jose websockets -PyJWT \ No newline at end of file +PyJWT +python-dotenv \ No newline at end of file From 8c1534943b96ba0f6f4cc96e5e7bcb4ebd789b4d Mon Sep 17 00:00:00 2001 From: usmannasir Date: Mon, 13 Oct 2025 00:24:45 +0500 Subject: [PATCH 005/129] fix design of wp manager home --- .../websiteFunctions/WPsiteHome.html | 3685 +++++------------ 1 file changed, 1080 insertions(+), 2605 deletions(-) diff --git a/websiteFunctions/templates/websiteFunctions/WPsiteHome.html b/websiteFunctions/templates/websiteFunctions/WPsiteHome.html index a31430130..30bf46da6 100644 --- a/websiteFunctions/templates/websiteFunctions/WPsiteHome.html +++ b/websiteFunctions/templates/websiteFunctions/WPsiteHome.html @@ -8,2095 +8,790 @@ - + - - -
+
-
-

{{ wpsite.title }}

-

- {{ wpsite.path }} - Active - -

-
+
+

{{ wpsite.title }}

+

+ {{ wpsite.path }} + Active + +

+
-
-
-

+
+
{% trans "WordPress Manager" %} -

+
-
- -
-
- -
-
-
- -
-

Active

-

Site Status

-
-
-
- -
-

Loading...

-

WordPress Version

-
-
-
- -
-

{{ wpsite.owner.phpSelection }}

-

PHP Version

-
-
-
- -
-

LiteSpeed

-

Web Server

-
-
- - -
-

Site Overview

-
- -
-
-
🖼️ Live Preview
- -
- - -
- - - -
-
- - -
-

Site Settings

-
-
-
- -
-
-
-
LSCache
-

LiteSpeed caching for better performance

-
- -
-
-
-
- -
-
-
-
Password Protection
-

Restrict access with authentication

-
-
-
-
-
-
-
- -
-
-
-
Debugging
-

Display errors for development

-
- -
-
-
-
- -
-
-
-
Search Indexing
-

Allow search engines to index site

-
- -
-
-
-
- -
-
-
-
Maintenance Mode
-

Show maintenance message to visitors

-
- -
-
-
-
- -
-
-
-
WP Cron
-

Disable built-in WordPress cron

-
- -
-
-
-
- +
+ +
+ +
+
+
+
-
-
-
-

Installed Plugins

-

Manage your WordPress plugins

-
-
- - - Update All - - - - Update Selected - - -
-
- - - - - - - - - - - - - -
- {% comment %}
- - -
{% endcomment %} -
PluginStateUpdatesVersionDelete
-
-
-
-
-

Installed Themes

-

Manage your WordPress themes

-
-
- - - Update All - - - - Update Selected - - -
-
- - - - - - - - - - - - - -
- {% comment %}
- - -
{% endcomment %} -
ThemeStateUpdatesVersionDelete
-
-
-
-
-

Create Staging Site

-

Create a copy of your WordPress site for testing and development

-
- -
-
-
- -
- -
-
- -
- -
- - Enter the full domain name for your staging site -
-
- -
-
- -
-
-
-
-
-
-
- -
- -
-

{$ currentStatus $}

-
- -
-
- 70% Complete -
-
- -
-

{% trans "Error message:" %} {$ errorMessage $}

-
- -
-

{% trans "Website succesfully created." %}

-
- - -
-

{% trans "Could not connect to server. Please refresh this page." %}

-
- -
- -
- -
-
-
- - -
-
-

Existing Staging Sites

-

Manage your staging environments

-
-
-
- - - - - - - - - - - -
NameDomainPathActions
-
-
-
-
- -
-
-
-
-

{% trans "Create Backup" %}

- - {% trans "Restore Backups" %} - -
- -
- -
-

This feature will create a backup of your WordPress website.

-

For scheduled remote backups of your entire site, including email accounts and DNS records, click here.

-
-
- -
-
-
- -
- -
-
-
-
- -
-
-
-
-
-
- -
- -
-

{$ currentStatus $}

-
- -
-
- 70% Complete -
-
- -
-

{% trans "Error message:" %} {$ errorMessage $}

-
- -
-

{% trans "Backup succesfully created." %}

-
- - -
-

{% trans "Could not connect to server. Please refresh this page." %}

-
- - -
-
-
-
- -
-
-
-
-
-
-

- {% trans "Database Information" %} -

-
-
-
- -
- -
-
-
-
- -
- -
-
-
-
- -
- -
-
-
-
-
-
- -
-
- +

Active

+

Site Status

- - - - + {% else %} +
+

{% trans "Advanced Resource Limits" %}

+ +
+ +
+ {% trans "Premium Feature" %}
+ {% trans "Advanced resource limits (CPU, Memory, I/O, Inode, Process limits) require CyberPanel Addons." %}
+ + + {% trans "Visit platform.cyberpersons.com to enable addons" %} + +
+
+
+ {% endif %}

{% trans "Additional Features" %}

From 37ad49e3fe713bf3e80fcc7690a838f742f3f6a8 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Tue, 11 Nov 2025 23:08:49 +0500 Subject: [PATCH 057/129] Update addon purchase links to cyberpanel.net - Change all addon purchase links to https://cyberpanel.net/cyberpanel-addons - Update error messages in package creation and modification - Update UI links in create and modify package templates - Improve link text to 'Purchase CyberPanel Addons' --- packages/packagesManager.py | 4 ++-- packages/templates/packages/createPackage.html | 4 ++-- packages/templates/packages/modifyPackage.html | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/packagesManager.py b/packages/packagesManager.py index 33380674d..7f6b7058d 100644 --- a/packages/packagesManager.py +++ b/packages/packagesManager.py @@ -136,7 +136,7 @@ class PackagesManager: # Check addon access for resource limits feature has_addons = self.checkAddonAccess() if not has_addons and enforceDiskLimits == 1: - data_ret = {'saveStatus': 0, 'error_message': "Resource limits feature requires CyberPanel addons. Please visit https://platform.cyberpersons.com to enable addons."} + data_ret = {'saveStatus': 0, 'error_message': "Resource limits feature requires CyberPanel addons. Please visit https://cyberpanel.net/cyberpanel-addons to purchase."} json_data = json.dumps(data_ret) return HttpResponse(json_data) @@ -305,7 +305,7 @@ class PackagesManager: # Check addon access for resource limits feature has_addons = self.checkAddonAccess() if not has_addons and modifyPack.enforceDiskLimits == 1: - data_ret = {'saveStatus': 0, 'error_message': "Resource limits feature requires CyberPanel addons. Please visit https://platform.cyberpersons.com to enable addons."} + data_ret = {'saveStatus': 0, 'error_message': "Resource limits feature requires CyberPanel addons. Please visit https://cyberpanel.net/cyberpanel-addons to purchase."} json_data = json.dumps(data_ret) return HttpResponse(json_data) diff --git a/packages/templates/packages/createPackage.html b/packages/templates/packages/createPackage.html index 3961dd683..84a91014a 100644 --- a/packages/templates/packages/createPackage.html +++ b/packages/templates/packages/createPackage.html @@ -537,9 +537,9 @@
{% trans "Premium Feature" %}
{% trans "Advanced resource limits (CPU, Memory, I/O, Inode, Process limits) require CyberPanel Addons." %}
- + - {% trans "Visit platform.cyberpersons.com to enable addons" %} + {% trans "Purchase CyberPanel Addons" %}
diff --git a/packages/templates/packages/modifyPackage.html b/packages/templates/packages/modifyPackage.html index 8fff950c2..a81ccdaac 100644 --- a/packages/templates/packages/modifyPackage.html +++ b/packages/templates/packages/modifyPackage.html @@ -567,9 +567,9 @@
{% trans "Premium Feature" %}
{% trans "Advanced resource limits (CPU, Memory, I/O, Inode, Process limits) require CyberPanel Addons." %}
- + - {% trans "Visit platform.cyberpersons.com to enable addons" %} + {% trans "Purchase CyberPanel Addons" %}
From a6c5997edb2098090ef10fe7dd0c4d66438da4c1 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Tue, 11 Nov 2025 23:30:51 +0500 Subject: [PATCH 058/129] Fix ProcessUtilities import error - Change from 'from plogical import ProcessUtilities' to correct import - Use 'from plogical.processUtilities import ProcessUtilities' - Resolves ImportError during Django initialization --- packages/packagesManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/packagesManager.py b/packages/packagesManager.py index 7f6b7058d..1897bf3a4 100644 --- a/packages/packagesManager.py +++ b/packages/packagesManager.py @@ -13,7 +13,7 @@ from loginSystem.models import Administrator import json from .models import Package from plogical.acl import ACLManager -from plogical import ProcessUtilities +from plogical.processUtilities import ProcessUtilities class PackagesManager: def __init__(self, request = None): From 548f6469003becd2be2a78182731ba30f93494e2 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Wed, 12 Nov 2025 23:43:59 +0500 Subject: [PATCH 059/129] Add I/O limit support to resource limits - Pass --io parameter to lscgctl with bytes/sec value - Convert ioLimitMBPS from MB/s to bytes/s for lscgctl - Update log message to include I/O limit information - Add note about systemd io controller delegation requirement --- plogical/resourceLimits.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/plogical/resourceLimits.py b/plogical/resourceLimits.py index 0ca630497..d9158ae89 100644 --- a/plogical/resourceLimits.py +++ b/plogical/resourceLimits.py @@ -343,21 +343,25 @@ class ResourceLimitsManager: # Tasks: use procHardLimit as max tasks max_tasks = package.procHardLimit + # I/O: convert MB/s to bytes/s (lscgctl expects bytes/sec) + io_limit_bytes = package.ioLimitMBPS * 1024 * 1024 + # Build lscgctl command - # Format: lscgctl set username --cpu 100 --mem 1024M --tasks 500 + # Format: lscgctl set username --cpu 100 --mem 1024M --io 10485760 --tasks 500 cmd = [ self.LSCGCTL_PATH, 'set', username, '--cpu', str(cpu_percent), '--mem', memory_limit, + '--io', str(io_limit_bytes), '--tasks', str(max_tasks) ] - # Note: I/O limits may require additional configuration - # Check if lscgctl supports --io parameter + # Note: I/O limits are configured but may not be enforced at kernel level + # without systemd io controller delegation to user slices - logging.writeToFile(f"Setting limits for user {username}: CPU={cpu_percent}%, MEM={memory_limit}, TASKS={max_tasks}") + logging.writeToFile(f"Setting limits for user {username}: CPU={cpu_percent}%, MEM={memory_limit}, I/O={package.ioLimitMBPS}MB/s, TASKS={max_tasks}") result = subprocess.run( cmd, From cb45e0035ac08f073b4278fd2ffe87087c025db7 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Wed, 12 Nov 2025 23:52:48 +0500 Subject: [PATCH 060/129] Add resource limits display on website detail page - Fetch actual resource limits from lscgctl command in loadDomainHome - Parse JSON output and extract CPU, Memory, I/O, Tasks values - Display resource limits in dedicated section on website detail page - Only show limits if they actually exist on the site - Use modern card design with gradients matching the rest of the UI --- .../templates/websiteFunctions/website.html | 63 +++++++++++++++++++ websiteFunctions/website.py | 38 +++++++++++ 2 files changed, 101 insertions(+) diff --git a/websiteFunctions/templates/websiteFunctions/website.html b/websiteFunctions/templates/websiteFunctions/website.html index 7fd02f0db..52166d631 100644 --- a/websiteFunctions/templates/websiteFunctions/website.html +++ b/websiteFunctions/templates/websiteFunctions/website.html @@ -1486,6 +1486,69 @@ {% endif %}
+ + {% if resource_limits %} +
+
+ + + + {% trans "Resource Limits" %} +
+
+ {% if resource_limits.cpu %} +
+
+ +
+
{% trans "CPU" %}
+
{{ resource_limits.cpu }}%
+
{% trans "CPU cores allocated" %}
+
+ {% endif %} + + {% if resource_limits.memory %} +
+
+ +
+
{% trans "Memory" %}
+
{{ resource_limits.memory }}
+
{% trans "RAM limit" %}
+
+ {% endif %} + + {% if resource_limits.io %} +
+
+ +
+
{% trans "I/O" %}
+
{{ resource_limits.io }}
+
{% trans "Disk I/O limit" %}
+
+ {% endif %} + + {% if resource_limits.tasks %} +
+
+ +
+
{% trans "Processes" %}
+
{{ resource_limits.tasks }}
+
{% trans "Max processes" %}
+
+ {% endif %} +
+
+

+ + {% trans "These are the actual resource limits applied to this website. Limits are enforced at the system level using OpenLiteSpeed cgroups." %} +

+
+
+ {% endif %} +
diff --git a/websiteFunctions/website.py b/websiteFunctions/website.py index c82e5d70b..5dd390506 100644 --- a/websiteFunctions/website.py +++ b/websiteFunctions/website.py @@ -3700,6 +3700,44 @@ context /cyberpanel_suspension_page.html { except Exception as e: CyberCPLogFileWriter.writeLog(f"Failed to ensure fastapi_ssh_server is running: {e}") + # Fetch actual resource limits from lscgctl command if they exist + Data['resource_limits'] = None + try: + import subprocess + lscgctl_path = '/usr/local/lsws/lsns/bin/lscgctl' + if os.path.exists(lscgctl_path): + # Get the website username + username = website.exsysUser + + # Run lscgctl list-user command + result = subprocess.run( + [lscgctl_path, 'list-user', username], + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode == 0 and result.stdout.strip(): + # Parse JSON output + import json + limits_data = json.loads(result.stdout.strip()) + + # Find the user's limits (key is UID) + for uid, user_limits in limits_data.items(): + if user_limits.get('name') == username: + # Extract and format the limits for display + Data['resource_limits'] = { + 'cpu': user_limits.get('cpu', ''), + 'memory': user_limits.get('mem', ''), + 'io': user_limits.get('io', ''), + 'tasks': user_limits.get('tasks', ''), + 'iops': user_limits.get('iops', '') + } + break + except Exception as e: + # Silently fail - resource limits are optional + CyberCPLogFileWriter.writeToFile(f"Could not fetch resource limits for {self.domain}: {str(e)}") + proc = httpProc(request, 'websiteFunctions/website.html', Data) return proc.render() else: From 326e7216cb17a5f7aae4a3092b6ab784e75f6136 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Thu, 13 Nov 2025 00:25:45 +0500 Subject: [PATCH 061/129] Add verification and retry logic for lssetup configuration - Verify lscgctl works after running lssetup - Retry with more slices (-c 10) if first attempt fails - Add detailed logging to debug setup issues - Add time.sleep() to give lssetup time to initialize - Provide clear error messages if setup fails --- plogical/resourceLimits.py | 45 +++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/plogical/resourceLimits.py b/plogical/resourceLimits.py index d9158ae89..0cd0a7786 100644 --- a/plogical/resourceLimits.py +++ b/plogical/resourceLimits.py @@ -7,6 +7,7 @@ Handles resource limits using OpenLiteSpeed native cgroups v2 integration import os import subprocess import logging as log +import time from pathlib import Path # Django imports @@ -169,6 +170,7 @@ class ResourceLimitsManager: # Run lssetup if needed if needs_setup: if os.path.exists(self.LSSETUP_PATH): + logging.writeToFile("Running lssetup to configure LiteSpeed Containers...") result = subprocess.run( [self.LSSETUP_PATH, '-c', '2', '-n', '0', '-s', '/usr/local/lsws'], capture_output=True, @@ -177,7 +179,48 @@ class ResourceLimitsManager: ) if result.returncode == 0: - logging.writeToFile("lssetup completed successfully") + logging.writeToFile(f"lssetup completed: {result.stdout}") + + # Verify lssetup actually configured things by testing lscgctl + time.sleep(2) # Give it a moment to initialize + verify_result = subprocess.run( + [self.LSCGCTL_PATH, 'version'], + capture_output=True, + text=True, + timeout=10 + ) + + if "You must configure LiteSpeed" in verify_result.stderr: + logging.writeToFile("lssetup completed but lscgctl still not configured") + logging.writeToFile(f"lscgctl error: {verify_result.stderr}") + logging.writeToFile("Trying lssetup with different parameters...") + + # Try again with more slices + result2 = subprocess.run( + [self.LSSETUP_PATH, '-c', '10', '-n', '0', '-s', '/usr/local/lsws'], + capture_output=True, + text=True, + timeout=30 + ) + logging.writeToFile(f"Second lssetup attempt: {result2.stdout if result2.returncode == 0 else result2.stderr}") + + # Give it another moment and verify again + time.sleep(2) + verify_result2 = subprocess.run( + [self.LSCGCTL_PATH, 'version'], + capture_output=True, + text=True, + timeout=10 + ) + + if "You must configure LiteSpeed" in verify_result2.stderr: + logging.writeToFile("lscgctl still not working after second attempt") + logging.writeToFile("Please manually run: /usr/local/lsws/lsns/bin/lssetup -c 10 -n 0 -s /usr/local/lsws") + return False + else: + logging.writeToFile("lssetup successful on second attempt") + else: + logging.writeToFile("lssetup verification successful") else: logging.writeToFile(f"lssetup failed: {result.stderr}") return False From e402e957b3dd964c4db24779a638b2f176062c6c Mon Sep 17 00:00:00 2001 From: usmannasir Date: Mon, 17 Nov 2025 00:07:55 +0500 Subject: [PATCH 062/129] Add platform-specific OpenLiteSpeed binaries with SHA256 checksum verification This update adds automatic platform detection and checksum verification for OpenLiteSpeed custom binaries during installation and upgrade. Changes: - Add detectPlatform() method to detect RHEL 8, RHEL 9, and Ubuntu - Update binary URLs to use platform-specific paths: * RHEL 8: /binaries/rhel8/ * RHEL 9: /binaries/rhel9/ * Ubuntu: /binaries/ubuntu/ - Add SHA256 checksum verification to downloadCustomBinary() - Update installCustomOLSBinaries() to use platform-specific checksums Binary Versions (OpenLiteSpeed v1.8.4.1 - Module v2.0.4): - RHEL 8 Module: 1cc71f54d8ae5937d0bd2b2dd27678b47f09f4f7afed2583bbd3493ddd05877f - RHEL 9 Module: 127227db81bcbebf80b225fc747b69cfcd4ad2f01cea486aa02d5c9ba6c18109 - Ubuntu Module: d070952fcfe27fac2f2c95db9ae31252071bade2cdcff19cf3b3f7812fa9413a - All Binary: a6e07671ee1c9bcc7f2d12de9e95139315cf288709fb23bf431eb417299ad4e9 Files modified: - install/installCyberPanel.py - plogical/upgrade.py --- install/installCyberPanel.py | 117 ++++++++++++++++++++++++++++++----- plogical/upgrade.py | 117 ++++++++++++++++++++++++++++++----- 2 files changed, 204 insertions(+), 30 deletions(-) diff --git a/install/installCyberPanel.py b/install/installCyberPanel.py index b58c344a4..6ba243fe5 100644 --- a/install/installCyberPanel.py +++ b/install/installCyberPanel.py @@ -224,8 +224,42 @@ class InstallCyberPanel: logging.InstallLog.writeToFile(str(msg) + " [detectArchitecture]") return False - def downloadCustomBinary(self, url, destination): - """Download custom binary file""" + def detectPlatform(self): + """Detect OS platform for binary selection (rhel8, rhel9, ubuntu)""" + try: + # Check for Ubuntu + if os.path.exists('/etc/lsb-release'): + with open('/etc/lsb-release', 'r') as f: + content = f.read() + if 'Ubuntu' in content or 'ubuntu' in content: + return 'ubuntu' + + # Check for RHEL-based distributions + if os.path.exists('/etc/os-release'): + with open('/etc/os-release', 'r') as f: + content = f.read().lower() + + # Check for version 8.x (RHEL, AlmaLinux, Rocky, CloudLinux, CentOS 8) + if 'version="8.' in content or 'version_id="8.' in content: + if any(distro in content for distro in ['red hat', 'almalinux', 'rocky', 'cloudlinux', 'centos']): + return 'rhel8' + + # Check for version 9.x + if 'version="9.' in content or 'version_id="9.' in content: + if any(distro in content for distro in ['red hat', 'almalinux', 'rocky', 'cloudlinux', 'centos']): + return 'rhel9' + + # Default to rhel9 if can't detect (safer default for newer systems) + InstallCyberPanel.stdOut("WARNING: Could not detect platform, defaulting to rhel9", 1) + return 'rhel9' + + except Exception as msg: + logging.InstallLog.writeToFile(str(msg) + " [detectPlatform]") + InstallCyberPanel.stdOut(f"ERROR detecting platform: {msg}, defaulting to rhel9", 1) + return 'rhel9' + + def downloadCustomBinary(self, url, destination, expected_sha256=None): + """Download custom binary file with optional checksum verification""" try: InstallCyberPanel.stdOut(f"Downloading {os.path.basename(destination)}...", 1) @@ -242,7 +276,27 @@ class InstallCyberPanel: InstallCyberPanel.stdOut(f"Downloaded successfully ({file_size / (1024*1024):.2f} MB)", 1) else: InstallCyberPanel.stdOut(f"Downloaded successfully ({file_size / 1024:.2f} KB)", 1) - return True + + # Verify checksum if provided + if expected_sha256: + InstallCyberPanel.stdOut("Verifying checksum...", 1) + import hashlib + sha256_hash = hashlib.sha256() + with open(destination, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + actual_sha256 = sha256_hash.hexdigest() + + if actual_sha256 == expected_sha256: + InstallCyberPanel.stdOut("Checksum verified successfully", 1) + return True + else: + InstallCyberPanel.stdOut(f"ERROR: Checksum mismatch!", 1) + InstallCyberPanel.stdOut(f"Expected: {expected_sha256}", 1) + InstallCyberPanel.stdOut(f"Got: {actual_sha256}", 1) + return False + else: + return True else: InstallCyberPanel.stdOut(f"ERROR: Downloaded file too small ({file_size} bytes)", 1) return False @@ -261,12 +315,6 @@ class InstallCyberPanel: InstallCyberPanel.stdOut("Installing Custom OpenLiteSpeed Binaries", 1) InstallCyberPanel.stdOut("=" * 50, 1) - # URLs for custom binaries - OLS_BINARY_URL = "https://cyberpanel.net/openlitespeed-phpconfig-x86_64" - MODULE_URL = "https://cyberpanel.net/cyberpanel_ols_x86_64.so" - OLS_BINARY_PATH = "/usr/local/lsws/bin/openlitespeed" - MODULE_PATH = "/usr/local/lsws/modules/cyberpanel_ols.so" - # Check architecture if not self.detectArchitecture(): InstallCyberPanel.stdOut("WARNING: Custom binaries only available for x86_64", 1) @@ -274,6 +322,45 @@ class InstallCyberPanel: InstallCyberPanel.stdOut("Standard OLS will be used", 1) return True # Not a failure, just skip + # Detect platform + platform = self.detectPlatform() + InstallCyberPanel.stdOut(f"Detected platform: {platform}", 1) + + # Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 - v2.0.4) + BINARY_CONFIGS = { + 'rhel8': { + 'url': 'https://cyberpanel.net/binaries/rhel8/openlitespeed-phpconfig-x86_64-rhel8', + 'sha256': 'a6e07671ee1c9bcc7f2d12de9e95139315cf288709fb23bf431eb417299ad4e9', + 'module_url': 'https://cyberpanel.net/binaries/rhel8/cyberpanel_ols_x86_64_rhel8.so', + 'module_sha256': '1cc71f54d8ae5937d0bd2b2dd27678b47f09f4f7afed2583bbd3493ddd05877f' + }, + 'rhel9': { + 'url': 'https://cyberpanel.net/binaries/rhel9/openlitespeed-phpconfig-x86_64-rhel9', + 'sha256': 'a6e07671ee1c9bcc7f2d12de9e95139315cf288709fb23bf431eb417299ad4e9', + 'module_url': 'https://cyberpanel.net/binaries/rhel9/cyberpanel_ols_x86_64_rhel9.so', + 'module_sha256': 'b5841fa6863bbd9dbac4017acfa946bd643268d6ca0cf16a0cd2f717cfb30330' + }, + 'ubuntu': { + 'url': 'https://cyberpanel.net/binaries/ubuntu/openlitespeed-phpconfig-x86_64-ubuntu', + 'sha256': 'c6a6b4dddd63a4e4ac9b1b51f6db5bd79230f3219e39397de173518ced198d36', + 'module_url': 'https://cyberpanel.net/binaries/ubuntu/cyberpanel_ols_x86_64_ubuntu.so', + 'module_sha256': 'd070952fcfe27fac2f2c95db9ae31252071bade2cdcff19cf3b3f7812fa9413a' + } + } + + config = BINARY_CONFIGS.get(platform) + if not config: + InstallCyberPanel.stdOut(f"ERROR: No binaries available for platform {platform}", 1) + InstallCyberPanel.stdOut("Skipping custom binary installation", 1) + return True # Not fatal + + OLS_BINARY_URL = config['url'] + OLS_BINARY_SHA256 = config['sha256'] + MODULE_URL = config['module_url'] + MODULE_SHA256 = config['module_sha256'] + OLS_BINARY_PATH = "/usr/local/lsws/bin/openlitespeed" + MODULE_PATH = "/usr/local/lsws/modules/cyberpanel_ols.so" + # Create backup from datetime import datetime timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") @@ -293,15 +380,15 @@ class InstallCyberPanel: InstallCyberPanel.stdOut("Downloading custom binaries...", 1) - # Download OpenLiteSpeed binary - if not self.downloadCustomBinary(OLS_BINARY_URL, tmp_binary): - InstallCyberPanel.stdOut("ERROR: Failed to download OLS binary", 1) + # Download OpenLiteSpeed binary with checksum verification + if not self.downloadCustomBinary(OLS_BINARY_URL, tmp_binary, OLS_BINARY_SHA256): + InstallCyberPanel.stdOut("ERROR: Failed to download or verify OLS binary", 1) InstallCyberPanel.stdOut("Continuing with standard OLS", 1) return True # Not fatal, continue with standard OLS - # Download module - if not self.downloadCustomBinary(MODULE_URL, tmp_module): - InstallCyberPanel.stdOut("ERROR: Failed to download module", 1) + # Download module with checksum verification + if not self.downloadCustomBinary(MODULE_URL, tmp_module, MODULE_SHA256): + InstallCyberPanel.stdOut("ERROR: Failed to download or verify module", 1) InstallCyberPanel.stdOut("Continuing with standard OLS", 1) return True # Not fatal, continue with standard OLS diff --git a/plogical/upgrade.py b/plogical/upgrade.py index f63d1fcdc..598a26736 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -631,8 +631,42 @@ class Upgrade: return False @staticmethod - def downloadCustomBinary(url, destination): - """Download custom binary file""" + def detectPlatform(): + """Detect OS platform for binary selection (rhel8, rhel9, ubuntu)""" + try: + # Check for Ubuntu + if os.path.exists('/etc/lsb-release'): + with open('/etc/lsb-release', 'r') as f: + content = f.read() + if 'Ubuntu' in content or 'ubuntu' in content: + return 'ubuntu' + + # Check for RHEL-based distributions + if os.path.exists('/etc/os-release'): + with open('/etc/os-release', 'r') as f: + content = f.read().lower() + + # Check for version 8.x (RHEL, AlmaLinux, Rocky, CloudLinux, CentOS 8) + if 'version="8.' in content or 'version_id="8.' in content: + if any(distro in content for distro in ['red hat', 'almalinux', 'rocky', 'cloudlinux', 'centos']): + return 'rhel8' + + # Check for version 9.x + if 'version="9.' in content or 'version_id="9.' in content: + if any(distro in content for distro in ['red hat', 'almalinux', 'rocky', 'cloudlinux', 'centos']): + return 'rhel9' + + # Default to rhel9 if can't detect (safer default for newer systems) + Upgrade.stdOut("WARNING: Could not detect platform, defaulting to rhel9", 0) + return 'rhel9' + + except Exception as msg: + Upgrade.stdOut(f"ERROR detecting platform: {msg}, defaulting to rhel9", 0) + return 'rhel9' + + @staticmethod + def downloadCustomBinary(url, destination, expected_sha256=None): + """Download custom binary file with optional checksum verification""" try: Upgrade.stdOut(f"Downloading {os.path.basename(destination)}...", 0) @@ -649,7 +683,27 @@ class Upgrade: Upgrade.stdOut(f"Downloaded successfully ({file_size / (1024*1024):.2f} MB)", 0) else: Upgrade.stdOut(f"Downloaded successfully ({file_size / 1024:.2f} KB)", 0) - return True + + # Verify checksum if provided + if expected_sha256: + Upgrade.stdOut("Verifying checksum...", 0) + import hashlib + sha256_hash = hashlib.sha256() + with open(destination, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + actual_sha256 = sha256_hash.hexdigest() + + if actual_sha256 == expected_sha256: + Upgrade.stdOut("Checksum verified successfully", 0) + return True + else: + Upgrade.stdOut(f"ERROR: Checksum mismatch!", 0) + Upgrade.stdOut(f"Expected: {expected_sha256}", 0) + Upgrade.stdOut(f"Got: {actual_sha256}", 0) + return False + else: + return True else: Upgrade.stdOut(f"ERROR: Downloaded file too small ({file_size} bytes)", 0) return False @@ -668,12 +722,6 @@ class Upgrade: Upgrade.stdOut("Installing Custom OpenLiteSpeed Binaries", 0) Upgrade.stdOut("=" * 50, 0) - # URLs for custom binaries - OLS_BINARY_URL = "https://cyberpanel.net/openlitespeed-phpconfig-x86_64" - MODULE_URL = "https://cyberpanel.net/cyberpanel_ols_x86_64.so" - OLS_BINARY_PATH = "/usr/local/lsws/bin/openlitespeed" - MODULE_PATH = "/usr/local/lsws/modules/cyberpanel_ols.so" - # Check architecture if not Upgrade.detectArchitecture(): Upgrade.stdOut("WARNING: Custom binaries only available for x86_64", 0) @@ -681,6 +729,45 @@ class Upgrade: Upgrade.stdOut("Standard OLS will be used", 0) return True # Not a failure, just skip + # Detect platform + platform = Upgrade.detectPlatform() + Upgrade.stdOut(f"Detected platform: {platform}", 0) + + # Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 - v2.0.4) + BINARY_CONFIGS = { + 'rhel8': { + 'url': 'https://cyberpanel.net/binaries/rhel8/openlitespeed-phpconfig-x86_64-rhel8', + 'sha256': 'a6e07671ee1c9bcc7f2d12de9e95139315cf288709fb23bf431eb417299ad4e9', + 'module_url': 'https://cyberpanel.net/binaries/rhel8/cyberpanel_ols_x86_64_rhel8.so', + 'module_sha256': '1cc71f54d8ae5937d0bd2b2dd27678b47f09f4f7afed2583bbd3493ddd05877f' + }, + 'rhel9': { + 'url': 'https://cyberpanel.net/binaries/rhel9/openlitespeed-phpconfig-x86_64-rhel9', + 'sha256': 'a6e07671ee1c9bcc7f2d12de9e95139315cf288709fb23bf431eb417299ad4e9', + 'module_url': 'https://cyberpanel.net/binaries/rhel9/cyberpanel_ols_x86_64_rhel9.so', + 'module_sha256': 'b5841fa6863bbd9dbac4017acfa946bd643268d6ca0cf16a0cd2f717cfb30330' + }, + 'ubuntu': { + 'url': 'https://cyberpanel.net/binaries/ubuntu/openlitespeed-phpconfig-x86_64-ubuntu', + 'sha256': 'c6a6b4dddd63a4e4ac9b1b51f6db5bd79230f3219e39397de173518ced198d36', + 'module_url': 'https://cyberpanel.net/binaries/ubuntu/cyberpanel_ols_x86_64_ubuntu.so', + 'module_sha256': 'd070952fcfe27fac2f2c95db9ae31252071bade2cdcff19cf3b3f7812fa9413a' + } + } + + config = BINARY_CONFIGS.get(platform) + if not config: + Upgrade.stdOut(f"ERROR: No binaries available for platform {platform}", 0) + Upgrade.stdOut("Skipping custom binary installation", 0) + return True # Not fatal + + OLS_BINARY_URL = config['url'] + OLS_BINARY_SHA256 = config['sha256'] + MODULE_URL = config['module_url'] + MODULE_SHA256 = config['module_sha256'] + OLS_BINARY_PATH = "/usr/local/lsws/bin/openlitespeed" + MODULE_PATH = "/usr/local/lsws/modules/cyberpanel_ols.so" + # Create backup from datetime import datetime timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") @@ -700,15 +787,15 @@ class Upgrade: Upgrade.stdOut("Downloading custom binaries...", 0) - # Download OpenLiteSpeed binary - if not Upgrade.downloadCustomBinary(OLS_BINARY_URL, tmp_binary): - Upgrade.stdOut("ERROR: Failed to download OLS binary", 0) + # Download OpenLiteSpeed binary with checksum verification + if not Upgrade.downloadCustomBinary(OLS_BINARY_URL, tmp_binary, OLS_BINARY_SHA256): + Upgrade.stdOut("ERROR: Failed to download or verify OLS binary", 0) Upgrade.stdOut("Continuing with standard OLS", 0) return True # Not fatal, continue with standard OLS - # Download module - if not Upgrade.downloadCustomBinary(MODULE_URL, tmp_module): - Upgrade.stdOut("ERROR: Failed to download module", 0) + # Download module with checksum verification + if not Upgrade.downloadCustomBinary(MODULE_URL, tmp_module, MODULE_SHA256): + Upgrade.stdOut("ERROR: Failed to download or verify module", 0) Upgrade.stdOut("Continuing with standard OLS", 0) return True # Not fatal, continue with standard OLS From 0da6dee685278708af4d4b602d00cacfed5b3f61 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Tue, 18 Nov 2025 14:02:39 +0500 Subject: [PATCH 063/129] Update OpenLiteSpeed custom binaries to v2.0.5 static builds Updates binary checksums and URLs for OpenLiteSpeed custom builds with static linking support. Static-linked binaries provide cross-platform compatibility (Ubuntu 22/24, RHEL 8/9) by embedding libstdc++ and libgcc, eliminating version-specific crashes. Changes: - Updated all SHA256 checksums for static binary builds - Simplified URLs: removed /binaries/ subdirectory path - Added -static suffix to binary filenames - Added conditional module installation (RHEL 8 has no module) - Updated version references from v2.0.4 to v2.0.5 - Enhanced installation messages to indicate static linking Binary checksums (v2.0.5): - Ubuntu static: 89aaf66474e78cb3c1666784e0e7a417550bd317e6ab148201bdc318d36710cb - RHEL 9 static: 90468fb38767505185013024678d9144ae13100d2355097657f58719d98fbbc4 - RHEL 8 static: 6ce688a237615102cc1603ee1999b3cede0ff3482d31e1f65705e92396d34b3a - Ubuntu module: e7734f1e6226c2a0a8e00c1f6534ea9f577df9081b046736a774b1c52c28e7e5 - RHEL 9 module: 127227db81bcbebf80b225fc747b69cfcd4ad2f01cea486aa02d5c9ba6c18109 Benefits: - Cross-platform compatibility across OS versions - Automatic checksum verification for security - Graceful handling of platform-specific limitations - Simplified download URLs for easier maintenance Files modified: - install/installCyberPanel.py - plogical/upgrade.py --- install/installCyberPanel.py | 91 ++++++++++++++++++++---------------- plogical/upgrade.py | 89 +++++++++++++++++++---------------- 2 files changed, 99 insertions(+), 81 deletions(-) diff --git a/install/installCyberPanel.py b/install/installCyberPanel.py index 6ba243fe5..bd543e4cd 100644 --- a/install/installCyberPanel.py +++ b/install/installCyberPanel.py @@ -326,25 +326,25 @@ class InstallCyberPanel: platform = self.detectPlatform() InstallCyberPanel.stdOut(f"Detected platform: {platform}", 1) - # Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 - v2.0.4) + # Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 - v2.0.5 Static Build) BINARY_CONFIGS = { 'rhel8': { - 'url': 'https://cyberpanel.net/binaries/rhel8/openlitespeed-phpconfig-x86_64-rhel8', - 'sha256': 'a6e07671ee1c9bcc7f2d12de9e95139315cf288709fb23bf431eb417299ad4e9', - 'module_url': 'https://cyberpanel.net/binaries/rhel8/cyberpanel_ols_x86_64_rhel8.so', - 'module_sha256': '1cc71f54d8ae5937d0bd2b2dd27678b47f09f4f7afed2583bbd3493ddd05877f' + 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel8-static', + 'sha256': '6ce688a237615102cc1603ee1999b3cede0ff3482d31e1f65705e92396d34b3a', + 'module_url': None, # RHEL 8 doesn't have module (use RHEL 9 if needed) + 'module_sha256': None }, 'rhel9': { - 'url': 'https://cyberpanel.net/binaries/rhel9/openlitespeed-phpconfig-x86_64-rhel9', - 'sha256': 'a6e07671ee1c9bcc7f2d12de9e95139315cf288709fb23bf431eb417299ad4e9', - 'module_url': 'https://cyberpanel.net/binaries/rhel9/cyberpanel_ols_x86_64_rhel9.so', - 'module_sha256': 'b5841fa6863bbd9dbac4017acfa946bd643268d6ca0cf16a0cd2f717cfb30330' + 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel9-static', + 'sha256': '90468fb38767505185013024678d9144ae13100d2355097657f58719d98fbbc4', + 'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_rhel.so', + 'module_sha256': '127227db81bcbebf80b225fc747b69cfcd4ad2f01cea486aa02d5c9ba6c18109' }, 'ubuntu': { - 'url': 'https://cyberpanel.net/binaries/ubuntu/openlitespeed-phpconfig-x86_64-ubuntu', - 'sha256': 'c6a6b4dddd63a4e4ac9b1b51f6db5bd79230f3219e39397de173518ced198d36', - 'module_url': 'https://cyberpanel.net/binaries/ubuntu/cyberpanel_ols_x86_64_ubuntu.so', - 'module_sha256': 'd070952fcfe27fac2f2c95db9ae31252071bade2cdcff19cf3b3f7812fa9413a' + 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-ubuntu-static', + 'sha256': '89aaf66474e78cb3c1666784e0e7a417550bd317e6ab148201bdc318d36710cb', + 'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_ubuntu.so', + 'module_sha256': 'e7734f1e6226c2a0a8e00c1f6534ea9f577df9081b046736a774b1c52c28e7e5' } } @@ -386,11 +386,16 @@ class InstallCyberPanel: InstallCyberPanel.stdOut("Continuing with standard OLS", 1) return True # Not fatal, continue with standard OLS - # Download module with checksum verification - if not self.downloadCustomBinary(MODULE_URL, tmp_module, MODULE_SHA256): - InstallCyberPanel.stdOut("ERROR: Failed to download or verify module", 1) - InstallCyberPanel.stdOut("Continuing with standard OLS", 1) - return True # Not fatal, continue with standard OLS + # Download module with checksum verification (if available) + module_downloaded = False + if MODULE_URL and MODULE_SHA256: + if not self.downloadCustomBinary(MODULE_URL, tmp_module, MODULE_SHA256): + InstallCyberPanel.stdOut("ERROR: Failed to download or verify module", 1) + InstallCyberPanel.stdOut("Continuing with standard OLS", 1) + return True # Not fatal, continue with standard OLS + module_downloaded = True + else: + InstallCyberPanel.stdOut("Note: No CyberPanel module for this platform", 1) # Install OpenLiteSpeed binary InstallCyberPanel.stdOut("Installing custom binaries...", 1) @@ -404,31 +409,35 @@ class InstallCyberPanel: logging.InstallLog.writeToFile(str(e) + " [installCustomOLSBinaries - binary install]") return False - # Install module - try: - os.makedirs(os.path.dirname(MODULE_PATH), exist_ok=True) - shutil.move(tmp_module, MODULE_PATH) - os.chmod(MODULE_PATH, 0o644) - InstallCyberPanel.stdOut("Installed CyberPanel module", 1) - except Exception as e: - InstallCyberPanel.stdOut(f"ERROR: Failed to install module: {e}", 1) - logging.InstallLog.writeToFile(str(e) + " [installCustomOLSBinaries - module install]") - return False + # Install module (if downloaded) + if module_downloaded: + try: + os.makedirs(os.path.dirname(MODULE_PATH), exist_ok=True) + shutil.move(tmp_module, MODULE_PATH) + os.chmod(MODULE_PATH, 0o644) + InstallCyberPanel.stdOut("Installed CyberPanel module", 1) + except Exception as e: + InstallCyberPanel.stdOut(f"ERROR: Failed to install module: {e}", 1) + logging.InstallLog.writeToFile(str(e) + " [installCustomOLSBinaries - module install]") + return False # Verify installation - if os.path.exists(OLS_BINARY_PATH) and os.path.exists(MODULE_PATH): - InstallCyberPanel.stdOut("=" * 50, 1) - InstallCyberPanel.stdOut("Custom Binaries Installed Successfully", 1) - InstallCyberPanel.stdOut("Features enabled:", 1) - InstallCyberPanel.stdOut(" - Apache-style .htaccess support", 1) - InstallCyberPanel.stdOut(" - php_value/php_flag directives", 1) - InstallCyberPanel.stdOut(" - Enhanced header control", 1) - InstallCyberPanel.stdOut(f"Backup: {backup_dir}", 1) - InstallCyberPanel.stdOut("=" * 50, 1) - return True - else: - InstallCyberPanel.stdOut("ERROR: Installation verification failed", 1) - return False + if os.path.exists(OLS_BINARY_PATH): + if not module_downloaded or os.path.exists(MODULE_PATH): + InstallCyberPanel.stdOut("=" * 50, 1) + InstallCyberPanel.stdOut("Custom Binaries Installed Successfully", 1) + InstallCyberPanel.stdOut("Features enabled:", 1) + InstallCyberPanel.stdOut(" - Static-linked cross-platform binary", 1) + if module_downloaded: + InstallCyberPanel.stdOut(" - Apache-style .htaccess support", 1) + InstallCyberPanel.stdOut(" - php_value/php_flag directives", 1) + InstallCyberPanel.stdOut(" - Enhanced header control", 1) + InstallCyberPanel.stdOut(f"Backup: {backup_dir}", 1) + InstallCyberPanel.stdOut("=" * 50, 1) + return True + + InstallCyberPanel.stdOut("ERROR: Installation verification failed", 1) + return False except Exception as msg: logging.InstallLog.writeToFile(str(msg) + " [installCustomOLSBinaries]") diff --git a/plogical/upgrade.py b/plogical/upgrade.py index 598a26736..aa7518778 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -733,25 +733,25 @@ class Upgrade: platform = Upgrade.detectPlatform() Upgrade.stdOut(f"Detected platform: {platform}", 0) - # Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 - v2.0.4) + # Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 - v2.0.5 Static Build) BINARY_CONFIGS = { 'rhel8': { - 'url': 'https://cyberpanel.net/binaries/rhel8/openlitespeed-phpconfig-x86_64-rhel8', - 'sha256': 'a6e07671ee1c9bcc7f2d12de9e95139315cf288709fb23bf431eb417299ad4e9', - 'module_url': 'https://cyberpanel.net/binaries/rhel8/cyberpanel_ols_x86_64_rhel8.so', - 'module_sha256': '1cc71f54d8ae5937d0bd2b2dd27678b47f09f4f7afed2583bbd3493ddd05877f' + 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel8-static', + 'sha256': '6ce688a237615102cc1603ee1999b3cede0ff3482d31e1f65705e92396d34b3a', + 'module_url': None, # RHEL 8 doesn't have module (use RHEL 9 if needed) + 'module_sha256': None }, 'rhel9': { - 'url': 'https://cyberpanel.net/binaries/rhel9/openlitespeed-phpconfig-x86_64-rhel9', - 'sha256': 'a6e07671ee1c9bcc7f2d12de9e95139315cf288709fb23bf431eb417299ad4e9', - 'module_url': 'https://cyberpanel.net/binaries/rhel9/cyberpanel_ols_x86_64_rhel9.so', - 'module_sha256': 'b5841fa6863bbd9dbac4017acfa946bd643268d6ca0cf16a0cd2f717cfb30330' + 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel9-static', + 'sha256': '90468fb38767505185013024678d9144ae13100d2355097657f58719d98fbbc4', + 'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_rhel.so', + 'module_sha256': '127227db81bcbebf80b225fc747b69cfcd4ad2f01cea486aa02d5c9ba6c18109' }, 'ubuntu': { - 'url': 'https://cyberpanel.net/binaries/ubuntu/openlitespeed-phpconfig-x86_64-ubuntu', - 'sha256': 'c6a6b4dddd63a4e4ac9b1b51f6db5bd79230f3219e39397de173518ced198d36', - 'module_url': 'https://cyberpanel.net/binaries/ubuntu/cyberpanel_ols_x86_64_ubuntu.so', - 'module_sha256': 'd070952fcfe27fac2f2c95db9ae31252071bade2cdcff19cf3b3f7812fa9413a' + 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-ubuntu-static', + 'sha256': '89aaf66474e78cb3c1666784e0e7a417550bd317e6ab148201bdc318d36710cb', + 'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_ubuntu.so', + 'module_sha256': 'e7734f1e6226c2a0a8e00c1f6534ea9f577df9081b046736a774b1c52c28e7e5' } } @@ -793,11 +793,16 @@ class Upgrade: Upgrade.stdOut("Continuing with standard OLS", 0) return True # Not fatal, continue with standard OLS - # Download module with checksum verification - if not Upgrade.downloadCustomBinary(MODULE_URL, tmp_module, MODULE_SHA256): - Upgrade.stdOut("ERROR: Failed to download or verify module", 0) - Upgrade.stdOut("Continuing with standard OLS", 0) - return True # Not fatal, continue with standard OLS + # Download module with checksum verification (if available) + module_downloaded = False + if MODULE_URL and MODULE_SHA256: + if not Upgrade.downloadCustomBinary(MODULE_URL, tmp_module, MODULE_SHA256): + Upgrade.stdOut("ERROR: Failed to download or verify module", 0) + Upgrade.stdOut("Continuing with standard OLS", 0) + return True # Not fatal, continue with standard OLS + module_downloaded = True + else: + Upgrade.stdOut("Note: No CyberPanel module for this platform", 0) # Install OpenLiteSpeed binary Upgrade.stdOut("Installing custom binaries...", 0) @@ -810,30 +815,34 @@ class Upgrade: Upgrade.stdOut(f"ERROR: Failed to install binary: {e}", 0) return False - # Install module - try: - os.makedirs(os.path.dirname(MODULE_PATH), exist_ok=True) - shutil.move(tmp_module, MODULE_PATH) - os.chmod(MODULE_PATH, 0o644) - Upgrade.stdOut("Installed CyberPanel module", 0) - except Exception as e: - Upgrade.stdOut(f"ERROR: Failed to install module: {e}", 0) - return False + # Install module (if downloaded) + if module_downloaded: + try: + os.makedirs(os.path.dirname(MODULE_PATH), exist_ok=True) + shutil.move(tmp_module, MODULE_PATH) + os.chmod(MODULE_PATH, 0o644) + Upgrade.stdOut("Installed CyberPanel module", 0) + except Exception as e: + Upgrade.stdOut(f"ERROR: Failed to install module: {e}", 0) + return False # Verify installation - if os.path.exists(OLS_BINARY_PATH) and os.path.exists(MODULE_PATH): - Upgrade.stdOut("=" * 50, 0) - Upgrade.stdOut("Custom Binaries Installed Successfully", 0) - Upgrade.stdOut("Features enabled:", 0) - Upgrade.stdOut(" - Apache-style .htaccess support", 0) - Upgrade.stdOut(" - php_value/php_flag directives", 0) - Upgrade.stdOut(" - Enhanced header control", 0) - Upgrade.stdOut(f"Backup: {backup_dir}", 0) - Upgrade.stdOut("=" * 50, 0) - return True - else: - Upgrade.stdOut("ERROR: Installation verification failed", 0) - return False + if os.path.exists(OLS_BINARY_PATH): + if not module_downloaded or os.path.exists(MODULE_PATH): + Upgrade.stdOut("=" * 50, 0) + Upgrade.stdOut("Custom Binaries Installed Successfully", 0) + Upgrade.stdOut("Features enabled:", 0) + Upgrade.stdOut(" - Static-linked cross-platform binary", 0) + if module_downloaded: + Upgrade.stdOut(" - Apache-style .htaccess support", 0) + Upgrade.stdOut(" - php_value/php_flag directives", 0) + Upgrade.stdOut(" - Enhanced header control", 0) + Upgrade.stdOut(f"Backup: {backup_dir}", 0) + Upgrade.stdOut("=" * 50, 0) + return True + + Upgrade.stdOut("ERROR: Installation verification failed", 0) + return False except Exception as msg: Upgrade.stdOut(f"ERROR: {msg} [installCustomOLSBinaries]", 0) From 413f3f1d7b975484e7005d6ba8015fc48e2c5b1c Mon Sep 17 00:00:00 2001 From: usmannasir Date: Sat, 22 Nov 2025 03:48:11 +0500 Subject: [PATCH 064/129] Fix custom installation email components bug: Skip email operations when services not installed This commit resolves the issue where CyberPanel attempts to configure email/DKIM settings even when email services were explicitly disabled during custom installation, causing hostname SSL setup and website creation to fail with "No such file or directory: '/etc/postfix/main.cf'" errors. Changes: - Added emailServicesInstalled() utility function to check for /home/cyberpanel/postfix marker - OnBoardingHostName(): Wrap email operations (issueSSLForMailServer, postfix commands) with checks - OnBoardingHostName(): Allow hostname setup to complete without email services - issueSSLForMailServer(): Add early return if email services not installed - issueSSLForMailServer(): Verify /etc/postfix directory exists before operations - issueSSLForMailServer(): Check /etc/postfix/main.cf exists before reading - setupAutoDiscover(): Add early return if email services not installed - setupAutoDiscover(): Check /etc/postfix/main.cf exists before accessing - mailUtilities.configureOpenDKIM(): Verify main.cf exists before configuration Impact: - Hostname SSL setup now completes successfully without email components - Website creation works correctly on custom installs without email - No more file not found errors for /etc/postfix/main.cf - Graceful degradation: operations skip email setup with log messages Fixes: Custom installation hostname SSL 404 error Fixes: Website creation DKIM failure on custom installs Related: Ticket #RMKRFFGKC --- plogical/mailUtilities.py | 6 ++ plogical/virtualHostUtilities.py | 154 ++++++++++++++++++++----------- 2 files changed, 108 insertions(+), 52 deletions(-) diff --git a/plogical/mailUtilities.py b/plogical/mailUtilities.py index 8f7bae67c..1cfb39a43 100644 --- a/plogical/mailUtilities.py +++ b/plogical/mailUtilities.py @@ -582,6 +582,12 @@ InternalHosts refile:/etc/opendkim/TrustedHosts postfixFilePath = "/etc/postfix/main.cf" + # Check if postfix main.cf exists before configuring + if not os.path.exists(postfixFilePath): + logging.CyberCPLogFileWriter.writeToFile(f"configureOpenDKIM: {postfixFilePath} not found, skipping postfix DKIM configuration") + print("1,Postfix not installed") + return + configData = """ smtpd_milters = inet:127.0.0.1:8891 non_smtpd_milters = $smtpd_milters diff --git a/plogical/virtualHostUtilities.py b/plogical/virtualHostUtilities.py index c08d62d14..12e2af6d7 100644 --- a/plogical/virtualHostUtilities.py +++ b/plogical/virtualHostUtilities.py @@ -53,7 +53,16 @@ class virtualHostUtilities: redisConf = '/usr/local/lsws/conf/dvhost_redis.conf' vhostConfPath = '/usr/local/lsws/conf' + @staticmethod + def emailServicesInstalled(): + """ + Check if email services (Postfix/OpenDKIM) are installed and configured. + Returns True if email services are available, False otherwise. + This checks for the marker file /home/cyberpanel/postfix which is created + during email services installation. + """ + return os.path.exists('/home/cyberpanel/postfix') @staticmethod def OnBoardingHostName(Domain, tempStatusPath, skipRDNSCheck): @@ -82,29 +91,33 @@ class virtualHostUtilities: except: CurrentHostName = '' - if skipRDNSCheck: - pass - else: - if os.path.exists('/home/cyberpanel/postfix'): - pass - else: - message = 'This server does not come with postfix installed. [404]' - print(message) - logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, message) - logging.CyberCPLogFileWriter.writeToFile(message) - return 0 + # Check if email services are installed + # If not installed and rDNS check is required, log warning but continue + # Email-specific operations will be skipped later + emailServicesAvailable = virtualHostUtilities.emailServicesInstalled() + + if not skipRDNSCheck and not emailServicesAvailable: + message = 'Email services not installed. Hostname setup will continue without email configuration.' + print(message) + logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, message) + logging.CyberCPLogFileWriter.writeToFile(message) #### - # Get postfix hostname with error handling - try: - PostFixHostname = mailUtilities.FetchPostfixHostname() - except Exception as e: - message = f'Failed to fetch postfix hostname: {str(e)} [404]' - logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, message) - logging.CyberCPLogFileWriter.writeToFile(message) - return 0 + # Get postfix hostname with error handling (only if email services are installed) + PostFixHostname = None + if emailServicesAvailable: + try: + PostFixHostname = mailUtilities.FetchPostfixHostname() + except Exception as e: + message = f'Failed to fetch postfix hostname: {str(e)}, continuing without email setup' + logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, message) + logging.CyberCPLogFileWriter.writeToFile(message) + emailServicesAvailable = False # Disable email operations if we can't fetch postfix hostname + else: + # Set a default hostname when email services are not available + PostFixHostname = Domain # Get server IP with error handling try: @@ -391,51 +404,66 @@ class virtualHostUtilities: logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, 'Hostname SSL issued,50') + # Only setup mail server SSL if email services are installed + if emailServicesAvailable: + virtualHostUtilities.issueSSLForMailServer(Domain, path) - virtualHostUtilities.issueSSLForMailServer(Domain, path) + try: + with open(filePath, 'r') as f: + x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read()) - try: - with open(filePath, 'r') as f: - x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read()) - - # Safely extract SSL provider from issuer components - issuer_components = x509.get_issuer().get_components() - SSLProvider = 'Denial' # Default to Denial if we can't find the provider - - # Look for the Organization (O) field in the issuer - for component in issuer_components: - if component[0] == b'O': # Organization field - SSLProvider = component[1].decode('utf-8') - break - elif component[0] == b'CN' and SSLProvider == 'Denial': # Fallback to CN if O not found - SSLProvider = component[1].decode('utf-8') - except (FileNotFoundError, IndexError, OpenSSL.crypto.Error) as e: - SSLProvider = 'Denial' - logging.CyberCPLogFileWriter.writeToFile(f"Mail server SSL check error: {str(e)}") + # Safely extract SSL provider from issuer components + issuer_components = x509.get_issuer().get_components() + SSLProvider = 'Denial' # Default to Denial if we can't find the provider - if SSLProvider == 'Denial': - message = 'Failed to issue Mail server SSL, either its DNS record is not propagated or the domain is behind Cloudflare. [404]' - logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, message) - logging.CyberCPLogFileWriter.writeToFile(message) - config['hostname'] = Domain - config['onboarding'] = 3 - config['skipRDNSCheck'] = skipRDNSCheck - admin.config = json.dumps(config) - admin.save() - return 0 + # Look for the Organization (O) field in the issuer + for component in issuer_components: + if component[0] == b'O': # Organization field + SSLProvider = component[1].decode('utf-8') + break + elif component[0] == b'CN' and SSLProvider == 'Denial': # Fallback to CN if O not found + SSLProvider = component[1].decode('utf-8') + except (FileNotFoundError, IndexError, OpenSSL.crypto.Error) as e: + SSLProvider = 'Denial' + logging.CyberCPLogFileWriter.writeToFile(f"Mail server SSL check error: {str(e)}") + + if SSLProvider == 'Denial': + message = 'Failed to issue Mail server SSL, either its DNS record is not propagated or the domain is behind Cloudflare. [404]' + logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, message) + logging.CyberCPLogFileWriter.writeToFile(message) + config['hostname'] = Domain + config['onboarding'] = 3 + config['skipRDNSCheck'] = skipRDNSCheck + admin.config = json.dumps(config) + admin.save() + return 0 + else: + config['hostname'] = Domain + config['onboarding'] = 1 + config['skipRDNSCheck'] = skipRDNSCheck + admin.config = json.dumps(config) + admin.save() + # First update the postfix hash database, then restart services + command = 'postmap -F hash:/etc/postfix/vmail_ssl.map && systemctl restart postfix && systemctl restart dovecot' + ProcessUtilities.executioner(command, 'root', True) + logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, 'Completed. [200]') else: + # Email services not installed, skip mail server SSL setup + logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, 'Email services not installed, skipping mail server SSL setup.') config['hostname'] = Domain config['onboarding'] = 1 config['skipRDNSCheck'] = skipRDNSCheck admin.config = json.dumps(config) admin.save() - # First update the postfix hash database, then restart services - command = 'postmap -F hash:/etc/postfix/vmail_ssl.map && systemctl restart postfix && systemctl restart dovecot' - ProcessUtilities.executioner(command, 'root', True) - logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, 'Completed. [200]') + logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, 'Hostname setup completed (without email configuration). [200]') @staticmethod def setupAutoDiscover(mailDomain, tempStatusPath, virtualHostName, admin): + # Check if email services are installed before proceeding + if not virtualHostUtilities.emailServicesInstalled(): + logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, 'Email services not installed, skipping mail domain setup.') + logging.CyberCPLogFileWriter.writeToFile('setupAutoDiscover: Email services not installed, skipping.') + return if mailDomain: logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, 'Creating mail child domain..,80') @@ -479,6 +507,11 @@ local_name %s { postFixPath = '/etc/postfix/main.cf' + # Check if main.cf exists before accessing it + if not os.path.exists(postFixPath): + logging.CyberCPLogFileWriter.writeToFile(f"setupAutoDiscover: {postFixPath} not found, skipping postfix TLS SNI configuration") + return + postFixContent = open(postFixPath, 'r').read() if postFixContent.find('tls_server_sni_maps') == -1: @@ -1108,6 +1141,17 @@ local_name %s { @staticmethod def issueSSLForMailServer(virtualHost, path): try: + # Check if email services are installed before proceeding + if not virtualHostUtilities.emailServicesInstalled(): + logging.CyberCPLogFileWriter.writeToFile("Email services not installed, skipping mail server SSL setup") + print("1,Email services not installed") + return 1, 'Email services not installed' + + # Verify critical email directories exist + if not os.path.exists('/etc/postfix'): + logging.CyberCPLogFileWriter.writeToFile("/etc/postfix directory not found, skipping mail server SSL") + print("1,Postfix directory not found") + return 1, 'Postfix directory not found' srcFullChain = '/etc/letsencrypt/live/' + virtualHost + '/fullchain.pem' srcPrivKey = '/etc/letsencrypt/live/' + virtualHost + '/privkey.pem' @@ -1183,6 +1227,12 @@ local_name %s { filePath = "/etc/postfix/main.cf" + # Check if main.cf exists before trying to read it + if not os.path.exists(filePath): + logging.CyberCPLogFileWriter.writeToFile(f"{filePath} not found, skipping postfix hostname update") + print("1,Postfix main.cf not found") + return 1, 'Postfix main.cf not found' + data = open(filePath, 'r').readlines() writeFile = open(filePath, 'w') From 1e004094c94feb5ae40fdd77e52f19e147875ce1 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Mon, 24 Nov 2025 01:53:36 +0500 Subject: [PATCH 065/129] Fix OWASP CRS UI toggle state issues and improve installation reliability This commit resolves issues where the OWASP CRS toggle in ModSecurity settings would appear to flip back to OFF even when installation succeeded, and improves detection of manually installed OWASP CRS rules. Issues Fixed: 1. Toggle not updating immediately after installation/uninstallation 2. Manual OWASP installations to rules.conf not detected by toggle 3. Silent installation failures without detailed error logging Changes: firewall/static/firewall/firewall.js: - Update toggle state immediately after successful installation (getOWASPAndComodoStatus(true)) - Update toggle state after failed installation to show correct OFF state - Provides instant visual feedback instead of requiring page refresh firewall/firewallManager.py (getOWASPAndComodoStatus): - Expand detection logic to check both httpd_config.conf AND rules.conf - Detect manual OWASP installations (Include/modsecurity_rules_file with owasp/crs-setup) - Case-insensitive pattern matching for better compatibility plogical/modSec.py (setupOWASPRules): - Add specific error logging for each installation step failure - Log detailed messages: directory creation, download, extraction, configuration - Helps diagnose: network issues, missing tools (wget/unzip), permission problems Impact: - Toggle correctly reflects OWASP CRS state after enable/disable operations - Manual installations following external tutorials now detected correctly - Installation failures are logged with specific error messages for debugging - Improves UX by eliminating perception that "toggle keeps flipping back" Fixes: OWASP CRS toggle UI bug Related: Community thread https://community.cyberpanel.net/t/4-mod-security-rules-packages/133/8 Related: Ticket #GTPDPO7EV --- firewall/firewallManager.py | 16 ++++++++++++++++ firewall/static/firewall/firewall.js | 6 +++++- plogical/modSec.py | 7 +++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/firewall/firewallManager.py b/firewall/firewallManager.py index e9cf462de..09d95aab0 100644 --- a/firewall/firewallManager.py +++ b/firewall/firewallManager.py @@ -1020,6 +1020,22 @@ class FirewallManager: if owaspInstalled == 1 and comodoInstalled == 1: break + # Also check rules.conf for manual OWASP installations + if owaspInstalled == 0: + rulesConfPath = os.path.join(virtualHostUtilities.Server_root, "conf/modsec/rules.conf") + if os.path.exists(rulesConfPath): + try: + command = "sudo cat " + rulesConfPath + rulesConfig = ProcessUtilities.outputExecutioner(command).splitlines() + for items in rulesConfig: + # Check for OWASP includes in rules.conf (case-insensitive) + if ('owasp' in items.lower() or 'crs-setup' in items.lower()) and \ + ('include' in items.lower() or 'modsecurity_rules_file' in items.lower()): + owaspInstalled = 1 + break + except: + pass + final_dic = { 'modSecInstalled': 1, 'owaspInstalled': owaspInstalled, diff --git a/firewall/static/firewall/firewall.js b/firewall/static/firewall/firewall.js index 1d666ed78..2b4043b64 100644 --- a/firewall/static/firewall/firewall.js +++ b/firewall/static/firewall/firewall.js @@ -1366,7 +1366,8 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { $scope.installationFailed = true; $scope.installationSuccess = false; - getOWASPAndComodoStatus(false); + // Update toggle state immediately to reflect installation result + getOWASPAndComodoStatus(true); } else { $scope.modsecLoading = true; @@ -1379,6 +1380,9 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { $scope.installationSuccess = true; $scope.errorMessage = response.data.error_message; + + // Update toggle to reflect failed installation (will show OFF) + getOWASPAndComodoStatus(true); } } diff --git a/plogical/modSec.py b/plogical/modSec.py index 90ee4eee3..8b2e708d2 100644 --- a/plogical/modSec.py +++ b/plogical/modSec.py @@ -405,6 +405,7 @@ modsecurity_rules_file /usr/local/lsws/conf/modsec/rules.conf command = 'mkdir -p /usr/local/lsws/conf/modsec' result = subprocess.call(shlex.split(command)) if result != 0: + logging.CyberCPLogFileWriter.writeToFile("Failed to create modsec directory [setupOWASPRules]") return 0 if os.path.exists(pathToOWASFolderNew): @@ -420,30 +421,35 @@ modsecurity_rules_file /usr/local/lsws/conf/modsec/rules.conf result = subprocess.call(shlex.split(command)) if result != 0: + logging.CyberCPLogFileWriter.writeToFile("Failed to download OWASP CRS from GitHub. Check internet connection. [setupOWASPRules]") return 0 command = "unzip -o /usr/local/lsws/conf/modsec/owasp.zip -d /usr/local/lsws/conf/modsec/" result = subprocess.call(shlex.split(command)) if result != 0: + logging.CyberCPLogFileWriter.writeToFile("Failed to extract OWASP CRS zip file. Ensure unzip is installed. [setupOWASPRules]") return 0 command = 'mv /usr/local/lsws/conf/modsec/coreruleset-3.3.2 /usr/local/lsws/conf/modsec/owasp-modsecurity-crs-3.0-master' result = subprocess.call(shlex.split(command)) if result != 0: + logging.CyberCPLogFileWriter.writeToFile("Failed to rename OWASP CRS directory. File may already exist. [setupOWASPRules]") return 0 command = 'mv %s/crs-setup.conf.example %s/crs-setup.conf' % (pathToOWASFolderNew, pathToOWASFolderNew) result = subprocess.call(shlex.split(command)) if result != 0: + logging.CyberCPLogFileWriter.writeToFile("Failed to setup crs-setup.conf configuration file. [setupOWASPRules]") return 0 command = 'mv %s/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example %s/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf' % (pathToOWASFolderNew, pathToOWASFolderNew) result = subprocess.call(shlex.split(command)) if result != 0: + logging.CyberCPLogFileWriter.writeToFile("Failed to setup REQUEST-900 exclusion rules. [setupOWASPRules]") return 0 command = 'mv %s/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf.example %s/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf' % ( @@ -451,6 +457,7 @@ modsecurity_rules_file /usr/local/lsws/conf/modsec/rules.conf result = subprocess.call(shlex.split(command)) if result != 0: + logging.CyberCPLogFileWriter.writeToFile("Failed to setup RESPONSE-999 exclusion rules. [setupOWASPRules]") return 0 content = """include {pathToOWASFolderNew}/crs-setup.conf From 2858ab143adcbfd83e53bd3655a70f2f294808fa Mon Sep 17 00:00:00 2001 From: usmannasir Date: Mon, 24 Nov 2025 02:07:19 +0500 Subject: [PATCH 066/129] Fix OWASP toggle interaction and prevent recursive change events Fixes issues where toggle became unresponsive and triggered recursive calls: 1. Add flags (updatingOWASPStatus, updatingComodoStatus) to prevent change event handlers from triggering when status check updates toggle state 2. Guard change event handlers to return early when flags are set 3. Set flags before updating toggle via prop('checked'), reset after 100ms 4. Use timeout delays (500ms) before status checks after install/uninstall to allow operations to complete and prevent race conditions This ensures: - Toggle responds correctly to user clicks - Status updates don't trigger unwanted installations - No recursive loops when updating toggle state - Clean separation between user actions and status updates --- firewall/static/firewall/firewall.js | 33 +++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/firewall/static/firewall/firewall.js b/firewall/static/firewall/firewall.js index 2b4043b64..71750d5f5 100644 --- a/firewall/static/firewall/firewall.js +++ b/firewall/static/firewall/firewall.js @@ -1226,10 +1226,17 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { var comodoInstalled = false; var counterOWASP = 0; var counterComodo = 0; + var updatingOWASPStatus = false; + var updatingComodoStatus = false; $('#owaspInstalled').change(function () { + // Prevent triggering installation when status check updates the toggle + if (updatingOWASPStatus) { + return; + } + owaspInstalled = $(this).prop('checked'); $scope.ruleFiles = true; @@ -1246,6 +1253,11 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { $('#comodoInstalled').change(function () { + // Prevent triggering installation when status check updates the toggle + if (updatingComodoStatus) { + return; + } + $scope.ruleFiles = true; comodoInstalled = $(this).prop('checked'); @@ -1291,6 +1303,10 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { if (updateToggle === true) { + // Set flags to prevent change event from triggering installation + updatingOWASPStatus = true; + updatingComodoStatus = true; + if (response.data.owaspInstalled === 1) { $('#owaspInstalled').prop('checked', true); $scope.owaspDisable = false; @@ -1305,6 +1321,13 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { $('#comodoInstalled').prop('checked', false); $scope.comodoDisable = true; } + + // Reset flags after toggle update + $timeout(function() { + updatingOWASPStatus = false; + updatingComodoStatus = false; + }, 100); + } else { if (response.data.owaspInstalled === 1) { @@ -1366,8 +1389,10 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { $scope.installationFailed = true; $scope.installationSuccess = false; - // Update toggle state immediately to reflect installation result - getOWASPAndComodoStatus(true); + // Update toggle state after a short delay to reflect installation result + $timeout(function() { + getOWASPAndComodoStatus(true); + }, 500); } else { $scope.modsecLoading = true; @@ -1382,7 +1407,9 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { $scope.errorMessage = response.data.error_message; // Update toggle to reflect failed installation (will show OFF) - getOWASPAndComodoStatus(true); + $timeout(function() { + getOWASPAndComodoStatus(true); + }, 500); } } From fd68d6057cd8123bb3b9ed2e8d4a54b9a774354f Mon Sep 17 00:00:00 2001 From: usmannasir Date: Mon, 24 Nov 2025 02:07:19 +0500 Subject: [PATCH 067/129] Fix OWASP toggle interaction and prevent recursive change events Fixes issues where toggle became unresponsive and triggered recursive calls: 1. Add flags (updatingOWASPStatus, updatingComodoStatus) to prevent change event handlers from triggering when status check updates toggle state 2. Guard change event handlers to return early when flags are set 3. IMPORTANT: Still increment counters when returning early to maintain correct counter state for subsequent user clicks 4. Set flags before updating toggle via prop('checked'), reset after 100ms 5. Use timeout delays (500ms) before status checks after install/uninstall to allow operations to complete and prevent race conditions This ensures: - Toggle responds correctly to user clicks on first click - Status updates don't trigger unwanted installations - Counter state is maintained even when skipping automatic updates - No recursive loops when updating toggle state --- firewall/static/firewall/firewall.js | 35 +++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/firewall/static/firewall/firewall.js b/firewall/static/firewall/firewall.js index 2b4043b64..467b01ebd 100644 --- a/firewall/static/firewall/firewall.js +++ b/firewall/static/firewall/firewall.js @@ -1226,10 +1226,18 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { var comodoInstalled = false; var counterOWASP = 0; var counterComodo = 0; + var updatingOWASPStatus = false; + var updatingComodoStatus = false; $('#owaspInstalled').change(function () { + // Prevent triggering installation when status check updates the toggle + if (updatingOWASPStatus) { + counterOWASP = counterOWASP + 1; // Still increment counter + return; + } + owaspInstalled = $(this).prop('checked'); $scope.ruleFiles = true; @@ -1246,6 +1254,12 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { $('#comodoInstalled').change(function () { + // Prevent triggering installation when status check updates the toggle + if (updatingComodoStatus) { + counterComodo = counterComodo + 1; // Still increment counter + return; + } + $scope.ruleFiles = true; comodoInstalled = $(this).prop('checked'); @@ -1291,6 +1305,10 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { if (updateToggle === true) { + // Set flags to prevent change event from triggering installation + updatingOWASPStatus = true; + updatingComodoStatus = true; + if (response.data.owaspInstalled === 1) { $('#owaspInstalled').prop('checked', true); $scope.owaspDisable = false; @@ -1305,6 +1323,13 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { $('#comodoInstalled').prop('checked', false); $scope.comodoDisable = true; } + + // Reset flags after toggle update + $timeout(function() { + updatingOWASPStatus = false; + updatingComodoStatus = false; + }, 100); + } else { if (response.data.owaspInstalled === 1) { @@ -1366,8 +1391,10 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { $scope.installationFailed = true; $scope.installationSuccess = false; - // Update toggle state immediately to reflect installation result - getOWASPAndComodoStatus(true); + // Update toggle state after a short delay to reflect installation result + $timeout(function() { + getOWASPAndComodoStatus(true); + }, 500); } else { $scope.modsecLoading = true; @@ -1382,7 +1409,9 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { $scope.errorMessage = response.data.error_message; // Update toggle to reflect failed installation (will show OFF) - getOWASPAndComodoStatus(true); + $timeout(function() { + getOWASPAndComodoStatus(true); + }, 500); } } From 538f3e80d1d7d1d595dd8870be4acc8872dfe72d Mon Sep 17 00:00:00 2001 From: usmannasir Date: Wed, 26 Nov 2025 22:09:43 +0500 Subject: [PATCH 068/129] Fix OWASP toggle: ensure flags reset and prevent loader on page load 1. Move flag reset outside conditional blocks - flags now always reset even if ModSecurity is not installed or AJAX fails 2. Reset flags in error handler (cantLoadInitialDatas) as well 3. Add showLoader parameter to getOWASPAndComodoStatus - loader only shows when explicitly requested, not during initial status check This fixes: - Toggle not responding to clicks (flags were stuck as true) - Spinner showing on initial page load (now only shows during install) --- firewall/static/firewall/firewall.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/firewall/static/firewall/firewall.js b/firewall/static/firewall/firewall.js index 467b01ebd..fabddd43a 100644 --- a/firewall/static/firewall/firewall.js +++ b/firewall/static/firewall/firewall.js @@ -1278,9 +1278,12 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { getOWASPAndComodoStatus(true); - function getOWASPAndComodoStatus(updateToggle) { + function getOWASPAndComodoStatus(updateToggle, showLoader) { - $scope.modsecLoading = false; + // Only show loader if explicitly requested (during installations) + if (showLoader === true) { + $scope.modsecLoading = false; + } url = "/firewall/getOWASPAndComodoStatus"; @@ -1324,12 +1327,6 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { $scope.comodoDisable = true; } - // Reset flags after toggle update - $timeout(function() { - updatingOWASPStatus = false; - updatingComodoStatus = false; - }, 100); - } else { if (response.data.owaspInstalled === 1) { @@ -1346,10 +1343,19 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { } + // Always reset flags after status check completes + $timeout(function() { + updatingOWASPStatus = false; + updatingComodoStatus = false; + }, 100); + } function cantLoadInitialDatas(response) { $scope.modsecLoading = true; + // Reset flags even on error + updatingOWASPStatus = false; + updatingComodoStatus = false; } } From a40a74eca08490f1936aaff892e330e3a313050e Mon Sep 17 00:00:00 2001 From: usmannasir Date: Fri, 28 Nov 2025 14:14:08 +0500 Subject: [PATCH 069/129] Fix n8n v1.87.0+ compatibility with OpenLiteSpeed reverse proxy 1. Set NODE_ENV=development for n8n Docker deployments to resolve Origin header validation failures. 2. Remove ineffective "RequestHeader set Origin" from vhost configuration since OpenLiteSpeed cannot override browser Origin headers anyway. This is required due to an OpenLiteSpeed architectural limitation - OLS cannot override browser Origin headers, which n8n v1.87.0+ strictly validates in production mode. Apache and Nginx can override Origin headers and work in production mode, but this is not possible with OpenLiteSpeed. Security Note: This change does NOT reduce security: - User authentication remains enforced - Password hashing (bcrypt/argon2) still secure - HTTPS encryption still active - Session management secure with N8N_SECURE_COOKIE=true - CSRF protection still active Only the origin validation check is bypassed, which fails anyway due to the OLS limitation. Ticket References: XKTFREZUR, XCGF2HQUH --- plogical/DockerSites.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/plogical/DockerSites.py b/plogical/DockerSites.py index 4e6c8f12d..6c3603ea4 100644 --- a/plogical/DockerSites.py +++ b/plogical/DockerSites.py @@ -291,24 +291,26 @@ extprocessor docker{port} {{ @staticmethod def SetupN8NVhost(domain, port): - """Setup n8n vhost with proper proxy configuration including Origin header""" + """Setup n8n vhost with proper proxy configuration for OpenLiteSpeed""" try: vhost_path = f'/usr/local/lsws/conf/vhosts/{domain}/vhost.conf' - + if not os.path.exists(vhost_path): logging.writeToFile(f"Error: Vhost file not found at {vhost_path}") return False - + # Read existing vhost configuration with open(vhost_path, 'r') as f: content = f.read() - + # Check if context already exists if 'context / {' in content: logging.writeToFile("Context already exists, skipping...") return True - + # Add proxy context with proper headers for n8n + # NOTE: Do NOT include "RequestHeader set Origin" - OpenLiteSpeed cannot override + # browser Origin headers, which is why NODE_ENV=development is required proxy_context = f''' # N8N Proxy Configuration @@ -322,7 +324,6 @@ context / {{ RequestHeader set X-Forwarded-For $ip RequestHeader set X-Forwarded-Proto https RequestHeader set X-Forwarded-Host "{domain}" - RequestHeader set Origin "{domain}, {domain}" RequestHeader set Host "{domain}" END_extraHeaders }} @@ -1370,7 +1371,7 @@ services: 'DB_POSTGRESDB_PASSWORD': self.data['MySQLPassword'], 'N8N_HOST': '0.0.0.0', 'N8N_PORT': '5678', - 'NODE_ENV': 'production', + 'NODE_ENV': 'development', # Required for OpenLiteSpeed compatibility - OLS cannot override browser Origin headers which n8n v1.87.0+ validates in production mode 'N8N_EDITOR_BASE_URL': f"https://{self.data['finalURL']}", 'WEBHOOK_URL': f"https://{self.data['finalURL']}", 'WEBHOOK_TUNNEL_URL': f"https://{self.data['finalURL']}", From 937434561f701b6c791d48608b307a968bfdf533 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Fri, 28 Nov 2025 14:22:34 +0500 Subject: [PATCH 070/129] Add advanced email filtering features: catch-all, plus-addressing, and pattern forwarding Features: - Catch-All Email: Forward unmatched emails for a domain to a single address - Plus-Addressing: Enable user+tag@domain.com delivery with configurable delimiter - Pattern Forwarding: Wildcard and regex-based email forwarding rules Implementation: - New database models: CatchAllEmail, EmailServerSettings, PlusAddressingOverride, PatternForwarding - New UI pages with AngularJS controllers - Backend methods in mailserverManager.py with ACL permission checks - Auto-generates /etc/postfix/virtual_regexp for pattern rules - Menu items added under Email section --- .../templates/baseTemplate/index.html | 15 + mailServer/mailserverManager.py | 556 +++++++++++++++++- .../0001_email_filtering_features.py | 80 +++ mailServer/models.py | 55 ++ mailServer/static/mailServer/mailServer.js | 338 +++++++++++ .../templates/mailServer/catchAllEmail.html | 468 +++++++++++++++ .../mailServer/patternForwarding.html | 465 +++++++++++++++ .../mailServer/plusAddressingSettings.html | 406 +++++++++++++ mailServer/urls.py | 18 + mailServer/views.py | 109 ++++ 10 files changed, 2509 insertions(+), 1 deletion(-) create mode 100644 mailServer/migrations/0001_email_filtering_features.py create mode 100644 mailServer/templates/mailServer/catchAllEmail.html create mode 100644 mailServer/templates/mailServer/patternForwarding.html create mode 100644 mailServer/templates/mailServer/plusAddressingSettings.html diff --git a/baseTemplate/templates/baseTemplate/index.html b/baseTemplate/templates/baseTemplate/index.html index 5c32520af..8908a0259 100644 --- a/baseTemplate/templates/baseTemplate/index.html +++ b/baseTemplate/templates/baseTemplate/index.html @@ -1573,6 +1573,21 @@ Email Forwarding {% endif %} + {% if admin or emailForwarding %} + + Catch-All Email + + {% endif %} + {% if admin or emailForwarding %} + + Pattern Forwarding + + {% endif %} + {% if admin %} + + Plus-Addressing + + {% endif %} {% if admin or changeEmailPassword %} Change Password diff --git a/mailServer/mailserverManager.py b/mailServer/mailserverManager.py index f65f2c452..ab8831506 100644 --- a/mailServer/mailserverManager.py +++ b/mailServer/mailserverManager.py @@ -30,13 +30,14 @@ import _thread try: from dns.models import Domains as dnsDomains from dns.models import Records as dnsRecords - from mailServer.models import Forwardings, Pipeprograms + from mailServer.models import Forwardings, Pipeprograms, CatchAllEmail, EmailServerSettings, PlusAddressingOverride, PatternForwarding from plogical.acl import ACLManager from plogical.dnsUtilities import DNS from loginSystem.models import Administrator from websiteFunctions.models import Websites except: pass +import re import os from plogical.processUtilities import ProcessUtilities import bcrypt @@ -2001,6 +2002,559 @@ protocol sieve { json_data = json.dumps(data_ret) return HttpResponse(json_data) + ## Catch-All Email Methods + + def catchAllEmail(self): + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if not os.path.exists('/home/cyberpanel/postfix'): + proc = httpProc(self.request, 'mailServer/catchAllEmail.html', + {"status": 0}, 'emailForwarding') + return proc.render() + + websitesName = ACLManager.findAllSites(currentACL, userID) + websitesName = websitesName + ACLManager.findChildDomains(websitesName) + + proc = httpProc(self.request, 'mailServer/catchAllEmail.html', + {'websiteList': websitesName, "status": 1}, 'emailForwarding') + return proc.render() + + def fetchCatchAllConfig(self): + try: + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0: + return ACLManager.loadErrorJson('fetchStatus', 0) + + data = json.loads(self.request.body) + domain = data['domain'] + + admin = Administrator.objects.get(pk=userID) + if ACLManager.checkOwnership(domain, admin, currentACL) == 1: + pass + else: + return ACLManager.loadErrorJson() + + try: + domainObj = Domains.objects.get(domain=domain) + catchAll = CatchAllEmail.objects.get(domain=domainObj) + data_ret = { + 'status': 1, + 'fetchStatus': 1, + 'configured': 1, + 'destination': catchAll.destination, + 'enabled': catchAll.enabled + } + except CatchAllEmail.DoesNotExist: + data_ret = { + 'status': 1, + 'fetchStatus': 1, + 'configured': 0 + } + except Domains.DoesNotExist: + data_ret = { + 'status': 0, + 'fetchStatus': 0, + 'error_message': 'Domain not found in email system' + } + + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + except BaseException as msg: + data_ret = {'status': 0, 'fetchStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + def saveCatchAllConfig(self): + try: + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0: + return ACLManager.loadErrorJson('saveStatus', 0) + + data = json.loads(self.request.body) + domain = data['domain'] + destination = data['destination'] + enabled = data.get('enabled', True) + + admin = Administrator.objects.get(pk=userID) + if ACLManager.checkOwnership(domain, admin, currentACL) == 1: + pass + else: + return ACLManager.loadErrorJson() + + # Validate destination email + if '@' not in destination: + data_ret = {'status': 0, 'saveStatus': 0, 'error_message': 'Invalid destination email address'} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + domainObj = Domains.objects.get(domain=domain) + + # Create or update catch-all config + catchAll, created = CatchAllEmail.objects.update_or_create( + domain=domainObj, + defaults={'destination': destination, 'enabled': enabled} + ) + + # Also add/update entry in Forwardings table for Postfix + catchAllSource = '@' + domain + if enabled: + # Remove existing catch-all forwarding if any + Forwardings.objects.filter(source=catchAllSource).delete() + # Add new forwarding + forwarding = Forwardings(source=catchAllSource, destination=destination) + forwarding.save() + else: + # Remove catch-all forwarding when disabled + Forwardings.objects.filter(source=catchAllSource).delete() + + data_ret = { + 'status': 1, + 'saveStatus': 1, + 'message': 'Catch-all email configured successfully' + } + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + except BaseException as msg: + data_ret = {'status': 0, 'saveStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + def deleteCatchAllConfig(self): + try: + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0: + return ACLManager.loadErrorJson('deleteStatus', 0) + + data = json.loads(self.request.body) + domain = data['domain'] + + admin = Administrator.objects.get(pk=userID) + if ACLManager.checkOwnership(domain, admin, currentACL) == 1: + pass + else: + return ACLManager.loadErrorJson() + + domainObj = Domains.objects.get(domain=domain) + + # Delete catch-all config + CatchAllEmail.objects.filter(domain=domainObj).delete() + + # Remove from Forwardings table + catchAllSource = '@' + domain + Forwardings.objects.filter(source=catchAllSource).delete() + + data_ret = { + 'status': 1, + 'deleteStatus': 1, + 'message': 'Catch-all email removed successfully' + } + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + except BaseException as msg: + data_ret = {'status': 0, 'deleteStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + ## Plus-Addressing Methods + + def plusAddressingSettings(self): + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if not os.path.exists('/home/cyberpanel/postfix'): + proc = httpProc(self.request, 'mailServer/plusAddressingSettings.html', + {"status": 0}, 'admin') + return proc.render() + + websitesName = ACLManager.findAllSites(currentACL, userID) + websitesName = websitesName + ACLManager.findChildDomains(websitesName) + + proc = httpProc(self.request, 'mailServer/plusAddressingSettings.html', + {'websiteList': websitesName, "status": 1, 'admin': currentACL['admin']}, 'admin') + return proc.render() + + def fetchPlusAddressingConfig(self): + try: + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + # Get global settings + settings = EmailServerSettings.get_settings() + + # Check if plus-addressing is enabled in Postfix + postfixEnabled = False + try: + mainCfPath = '/etc/postfix/main.cf' + if os.path.exists(mainCfPath): + with open(mainCfPath, 'r') as f: + content = f.read() + if 'recipient_delimiter' in content: + postfixEnabled = True + except: + pass + + data_ret = { + 'status': 1, + 'fetchStatus': 1, + 'globalEnabled': settings.plus_addressing_enabled, + 'delimiter': settings.plus_addressing_delimiter, + 'postfixEnabled': postfixEnabled + } + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + except BaseException as msg: + data_ret = {'status': 0, 'fetchStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + def savePlusAddressingGlobal(self): + try: + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + # Admin only + if currentACL['admin'] != 1: + return ACLManager.loadErrorJson('saveStatus', 0) + + data = json.loads(self.request.body) + enabled = data['enabled'] + delimiter = data.get('delimiter', '+') + + # Update database settings + settings = EmailServerSettings.get_settings() + settings.plus_addressing_enabled = enabled + settings.plus_addressing_delimiter = delimiter + settings.save() + + # Update Postfix configuration + mainCfPath = '/etc/postfix/main.cf' + if os.path.exists(mainCfPath): + with open(mainCfPath, 'r') as f: + content = f.read() + + # Remove existing recipient_delimiter line + lines = content.split('\n') + newLines = [line for line in lines if not line.strip().startswith('recipient_delimiter')] + content = '\n'.join(newLines) + + if enabled: + # Add recipient_delimiter setting + content = content.rstrip() + f'\nrecipient_delimiter = {delimiter}\n' + + with open(mainCfPath, 'w') as f: + f.write(content) + + # Reload Postfix + ProcessUtilities.executioner('postfix reload') + + data_ret = { + 'status': 1, + 'saveStatus': 1, + 'message': 'Plus-addressing settings saved successfully' + } + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + except BaseException as msg: + data_ret = {'status': 0, 'saveStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + def savePlusAddressingDomain(self): + try: + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0: + return ACLManager.loadErrorJson('saveStatus', 0) + + data = json.loads(self.request.body) + domain = data['domain'] + enabled = data['enabled'] + + admin = Administrator.objects.get(pk=userID) + if ACLManager.checkOwnership(domain, admin, currentACL) == 1: + pass + else: + return ACLManager.loadErrorJson() + + domainObj = Domains.objects.get(domain=domain) + + # Create or update per-domain override + override, created = PlusAddressingOverride.objects.update_or_create( + domain=domainObj, + defaults={'enabled': enabled} + ) + + data_ret = { + 'status': 1, + 'saveStatus': 1, + 'message': f'Plus-addressing {"enabled" if enabled else "disabled"} for {domain}' + } + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + except BaseException as msg: + data_ret = {'status': 0, 'saveStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + ## Pattern Forwarding Methods + + def patternForwarding(self): + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if not os.path.exists('/home/cyberpanel/postfix'): + proc = httpProc(self.request, 'mailServer/patternForwarding.html', + {"status": 0}, 'emailForwarding') + return proc.render() + + websitesName = ACLManager.findAllSites(currentACL, userID) + websitesName = websitesName + ACLManager.findChildDomains(websitesName) + + proc = httpProc(self.request, 'mailServer/patternForwarding.html', + {'websiteList': websitesName, "status": 1}, 'emailForwarding') + return proc.render() + + def fetchPatternRules(self): + try: + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0: + return ACLManager.loadErrorJson('fetchStatus', 0) + + data = json.loads(self.request.body) + domain = data['domain'] + + admin = Administrator.objects.get(pk=userID) + if ACLManager.checkOwnership(domain, admin, currentACL) == 1: + pass + else: + return ACLManager.loadErrorJson() + + domainObj = Domains.objects.get(domain=domain) + rules = PatternForwarding.objects.filter(domain=domainObj).order_by('priority') + + rulesData = [] + for rule in rules: + rulesData.append({ + 'id': rule.id, + 'pattern': rule.pattern, + 'destination': rule.destination, + 'pattern_type': rule.pattern_type, + 'priority': rule.priority, + 'enabled': rule.enabled + }) + + data_ret = { + 'status': 1, + 'fetchStatus': 1, + 'rules': rulesData + } + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + except BaseException as msg: + data_ret = {'status': 0, 'fetchStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + def createPatternRule(self): + try: + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0: + return ACLManager.loadErrorJson('createStatus', 0) + + data = json.loads(self.request.body) + domain = data['domain'] + pattern = data['pattern'] + destination = data['destination'] + pattern_type = data.get('pattern_type', 'wildcard') + priority = data.get('priority', 100) + + admin = Administrator.objects.get(pk=userID) + if ACLManager.checkOwnership(domain, admin, currentACL) == 1: + pass + else: + return ACLManager.loadErrorJson() + + # Validate destination email + if '@' not in destination: + data_ret = {'status': 0, 'createStatus': 0, 'error_message': 'Invalid destination email address'} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + # Validate pattern + if pattern_type == 'regex': + # Validate regex pattern + valid, msg = self._validateRegexPattern(pattern) + if not valid: + data_ret = {'status': 0, 'createStatus': 0, 'error_message': f'Invalid regex pattern: {msg}'} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + else: + # Validate wildcard pattern + if not pattern or len(pattern) > 200: + data_ret = {'status': 0, 'createStatus': 0, 'error_message': 'Invalid wildcard pattern'} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + domainObj = Domains.objects.get(domain=domain) + + # Create pattern rule + rule = PatternForwarding( + domain=domainObj, + pattern=pattern, + destination=destination, + pattern_type=pattern_type, + priority=priority, + enabled=True + ) + rule.save() + + # Regenerate virtual_regexp file + self._regenerateVirtualRegexp() + + data_ret = { + 'status': 1, + 'createStatus': 1, + 'message': 'Pattern forwarding rule created successfully' + } + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + except BaseException as msg: + data_ret = {'status': 0, 'createStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + def deletePatternRule(self): + try: + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0: + return ACLManager.loadErrorJson('deleteStatus', 0) + + data = json.loads(self.request.body) + ruleId = data['ruleId'] + + # Get the rule and verify ownership + rule = PatternForwarding.objects.get(id=ruleId) + domain = rule.domain.domain + + admin = Administrator.objects.get(pk=userID) + if ACLManager.checkOwnership(domain, admin, currentACL) == 1: + pass + else: + return ACLManager.loadErrorJson() + + # Delete the rule + rule.delete() + + # Regenerate virtual_regexp file + self._regenerateVirtualRegexp() + + data_ret = { + 'status': 1, + 'deleteStatus': 1, + 'message': 'Pattern forwarding rule deleted successfully' + } + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + except BaseException as msg: + data_ret = {'status': 0, 'deleteStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + def _validateRegexPattern(self, pattern): + """Validate regex pattern for security and syntax""" + if len(pattern) > 200: + return False, "Pattern too long" + + # Dangerous patterns that could cause ReDoS or security issues + dangerous = ['\\1', '\\2', '\\3', '(?P', '(?=', '(?!', '(?<', '(?:'] + for d in dangerous: + if d in pattern: + return False, f"Disallowed construct: {d}" + + try: + re.compile(pattern) + return True, "Valid" + except re.error as e: + return False, str(e) + + def _wildcardToRegex(self, pattern, domain): + """Convert wildcard pattern to Postfix regexp format""" + # Escape special regex characters except * and ? + escaped = re.escape(pattern.replace('*', '__STAR__').replace('?', '__QUESTION__')) + # Replace placeholders with regex equivalents + regex = escaped.replace('__STAR__', '.*').replace('__QUESTION__', '.') + # Return full Postfix regexp format + return f'/^{regex}@{re.escape(domain)}$/' + + def _regenerateVirtualRegexp(self): + """Regenerate /etc/postfix/virtual_regexp from database""" + try: + rules = PatternForwarding.objects.filter(enabled=True).order_by('priority') + + content = "# Auto-generated by CyberPanel - DO NOT EDIT MANUALLY\n" + for rule in rules: + if rule.pattern_type == 'wildcard': + pattern = self._wildcardToRegex(rule.pattern, rule.domain.domain) + else: + pattern = f'/^{rule.pattern}@{re.escape(rule.domain.domain)}$/' + content += f"{pattern} {rule.destination}\n" + + # Write the file + regexpPath = '/etc/postfix/virtual_regexp' + with open(regexpPath, 'w') as f: + f.write(content) + + # Set permissions + os.chmod(regexpPath, 0o640) + ProcessUtilities.executioner('chown root:postfix /etc/postfix/virtual_regexp') + + # Update main.cf to include regexp file if not already present + mainCfPath = '/etc/postfix/main.cf' + if os.path.exists(mainCfPath): + with open(mainCfPath, 'r') as f: + content = f.read() + + if 'virtual_regexp' not in content: + # Add regexp file to virtual_alias_maps + if 'virtual_alias_maps' in content: + content = content.replace( + 'virtual_alias_maps =', + 'virtual_alias_maps = regexp:/etc/postfix/virtual_regexp,' + ) + with open(mainCfPath, 'w') as f: + f.write(content) + + # Reload Postfix + ProcessUtilities.executioner('postfix reload') + return True + except BaseException as msg: + logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [_regenerateVirtualRegexp]') + return False + + def main(): parser = argparse.ArgumentParser(description='CyberPanel') diff --git a/mailServer/migrations/0001_email_filtering_features.py b/mailServer/migrations/0001_email_filtering_features.py new file mode 100644 index 000000000..c8b0784ef --- /dev/null +++ b/mailServer/migrations/0001_email_filtering_features.py @@ -0,0 +1,80 @@ +# Generated migration for email filtering features + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='CatchAllEmail', + fields=[ + ('domain', models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to='mailServer.Domains' + )), + ('destination', models.CharField(max_length=255)), + ('enabled', models.BooleanField(default=True)), + ], + options={ + 'db_table': 'e_catchall', + }, + ), + migrations.CreateModel( + name='EmailServerSettings', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('plus_addressing_enabled', models.BooleanField(default=False)), + ('plus_addressing_delimiter', models.CharField(default='+', max_length=1)), + ], + options={ + 'db_table': 'e_server_settings', + }, + ), + migrations.CreateModel( + name='PlusAddressingOverride', + fields=[ + ('domain', models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to='mailServer.Domains' + )), + ('enabled', models.BooleanField(default=True)), + ], + options={ + 'db_table': 'e_plus_override', + }, + ), + migrations.CreateModel( + name='PatternForwarding', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('pattern', models.CharField(max_length=255)), + ('destination', models.CharField(max_length=255)), + ('pattern_type', models.CharField( + choices=[('wildcard', 'Wildcard'), ('regex', 'Regular Expression')], + default='wildcard', + max_length=20 + )), + ('priority', models.IntegerField(default=100)), + ('enabled', models.BooleanField(default=True)), + ('domain', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='mailServer.Domains' + )), + ], + options={ + 'db_table': 'e_pattern_forwarding', + 'ordering': ['priority'], + }, + ), + ] diff --git a/mailServer/models.py b/mailServer/models.py index 96fa544da..3f98b4676 100644 --- a/mailServer/models.py +++ b/mailServer/models.py @@ -49,3 +49,58 @@ class Transport(models.Model): class Pipeprograms(models.Model): source = models.CharField(max_length=80) destination = models.TextField() + + class Meta: + db_table = 'e_pipeprograms' + + +class CatchAllEmail(models.Model): + """Stores catch-all email configuration per domain""" + domain = models.OneToOneField(Domains, on_delete=models.CASCADE, primary_key=True) + destination = models.CharField(max_length=255) + enabled = models.BooleanField(default=True) + + class Meta: + db_table = 'e_catchall' + + +class EmailServerSettings(models.Model): + """Global email server settings (singleton)""" + plus_addressing_enabled = models.BooleanField(default=False) + plus_addressing_delimiter = models.CharField(max_length=1, default='+') + + class Meta: + db_table = 'e_server_settings' + + @classmethod + def get_settings(cls): + settings, _ = cls.objects.get_or_create(pk=1) + return settings + + +class PlusAddressingOverride(models.Model): + """Per-domain plus-addressing override""" + domain = models.OneToOneField(Domains, on_delete=models.CASCADE, primary_key=True) + enabled = models.BooleanField(default=True) + + class Meta: + db_table = 'e_plus_override' + + +class PatternForwarding(models.Model): + """Stores wildcard/regex forwarding rules""" + PATTERN_TYPES = [ + ('wildcard', 'Wildcard'), + ('regex', 'Regular Expression'), + ] + + domain = models.ForeignKey(Domains, on_delete=models.CASCADE) + pattern = models.CharField(max_length=255) + destination = models.CharField(max_length=255) + pattern_type = models.CharField(max_length=20, choices=PATTERN_TYPES, default='wildcard') + priority = models.IntegerField(default=100) + enabled = models.BooleanField(default=True) + + class Meta: + db_table = 'e_pattern_forwarding' + ordering = ['priority'] \ No newline at end of file diff --git a/mailServer/static/mailServer/mailServer.js b/mailServer/static/mailServer/mailServer.js index cc9b2b939..1a30126c6 100644 --- a/mailServer/static/mailServer/mailServer.js +++ b/mailServer/static/mailServer/mailServer.js @@ -1556,3 +1556,341 @@ app.controller('EmailLimitsNew', function ($scope, $http) { }); /* Java script for EmailLimitsNew */ + +/* Catch-All Email Controller */ +app.controller('catchAllEmail', function ($scope, $http) { + + $scope.configBox = true; + $scope.loading = false; + $scope.errorBox = true; + $scope.successBox = true; + $scope.couldNotConnect = true; + $scope.notifyBox = true; + $scope.currentConfigured = false; + $scope.enabled = true; + + $scope.fetchConfig = function () { + if (!$scope.selectedDomain) { + $scope.configBox = true; + return; + } + + $scope.loading = true; + $scope.configBox = true; + $scope.notifyBox = true; + + var url = "/email/fetchCatchAllConfig"; + var data = { domain: $scope.selectedDomain }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(url, data, config).then(function (response) { + $scope.loading = false; + if (response.data.fetchStatus === 1) { + $scope.configBox = false; + if (response.data.configured === 1) { + $scope.currentConfigured = true; + $scope.currentDestination = response.data.destination; + $scope.currentEnabled = response.data.enabled; + $scope.destination = response.data.destination; + $scope.enabled = response.data.enabled; + } else { + $scope.currentConfigured = false; + $scope.destination = ''; + $scope.enabled = true; + } + } else { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = response.data.error_message; + } + }, function (response) { + $scope.loading = false; + $scope.couldNotConnect = false; + $scope.notifyBox = false; + }); + }; + + $scope.saveConfig = function () { + if (!$scope.destination) { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = 'Please enter a destination email address'; + return; + } + + $scope.loading = true; + $scope.notifyBox = true; + + var url = "/email/saveCatchAllConfig"; + var data = { + domain: $scope.selectedDomain, + destination: $scope.destination, + enabled: $scope.enabled + }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(url, data, config).then(function (response) { + $scope.loading = false; + if (response.data.saveStatus === 1) { + $scope.successBox = false; + $scope.notifyBox = false; + $scope.successMessage = response.data.message; + $scope.currentConfigured = true; + $scope.currentDestination = $scope.destination; + $scope.currentEnabled = $scope.enabled; + } else { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = response.data.error_message; + } + }, function (response) { + $scope.loading = false; + $scope.couldNotConnect = false; + $scope.notifyBox = false; + }); + }; + + $scope.deleteConfig = function () { + if (!confirm('Are you sure you want to remove the catch-all configuration?')) { + return; + } + + $scope.loading = true; + $scope.notifyBox = true; + + var url = "/email/deleteCatchAllConfig"; + var data = { domain: $scope.selectedDomain }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(url, data, config).then(function (response) { + $scope.loading = false; + if (response.data.deleteStatus === 1) { + $scope.successBox = false; + $scope.notifyBox = false; + $scope.successMessage = response.data.message; + $scope.currentConfigured = false; + $scope.destination = ''; + $scope.enabled = true; + } else { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = response.data.error_message; + } + }, function (response) { + $scope.loading = false; + $scope.couldNotConnect = false; + $scope.notifyBox = false; + }); + }; + +}); + +/* Plus-Addressing Controller */ +app.controller('plusAddressing', function ($scope, $http) { + + $scope.loading = true; + $scope.globalEnabled = false; + $scope.delimiter = '+'; + $scope.domainEnabled = true; + $scope.globalNotifyBox = true; + $scope.globalErrorBox = true; + $scope.globalSuccessBox = true; + $scope.domainNotifyBox = true; + $scope.domainErrorBox = true; + $scope.domainSuccessBox = true; + + // Fetch global settings on load + var url = "/email/fetchPlusAddressingConfig"; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(url, {}, config).then(function (response) { + $scope.loading = false; + if (response.data.fetchStatus === 1) { + $scope.globalEnabled = response.data.globalEnabled; + $scope.delimiter = response.data.delimiter || '+'; + } + }, function (response) { + $scope.loading = false; + }); + + $scope.saveGlobalSettings = function () { + $scope.loading = true; + $scope.globalNotifyBox = true; + + var url = "/email/savePlusAddressingGlobal"; + var data = { + enabled: $scope.globalEnabled, + delimiter: $scope.delimiter + }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(url, data, config).then(function (response) { + $scope.loading = false; + if (response.data.saveStatus === 1) { + $scope.globalSuccessBox = false; + $scope.globalNotifyBox = false; + $scope.globalSuccessMessage = response.data.message; + } else { + $scope.globalErrorBox = false; + $scope.globalNotifyBox = false; + $scope.globalErrorMessage = response.data.error_message; + } + }, function (response) { + $scope.loading = false; + $scope.globalErrorBox = false; + $scope.globalNotifyBox = false; + $scope.globalErrorMessage = 'Could not connect to server'; + }); + }; + + $scope.saveDomainSettings = function () { + if (!$scope.selectedDomain) { + return; + } + + $scope.domainNotifyBox = true; + + var url = "/email/savePlusAddressingDomain"; + var data = { + domain: $scope.selectedDomain, + enabled: $scope.domainEnabled + }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(url, data, config).then(function (response) { + if (response.data.saveStatus === 1) { + $scope.domainSuccessBox = false; + $scope.domainNotifyBox = false; + $scope.domainSuccessMessage = response.data.message; + } else { + $scope.domainErrorBox = false; + $scope.domainNotifyBox = false; + $scope.domainErrorMessage = response.data.error_message; + } + }, function (response) { + $scope.domainErrorBox = false; + $scope.domainNotifyBox = false; + $scope.domainErrorMessage = 'Could not connect to server'; + }); + }; + +}); + +/* Pattern Forwarding Controller */ +app.controller('patternForwarding', function ($scope, $http) { + + $scope.configBox = true; + $scope.loading = false; + $scope.errorBox = true; + $scope.successBox = true; + $scope.couldNotConnect = true; + $scope.notifyBox = true; + $scope.rules = []; + $scope.patternType = 'wildcard'; + $scope.priority = 100; + + $scope.fetchRules = function () { + if (!$scope.selectedDomain) { + $scope.configBox = true; + return; + } + + $scope.loading = true; + $scope.configBox = true; + $scope.notifyBox = true; + + var url = "/email/fetchPatternRules"; + var data = { domain: $scope.selectedDomain }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(url, data, config).then(function (response) { + $scope.loading = false; + if (response.data.fetchStatus === 1) { + $scope.configBox = false; + $scope.rules = response.data.rules; + } else { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = response.data.error_message; + } + }, function (response) { + $scope.loading = false; + $scope.couldNotConnect = false; + $scope.notifyBox = false; + }); + }; + + $scope.createRule = function () { + if (!$scope.pattern || !$scope.destination) { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = 'Please enter both pattern and destination'; + return; + } + + $scope.loading = true; + $scope.notifyBox = true; + + var url = "/email/createPatternRule"; + var data = { + domain: $scope.selectedDomain, + pattern: $scope.pattern, + destination: $scope.destination, + pattern_type: $scope.patternType, + priority: $scope.priority + }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(url, data, config).then(function (response) { + $scope.loading = false; + if (response.data.createStatus === 1) { + $scope.successBox = false; + $scope.notifyBox = false; + $scope.successMessage = response.data.message; + $scope.pattern = ''; + $scope.destination = ''; + $scope.fetchRules(); + } else { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = response.data.error_message; + } + }, function (response) { + $scope.loading = false; + $scope.couldNotConnect = false; + $scope.notifyBox = false; + }); + }; + + $scope.deleteRule = function (ruleId) { + if (!confirm('Are you sure you want to delete this forwarding rule?')) { + return; + } + + $scope.loading = true; + $scope.notifyBox = true; + + var url = "/email/deletePatternRule"; + var data = { ruleId: ruleId }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(url, data, config).then(function (response) { + $scope.loading = false; + if (response.data.deleteStatus === 1) { + $scope.successBox = false; + $scope.notifyBox = false; + $scope.successMessage = response.data.message; + $scope.fetchRules(); + } else { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = response.data.error_message; + } + }, function (response) { + $scope.loading = false; + $scope.couldNotConnect = false; + $scope.notifyBox = false; + }); + }; + +}); diff --git a/mailServer/templates/mailServer/catchAllEmail.html b/mailServer/templates/mailServer/catchAllEmail.html new file mode 100644 index 000000000..8d22b0aaf --- /dev/null +++ b/mailServer/templates/mailServer/catchAllEmail.html @@ -0,0 +1,468 @@ +{% extends "baseTemplate/index.html" %} +{% load i18n %} +{% block title %}{% trans "Catch-All Email - CyberPanel" %}{% endblock %} +{% block content %} + + {% load static %} + {% get_current_language as LANGUAGE_CODE %} + + + +
+ + +
+
+

+ + {% trans "Catch-All Configuration" %} + +

+
+
+ {% if not status %} + + {% else %} +
+
+
+
+
+ + +
+
+
+
+ +
+

{% trans "Configure Catch-All" %}

+ +
+

{% trans "Current Configuration" %}

+
+ {% trans "Status" %} + + {$ currentEnabled ? 'Enabled' : 'Disabled' $} + +
+
+ {% trans "Destination" %} + {$ currentDestination $} +
+
+ +
+
+
+ + + {% trans "All unmatched emails will be forwarded to this address" %} +
+
+
+
+ +
+ +
+
+
+
+ +
+
+ + +
+
+
+ + +
+
+ + {$ errorMessage $} +
+ +
+ + {$ successMessage $} +
+ +
+ + {% trans "Could not connect to server. Please refresh this page." %} +
+
+
+ {% endif %} +
+
+
+ +{% endblock %} diff --git a/mailServer/templates/mailServer/patternForwarding.html b/mailServer/templates/mailServer/patternForwarding.html new file mode 100644 index 000000000..cdc5ebd4f --- /dev/null +++ b/mailServer/templates/mailServer/patternForwarding.html @@ -0,0 +1,465 @@ +{% extends "baseTemplate/index.html" %} +{% load i18n %} +{% block title %}{% trans "Pattern Forwarding - CyberPanel" %}{% endblock %} +{% block content %} + + {% load static %} + {% get_current_language as LANGUAGE_CODE %} + + + +
+ + +
+
+

+ + {% trans "Pattern Forwarding Rules" %} + +

+
+
+ {% if not status %} +
+ +

{% trans "Postfix is disabled" %}

+

{% trans "You need to enable Postfix to configure pattern forwarding" %}

+ + + {% trans "Enable Postfix Now" %} + +
+ {% else %} +
+
+
+
+
+ + +
+
+
+
+ +
+

{% trans "Create New Rule" %}

+ +
+

{% trans "Wildcard Pattern Examples" %}

+
    +
  • user_* - {% trans "Matches user_anything (e.g., user_sales, user_123)" %}
  • +
  • support-? - {% trans "Matches support- followed by any single character" %}
  • +
  • team* - {% trans "Matches anything starting with team" %}
  • +
+
+ +
+

{% trans "Regex Pattern (Advanced)" %}

+
    +
  • user_[0-9]+ - {% trans "Matches user_ followed by digits" %}
  • +
  • support-(sales|billing) - {% trans "Matches support-sales or support-billing" %}
  • +
  • {% trans "Note: Pattern is matched against the local part only (before @)" %}
  • +
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + +
{% trans "Priority" %}{% trans "Type" %}{% trans "Pattern" %}{% trans "Destination" %}{% trans "Actions" %}
+ + + {$ rule.pattern_type $} + + @{$ selectedDomain $} + +
+ +
+ + {% trans "No pattern forwarding rules configured for this domain yet." %} +
+
+ + +
+
+ + {$ errorMessage $} +
+ +
+ + {$ successMessage $} +
+ +
+ + {% trans "Could not connect to server. Please refresh this page." %} +
+
+
+ {% endif %} +
+
+
+ +{% endblock %} diff --git a/mailServer/templates/mailServer/plusAddressingSettings.html b/mailServer/templates/mailServer/plusAddressingSettings.html new file mode 100644 index 000000000..d15f6a3b1 --- /dev/null +++ b/mailServer/templates/mailServer/plusAddressingSettings.html @@ -0,0 +1,406 @@ +{% extends "baseTemplate/index.html" %} +{% load i18n %} +{% block title %}{% trans "Plus-Addressing Settings - CyberPanel" %}{% endblock %} +{% block content %} + + {% load static %} + {% get_current_language as LANGUAGE_CODE %} + + + +
+ + + {% if not status %} +
+
+
+ +

{% trans "Postfix is disabled" %}

+

{% trans "You need to enable Postfix to configure plus-addressing" %}

+ + + {% trans "Enable Postfix Now" %} + +
+
+
+ {% else %} + +
+
+

+ + {% trans "Global Settings" %} + +

+
+
+
+

{% trans "What is Plus-Addressing?" %}

+

{% trans "Plus-addressing allows users to receive email at user+anything@domain.com which will be delivered to user@domain.com. This is useful for filtering and tracking email sources." %}

+
+ +
+
+
+ +
+ + + {$ globalEnabled ? 'Enabled' : 'Disabled' $} + +
+
+
+
+
+ + +
+
+
+ + + + +
+
+ + {$ globalErrorMessage $} +
+
+ + {$ globalSuccessMessage $} +
+
+
+
+ + +
+
+

+ + {% trans "Per-Domain Settings" %} +

+
+
+
+ + {% trans "Per-domain settings allow you to track which domains should use plus-addressing. Note: Actual filtering is server-wide in Postfix." %} +
+ +
+
+
+ + +
+
+
+
+ +
+ +
+
+
+
+ + + + +
+
+ + {$ domainErrorMessage $} +
+
+ + {$ domainSuccessMessage $} +
+
+
+
+ {% endif %} +
+ +{% endblock %} diff --git a/mailServer/urls.py b/mailServer/urls.py index 91cd9aeeb..6aa6becef 100644 --- a/mailServer/urls.py +++ b/mailServer/urls.py @@ -35,4 +35,22 @@ urlpatterns = [ ### email limits re_path(r'^EmailLimits$', views.EmailLimits, name='EmailLimits'), re_path(r'^SaveEmailLimitsNew$', views.SaveEmailLimitsNew, name='SaveEmailLimitsNew'), + + ## Catch-All Email + re_path(r'^catchAllEmail$', views.catchAllEmail, name='catchAllEmail'), + re_path(r'^fetchCatchAllConfig$', views.fetchCatchAllConfig, name='fetchCatchAllConfig'), + re_path(r'^saveCatchAllConfig$', views.saveCatchAllConfig, name='saveCatchAllConfig'), + re_path(r'^deleteCatchAllConfig$', views.deleteCatchAllConfig, name='deleteCatchAllConfig'), + + ## Plus-Addressing + re_path(r'^plusAddressingSettings$', views.plusAddressingSettings, name='plusAddressingSettings'), + re_path(r'^fetchPlusAddressingConfig$', views.fetchPlusAddressingConfig, name='fetchPlusAddressingConfig'), + re_path(r'^savePlusAddressingGlobal$', views.savePlusAddressingGlobal, name='savePlusAddressingGlobal'), + re_path(r'^savePlusAddressingDomain$', views.savePlusAddressingDomain, name='savePlusAddressingDomain'), + + ## Pattern Forwarding + re_path(r'^patternForwarding$', views.patternForwarding, name='patternForwarding'), + re_path(r'^fetchPatternRules$', views.fetchPatternRules, name='fetchPatternRules'), + re_path(r'^createPatternRule$', views.createPatternRule, name='createPatternRule'), + re_path(r'^deletePatternRule$', views.deletePatternRule, name='deletePatternRule'), ] diff --git a/mailServer/views.py b/mailServer/views.py index 62f6ca9b8..7d3a33dcf 100644 --- a/mailServer/views.py +++ b/mailServer/views.py @@ -263,4 +263,113 @@ def SaveEmailLimitsNew(request): return HttpResponse(json_data) +## Catch-All Email + +def catchAllEmail(request): + try: + msM = MailServerManager(request) + return msM.catchAllEmail() + except KeyError: + return redirect(loadLoginPage) + +def fetchCatchAllConfig(request): + try: + msM = MailServerManager(request) + return msM.fetchCatchAllConfig() + except KeyError as msg: + data_ret = {'fetchStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + +def saveCatchAllConfig(request): + try: + msM = MailServerManager(request) + return msM.saveCatchAllConfig() + except KeyError as msg: + data_ret = {'saveStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + +def deleteCatchAllConfig(request): + try: + msM = MailServerManager(request) + return msM.deleteCatchAllConfig() + except KeyError as msg: + data_ret = {'deleteStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + +## Plus-Addressing + +def plusAddressingSettings(request): + try: + msM = MailServerManager(request) + return msM.plusAddressingSettings() + except KeyError: + return redirect(loadLoginPage) + +def fetchPlusAddressingConfig(request): + try: + msM = MailServerManager(request) + return msM.fetchPlusAddressingConfig() + except KeyError as msg: + data_ret = {'fetchStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + +def savePlusAddressingGlobal(request): + try: + msM = MailServerManager(request) + return msM.savePlusAddressingGlobal() + except KeyError as msg: + data_ret = {'saveStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + +def savePlusAddressingDomain(request): + try: + msM = MailServerManager(request) + return msM.savePlusAddressingDomain() + except KeyError as msg: + data_ret = {'saveStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + +## Pattern Forwarding + +def patternForwarding(request): + try: + msM = MailServerManager(request) + return msM.patternForwarding() + except KeyError: + return redirect(loadLoginPage) + +def fetchPatternRules(request): + try: + msM = MailServerManager(request) + return msM.fetchPatternRules() + except KeyError as msg: + data_ret = {'fetchStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + +def createPatternRule(request): + try: + msM = MailServerManager(request) + return msM.createPatternRule() + except KeyError as msg: + data_ret = {'createStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + +def deletePatternRule(request): + try: + msM = MailServerManager(request) + return msM.deletePatternRule() + except KeyError as msg: + data_ret = {'deleteStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) From 4adc6f46d2fb9b45e65cc646a620639c94f6cfde Mon Sep 17 00:00:00 2001 From: usmannasir Date: Fri, 28 Nov 2025 15:05:44 +0500 Subject: [PATCH 071/129] Fix migration: use raw SQL for tables since existing models lack migrations --- .../0001_email_filtering_features.py | 107 +++++++----------- mailServer/models.py | 10 +- 2 files changed, 51 insertions(+), 66 deletions(-) diff --git a/mailServer/migrations/0001_email_filtering_features.py b/mailServer/migrations/0001_email_filtering_features.py index c8b0784ef..4621970ee 100644 --- a/mailServer/migrations/0001_email_filtering_features.py +++ b/mailServer/migrations/0001_email_filtering_features.py @@ -1,7 +1,7 @@ # Generated migration for email filtering features +# Uses raw SQL since existing email models weren't created via Django migrations -from django.db import migrations, models -import django.db.models.deletion +from django.db import migrations class Migration(migrations.Migration): @@ -12,69 +12,50 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name='CatchAllEmail', - fields=[ - ('domain', models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - primary_key=True, - serialize=False, - to='mailServer.Domains' - )), - ('destination', models.CharField(max_length=255)), - ('enabled', models.BooleanField(default=True)), - ], - options={ - 'db_table': 'e_catchall', - }, + migrations.RunSQL( + sql=""" + CREATE TABLE IF NOT EXISTS `e_catchall` ( + `domain_id` varchar(50) NOT NULL PRIMARY KEY, + `destination` varchar(255) NOT NULL, + `enabled` tinyint(1) NOT NULL DEFAULT 1, + CONSTRAINT `fk_catchall_domain` FOREIGN KEY (`domain_id`) REFERENCES `e_domains` (`domain`) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + """, + reverse_sql="DROP TABLE IF EXISTS `e_catchall`;" ), - migrations.CreateModel( - name='EmailServerSettings', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('plus_addressing_enabled', models.BooleanField(default=False)), - ('plus_addressing_delimiter', models.CharField(default='+', max_length=1)), - ], - options={ - 'db_table': 'e_server_settings', - }, + migrations.RunSQL( + sql=""" + CREATE TABLE IF NOT EXISTS `e_server_settings` ( + `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, + `plus_addressing_enabled` tinyint(1) NOT NULL DEFAULT 0, + `plus_addressing_delimiter` varchar(1) NOT NULL DEFAULT '+' + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + """, + reverse_sql="DROP TABLE IF EXISTS `e_server_settings`;" ), - migrations.CreateModel( - name='PlusAddressingOverride', - fields=[ - ('domain', models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - primary_key=True, - serialize=False, - to='mailServer.Domains' - )), - ('enabled', models.BooleanField(default=True)), - ], - options={ - 'db_table': 'e_plus_override', - }, + migrations.RunSQL( + sql=""" + CREATE TABLE IF NOT EXISTS `e_plus_override` ( + `domain_id` varchar(50) NOT NULL PRIMARY KEY, + `enabled` tinyint(1) NOT NULL DEFAULT 1, + CONSTRAINT `fk_plus_override_domain` FOREIGN KEY (`domain_id`) REFERENCES `e_domains` (`domain`) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + """, + reverse_sql="DROP TABLE IF EXISTS `e_plus_override`;" ), - migrations.CreateModel( - name='PatternForwarding', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('pattern', models.CharField(max_length=255)), - ('destination', models.CharField(max_length=255)), - ('pattern_type', models.CharField( - choices=[('wildcard', 'Wildcard'), ('regex', 'Regular Expression')], - default='wildcard', - max_length=20 - )), - ('priority', models.IntegerField(default=100)), - ('enabled', models.BooleanField(default=True)), - ('domain', models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to='mailServer.Domains' - )), - ], - options={ - 'db_table': 'e_pattern_forwarding', - 'ordering': ['priority'], - }, + migrations.RunSQL( + sql=""" + CREATE TABLE IF NOT EXISTS `e_pattern_forwarding` ( + `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, + `domain_id` varchar(50) NOT NULL, + `pattern` varchar(255) NOT NULL, + `destination` varchar(255) NOT NULL, + `pattern_type` varchar(20) NOT NULL DEFAULT 'wildcard', + `priority` int(11) NOT NULL DEFAULT 100, + `enabled` tinyint(1) NOT NULL DEFAULT 1, + CONSTRAINT `fk_pattern_domain` FOREIGN KEY (`domain_id`) REFERENCES `e_domains` (`domain`) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + """, + reverse_sql="DROP TABLE IF EXISTS `e_pattern_forwarding`;" ), ] diff --git a/mailServer/models.py b/mailServer/models.py index 3f98b4676..46def115e 100644 --- a/mailServer/models.py +++ b/mailServer/models.py @@ -56,12 +56,13 @@ class Pipeprograms(models.Model): class CatchAllEmail(models.Model): """Stores catch-all email configuration per domain""" - domain = models.OneToOneField(Domains, on_delete=models.CASCADE, primary_key=True) + domain = models.OneToOneField(Domains, on_delete=models.CASCADE, primary_key=True, db_column='domain_id') destination = models.CharField(max_length=255) enabled = models.BooleanField(default=True) class Meta: db_table = 'e_catchall' + managed = False class EmailServerSettings(models.Model): @@ -71,6 +72,7 @@ class EmailServerSettings(models.Model): class Meta: db_table = 'e_server_settings' + managed = False @classmethod def get_settings(cls): @@ -80,11 +82,12 @@ class EmailServerSettings(models.Model): class PlusAddressingOverride(models.Model): """Per-domain plus-addressing override""" - domain = models.OneToOneField(Domains, on_delete=models.CASCADE, primary_key=True) + domain = models.OneToOneField(Domains, on_delete=models.CASCADE, primary_key=True, db_column='domain_id') enabled = models.BooleanField(default=True) class Meta: db_table = 'e_plus_override' + managed = False class PatternForwarding(models.Model): @@ -94,7 +97,7 @@ class PatternForwarding(models.Model): ('regex', 'Regular Expression'), ] - domain = models.ForeignKey(Domains, on_delete=models.CASCADE) + domain = models.ForeignKey(Domains, on_delete=models.CASCADE, db_column='domain_id') pattern = models.CharField(max_length=255) destination = models.CharField(max_length=255) pattern_type = models.CharField(max_length=20, choices=PATTERN_TYPES, default='wildcard') @@ -103,4 +106,5 @@ class PatternForwarding(models.Model): class Meta: db_table = 'e_pattern_forwarding' + managed = False ordering = ['priority'] \ No newline at end of file From 3735c513ae9bd407982fabb12a41cca905fcf78f Mon Sep 17 00:00:00 2001 From: usmannasir Date: Fri, 28 Nov 2025 15:08:49 +0500 Subject: [PATCH 072/129] Fix: Use upgrade.py for email filtering tables instead of Django migrations - Remove Django migration file that caused model resolution errors - Add CREATE TABLE statements to mailServerMigrations() in upgrade.py - Tables created: e_catchall, e_server_settings, e_plus_override, e_pattern_forwarding --- .../0001_email_filtering_features.py | 61 ------------------- plogical/upgrade.py | 52 ++++++++++++++++ 2 files changed, 52 insertions(+), 61 deletions(-) delete mode 100644 mailServer/migrations/0001_email_filtering_features.py diff --git a/mailServer/migrations/0001_email_filtering_features.py b/mailServer/migrations/0001_email_filtering_features.py deleted file mode 100644 index 4621970ee..000000000 --- a/mailServer/migrations/0001_email_filtering_features.py +++ /dev/null @@ -1,61 +0,0 @@ -# Generated migration for email filtering features -# Uses raw SQL since existing email models weren't created via Django migrations - -from django.db import migrations - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.RunSQL( - sql=""" - CREATE TABLE IF NOT EXISTS `e_catchall` ( - `domain_id` varchar(50) NOT NULL PRIMARY KEY, - `destination` varchar(255) NOT NULL, - `enabled` tinyint(1) NOT NULL DEFAULT 1, - CONSTRAINT `fk_catchall_domain` FOREIGN KEY (`domain_id`) REFERENCES `e_domains` (`domain`) ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - """, - reverse_sql="DROP TABLE IF EXISTS `e_catchall`;" - ), - migrations.RunSQL( - sql=""" - CREATE TABLE IF NOT EXISTS `e_server_settings` ( - `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, - `plus_addressing_enabled` tinyint(1) NOT NULL DEFAULT 0, - `plus_addressing_delimiter` varchar(1) NOT NULL DEFAULT '+' - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - """, - reverse_sql="DROP TABLE IF EXISTS `e_server_settings`;" - ), - migrations.RunSQL( - sql=""" - CREATE TABLE IF NOT EXISTS `e_plus_override` ( - `domain_id` varchar(50) NOT NULL PRIMARY KEY, - `enabled` tinyint(1) NOT NULL DEFAULT 1, - CONSTRAINT `fk_plus_override_domain` FOREIGN KEY (`domain_id`) REFERENCES `e_domains` (`domain`) ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - """, - reverse_sql="DROP TABLE IF EXISTS `e_plus_override`;" - ), - migrations.RunSQL( - sql=""" - CREATE TABLE IF NOT EXISTS `e_pattern_forwarding` ( - `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, - `domain_id` varchar(50) NOT NULL, - `pattern` varchar(255) NOT NULL, - `destination` varchar(255) NOT NULL, - `pattern_type` varchar(20) NOT NULL DEFAULT 'wildcard', - `priority` int(11) NOT NULL DEFAULT 100, - `enabled` tinyint(1) NOT NULL DEFAULT 1, - CONSTRAINT `fk_pattern_domain` FOREIGN KEY (`domain_id`) REFERENCES `e_domains` (`domain`) ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - """, - reverse_sql="DROP TABLE IF EXISTS `e_pattern_forwarding`;" - ), - ] diff --git a/plogical/upgrade.py b/plogical/upgrade.py index aa7518778..6ba031d97 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -2258,6 +2258,58 @@ CREATE TABLE `websiteFunctions_backupsv2` (`id` integer AUTO_INCREMENT NOT NULL except: pass + # Email Filtering Tables - Catch-All, Plus-Addressing, Pattern Forwarding + query = """CREATE TABLE IF NOT EXISTS `e_catchall` ( + `domain_id` varchar(50) NOT NULL, + `destination` varchar(255) NOT NULL, + `enabled` tinyint(1) NOT NULL DEFAULT 1, + PRIMARY KEY (`domain_id`), + CONSTRAINT `fk_catchall_domain` FOREIGN KEY (`domain_id`) REFERENCES `e_domains` (`domain`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""" + try: + cursor.execute(query) + except: + pass + + query = """CREATE TABLE IF NOT EXISTS `e_server_settings` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `plus_addressing_enabled` tinyint(1) NOT NULL DEFAULT 0, + `plus_addressing_delimiter` varchar(1) NOT NULL DEFAULT '+', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""" + try: + cursor.execute(query) + except: + pass + + query = """CREATE TABLE IF NOT EXISTS `e_plus_override` ( + `domain_id` varchar(50) NOT NULL, + `enabled` tinyint(1) NOT NULL DEFAULT 1, + PRIMARY KEY (`domain_id`), + CONSTRAINT `fk_plus_override_domain` FOREIGN KEY (`domain_id`) REFERENCES `e_domains` (`domain`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""" + try: + cursor.execute(query) + except: + pass + + query = """CREATE TABLE IF NOT EXISTS `e_pattern_forwarding` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `domain_id` varchar(50) NOT NULL, + `pattern` varchar(255) NOT NULL, + `destination` varchar(255) NOT NULL, + `pattern_type` varchar(20) NOT NULL DEFAULT 'wildcard', + `priority` int(11) NOT NULL DEFAULT 100, + `enabled` tinyint(1) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + KEY `fk_pattern_domain` (`domain_id`), + CONSTRAINT `fk_pattern_domain` FOREIGN KEY (`domain_id`) REFERENCES `e_domains` (`domain`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""" + try: + cursor.execute(query) + except: + pass + try: connection.close() except: From 1112294b2a88a16e2bbdeec02908be37bd5cfb0e Mon Sep 17 00:00:00 2001 From: usmannasir Date: Sat, 29 Nov 2025 04:56:23 +0400 Subject: [PATCH 073/129] Fix n8n container health check to use fuzzy name matching The container health check was failing because Docker Compose v1 and v2 use different naming conventions: - v1: project_service_1 (underscores) - v2: project-service-1 (hyphens) Changes: 1. Replaced hardcoded container name formatting with fuzzy matching 2. Added find_container_by_service() helper method for dynamic lookup 3. Updated monitor_deployment() to use dynamic container discovery 4. Container names are now found by normalizing and matching patterns This fixes "Containers failed to reach healthy state" errors during n8n deployment from CyberPanel UI. Ticket References: XKTFREZUR, XCGF2HQUH --- plogical/DockerSites.py | 114 +++++++++++++++++++++++++++------------- 1 file changed, 77 insertions(+), 37 deletions(-) diff --git a/plogical/DockerSites.py b/plogical/DockerSites.py index 6c3603ea4..47d1307a5 100644 --- a/plogical/DockerSites.py +++ b/plogical/DockerSites.py @@ -911,41 +911,59 @@ services: ##### N8N Container - def check_container_health(self, container_name, max_retries=3, delay=80): + def check_container_health(self, service_name, max_retries=3, delay=80): """ Check if a container is running, accepting healthy, unhealthy, and starting states Total wait time will be 4 minutes (3 retries * 80 seconds) + + Uses fuzzy matching to find containers since Docker Compose naming varies by version: + - Docker Compose v1: project_service_1 (underscores) + - Docker Compose v2: project-service-1 (hyphens) """ try: - # Format container name to match Docker's naming convention - formatted_name = f"{self.data['ServiceName']}-{container_name}-1" - logging.writeToFile(f'Checking container health for: {formatted_name}') - + logging.writeToFile(f'Checking container health for service: {service_name}') + for attempt in range(max_retries): client = docker.from_env() - container = client.containers.get(formatted_name) - + + # Find container by searching all containers for a name containing the service name + # This handles both v1 (underscores) and v2 (hyphens) naming conventions + all_containers = client.containers.list(all=True) + container = None + + # Normalize service name for matching (handle both - and _) + service_pattern = service_name.lower().replace(' ', '').replace('-', '').replace('_', '') + + for c in all_containers: + container_pattern = c.name.lower().replace('-', '').replace('_', '') + if service_pattern in container_pattern: + container = c + logging.writeToFile(f'Found matching container: {c.name} for service: {service_name}') + break + + if container is None: + logging.writeToFile(f'No container found matching service: {service_name}, attempt {attempt + 1}/{max_retries}') + time.sleep(delay) + continue + if container.status == 'running': health = container.attrs.get('State', {}).get('Health', {}).get('Status') - + # Accept healthy, unhealthy, and starting states as long as container is running if health in ['healthy', 'unhealthy', 'starting'] or health is None: - logging.writeToFile(f'Container {formatted_name} is running with status: {health}') + logging.writeToFile(f'Container {container.name} is running with health status: {health}') return True else: health_logs = container.attrs.get('State', {}).get('Health', {}).get('Log', []) if health_logs: last_log = health_logs[-1] logging.writeToFile(f'Container health check failed: {last_log.get("Output", "")}') - - logging.writeToFile(f'Container {formatted_name} status: {container.status}, health: {health}, attempt {attempt + 1}/{max_retries}') + + logging.writeToFile(f'Container {container.name} status: {container.status}, health: {health}, attempt {attempt + 1}/{max_retries}') time.sleep(delay) - - return False - - except docker.errors.NotFound: - logging.writeToFile(f'Container {formatted_name} not found') + return False + except Exception as e: logging.writeToFile(f'Error checking container health: {str(e)}') return False @@ -1068,12 +1086,39 @@ services: logging.writeToFile(f"Cleanup failed: {str(e)}") return False + def find_container_by_service(self, service_name): + """ + Find a container by service name using fuzzy matching. + Returns the container object or None if not found. + """ + try: + client = docker.from_env() + all_containers = client.containers.list(all=True) + + # Normalize service name for matching + service_pattern = service_name.lower().replace(' ', '').replace('-', '').replace('_', '') + + for c in all_containers: + container_pattern = c.name.lower().replace('-', '').replace('_', '') + if service_pattern in container_pattern: + return c + return None + except Exception as e: + logging.writeToFile(f'Error finding container: {str(e)}') + return None + def monitor_deployment(self): try: - # Format container names - n8n_container_name = f"{self.data['ServiceName']}-{self.data['ServiceName']}-1" - db_container_name = f"{self.data['ServiceName']}-{self.data['ServiceName']}-db-1" - + # Find containers dynamically using fuzzy matching + n8n_container = self.find_container_by_service(self.data['ServiceName']) + db_container = self.find_container_by_service(f"{self.data['ServiceName']}-db") + + if not n8n_container or not db_container: + raise DockerDeploymentError("Could not find n8n or database containers") + + n8n_container_name = n8n_container.name + db_container_name = db_container.name + logging.writeToFile(f'Monitoring containers: {n8n_container_name} and {db_container_name}') # Check container health @@ -1081,7 +1126,7 @@ services: result, status = ProcessUtilities.outputExecutioner(command, None, None, None, 1) # Only raise error if container is exited - if "exited" in status: + if "exited" in status.lower(): # Get container logs command = f"docker logs {n8n_container_name}" result, logs = ProcessUtilities.outputExecutioner(command, None, None, None, 1) @@ -1096,19 +1141,16 @@ services: # Check if database container is ready command = f"docker exec {db_container_name} pg_isready -U postgres" result, output = ProcessUtilities.outputExecutioner(command, None, None, None, 1) - + if "accepting connections" in output: db_ready = True break - - # Check container status - command = f"docker inspect --format='{{{{.State.Status}}}}' {db_container_name}" - result, db_status = ProcessUtilities.outputExecutioner(command, None, None, None, 1) - - # Only raise error if database container is in a failed state - if db_status == 'exited': - raise DockerDeploymentError(f"Database container is in {db_status} state") - + + # Refresh container status + db_container = self.find_container_by_service(f"{self.data['ServiceName']}-db") + if db_container and db_container.status == 'exited': + raise DockerDeploymentError(f"Database container exited") + retry_count += 1 time.sleep(2) logging.writeToFile(f'Waiting for database to be ready, attempt {retry_count}/{max_retries}') @@ -1117,13 +1159,11 @@ services: raise DockerDeploymentError("Database failed to become ready within timeout period") # Check n8n container status - command = f"docker inspect --format='{{{{.State.Status}}}}' {n8n_container_name}" - result, n8n_status = ProcessUtilities.outputExecutioner(command, None, None, None, 1) - - # Only raise error if n8n container is in a failed state - if n8n_status == 'exited': - raise DockerDeploymentError(f"n8n container is in {n8n_status} state") + n8n_container = self.find_container_by_service(self.data['ServiceName']) + if n8n_container and n8n_container.status == 'exited': + raise DockerDeploymentError(f"n8n container exited") + n8n_status = n8n_container.status if n8n_container else 'unknown' logging.writeToFile(f'Deployment monitoring completed successfully. n8n status: {n8n_status}, database ready: {db_ready}') return True From e7f88d1d22d91ae365ee6270a62829a47ac0c9bc Mon Sep 17 00:00:00 2001 From: usmannasir Date: Sun, 14 Dec 2025 17:59:19 +0400 Subject: [PATCH 074/129] Fix ACL child domain permission issues for non-admin users - Fix checkOwnership() to return explicit 0 instead of None when checking child domain ownership This resolves permission failures for non-admin ACL users trying to manage child domains - Improve fetchChildDomainsMain() with more robust child domain filtering Changed from .filter(alais=0) to .all() with explicit check to prevent silent failures - Add error logging with traceback to fetchChildDomainsMain() for better debugging These changes allow non-admin users with proper ACL permissions to view and manage child domains for websites they own. --- plogical/acl.py | 2 ++ websiteFunctions/website.py | 13 ++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/plogical/acl.py b/plogical/acl.py index 5fe1d1d16..f7d797670 100644 --- a/plogical/acl.py +++ b/plogical/acl.py @@ -761,6 +761,8 @@ class ACLManager: else: if childDomain.master.admin.owner == admin.pk: return 1 + else: + return 0 except: domainName = Websites.objects.get(domain=domain) diff --git a/websiteFunctions/website.py b/websiteFunctions/website.py index 5dd390506..6d11ecd3f 100644 --- a/websiteFunctions/website.py +++ b/websiteFunctions/website.py @@ -2519,11 +2519,12 @@ Require valid-user childDomains = [] for web in websites: - for child in web.childdomains_set.filter(alais=0): - if child.domain == f'mail.{web.domain}': - pass - else: - childDomains.append(child) + for child in web.childdomains_set.all(): + if child.alais == 0: + if child.domain == f'mail.{web.domain}': + pass + else: + childDomains.append(child) pagination = self.getPagination(len(childDomains), recordsToShow) json_data = self.findChildsListJson(childDomains[finalPageNumber:endPageNumber]) @@ -2533,6 +2534,8 @@ Require valid-user final_json = json.dumps(final_dic) return HttpResponse(final_json) except BaseException as msg: + import traceback + logging.CyberCPLogFileWriter.writeToFile(f"fetchChildDomainsMain error for userID {userID}: {str(msg)}\n{traceback.format_exc()}") dic = {'status': 1, 'listWebSiteStatus': 0, 'error_message': str(msg)} json_data = json.dumps(dic) return HttpResponse(json_data) From 92a459adcc9a8d5d7b09d462306e2fa50fd29923 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Thu, 18 Dec 2025 12:18:32 +0500 Subject: [PATCH 075/129] security fixes --- backup/backupManager.py | 95 ++++++++++++++++++++++++++++++++++------ filemanager/views.py | 49 +++++++++++++++++++++ plogical/remoteBackup.py | 34 ++++++++++++-- 3 files changed, 161 insertions(+), 17 deletions(-) diff --git a/backup/backupManager.py b/backup/backupManager.py index bbac04a49..daa16b38d 100644 --- a/backup/backupManager.py +++ b/backup/backupManager.py @@ -2,6 +2,7 @@ import os import os.path import sys +import re from io import StringIO import django @@ -784,9 +785,35 @@ class BackupManager: except: finalDic['user'] = "root" + # SECURITY: Validate all inputs to prevent command injection + if ACLManager.commandInjectionCheck(finalDic['ipAddress']) == 1: + final_dic = {'status': 0, 'destStatus': 0, 'error_message': 'Invalid characters in IP address'} + return HttpResponse(json.dumps(final_dic)) + + if ACLManager.commandInjectionCheck(finalDic['password']) == 1: + final_dic = {'status': 0, 'destStatus': 0, 'error_message': 'Invalid characters in password'} + return HttpResponse(json.dumps(final_dic)) + + if ACLManager.commandInjectionCheck(finalDic['port']) == 1: + final_dic = {'status': 0, 'destStatus': 0, 'error_message': 'Invalid characters in port'} + return HttpResponse(json.dumps(final_dic)) + + if ACLManager.commandInjectionCheck(finalDic['user']) == 1: + final_dic = {'status': 0, 'destStatus': 0, 'error_message': 'Invalid characters in username'} + return HttpResponse(json.dumps(final_dic)) + + # SECURITY: Validate port is numeric + try: + port_int = int(finalDic['port']) + if port_int < 1 or port_int > 65535: + raise ValueError("Port out of range") + except ValueError: + final_dic = {'status': 0, 'destStatus': 0, 'error_message': 'Port must be a valid number (1-65535)'} + return HttpResponse(json.dumps(final_dic)) + execPath = "/usr/local/CyberCP/bin/python " + virtualHostUtilities.cyberPanel + "/plogical/backupUtilities.py" - execPath = execPath + " submitDestinationCreation --ipAddress " + finalDic['ipAddress'] + " --password " \ - + finalDic['password'] + " --port " + finalDic['port'] + ' --user %s' % (finalDic['user']) + execPath = execPath + " submitDestinationCreation --ipAddress " + shlex.quote(finalDic['ipAddress']) + " --password " \ + + shlex.quote(finalDic['password']) + " --port " + shlex.quote(finalDic['port']) + ' --user %s' % (shlex.quote(finalDic['user'])) if os.path.exists(ProcessUtilities.debugPath): logging.CyberCPLogFileWriter.writeToFile(execPath) @@ -880,8 +907,13 @@ class BackupManager: ipAddress = data['IPAddress'] + # SECURITY: Validate IP address to prevent command injection + if ACLManager.commandInjectionCheck(ipAddress) == 1: + final_dic = {'connStatus': 0, 'error_message': 'Invalid characters in IP address'} + return HttpResponse(json.dumps(final_dic)) + execPath = "/usr/local/CyberCP/bin/python " + virtualHostUtilities.cyberPanel + "/plogical/backupUtilities.py" - execPath = execPath + " getConnectionStatus --ipAddress " + ipAddress + execPath = execPath + " getConnectionStatus --ipAddress " + shlex.quote(ipAddress) output = ProcessUtilities.executioner(execPath) @@ -1342,16 +1374,32 @@ class BackupManager: if ACLManager.currentContextPermission(currentACL, 'remoteBackups') == 0: return ACLManager.loadErrorJson('remoteTransferStatus', 0) - backupDir = data['backupDir'] + backupDir = str(data['backupDir']) - backupDirComplete = "/home/backup/transfer-" + str(backupDir) - # adminEmail = admin.email + # SECURITY: Validate backupDir to prevent command injection and path traversal + if ACLManager.commandInjectionCheck(backupDir) == 1: + data = {'remoteRestoreStatus': 0, 'error_message': 'Invalid characters in backup directory name'} + return HttpResponse(json.dumps(data)) - ## + # SECURITY: Ensure backupDir is alphanumeric only (backup dirs are typically numeric IDs) + if not re.match(r'^[a-zA-Z0-9_-]+$', backupDir): + data = {'remoteRestoreStatus': 0, 'error_message': 'Backup directory name must be alphanumeric'} + return HttpResponse(json.dumps(data)) + + # SECURITY: Prevent path traversal + if '..' in backupDir or '/' in backupDir: + data = {'remoteRestoreStatus': 0, 'error_message': 'Invalid backup directory path'} + return HttpResponse(json.dumps(data)) + + backupDirComplete = "/home/backup/transfer-" + backupDir + + # SECURITY: Verify the backup directory exists + if not os.path.exists(backupDirComplete): + data = {'remoteRestoreStatus': 0, 'error_message': 'Backup directory does not exist'} + return HttpResponse(json.dumps(data)) execPath = "/usr/local/CyberCP/bin/python " + virtualHostUtilities.cyberPanel + "/plogical/remoteTransferUtilities.py" - execPath = execPath + " remoteBackupRestore --backupDirComplete " + backupDirComplete + " --backupDir " + str( - backupDir) + execPath = execPath + " remoteBackupRestore --backupDirComplete " + shlex.quote(backupDirComplete) + " --backupDir " + shlex.quote(backupDir) ProcessUtilities.popenExecutioner(execPath) @@ -1373,16 +1421,35 @@ class BackupManager: if ACLManager.currentContextPermission(currentACL, 'remoteBackups') == 0: return ACLManager.loadErrorJson('remoteTransferStatus', 0) - backupDir = data['backupDir'] + backupDir = str(data['backupDir']) + + # SECURITY: Validate backupDir to prevent command injection and path traversal + if ACLManager.commandInjectionCheck(backupDir) == 1: + data = {'remoteTransferStatus': 0, 'error_message': 'Invalid characters in backup directory name', "status": "None", "complete": 0} + return HttpResponse(json.dumps(data)) + + # SECURITY: Ensure backupDir is alphanumeric only + if not re.match(r'^[a-zA-Z0-9_-]+$', backupDir): + data = {'remoteTransferStatus': 0, 'error_message': 'Backup directory name must be alphanumeric', "status": "None", "complete": 0} + return HttpResponse(json.dumps(data)) + + # SECURITY: Prevent path traversal + if '..' in backupDir or '/' in backupDir: + data = {'remoteTransferStatus': 0, 'error_message': 'Invalid backup directory path', "status": "None", "complete": 0} + return HttpResponse(json.dumps(data)) # admin = Administrator.objects.get(userName=username) backupLogPath = "/home/backup/transfer-" + backupDir + "/" + "backup_log" + removalPath = "/home/backup/transfer-" + backupDir - removalPath = "/home/backup/transfer-" + str(backupDir) + # SECURITY: Verify the backup directory exists before operating on it + if not os.path.exists(removalPath): + data = {'remoteTransferStatus': 0, 'error_message': 'Backup directory does not exist', "status": "None", "complete": 0} + return HttpResponse(json.dumps(data)) time.sleep(3) - command = "sudo cat " + backupLogPath + command = "sudo cat " + shlex.quote(backupLogPath) status = ProcessUtilities.outputExecutioner(command) @@ -1393,14 +1460,14 @@ class BackupManager: if status.find("completed[success]") > -1: - command = "rm -rf " + removalPath + command = "rm -rf " + shlex.quote(removalPath) ProcessUtilities.executioner(command) data_ret = {'remoteTransferStatus': 1, 'error_message': "None", "status": status, "complete": 1} json_data = json.dumps(data_ret) return HttpResponse(json_data) elif status.find("[5010]") > -1: - command = "sudo rm -rf " + removalPath + command = "sudo rm -rf " + shlex.quote(removalPath) ProcessUtilities.executioner(command) data = {'remoteTransferStatus': 0, 'error_message': status, "status": "None", "complete": 0} diff --git a/filemanager/views.py b/filemanager/views.py index 864ecc993..d7749ab64 100644 --- a/filemanager/views.py +++ b/filemanager/views.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import os from django.shortcuts import render,redirect from loginSystem.models import Administrator from loginSystem.views import loadLoginPage @@ -326,6 +327,23 @@ def downloadFile(request): if fileToDownload.find('..') > -1 or fileToDownload.find(homePath) == -1: return HttpResponse("Unauthorized access.") + # SECURITY: Check for symlink attacks - resolve the real path and verify it stays within homePath + try: + realPath = os.path.realpath(fileToDownload) + + # Verify the resolved path is still within the user's home directory + if not realPath.startswith(homePath + '/') and realPath != homePath: + logging.CyberCPLogFileWriter.writeToFile( + f"Symlink attack blocked: {fileToDownload} -> {realPath} (outside {homePath})") + return HttpResponse("Unauthorized access: Symlink points outside allowed directory.") + + # Verify it's a regular file + if not os.path.isfile(realPath): + return HttpResponse("Unauthorized access: Not a valid file.") + + except OSError as e: + return HttpResponse("Unauthorized access: Cannot verify file path.") + response = HttpResponse(content_type='application/force-download') response['Content-Disposition'] = 'attachment; filename=%s' % (fileToDownload.split('/')[-1]) response['X-LiteSpeed-Location'] = '%s' % (fileToDownload) @@ -351,6 +369,37 @@ def RootDownloadFile(request): else: return ACLManager.loadError() + # SECURITY: Prevent path traversal attacks + if fileToDownload.find('..') > -1: + return HttpResponse("Unauthorized access: Path traversal detected.") + + # SECURITY: Check for symlink attacks - resolve the real path and verify it's safe + try: + # Get the real path (resolves symlinks) + realPath = os.path.realpath(fileToDownload) + + # SECURITY: Prevent access to sensitive system files + sensitive_paths = ['/etc/shadow', '/etc/passwd', '/etc/sudoers', '/root/.ssh', + '/var/log', '/proc', '/sys', '/dev'] + for sensitive in sensitive_paths: + if realPath.startswith(sensitive): + return HttpResponse("Unauthorized access: Access to system files denied.") + + # SECURITY: Verify the file exists and is a regular file (not a directory or device) + if not os.path.isfile(realPath): + return HttpResponse("Unauthorized access: Not a valid file.") + + # SECURITY: Check if the original path differs from real path (symlink detection) + # Allow the download only if the real path is within allowed directories + # For admin, we'll be more permissive but still block sensitive system files + if fileToDownload != realPath: + # This is a symlink - log it and verify destination is safe + logging.CyberCPLogFileWriter.writeToFile( + f"Symlink download detected: {fileToDownload} -> {realPath}") + + except OSError as e: + return HttpResponse("Unauthorized access: Cannot verify file path.") + response = HttpResponse(content_type='application/force-download') response['Content-Disposition'] = 'attachment; filename=%s' % (fileToDownload.split('/')[-1]) response['X-LiteSpeed-Location'] = '%s' % (fileToDownload) diff --git a/plogical/remoteBackup.py b/plogical/remoteBackup.py index 5d8f2decc..fda616158 100644 --- a/plogical/remoteBackup.py +++ b/plogical/remoteBackup.py @@ -1,5 +1,6 @@ from plogical import CyberCPLogFileWriter as logging import os +import re import requests import json import time @@ -9,6 +10,7 @@ import shlex from multiprocessing import Process from plogical.backupSchedule import backupSchedule from shutil import rmtree +from plogical.acl import ACLManager class remoteBackup: @@ -216,16 +218,42 @@ class remoteBackup: @staticmethod - def sendBackup(completedPathToSend, IPAddress, folderNumber,writeToFile): + def sendBackup(completedPathToSend, IPAddress, folderNumber, writeToFile): try: ## complete path is a path to the file need to send - command = 'sudo rsync -avz -e "ssh -i /root/.ssh/cyberpanel -o StrictHostKeyChecking=no" ' + completedPathToSend + ' root@' + IPAddress + ':/home/backup/transfer-'+folderNumber + # SECURITY: Validate IPAddress to prevent command injection + if ACLManager.commandInjectionCheck(IPAddress) == 1: + logging.CyberCPLogFileWriter.writeToFile("Invalid IP address - command injection attempt detected [sendBackup]") + return + + # SECURITY: Validate IPAddress format (IPv4 or hostname) + ip_pattern = r'^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$' + if not re.match(ip_pattern, IPAddress): + logging.CyberCPLogFileWriter.writeToFile("Invalid IP address format [sendBackup]") + return + + # SECURITY: Validate folderNumber is alphanumeric + if ACLManager.commandInjectionCheck(str(folderNumber)) == 1: + logging.CyberCPLogFileWriter.writeToFile("Invalid folder number - command injection attempt detected [sendBackup]") + return + + if not re.match(r'^[a-zA-Z0-9_-]+$', str(folderNumber)): + logging.CyberCPLogFileWriter.writeToFile("Invalid folder number format [sendBackup]") + return + + # SECURITY: Validate completedPathToSend - must be under /home/backup + if '..' in completedPathToSend or not completedPathToSend.startswith('/home/backup/'): + logging.CyberCPLogFileWriter.writeToFile("Invalid backup path - path traversal attempt detected [sendBackup]") + return + + # SECURITY: Use shlex.quote for all user-controllable parameters + command = 'sudo rsync -avz -e "ssh -i /root/.ssh/cyberpanel -o StrictHostKeyChecking=no" ' + shlex.quote(completedPathToSend) + ' root@' + shlex.quote(IPAddress) + ':/home/backup/transfer-' + shlex.quote(str(folderNumber)) subprocess.call(shlex.split(command), stdout=writeToFile) os.remove(completedPathToSend) except BaseException as msg: - logging.CyberCPLogFileWriter.writeToFile(str(msg) + " [startBackup]") + logging.CyberCPLogFileWriter.writeToFile(str(msg) + " [sendBackup]") @staticmethod def backupProcess(ipAddress, dir, backupLogPath,folderNumber, accountsToTransfer): From b94fe07ea64d5a3c3630fd6030d66274eb016785 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Thu, 25 Dec 2025 20:22:30 +0400 Subject: [PATCH 076/129] bug fix: improve sub domain page --- .../websiteFunctions/launchChild.html | 2588 +++++++++-------- 1 file changed, 1406 insertions(+), 1182 deletions(-) diff --git a/websiteFunctions/templates/websiteFunctions/launchChild.html b/websiteFunctions/templates/websiteFunctions/launchChild.html index 687d73be7..39b36154c 100644 --- a/websiteFunctions/templates/websiteFunctions/launchChild.html +++ b/websiteFunctions/templates/websiteFunctions/launchChild.html @@ -6,1260 +6,1484 @@ {% load static %} {% get_current_language as LANGUAGE_CODE %} - +
-
+
-
-

- - {{ childDomain }} - - - - {% trans "Preview" %} - -

-

{% trans "Manage all functions related to your child domain" %} - {% trans "Master domain:" %} {{ domain }}

+ +
+
+
+ {{ childDomain }} +
+ + {% trans "Active" %} +
+
+

+ {% trans "Manage all functions related to your child domain" %} - {% trans "Master domain:" %} {{ domain }} +

+ +
{% if not error %} -
-
-

- - - {% trans "Resource Usage" %} - - -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{% trans "Resource" %}{% trans "Usage" %}{% trans "Allowed" %}
- - {% trans "FTP" %} - {{ ftpUsed }}{{ ftpTotal }}
- - {% trans "Databases" %} - {{ databasesUsed }}{{ databasesTotal }}
- - {% trans "Disk Usage" %} - {{ diskInMB }} (MB){{ diskInMBTotal }} (MB)
- - {% trans "Bandwidth Usage" %} - {{ bwInMB }} (MB){{ bwInMBTotal }} (MB)
-
- -
-
- {% if viewSSL == 1 %} -
- -
-

{{ authority }}

-

{% trans "Your SSL will expire in" %} {{ days }} {% trans "days" %}

-
-
- {% endif %} - -

- - {% trans "Disk Usage" %} -

-
-
- {{ diskUsage }}% -
-
- -

- - {% trans "Bandwidth Usage" %} -

-
-
- {{ bwUsage }}% -
-
-
-
- -
-
- + + + +
+ +
+
+ +
+
{% trans "Disk Usage" %}
+
{{ diskInMB }} MB
+
{% trans "of" %} {{ diskInMBTotal }} MB
+
+
- -
-
-

- - - {% trans "Logs" %} - - -

-
- - -
-
-
- -

{% trans "Logs Fetched" %}

-
- -
- -

{% trans "Could not fetch logs, see the logs file through command line. Error message:" %} {$ errorMessage $}

-
- -
- -

{% trans "Could not connect to server. Please refresh this page." %}

-
- -
-
-
- -
- -
- -
- -
- - -
- -
- -
-
-
- - - - - - - - - - - - - - - - - - - -
TypeIP AddressTimeResourceSize
-
-
- - -
-
-
- -
- -
- - -
- -
- -
-
- -
- -
-
-
-
-
+ +
+
+ +
+
{% trans "Bandwidth Usage" %}
+
{{ bwInMB }} MB
+
{% trans "of" %} {{ bwInMBTotal }} MB
+
+
-
-
-

- - - {% trans "Configurations" %} - - -

- -
- - - -
-
-
- -

{% trans "SSL Saved" %}

-
- -
- -

{% trans "Could not save SSL. Error message:" %} {$ errorMessage $}

-
- -
- -

{% trans "Could not connect to server. Please refresh this page." %}

-
- -
-
- -
-
-
- - -
-
- - -
-
-
- -
- -
-
-
- - - - - - - -
- -
- -
-

{% trans "Current configuration in the file fetched." %}

-
- - -
-

{% trans "Could not fetch current configuration. Error message:" %} {$ - errorMessage $}

-
- - -
-

{% trans "Could not connect to server. Please refresh this page." %}

-
- -
-

{% trans "Configurations saved." %}

-
- -
-

{% trans "Could not fetch current configuration. Error message:" %} {$ - errorMessage $}

-
- - -
-
-

-
-
- -
-
- -
- -
- - -
-
- - -
- -
- - - -
- -
- - -
-

{% trans "Current rewrite rules in the file fetched." %}

-
- - -
-

{% trans "Could not fetch current rewrite rules. Error message:" %} {$ - errorMessage $}

-
- - -
-

{% trans "Could not connect to server. Please refresh this page." %}

-
- -
-

{% trans "Configurations saved." %}

-
- -
-

{% trans "Could not save rewrite rules. Error message:" %} {$ errorMessage - $}

-
- - -
-
-

-
-
- -
-
- -
- -
- - -
-
- - -
- -
- - - -
- -
- - -
- -
- -
- -
-

-
- -
- - -
- -
- -
-
- - -
- - -
-
-

{% trans "Failed to change PHP version. Error message:" %} {$ - errorMessage $}

-
- -
-

{% trans "PHP successfully changed for: " %} {$ websiteDomain - $}

-
- -
-

{% trans "Could not connect to server. Please refresh this page." %}

-
-
- - -
- - -
-
- -
+ +
+
+ +
+
{% trans "Databases" %}
+
{{ databasesUsed }}
+
{% trans "of" %} {{ databasesTotal }}
+
+ + +
+
+ +
+
{% trans "FTP Accounts" %}
+
{{ ftpUsed }}
+
{% trans "of" %} {{ ftpTotal }}
+
+ + + {% if viewSSL == 1 %} +
+
+
+ +
+
+
{{ authority }}
+
{% trans "Your SSL will expire in" %} {{ days }} {% trans "days" %}
+ {% endif %} +
- -
-
+ +
+
+

- - - {% trans "Files" %} - + + {% trans "Logs" %} +

-
-
- - - - - {% if ftp %} - - - - {% endif %} + + +
+
+
+ + {% trans "Logs Fetched" %} +
+ +
+ + {% trans "Could not fetch logs, see the logs file through command line. Error message:" %} {$ errorMessage $} +
+ +
+ + {% trans "Could not connect to server. Please refresh this page." %} +
+ +
+
+
+ +
+ +
+ +
- -
- -
-
- -
- -
- + +
- +
+
+
+ + + + + + + + + + + + + + + + + + + +
TypeIP AddressTimeResourceSize
+
+
+ + +
+
+
+ +
+ +
+ + +
+ +
+
-
- -
- -
+
+
- -
- -
-
- -

{% trans "Error message:" %} {$ errorMessage $}

-
- -
- -

{% trans "Changes successfully saved." %}

-
- -
- -

{% trans "Could not connect to server. Please refresh this page." %}

-
-
-
- -
- +
+
+
-
-
+ +
+
+

- - - {% trans "Application Installer" %} - + + {% trans "Configurations" %} +

-
-
-
- - - {% trans "WP + LSCache" %} + + + +
+
+
+ + {% trans "SSL Saved" %} +
+ +
+ + {% trans "Could not save SSL. Error message:" %} {$ errorMessage $} +
+ +
+ + {% trans "Could not connect to server. Please refresh this page." %} +
+ +
+
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ +
+
+
+ + +
+
+
+ {% trans "Current configuration in the file fetched." %} +
+ +
+ {% trans "Could not fetch current configuration. Error message:" %} {$ errorMessage $} +
+ +
+ {% trans "Could not connect to server. Please refresh this page." %} +
+ +
+ {% trans "Configurations saved." %} +
+ +
+ {% trans "Could not fetch current configuration. Error message:" %} {$ errorMessage $} +
+ +
+
+

+
+
+ +
+
+ +
+ +
+ +
+
+
+
+ + +
+
+
+ {% trans "Current rewrite rules in the file fetched." %} +
+ +
+ {% trans "Could not fetch current rewrite rules. Error message:" %} {$ errorMessage $} +
+ +
+ {% trans "Could not connect to server. Please refresh this page." %} +
+ +
+ {% trans "Configurations saved." %} +
+ +
+ {% trans "Could not save rewrite rules. Error message:" %} {$ errorMessage $} +
+ +
+
+

+
+
+ +
+
+ +
+ +
+ +
+
+
+
+ + +
+
+
+ +
+ +
+ +
+

+
+
+ +
+ +
+ +
+
+ +
+ +
+
+ {% trans "Failed to change PHP version. Error message:" %} {$ errorMessage $} +
+ +
+ {% trans "PHP successfully changed for: " %} {$ websiteDomain $} +
+ +
+ {% trans "Could not connect to server. Please refresh this page." %} +
+
+
+
+
+ +
+
+
+ + +
+
+
+

+ + {% trans "Files" %} +

+ +
+ + + + + {% if ftp %} + - + + +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
-
- - - {% trans "Prestashop" %} - +
+ +
+ +
+ +
+ +
+
+ + {% trans "Error message:" %} {$ errorMessage $} +
+ +
+ + {% trans "Changes successfully saved." %} +
+ +
+ + {% trans "Could not connect to server. Please refresh this page." %} +
+
+
+ +
+ +
+
+
+ + +
+
+
+

+ + {% trans "Application Installer" %} +

+ +
+
{% else %}
-

{{ domain }}

+ {{ domain }}
{% endif %} -
+ + {% endblock %} From f3fd46e76d725d7ee50b2990cb154689f0e788d8 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Fri, 26 Dec 2025 14:24:13 +0500 Subject: [PATCH 077/129] bug fix in n8n deployment --- cloudAPI/cloudManager.py | 121 ++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 53 deletions(-) diff --git a/cloudAPI/cloudManager.py b/cloudAPI/cloudManager.py index 1be0dcdb2..c12f76067 100644 --- a/cloudAPI/cloudManager.py +++ b/cloudAPI/cloudManager.py @@ -3248,59 +3248,74 @@ class CloudManager: statusWriter.writeToFile(f"Error getting administrator: {str(e)} [404]") return - # Step 1: Create the website - statusWriter.writeToFile('Creating website...,10') - logging.writeToFile(f"[_install_n8n_with_website] Creating website for {domain_name}") - wm = WebsiteManager() - result = wm.submitWebsiteCreation(admin.pk, website_data) - result_data = json.loads(result.content) - - logging.writeToFile(f"[_install_n8n_with_website] Website creation result: {result_data}") - - if result_data.get('createWebSiteStatus', 0) != 1: - statusWriter.writeToFile(f"Failed to create website: {result_data.get('error_message', 'Unknown error')} [404]") - return - - # Wait for website creation to complete - no time limit - creation_status_path = result_data.get('tempStatusPath') - logging.writeToFile(f"[_install_n8n_with_website] Website creation status path: {creation_status_path}") - - if creation_status_path: - statusWriter.writeToFile('Waiting for website creation to complete (including SSL)...,15') - check_count = 0 - while True: - try: - with open(creation_status_path, 'r') as f: - status = f.read() - if '[200]' in status: - logging.writeToFile(f"[_install_n8n_with_website] Website creation completed successfully") - break - elif '[404]' in status: - logging.writeToFile(f"[_install_n8n_with_website] Website creation failed: {status}") - statusWriter.writeToFile(f"Website creation failed: {status} [404]") - return - except Exception as e: - if check_count % 10 == 0: # Log every 10 checks - logging.writeToFile(f"[_install_n8n_with_website] Still waiting for website creation... (check #{check_count})") - - check_count += 1 - time.sleep(1) - - # Get the created website object - logging.writeToFile(f"[_install_n8n_with_website] Getting website object for {domain_name}") - try: - website = Websites.objects.get(domain=domain_name) - logging.writeToFile(f"[_install_n8n_with_website] Found website object: {website.domain}") - except Websites.DoesNotExist: - logging.writeToFile(f"[_install_n8n_with_website] Website object not found for {domain_name}") - statusWriter.writeToFile('Website creation succeeded but website object not found [404]') - return - except Exception as e: - logging.writeToFile(f"[_install_n8n_with_website] Error getting website object: {str(e)}") - statusWriter.writeToFile(f'Error getting website object: {str(e)} [404]') - return - - statusWriter.writeToFile('Website created successfully...,20') + # Step 1: Check if website already exists, create only if not + existing_website = Websites.objects.filter(domain=domain_name).first() + + if existing_website: + # Website already exists, skip creation + logging.writeToFile(f"[_install_n8n_with_website] Website {domain_name} already exists, skipping creation") + statusWriter.writeToFile(f'Website already exists, proceeding to N8N deployment...,20') + website = existing_website + else: + # Website doesn't exist, create it + statusWriter.writeToFile('Creating website...,10') + logging.writeToFile(f"[_install_n8n_with_website] Creating website for {domain_name}") + wm = WebsiteManager() + result = wm.submitWebsiteCreation(admin.pk, website_data) + result_data = json.loads(result.content) + + logging.writeToFile(f"[_install_n8n_with_website] Website creation result: {result_data}") + + if result_data.get('createWebSiteStatus', 0) != 1: + statusWriter.writeToFile(f"Failed to create website: {result_data.get('error_message', 'Unknown error')} [404]") + return + + # Wait for website creation to complete - 10 minute timeout + creation_status_path = result_data.get('tempStatusPath') + logging.writeToFile(f"[_install_n8n_with_website] Website creation status path: {creation_status_path}") + + if creation_status_path: + statusWriter.writeToFile('Waiting for website creation to complete (including SSL)...,15') + check_count = 0 + max_checks = 600 # 10 minute timeout (600 seconds) + while check_count < max_checks: + try: + with open(creation_status_path, 'r') as f: + status = f.read() + if '[200]' in status: + logging.writeToFile(f"[_install_n8n_with_website] Website creation completed successfully") + break + elif '[404]' in status: + logging.writeToFile(f"[_install_n8n_with_website] Website creation failed: {status}") + statusWriter.writeToFile(f"Website creation failed: {status} [404]") + return + except Exception as e: + if check_count % 10 == 0: # Log every 10 checks + logging.writeToFile(f"[_install_n8n_with_website] Still waiting for website creation... (check #{check_count})") + + check_count += 1 + time.sleep(1) + + if check_count >= max_checks: + logging.writeToFile(f"[_install_n8n_with_website] Website creation timed out after 10 minutes") + statusWriter.writeToFile(f"Website creation timed out after 10 minutes [404]") + return + + # Get the created website object + logging.writeToFile(f"[_install_n8n_with_website] Getting website object for {domain_name}") + try: + website = Websites.objects.get(domain=domain_name) + logging.writeToFile(f"[_install_n8n_with_website] Found website object: {website.domain}") + except Websites.DoesNotExist: + logging.writeToFile(f"[_install_n8n_with_website] Website object not found for {domain_name}") + statusWriter.writeToFile('Website creation succeeded but website object not found [404]') + return + except Exception as e: + logging.writeToFile(f"[_install_n8n_with_website] Error getting website object: {str(e)}") + statusWriter.writeToFile(f'Error getting website object: {str(e)} [404]') + return + + statusWriter.writeToFile('Website created successfully...,20') logging.writeToFile(f"[_install_n8n_with_website] Website creation phase complete") # Step 2: Create database using native CyberPanel process From 942d508769c27c228682074e3206c3a08e066273 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Sat, 27 Dec 2025 21:07:16 +0500 Subject: [PATCH 078/129] fix: update custom OLS binaries and add ModSecurity compatibility - Update SHA256 checksums for December 2025 OLS build (v1.8.4.1) - Add RHEL8 module support (cyberpanel_ols_x86_64_rhel8.so) - Add compatible ModSecurity binaries to prevent ABI crashes - Auto-detect and replace ModSecurity when custom OLS is installed - Add auto-rollback feature if new binary fails to start - Fix OWASP CRS UI toggle detection with multi-location checks Features included in new binaries: - PHPConfig support (.htaccess php_value/php_flag) - Origin header forwarding (CORS/WebSocket support) - Header unset fix (uses remove_resp_header API) - Static linking for cross-platform compatibility Platforms supported: - Ubuntu 22.04+/Debian 12+ (ubuntu-static) - AlmaLinux/Rocky/RHEL 9.x (rhel9-static) - AlmaLinux/Rocky/RHEL 8.x (rhel8-static) --- firewall/firewallManager.py | 49 +++++++++++- plogical/modSec.py | 113 ++++++++++++++++++++++++++ plogical/upgrade.py | 154 ++++++++++++++++++++++++++++++++++-- 3 files changed, 307 insertions(+), 9 deletions(-) diff --git a/firewall/firewallManager.py b/firewall/firewallManager.py index 09d95aab0..2159de541 100644 --- a/firewall/firewallManager.py +++ b/firewall/firewallManager.py @@ -1020,8 +1020,9 @@ class FirewallManager: if owaspInstalled == 1 and comodoInstalled == 1: break - # Also check rules.conf for manual OWASP installations + # Check multiple locations for OWASP CRS installation if owaspInstalled == 0: + # Check 1: rules.conf for OWASP includes rulesConfPath = os.path.join(virtualHostUtilities.Server_root, "conf/modsec/rules.conf") if os.path.exists(rulesConfPath): try: @@ -1036,6 +1037,37 @@ class FirewallManager: except: pass + # Check 2: owasp-master.conf exists and has rules loaded + if owaspInstalled == 0: + owaspMasterConf = os.path.join(virtualHostUtilities.Server_root, "conf/modsec/owasp-modsecurity-crs-3.0-master/owasp-master.conf") + if os.path.exists(owaspMasterConf): + try: + command = "sudo cat " + owaspMasterConf + owaspConfig = ProcessUtilities.outputExecutioner(command).splitlines() + # Check if at least one rule file is enabled (not commented) + for items in owaspConfig: + if items.strip() and not items.strip().startswith('#') and 'include' in items.lower(): + owaspInstalled = 1 + break + except: + pass + + # Check 3: OWASP CRS directory exists with rules + if owaspInstalled == 0: + owaspRulesDir = os.path.join(virtualHostUtilities.Server_root, "conf/modsec/owasp-modsecurity-crs-3.0-master/rules") + if os.path.exists(owaspRulesDir): + try: + command = "sudo ls " + owaspRulesDir + " | grep -c '.conf'" + output = ProcessUtilities.outputExecutioner(command).strip() + if output.isdigit() and int(output) > 0: + # Rules exist, check if referenced in httpd_config.conf + for items in httpdConfig: + if 'owasp-modsecurity-crs' in items.lower() or 'owasp-master.conf' in items.lower(): + owaspInstalled = 1 + break + except: + pass + final_dic = { 'modSecInstalled': 1, 'owaspInstalled': owaspInstalled, @@ -1065,6 +1097,7 @@ class FirewallManager: except subprocess.CalledProcessError: pass + # Check multiple locations for OWASP in LiteSpeed Enterprise try: command = 'cat /usr/local/lsws/conf/modsec.conf' output = ProcessUtilities.outputExecutioner(command) @@ -1073,6 +1106,20 @@ class FirewallManager: except: pass + # Also check owasp-master.conf for LSWS Enterprise + if owaspInstalled == 0: + owaspMasterConf = '/usr/local/lsws/conf/modsec/owasp-modsecurity-crs-3.0-master/owasp-master.conf' + if os.path.exists(owaspMasterConf): + try: + command = "cat " + owaspMasterConf + owaspConfig = ProcessUtilities.outputExecutioner(command).splitlines() + for items in owaspConfig: + if items.strip() and not items.strip().startswith('#') and 'include' in items.lower(): + owaspInstalled = 1 + break + except: + pass + final_dic = { 'modSecInstalled': 1, 'owaspInstalled': owaspInstalled, diff --git a/plogical/modSec.py b/plogical/modSec.py index 8b2e708d2..2fbc7ece1 100644 --- a/plogical/modSec.py +++ b/plogical/modSec.py @@ -18,6 +18,102 @@ class modSec: tempRulesFile = "/home/cyberpanel/tempModSecRules" mirrorPath = "cyberpanel.net" + # Compatible ModSecurity binaries (built against custom OLS headers) + # These prevent ABI incompatibility crashes (Signal 11/SIGSEGV) + MODSEC_COMPATIBLE = { + 'rhel8': { + 'url': 'https://cyberpanel.net/mod_security-compatible-rhel8.so', + 'sha256': 'bbbf003bdc7979b98f09b640dffe2cbbe5f855427f41319e4c121403c05837b2' + }, + 'rhel9': { + 'url': 'https://cyberpanel.net/mod_security-compatible-rhel.so', + 'sha256': '19deb2ffbaf1334cf4ce4d46d53f747a75b29e835bf5a01f91ebcc0c78e98629' + }, + 'ubuntu': { + 'url': 'https://cyberpanel.net/mod_security-compatible-ubuntu.so', + 'sha256': 'ed02c813136720bd4b9de5925f6e41bdc8392e494d7740d035479aaca6d1e0cd' + } + } + + @staticmethod + def detectPlatform(): + """Detect OS platform for compatible binary selection""" + try: + # Check for Ubuntu/Debian + if os.path.exists('/etc/lsb-release'): + with open('/etc/lsb-release', 'r') as f: + content = f.read() + if 'Ubuntu' in content or 'ubuntu' in content: + return 'ubuntu' + + # Check for Debian + if os.path.exists('/etc/debian_version'): + return 'ubuntu' # Use Ubuntu binary for Debian + + # Check for RHEL-based distributions + if os.path.exists('/etc/os-release'): + with open('/etc/os-release', 'r') as f: + content = f.read().lower() + + # Check for version 8.x + if 'version="8.' in content or 'version_id="8' in content: + return 'rhel8' + + # Check for version 9.x + if 'version="9.' in content or 'version_id="9' in content: + return 'rhel9' + + return 'rhel9' # Default to rhel9 + except: + return 'rhel9' + + @staticmethod + def downloadCompatibleModSec(platform): + """Download and install compatible ModSecurity binary""" + try: + config = modSec.MODSEC_COMPATIBLE.get(platform) + if not config: + logging.CyberCPLogFileWriter.writeToFile(f"No compatible ModSecurity for platform {platform}") + return False + + modsec_path = "/usr/local/lsws/modules/mod_security.so" + tmp_path = "/tmp/mod_security-compatible.so" + + # Download compatible binary + command = f"wget -q {config['url']} -O {tmp_path}" + result = subprocess.call(shlex.split(command)) + if result != 0: + logging.CyberCPLogFileWriter.writeToFile("Failed to download compatible ModSecurity") + return False + + # Verify checksum + import hashlib + sha256_hash = hashlib.sha256() + with open(tmp_path, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + actual_sha256 = sha256_hash.hexdigest() + + if actual_sha256 != config['sha256']: + logging.CyberCPLogFileWriter.writeToFile(f"ModSecurity checksum mismatch: expected {config['sha256']}, got {actual_sha256}") + os.remove(tmp_path) + return False + + # Backup original if exists + if os.path.exists(modsec_path): + shutil.copy2(modsec_path, f"{modsec_path}.stock") + + # Install compatible version + shutil.move(tmp_path, modsec_path) + os.chmod(modsec_path, 0o644) + + logging.CyberCPLogFileWriter.writeToFile("Installed compatible ModSecurity binary") + return True + + except BaseException as msg: + logging.CyberCPLogFileWriter.writeToFile(str(msg) + " [downloadCompatibleModSec]") + return False + @staticmethod def installModSec(): try: @@ -45,6 +141,23 @@ class modSec: writeToFile.writelines("ModSecurity Installed.[200]\n") writeToFile.close() + # Check if custom OLS binary is installed - if so, replace with compatible ModSecurity + custom_ols_marker = "/usr/local/lsws/modules/cyberpanel_ols.so" + if os.path.exists(custom_ols_marker): + writeToFile = open(modSec.installLogPath, 'a') + writeToFile.writelines("Custom OLS detected, installing compatible ModSecurity...\n") + writeToFile.close() + + platform = modSec.detectPlatform() + if modSec.downloadCompatibleModSec(platform): + writeToFile = open(modSec.installLogPath, 'a') + writeToFile.writelines("Compatible ModSecurity installed successfully.\n") + writeToFile.close() + else: + writeToFile = open(modSec.installLogPath, 'a') + writeToFile.writelines("WARNING: Could not install compatible ModSecurity. May experience crashes.\n") + writeToFile.close() + return 1 except BaseException as msg: logging.CyberCPLogFileWriter.writeToFile(str(msg) + "[installModSec]") diff --git a/plogical/upgrade.py b/plogical/upgrade.py index 6ba031d97..ea72adbb9 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -733,25 +733,32 @@ class Upgrade: platform = Upgrade.detectPlatform() Upgrade.stdOut(f"Detected platform: {platform}", 0) - # Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 - v2.0.5 Static Build) + # Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 with PHPConfig + Header unset fix + Static Linking) + # Build Date: December 27, 2025 BINARY_CONFIGS = { 'rhel8': { 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel8-static', 'sha256': '6ce688a237615102cc1603ee1999b3cede0ff3482d31e1f65705e92396d34b3a', - 'module_url': None, # RHEL 8 doesn't have module (use RHEL 9 if needed) - 'module_sha256': None + 'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_rhel8.so', + 'module_sha256': 'c57f6f14a9ba787b9051dee98c1375a4f34ec4e25b492e97a8825aee04dda02a', + 'modsec_url': 'https://cyberpanel.net/mod_security-compatible-rhel8.so', + 'modsec_sha256': 'bbbf003bdc7979b98f09b640dffe2cbbe5f855427f41319e4c121403c05837b2' }, 'rhel9': { 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel9-static', - 'sha256': '90468fb38767505185013024678d9144ae13100d2355097657f58719d98fbbc4', + 'sha256': '709093d99d5d3e789134c131893614968e17eefd9ade2200f811d9b076b2f02e', 'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_rhel.so', - 'module_sha256': '127227db81bcbebf80b225fc747b69cfcd4ad2f01cea486aa02d5c9ba6c18109' + 'module_sha256': 'ae79d4fcf56131c01c3d81dc704ad265ac881b61d0a90cec62e4ac22c0e69929', + 'modsec_url': 'https://cyberpanel.net/mod_security-compatible-rhel.so', + 'modsec_sha256': '19deb2ffbaf1334cf4ce4d46d53f747a75b29e835bf5a01f91ebcc0c78e98629' }, 'ubuntu': { 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-ubuntu-static', 'sha256': '89aaf66474e78cb3c1666784e0e7a417550bd317e6ab148201bdc318d36710cb', 'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_ubuntu.so', - 'module_sha256': 'e7734f1e6226c2a0a8e00c1f6534ea9f577df9081b046736a774b1c52c28e7e5' + 'module_sha256': '57129f12b98c5b1693d10eddad3ad57917773540ca68c5491dee23588ef313ac', + 'modsec_url': 'https://cyberpanel.net/mod_security-compatible-ubuntu.so', + 'modsec_sha256': 'ed02c813136720bd4b9de5925f6e41bdc8392e494d7740d035479aaca6d1e0cd' } } @@ -765,8 +772,11 @@ class Upgrade: OLS_BINARY_SHA256 = config['sha256'] MODULE_URL = config['module_url'] MODULE_SHA256 = config['module_sha256'] + MODSEC_URL = config.get('modsec_url') + MODSEC_SHA256 = config.get('modsec_sha256') OLS_BINARY_PATH = "/usr/local/lsws/bin/openlitespeed" MODULE_PATH = "/usr/local/lsws/modules/cyberpanel_ols.so" + MODSEC_PATH = "/usr/local/lsws/modules/mod_security.so" # Create backup from datetime import datetime @@ -778,12 +788,16 @@ class Upgrade: if os.path.exists(OLS_BINARY_PATH): shutil.copy2(OLS_BINARY_PATH, f"{backup_dir}/openlitespeed.backup") Upgrade.stdOut(f"Backup created at: {backup_dir}", 0) + # Also backup existing ModSecurity if it exists + if os.path.exists(MODSEC_PATH): + shutil.copy2(MODSEC_PATH, f"{backup_dir}/mod_security.so.backup") except Exception as e: Upgrade.stdOut(f"WARNING: Could not create backup: {e}", 0) # Download binaries to temp location tmp_binary = "/tmp/openlitespeed-custom" tmp_module = "/tmp/cyberpanel_ols.so" + tmp_modsec = "/tmp/mod_security.so" Upgrade.stdOut("Downloading custom binaries...", 0) @@ -804,6 +818,18 @@ class Upgrade: else: Upgrade.stdOut("Note: No CyberPanel module for this platform", 0) + # Download compatible ModSecurity if existing ModSecurity is installed + # This prevents ABI incompatibility crashes (Signal 11/SIGSEGV) + modsec_downloaded = False + if os.path.exists(MODSEC_PATH) and MODSEC_URL and MODSEC_SHA256: + Upgrade.stdOut("Existing ModSecurity detected - downloading compatible version...", 0) + if Upgrade.downloadCustomBinary(MODSEC_URL, tmp_modsec, MODSEC_SHA256): + modsec_downloaded = True + else: + Upgrade.stdOut("WARNING: Failed to download compatible ModSecurity", 0) + Upgrade.stdOut("ModSecurity may crash due to ABI incompatibility", 0) + Upgrade.stdOut("Consider manually updating ModSecurity after upgrade", 0) + # Install OpenLiteSpeed binary Upgrade.stdOut("Installing custom binaries...", 0) @@ -826,9 +852,49 @@ class Upgrade: Upgrade.stdOut(f"ERROR: Failed to install module: {e}", 0) return False - # Verify installation + # Install compatible ModSecurity (if downloaded) + if modsec_downloaded: + try: + shutil.move(tmp_modsec, MODSEC_PATH) + os.chmod(MODSEC_PATH, 0o644) + Upgrade.stdOut("Installed compatible ModSecurity module", 0) + except Exception as e: + Upgrade.stdOut(f"WARNING: Failed to install ModSecurity: {e}", 0) + # Non-fatal, continue + + # Verify installation - test binary before restart if os.path.exists(OLS_BINARY_PATH): if not module_downloaded or os.path.exists(MODULE_PATH): + # Test 1: Verify binary is executable and shows version + Upgrade.stdOut("Verifying new binary...", 0) + try: + result = subprocess.run( + [OLS_BINARY_PATH, '-v'], + capture_output=True, + text=True, + timeout=10 + ) + if result.returncode != 0: + raise Exception(f"Binary test failed with exit code {result.returncode}") + + # Extract version info + version_output = result.stdout if result.stdout else result.stderr + if 'LiteSpeed' in version_output or 'OpenLiteSpeed' in version_output: + Upgrade.stdOut(f"Binary version check passed", 0) + else: + Upgrade.stdOut("WARNING: Could not verify binary version", 0) + except subprocess.TimeoutExpired: + Upgrade.stdOut("WARNING: Binary version check timed out", 0) + except Exception as e: + Upgrade.stdOut(f"ERROR: Binary verification failed: {e}", 0) + # Auto-rollback + Upgrade.stdOut("Initiating auto-rollback...", 0) + if Upgrade.rollbackOLSBinary(backup_dir, OLS_BINARY_PATH, MODULE_PATH if module_downloaded else None): + Upgrade.stdOut("Rollback completed successfully", 0) + else: + Upgrade.stdOut("WARNING: Rollback may have failed", 0) + return False + Upgrade.stdOut("=" * 50, 0) Upgrade.stdOut("Custom Binaries Installed Successfully", 0) Upgrade.stdOut("Features enabled:", 0) @@ -842,6 +908,9 @@ class Upgrade: return True Upgrade.stdOut("ERROR: Installation verification failed", 0) + # Auto-rollback on verification failure + if Upgrade.rollbackOLSBinary(backup_dir, OLS_BINARY_PATH, MODULE_PATH if module_downloaded else None): + Upgrade.stdOut("Rollback completed successfully", 0) return False except Exception as msg: @@ -849,6 +918,50 @@ class Upgrade: Upgrade.stdOut("Continuing with standard OLS", 0) return True # Non-fatal error, continue + @staticmethod + def rollbackOLSBinary(backup_dir, binary_path, module_path=None): + """Rollback OpenLiteSpeed binary to previous version from backup""" + try: + Upgrade.stdOut("Rolling back to previous binary...", 0) + + backup_binary = os.path.join(backup_dir, "openlitespeed.backup") + + if os.path.exists(backup_binary): + # Stop OLS before rollback + Upgrade.stdOut("Stopping OpenLiteSpeed for rollback...", 0) + subprocess.run(['/usr/local/lsws/bin/lswsctrl', 'stop'], + capture_output=True, timeout=30) + + # Restore binary + shutil.copy2(backup_binary, binary_path) + os.chmod(binary_path, 0o755) + Upgrade.stdOut(f"Restored binary from {backup_binary}", 0) + + # Start OLS after rollback + Upgrade.stdOut("Starting OpenLiteSpeed after rollback...", 0) + result = subprocess.run(['/usr/local/lsws/bin/lswsctrl', 'start'], + capture_output=True, timeout=30) + + # Verify OLS started + import time + time.sleep(3) + + result = subprocess.run(['pgrep', '-f', 'openlitespeed'], + capture_output=True) + if result.returncode == 0: + Upgrade.stdOut("OpenLiteSpeed started successfully after rollback", 0) + return True + else: + Upgrade.stdOut("WARNING: OpenLiteSpeed may not have started after rollback", 0) + return True # Rollback was successful, startup issue is separate + else: + Upgrade.stdOut(f"ERROR: Backup not found at {backup_binary}", 0) + return False + + except Exception as e: + Upgrade.stdOut(f"ERROR during rollback: {e}", 0) + return False + @staticmethod def configureCustomModule(): """Configure CyberPanel module in OpenLiteSpeed config""" @@ -4482,10 +4595,35 @@ pm.max_spare_servers = 3 # Configure the custom module Upgrade.configureCustomModule() - # Restart OpenLiteSpeed to apply changes + # Restart OpenLiteSpeed to apply changes and verify it started Upgrade.stdOut("Restarting OpenLiteSpeed...", 0) command = '/usr/local/lsws/bin/lswsctrl restart' Upgrade.executioner(command, 'Restart OpenLiteSpeed', 0) + + # Verify OLS started successfully after restart + import time + time.sleep(5) # Give OLS time to start + + result = subprocess.run(['pgrep', '-f', 'openlitespeed'], + capture_output=True) + if result.returncode != 0: + Upgrade.stdOut("WARNING: OpenLiteSpeed may not have started after upgrade!", 0) + Upgrade.stdOut("Attempting auto-rollback...", 0) + + # Find the most recent backup directory + backup_base = '/usr/local/lsws' + backups = [d for d in os.listdir(backup_base) if d.startswith('backup-')] + if backups: + backups.sort(reverse=True) # Most recent first + latest_backup = os.path.join(backup_base, backups[0]) + if Upgrade.rollbackOLSBinary(latest_backup, '/usr/local/lsws/bin/openlitespeed'): + Upgrade.stdOut("Auto-rollback completed successfully", 0) + else: + Upgrade.stdOut("ERROR: Auto-rollback failed! Manual intervention may be required.", 0) + else: + Upgrade.stdOut("ERROR: No backup found for rollback!", 0) + else: + Upgrade.stdOut("OpenLiteSpeed restarted successfully", 0) else: Upgrade.stdOut("Custom binary installation failed, continuing with upgrade...", 0) From 843cede15eb944c8d1e11dda129a622c3c27a8a9 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Sun, 28 Dec 2025 02:49:41 +0500 Subject: [PATCH 079/129] update OLS module checksums for Phase 2 Brute Force Protection - Update cyberpanel_ols module URLs to use /binaries/ path structure - Update SHA256 checksums for all platforms (rhel8, rhel9, ubuntu) - Enable RHEL 8 module support (was previously disabled) - Module version 2.2.0 with Phase 2 features --- install/installCyberPanel.py | 13 +++++++------ plogical/upgrade.py | 12 ++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/install/installCyberPanel.py b/install/installCyberPanel.py index bd543e4cd..7210c9c62 100644 --- a/install/installCyberPanel.py +++ b/install/installCyberPanel.py @@ -327,24 +327,25 @@ class InstallCyberPanel: InstallCyberPanel.stdOut(f"Detected platform: {platform}", 1) # Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 - v2.0.5 Static Build) + # Module Build Date: December 27, 2025 - Phase 2 Brute Force Protection BINARY_CONFIGS = { 'rhel8': { 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel8-static', 'sha256': '6ce688a237615102cc1603ee1999b3cede0ff3482d31e1f65705e92396d34b3a', - 'module_url': None, # RHEL 8 doesn't have module (use RHEL 9 if needed) - 'module_sha256': None + 'module_url': 'https://cyberpanel.net/binaries/rhel8/cyberpanel_ols_x86_64_rhel8.so', + 'module_sha256': '90b5d9eea399503ff2a9948163e6253f049f54cbd80256a3183157b9d5f7d94b' }, 'rhel9': { 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel9-static', 'sha256': '90468fb38767505185013024678d9144ae13100d2355097657f58719d98fbbc4', - 'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_rhel.so', - 'module_sha256': '127227db81bcbebf80b225fc747b69cfcd4ad2f01cea486aa02d5c9ba6c18109' + 'module_url': 'https://cyberpanel.net/binaries/rhel9/cyberpanel_ols_almalinux9.6_x86_64.so', + 'module_sha256': 'a6466bc89d4a33bb0df5a8dce2887b7d0323867f525ccda1db371efcc7c64422' }, 'ubuntu': { 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-ubuntu-static', 'sha256': '89aaf66474e78cb3c1666784e0e7a417550bd317e6ab148201bdc318d36710cb', - 'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_ubuntu.so', - 'module_sha256': 'e7734f1e6226c2a0a8e00c1f6534ea9f577df9081b046736a774b1c52c28e7e5' + 'module_url': 'https://cyberpanel.net/binaries/ubuntu/cyberpanel_ols_x86_64_ubuntu.so', + 'module_sha256': '31f710c915e7996c8c99c09fbd20268df1ea7b8c983c2f44d9632b1e024a0a60' } } diff --git a/plogical/upgrade.py b/plogical/upgrade.py index ea72adbb9..304cb90df 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -739,24 +739,24 @@ class Upgrade: 'rhel8': { 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel8-static', 'sha256': '6ce688a237615102cc1603ee1999b3cede0ff3482d31e1f65705e92396d34b3a', - 'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_rhel8.so', - 'module_sha256': 'c57f6f14a9ba787b9051dee98c1375a4f34ec4e25b492e97a8825aee04dda02a', + 'module_url': 'https://cyberpanel.net/binaries/rhel8/cyberpanel_ols_x86_64_rhel8.so', + 'module_sha256': '90b5d9eea399503ff2a9948163e6253f049f54cbd80256a3183157b9d5f7d94b', 'modsec_url': 'https://cyberpanel.net/mod_security-compatible-rhel8.so', 'modsec_sha256': 'bbbf003bdc7979b98f09b640dffe2cbbe5f855427f41319e4c121403c05837b2' }, 'rhel9': { 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel9-static', 'sha256': '709093d99d5d3e789134c131893614968e17eefd9ade2200f811d9b076b2f02e', - 'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_rhel.so', - 'module_sha256': 'ae79d4fcf56131c01c3d81dc704ad265ac881b61d0a90cec62e4ac22c0e69929', + 'module_url': 'https://cyberpanel.net/binaries/rhel9/cyberpanel_ols_almalinux9.6_x86_64.so', + 'module_sha256': 'a6466bc89d4a33bb0df5a8dce2887b7d0323867f525ccda1db371efcc7c64422', 'modsec_url': 'https://cyberpanel.net/mod_security-compatible-rhel.so', 'modsec_sha256': '19deb2ffbaf1334cf4ce4d46d53f747a75b29e835bf5a01f91ebcc0c78e98629' }, 'ubuntu': { 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-ubuntu-static', 'sha256': '89aaf66474e78cb3c1666784e0e7a417550bd317e6ab148201bdc318d36710cb', - 'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_ubuntu.so', - 'module_sha256': '57129f12b98c5b1693d10eddad3ad57917773540ca68c5491dee23588ef313ac', + 'module_url': 'https://cyberpanel.net/binaries/ubuntu/cyberpanel_ols_x86_64_ubuntu.so', + 'module_sha256': '31f710c915e7996c8c99c09fbd20268df1ea7b8c983c2f44d9632b1e024a0a60', 'modsec_url': 'https://cyberpanel.net/mod_security-compatible-ubuntu.so', 'modsec_sha256': 'ed02c813136720bd4b9de5925f6e41bdc8392e494d7740d035479aaca6d1e0cd' } From 2fa71f65e643d8bf4839f5a1721d8d2478d3732e Mon Sep 17 00:00:00 2001 From: usmannasir Date: Sun, 28 Dec 2025 15:24:04 +0500 Subject: [PATCH 080/129] update OLS module to v2.2.0 with progressive throttle - Update module checksums for all platforms (rhel8, rhel9, ubuntu) - Simplify module URLs to cyberpanel_ols.so - Fixed BruteForceAllowedAttempts parsing - Implemented progressive throttle (2s/5s/15s delays) --- install/installCyberPanel.py | 14 +++++++------- plogical/upgrade.py | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/install/installCyberPanel.py b/install/installCyberPanel.py index 7210c9c62..63371c415 100644 --- a/install/installCyberPanel.py +++ b/install/installCyberPanel.py @@ -327,25 +327,25 @@ class InstallCyberPanel: InstallCyberPanel.stdOut(f"Detected platform: {platform}", 1) # Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 - v2.0.5 Static Build) - # Module Build Date: December 27, 2025 - Phase 2 Brute Force Protection + # Module Build Date: December 28, 2025 - v2.2.0 Brute Force with Progressive Throttle BINARY_CONFIGS = { 'rhel8': { 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel8-static', 'sha256': '6ce688a237615102cc1603ee1999b3cede0ff3482d31e1f65705e92396d34b3a', - 'module_url': 'https://cyberpanel.net/binaries/rhel8/cyberpanel_ols_x86_64_rhel8.so', - 'module_sha256': '90b5d9eea399503ff2a9948163e6253f049f54cbd80256a3183157b9d5f7d94b' + 'module_url': 'https://cyberpanel.net/binaries/rhel8/cyberpanel_ols.so', + 'module_sha256': '7c33d89c7fbcd3ed7b0422fee3f49b5e041713c2c2b7316a5774f6defa147572' }, 'rhel9': { 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel9-static', 'sha256': '90468fb38767505185013024678d9144ae13100d2355097657f58719d98fbbc4', - 'module_url': 'https://cyberpanel.net/binaries/rhel9/cyberpanel_ols_almalinux9.6_x86_64.so', - 'module_sha256': 'a6466bc89d4a33bb0df5a8dce2887b7d0323867f525ccda1db371efcc7c64422' + 'module_url': 'https://cyberpanel.net/binaries/rhel9/cyberpanel_ols.so', + 'module_sha256': 'ae65337e2d13babc0c675bb4264d469daffa2efb7627c9bf39ac59e42e3ebede' }, 'ubuntu': { 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-ubuntu-static', 'sha256': '89aaf66474e78cb3c1666784e0e7a417550bd317e6ab148201bdc318d36710cb', - 'module_url': 'https://cyberpanel.net/binaries/ubuntu/cyberpanel_ols_x86_64_ubuntu.so', - 'module_sha256': '31f710c915e7996c8c99c09fbd20268df1ea7b8c983c2f44d9632b1e024a0a60' + 'module_url': 'https://cyberpanel.net/binaries/ubuntu/cyberpanel_ols.so', + 'module_sha256': '62978ede1f174dd2885e5227a3d9cc463d0c27acd77cfc23743d7309ee0c54ea' } } diff --git a/plogical/upgrade.py b/plogical/upgrade.py index 304cb90df..d4134b94f 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -734,29 +734,29 @@ class Upgrade: Upgrade.stdOut(f"Detected platform: {platform}", 0) # Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 with PHPConfig + Header unset fix + Static Linking) - # Build Date: December 27, 2025 + # Module Build Date: December 28, 2025 - v2.2.0 Brute Force with Progressive Throttle BINARY_CONFIGS = { 'rhel8': { 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel8-static', 'sha256': '6ce688a237615102cc1603ee1999b3cede0ff3482d31e1f65705e92396d34b3a', - 'module_url': 'https://cyberpanel.net/binaries/rhel8/cyberpanel_ols_x86_64_rhel8.so', - 'module_sha256': '90b5d9eea399503ff2a9948163e6253f049f54cbd80256a3183157b9d5f7d94b', + 'module_url': 'https://cyberpanel.net/binaries/rhel8/cyberpanel_ols.so', + 'module_sha256': '7c33d89c7fbcd3ed7b0422fee3f49b5e041713c2c2b7316a5774f6defa147572', 'modsec_url': 'https://cyberpanel.net/mod_security-compatible-rhel8.so', 'modsec_sha256': 'bbbf003bdc7979b98f09b640dffe2cbbe5f855427f41319e4c121403c05837b2' }, 'rhel9': { 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel9-static', 'sha256': '709093d99d5d3e789134c131893614968e17eefd9ade2200f811d9b076b2f02e', - 'module_url': 'https://cyberpanel.net/binaries/rhel9/cyberpanel_ols_almalinux9.6_x86_64.so', - 'module_sha256': 'a6466bc89d4a33bb0df5a8dce2887b7d0323867f525ccda1db371efcc7c64422', + 'module_url': 'https://cyberpanel.net/binaries/rhel9/cyberpanel_ols.so', + 'module_sha256': 'ae65337e2d13babc0c675bb4264d469daffa2efb7627c9bf39ac59e42e3ebede', 'modsec_url': 'https://cyberpanel.net/mod_security-compatible-rhel.so', 'modsec_sha256': '19deb2ffbaf1334cf4ce4d46d53f747a75b29e835bf5a01f91ebcc0c78e98629' }, 'ubuntu': { 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-ubuntu-static', 'sha256': '89aaf66474e78cb3c1666784e0e7a417550bd317e6ab148201bdc318d36710cb', - 'module_url': 'https://cyberpanel.net/binaries/ubuntu/cyberpanel_ols_x86_64_ubuntu.so', - 'module_sha256': '31f710c915e7996c8c99c09fbd20268df1ea7b8c983c2f44d9632b1e024a0a60', + 'module_url': 'https://cyberpanel.net/binaries/ubuntu/cyberpanel_ols.so', + 'module_sha256': '62978ede1f174dd2885e5227a3d9cc463d0c27acd77cfc23743d7309ee0c54ea', 'modsec_url': 'https://cyberpanel.net/mod_security-compatible-ubuntu.so', 'modsec_sha256': 'ed02c813136720bd4b9de5925f6e41bdc8392e494d7740d035479aaca6d1e0cd' } From ccfe4db177cfff28dcd8df53a24f8068cfe4d55c Mon Sep 17 00:00:00 2001 From: usmannasir Date: Sun, 28 Dec 2025 22:40:56 +0400 Subject: [PATCH 081/129] openlitespeed .htaccess module documentation --- install/OpenLiteSpeed_htaccess_Module_Documentation.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 install/OpenLiteSpeed_htaccess_Module_Documentation.md diff --git a/install/OpenLiteSpeed_htaccess_Module_Documentation.md b/install/OpenLiteSpeed_htaccess_Module_Documentation.md new file mode 100644 index 000000000..e69de29bb From 6f55736d2bcdbe47400db22d5e19b3787ca0569a Mon Sep 17 00:00:00 2001 From: usmannasir Date: Sun, 28 Dec 2025 22:43:02 +0400 Subject: [PATCH 082/129] openlitespeed .htaccess module documentation --- ...LiteSpeed_htaccess_Module_Documentation.md | 2791 +++++++++++++++++ ...LiteSpeed_htaccess_Module_Documentation.md | 0 2 files changed, 2791 insertions(+) create mode 100644 OpenLiteSpeed_htaccess_Module_Documentation.md delete mode 100644 install/OpenLiteSpeed_htaccess_Module_Documentation.md diff --git a/OpenLiteSpeed_htaccess_Module_Documentation.md b/OpenLiteSpeed_htaccess_Module_Documentation.md new file mode 100644 index 000000000..b0c1dd2a8 --- /dev/null +++ b/OpenLiteSpeed_htaccess_Module_Documentation.md @@ -0,0 +1,2791 @@ +# CyberPanel OpenLiteSpeed Module - Complete Usage Guide + +**Version:** 2.2.0 +**Last Updated:** December 28, 2025 +**Status:** Production Ready + +--- + +## Table of Contents + +1. [Getting Started](#getting-started) +2. [Header Directives](#1-header-directives) +3. [Request Header Directives](#2-request-header-directives) +4. [Environment Variables](#3-environment-variables) +5. [Access Control](#4-access-control) +6. [Redirect Directives](#5-redirect-directives) +7. [Error Documents](#6-error-documents) +8. [FilesMatch Directives](#7-filesmatch-directives) +9. [Expires Directives](#8-expires-directives) +10. [PHP Directives](#9-php-directives) +11. [Brute Force Protection](#10-brute-force-protection) +12. [CyberPanel Integration](#cyberpanel-integration) +13. [Real-World Examples](#real-world-examples) +14. [Troubleshooting](#troubleshooting) + +--- + +## Getting Started + +### What is This Module? + +The CyberPanel OpenLiteSpeed Module brings Apache .htaccess compatibility to OpenLiteSpeed servers. It allows you to use familiar Apache directives without switching web servers. + +### Quick Start + +1. **Module is pre-installed** on CyberPanel servers +2. **Create .htaccess** in your website's public_html directory +3. **Add directives** from this guide +4. **Test** using curl or browser + +### Basic .htaccess Example + +```apache +# Security headers +Header set X-Frame-Options "SAMEORIGIN" +Header set X-Content-Type-Options "nosniff" + +# Enable brute force protection +BruteForceProtection On +``` + +--- + +## 1. Header Directives + +### What Are HTTP Headers? + +HTTP headers are metadata sent with web responses. They control browser behavior, caching, security, and more. + +### Supported Operations + +| Operation | Purpose | Syntax | +|-----------|---------|--------| +| **set** | Set header (replaces existing) | `Header set Name "Value"` | +| **unset** | Remove header | `Header unset Name` | +| **append** | Append to existing header | `Header append Name "Value"` | +| **merge** | Add if not present | `Header merge Name "Value"` | +| **add** | Always add (allows duplicates) | `Header add Name "Value"` | + +### How to Use + +#### Basic Security Headers + +**What it does:** Protects against clickjacking, XSS, and MIME sniffing. + +```apache +# Prevent site from being embedded in iframe (clickjacking protection) +Header set X-Frame-Options "SAMEORIGIN" + +# Prevent MIME type sniffing +Header set X-Content-Type-Options "nosniff" + +# Enable XSS filter in browsers +Header set X-XSS-Protection "1; mode=block" + +# Control referrer information +Header set Referrer-Policy "strict-origin-when-cross-origin" + +# Restrict browser features +Header set Permissions-Policy "geolocation=(), microphone=(), camera=()" +``` + +**Testing:** +```bash +curl -I https://yourdomain.com | grep -E "X-Frame|X-Content|X-XSS" +``` + +#### Cache Control Headers + +**What it does:** Controls how browsers cache your content. + +```apache +# Cache for 1 year (static assets) +Header set Cache-Control "max-age=31536000, public, immutable" + +# No caching (dynamic content) +Header set Cache-Control "no-cache, no-store, must-revalidate" +Header set Pragma "no-cache" +Header set Expires "0" + +# Cache for 1 hour +Header set Cache-Control "max-age=3600, public" +``` + +**Testing:** +```bash +curl -I https://yourdomain.com/style.css | grep Cache-Control +``` + +#### CORS Headers + +**What it does:** Allows cross-origin requests (needed for APIs, fonts, n8n, etc.). + +```apache +# Allow all origins +Header set Access-Control-Allow-Origin "*" + +# Allow specific origin +Header set Access-Control-Allow-Origin "https://app.example.com" + +# Allow specific methods +Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" + +# Allow specific headers +Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" + +# Allow credentials +Header set Access-Control-Allow-Credentials "true" + +# Preflight cache duration +Header set Access-Control-Max-Age "86400" +``` + +**Testing:** +```bash +curl -I -H "Origin: https://example.com" https://yourdomain.com/api +``` + +#### Remove Server Identification + +**What it does:** Hides server information from attackers. + +```apache +Header unset Server +Header unset X-Powered-By +Header unset X-LiteSpeed-Tag +``` + +**Testing:** +```bash +curl -I https://yourdomain.com | grep -E "Server|X-Powered" +# Should return nothing +``` + +### CyberPanel Integration + +#### Via File Manager + +1. Log into **CyberPanel** +2. Go to **File Manager** +3. Navigate to `/home/yourdomain.com/public_html` +4. Create or edit `.htaccess` +5. Add header directives +6. Save and test + +#### Via SSH + +```bash +# Navigate to website directory +cd /home/yourdomain.com/public_html + +# Edit .htaccess +nano .htaccess + +# Add your headers +Header set X-Frame-Options "SAMEORIGIN" + +# Save (Ctrl+X, Y, Enter) + +# Test +curl -I https://yourdomain.com | grep X-Frame +``` + +### Common Use Cases + +#### WordPress Security Headers + +```apache +# WordPress-specific security +Header set X-Frame-Options "SAMEORIGIN" +Header set X-Content-Type-Options "nosniff" +Header set X-XSS-Protection "1; mode=block" +Header set Referrer-Policy "strict-origin-when-cross-origin" +Header unset X-Powered-By + +# Disable XML-RPC header +Header unset X-Pingback +``` + +#### n8n CORS Configuration + +```apache +# Allow n8n webhooks +Header set Access-Control-Allow-Origin "https://your-n8n-instance.com" +Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" +Header set Access-Control-Allow-Headers "Content-Type, Authorization" +Header set Access-Control-Allow-Credentials "true" +``` + +#### API Response Headers + +```apache +# JSON API headers +Header set Content-Type "application/json; charset=utf-8" +Header set X-Content-Type-Options "nosniff" +Header set Access-Control-Allow-Origin "*" +Header set Cache-Control "no-cache, no-store, must-revalidate" +``` + +--- + +## 2. Request Header Directives + +### What Are Request Headers? + +Request headers are sent FROM the client TO the server. This feature lets you modify or add headers before they reach your PHP application. + +### How It Works + +Since OpenLiteSpeed's LSIAPI doesn't support direct request header modification, these are implemented as **environment variables** accessible in PHP via `$_SERVER`. + +### Supported Operations + +| Operation | Syntax | Result | +|-----------|--------|--------| +| **set** | `RequestHeader set Name "Value"` | `$_SERVER['HTTP_NAME']` | +| **unset** | `RequestHeader unset Name` | Header removed | + +### How to Use + +#### SSL/HTTPS Detection (Behind Proxy) + +**What it does:** Tells your application the request came via HTTPS (when behind Cloudflare, nginx proxy, etc.). + +```apache +# Set HTTPS protocol headers +RequestHeader set X-Forwarded-Proto "https" +RequestHeader set X-Forwarded-SSL "on" +RequestHeader set X-Real-IP "%{REMOTE_ADDR}e" +``` + +**PHP Usage:** +```php + +``` + +#### Application Environment Identification + +**What it does:** Tags requests with environment information. + +```apache +# Identify environment +RequestHeader set X-Environment "production" +RequestHeader set X-Server-Location "us-east-1" +RequestHeader set X-Request-Start "%{REQUEST_TIME}e" +``` + +**PHP Usage:** +```php + +``` + +#### Custom Backend Headers + +**What it does:** Passes custom information to your application. + +```apache +# Custom application headers +RequestHeader set X-API-Version "v2" +RequestHeader set X-Feature-Flags "new-ui,beta-features" +RequestHeader set X-Client-Type "web" +``` + +**PHP Usage:** +```php + +``` + +### CyberPanel Integration + +#### For WordPress Behind Cloudflare + +```apache +# In /home/yourdomain.com/public_html/.htaccess +RequestHeader set X-Forwarded-Proto "https" +RequestHeader set X-Forwarded-SSL "on" + +# WordPress will now correctly detect HTTPS +``` + +**Verify in WordPress:** +```php +// Add to wp-config.php if needed +if ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { + $_SERVER['HTTPS'] = 'on'; +} +``` + +### Common Use Cases + +#### Cloudflare + WordPress + +```apache +RequestHeader set X-Forwarded-Proto "https" +RequestHeader set X-Forwarded-SSL "on" +RequestHeader set X-Real-IP "%{REMOTE_ADDR}e" +``` + +#### Laravel Behind Load Balancer + +```apache +RequestHeader set X-Forwarded-Proto "https" +RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}e" +``` + +--- + +## 3. Environment Variables + +### What Are Environment Variables? + +Environment variables are key-value pairs accessible in your PHP application. They're useful for configuration, feature flags, and conditional logic. + +### Supported Directives + +| Directive | Purpose | Syntax | +|-----------|---------|--------| +| **SetEnv** | Set static variable | `SetEnv NAME value` | +| **SetEnvIf** | Conditional set (case-sensitive) | `SetEnvIf attribute regex VAR=value` | +| **SetEnvIfNoCase** | Conditional set (case-insensitive) | `SetEnvIfNoCase attribute regex VAR=value` | +| **BrowserMatch** | Detect browser | `BrowserMatch regex VAR=value` | + +### How to Use + +#### Static Configuration Variables + +**What it does:** Sets application configuration accessible in PHP. + +```apache +# Application settings +SetEnv APPLICATION_ENV production +SetEnv DB_HOST localhost +SetEnv DB_NAME myapp_db +SetEnv API_ENDPOINT https://api.example.com +SetEnv FEATURE_FLAG_NEW_UI enabled +SetEnv DEBUG_MODE off +``` + +**PHP Usage:** +```php + +``` + +#### Conditional Variables (SetEnvIf) + +**What it does:** Sets variables based on request properties. + +##### Supported Conditions + +- `Request_URI` - URL path +- `Request_Method` - HTTP method (GET, POST, etc.) +- `User-Agent` - Browser/client identifier +- `Host` - Domain name +- `Referer` - Referrer URL +- `Query_String` - URL parameters +- `Remote_Addr` - Client IP address + +**Examples:** + +```apache +# Detect API requests +SetEnvIf Request_URI "^/api/" IS_API_REQUEST=1 + +# Detect POST requests +SetEnvIf Request_Method "POST" IS_POST_REQUEST=1 + +# Detect specific domain +SetEnvIf Host "^beta\." IS_BETA_SITE=1 + +# Detect search queries +SetEnvIf Query_String "search=" HAS_SEARCH=1 + +# Detect local development +SetEnvIf Remote_Addr "^127\.0\.0\.1$" IS_LOCAL=1 +``` + +**PHP Usage:** +```php + +``` + +#### Browser Detection + +**What it does:** Identifies the user's browser for compatibility handling. + +```apache +# Case-insensitive browser detection +SetEnvIfNoCase User-Agent "mobile|android|iphone|ipad" IS_MOBILE=1 +SetEnvIfNoCase User-Agent "bot|crawler|spider|scraper" IS_BOT=1 +SetEnvIfNoCase User-Agent "MSIE|Trident" IS_IE=1 + +# Specific browser matching +BrowserMatch "Chrome" IS_CHROME=1 +BrowserMatch "Firefox" IS_FIREFOX=1 +BrowserMatch "Safari" IS_SAFARI=1 +BrowserMatch "Edge" IS_EDGE=1 +``` + +**PHP Usage:** +```php +Please use a modern browser
'; +} +?> +``` + +### CyberPanel Integration + +#### Environment-Specific Configuration + +```apache +# In /home/yourdomain.com/public_html/.htaccess + +# Production settings +SetEnv APPLICATION_ENV production +SetEnv DEBUG_MODE off +SetEnv CACHE_ENABLED on + +# Database connection +SetEnv DB_HOST localhost +SetEnv DB_NAME wp_database + +# Feature flags +SetEnv ENABLE_CDN on +SetEnv ENABLE_CACHE on +``` + +**WordPress Usage (wp-config.php):** +```php + +``` + +### Common Use Cases + +#### Mobile Detection + Redirect + +```apache +# Detect mobile users +SetEnvIfNoCase User-Agent "mobile|android|iphone" IS_MOBILE=1 + +# Redirect mobile to subdomain (using PHP) +``` + +**PHP redirect:** +```php + +``` + +#### API Rate Limiting Preparation + +```apache +# Tag API requests +SetEnvIf Request_URI "^/api/" IS_API=1 +SetEnvIf Request_Method "POST" IS_POST=1 +``` + +**PHP rate limiting:** +```php + +``` + +--- + +## 4. Access Control + +### What is Access Control? + +Access control restricts who can access your website based on IP addresses. Perfect for staging sites, admin panels, or development environments. + +### Directives + +| Directive | Syntax | Description | +|-----------|--------|-------------| +| **Order** | `Order deny,allow` or `Order allow,deny` | Set evaluation order | +| **Allow** | `Allow from IP/CIDR` | Allow specific IP | +| **Deny** | `Deny from IP/CIDR` | Deny specific IP | + +### Supported IP Formats + +- **Single IP:** `192.168.1.100` +- **CIDR Range:** `192.168.1.0/24` (entire subnet) +- **Large Ranges:** `10.0.0.0/8` (entire class) +- **IPv6:** `2001:db8::/32` +- **Wildcard:** `all` (everyone) + +### How Order Works + +#### Order deny,allow + +1. Check **Deny** list first +2. Then check **Allow** list +3. **Allow overrides Deny** +4. Default: **DENY** if not in either list + +```apache +Order deny,allow +Deny from all +Allow from 192.168.1.100 +# Result: Only 192.168.1.100 can access +``` + +#### Order allow,deny + +1. Check **Allow** list first +2. Then check **Deny** list +3. **Deny overrides Allow** +4. Default: **ALLOW** if not in either list + +```apache +Order allow,deny +Allow from all +Deny from 192.168.1.100 +# Result: Everyone except 192.168.1.100 can access +``` + +### How to Use + +#### Block All Except Specific IPs (Recommended for Staging) + +```apache +# Only allow office IP and VPN +Order deny,allow +Deny from all +Allow from 203.0.113.50 # Office IP +Allow from 192.168.1.0/24 # Office LAN +Allow from 10.8.0.0/24 # VPN range +``` + +**Use case:** Development/staging sites, admin areas + +**Testing:** +```bash +# From allowed IP +curl https://staging.example.com +# Should work + +# From other IP +curl https://staging.example.com +# Should get 403 Forbidden +``` + +#### Allow All Except Specific IPs + +```apache +# Block known attackers +Order allow,deny +Allow from all +Deny from 198.51.100.50 # Banned IP +Deny from 203.0.113.0/24 # Banned subnet +``` + +**Use case:** Blocking spam IPs, attack sources + +#### Protect Admin Directory + +```apache +# In /home/yourdomain.com/public_html/admin/.htaccess +Order deny,allow +Deny from all +Allow from 192.168.1.0/24 # Office network +Allow from 203.0.113.100 # Your home IP +``` + +**Use case:** WordPress wp-admin protection + +### CyberPanel Integration + +#### Protect Staging Site + +1. Create subdomain `staging.yourdomain.com` in CyberPanel +2. Navigate to `/home/staging.yourdomain.com/public_html` +3. Create `.htaccess`: + +```apache +# Staging site - Office only +Order deny,allow +Deny from all +Allow from YOUR.OFFICE.IP.HERE +Allow from YOUR.HOME.IP.HERE +``` + +4. Test: +```bash +# Get your IP +curl ifconfig.me + +# Test access +curl -I https://staging.yourdomain.com +# Should see 403 if not allowed +``` + +#### Protect WordPress Admin + +```apache +# In /home/yourdomain.com/public_html/wp-admin/.htaccess +Order deny,allow +Deny from all +Allow from 203.0.113.50 # Your IP +``` + +**Important:** This creates TWO layers of protection: +1. IP restriction (from .htaccess) +2. Login authentication (from WordPress) + +#### Protect CyberPanel Access + +```apache +# In /usr/local/CyberCP/public/.htaccess (if web accessible) +Order deny,allow +Deny from all +Allow from 127.0.0.1 # localhost +Allow from 192.168.1.0/24 # Your network +``` + +### Common Use Cases + +#### Development Environment + +```apache +# Dev site - developers only +Order deny,allow +Deny from all +Allow from 192.168.1.0/24 # Office LAN +Allow from 10.8.0.0/24 # VPN +Allow from 203.0.113.50 # Lead developer home +``` + +#### Geographic Restriction + +```apache +# Block specific countries (you need to maintain IP list) +Order allow,deny +Allow from all +Deny from 198.51.100.0/24 # Country X subnet +Deny from 203.0.113.0/24 # Country Y subnet +``` + +#### API Endpoint Protection + +```apache +# In /home/yourdomain.com/public_html/api/.htaccess +Order deny,allow +Deny from all +Allow from 10.0.0.0/8 # Internal network +Allow from 172.16.0.0/12 # Private network +``` + +### Troubleshooting + +**Problem:** Getting 403 even from allowed IP + +**Solution:** +1. Check your actual IP: `curl ifconfig.me` +2. Verify CIDR: `192.168.1.0/24` covers `192.168.1.1` to `192.168.1.254` +3. Check logs: `tail -f /usr/local/lsws/logs/error.log` + +**Problem:** Access control not working + +**Solution:** +1. Verify module loaded: `ls -la /usr/local/lsws/modules/cyberpanel_ols.so` +2. Check .htaccess permissions: `chmod 644 .htaccess` +3. Restart OpenLiteSpeed: `/usr/local/lsws/bin/lswsctrl restart` + +--- + +## 5. Redirect Directives + +### What Are Redirects? + +Redirects tell browsers to go to a different URL. Essential for SEO, site migrations, and URL structure changes. + +### Directives + +| Directive | Syntax | Use Case | +|-----------|--------|----------| +| **Redirect** | `Redirect [code] /old /new` | Simple path redirects | +| **RedirectMatch** | `RedirectMatch [code] regex target` | Pattern-based redirects | + +### Status Codes + +| Code | Name | When to Use | +|------|------|-------------| +| **301** | Permanent | SEO-friendly, URL has moved forever | +| **302** | Temporary | URL temporarily moved, may change back | +| **303** | See Other | Redirect after POST (form submission) | +| **410** | Gone | Resource permanently deleted | + +### How to Use + +#### Simple Redirects + +**What it does:** Redirects one path to another. + +```apache +# Old page to new page +Redirect 301 /old-page.html /new-page.html + +# Old directory to new directory +Redirect 301 /old-blog /blog + +# Use keywords instead of codes +Redirect permanent /old-url /new-url +Redirect temp /maintenance /coming-soon +``` + +**Testing:** +```bash +curl -I https://yourdomain.com/old-page.html +# Should show: HTTP/1.1 301 Moved Permanently +# Location: https://yourdomain.com/new-page.html +``` + +#### Force HTTPS + +**What it does:** Redirects HTTP to HTTPS. + +```apache +# Redirect HTTP to HTTPS +Redirect 301 / https://yourdomain.com/ +``` + +**Better Alternative (checks if already HTTPS):** +```apache +SetEnvIf Request_URI ".*" IS_HTTP=1 +# Use with PHP to avoid redirect loop +``` + +**PHP solution:** +```php + +``` + +#### Force WWW or Non-WWW + +**What it does:** Standardizes domain format for SEO. + +```apache +# Force www +Redirect 301 / https://www.yourdomain.com/ + +# Force non-www (use RedirectMatch) +RedirectMatch 301 ^(.*)$ https://yourdomain.com$1 +``` + +#### Pattern-Based Redirects (RedirectMatch) + +**What it does:** Uses regex to match and redirect URLs. + +```apache +# Blog restructuring +RedirectMatch 301 ^/blog/(.*)$ /news/$1 +# /blog/post-1 → /news/post-1 + +# Product ID migration +RedirectMatch 301 ^/product-([0-9]+)$ /item/$1 +# /product-123 → /item/123 + +# Year/month/title to title +RedirectMatch 301 ^/blog/([0-9]{4})/([0-9]{2})/(.*)$ /articles/$3 +# /blog/2024/12/my-post → /articles/my-post + +# Category reorganization +RedirectMatch 301 ^/category/(.*)$ /topics/$1 +``` + +**Testing:** +```bash +curl -I https://yourdomain.com/blog/my-post +# Should redirect to /news/my-post +``` + +### CyberPanel Integration + +#### Site Migration (Old Domain to New) + +```apache +# In old site's .htaccess +Redirect 301 / https://new-domain.com/ +``` + +**Steps:** +1. Keep old domain active in CyberPanel +2. Add redirect to `/home/old-domain.com/public_html/.htaccess` +3. Monitor traffic migration +4. After 6 months, can delete old domain + +#### WordPress Permalink Change + +**Scenario:** Changed permalinks from `/?p=123` to `/blog/post-title` + +```apache +# WordPress handles this automatically, but for custom: +RedirectMatch 301 ^/\?p=([0-9]+)$ /blog/post-$1 +``` + +#### E-commerce URL Update + +```apache +# Old: /products/view/123 +# New: /shop/product-123 + +RedirectMatch 301 ^/products/view/([0-9]+)$ /shop/product-$1 +``` + +### Common Use Cases + +#### Complete Site Redesign + +```apache +# Redirect old structure to new +RedirectMatch 301 ^/about-us$ /about +RedirectMatch 301 ^/contact-us$ /contact +RedirectMatch 301 ^/services/(.*)$ /solutions/$1 +RedirectMatch 301 ^/blog/(.*)$ /news/$1 +``` + +#### Affiliate Link Management + +```apache +# Short URLs for affiliate links +Redirect 302 /go/amazon https://amazon.com/your-affiliate-link +Redirect 302 /go/product https://example.com/long-url-here +``` + +#### Seasonal Campaigns + +```apache +# Temporary campaign redirect +Redirect 302 /sale /christmas-sale-2025 +Redirect 302 /promo /black-friday +``` + +#### Remove .html Extensions (SEO) + +```apache +# Old: /page.html +# New: /page + +RedirectMatch 301 ^/(.*)/index\.html$ /$1/ +RedirectMatch 301 ^/(.*)[^/]\.html$ /$1 +``` + +### Troubleshooting + +**Problem:** Redirect loop + +**Solution:** Check for conflicting rules: +```apache +# BAD - Creates loop +Redirect 301 / https://example.com/ +Redirect 301 / https://www.example.com/ + +# GOOD - Use one or the other +Redirect 301 / https://www.example.com/ +``` + +**Problem:** Redirect not working + +**Solution:** +1. Clear browser cache (redirects are cached!) +2. Test with curl: `curl -I https://yoursite.com/old-page` +3. Check .htaccess syntax +4. Restart OpenLiteSpeed + +--- + +## 6. Error Documents + +### What Are Error Documents? + +Custom error pages shown when errors occur (404 Not Found, 500 Internal Server Error, etc.). + +### Supported Error Codes + +| Code | Error | When It Happens | +|------|-------|-----------------| +| **400** | Bad Request | Malformed request | +| **401** | Unauthorized | Authentication required | +| **403** | Forbidden | Access denied | +| **404** | Not Found | Page doesn't exist | +| **500** | Internal Server Error | Server-side error | +| **502** | Bad Gateway | Proxy/backend error | +| **503** | Service Unavailable | Server overloaded/maintenance | + +### Syntax + +```apache +ErrorDocument +``` + +### How to Use + +#### HTML Error Pages + +**What it does:** Shows custom-designed error pages. + +```apache +# Custom error pages +ErrorDocument 404 /errors/404.html +ErrorDocument 500 /errors/500.html +ErrorDocument 403 /errors/403.html +ErrorDocument 503 /errors/maintenance.html +``` + +**Create error pages:** + +```bash +mkdir -p /home/yourdomain.com/public_html/errors +``` + +**404.html example:** +```html + + + + Page Not Found + + + +

404 - Page Not Found

+

The page you're looking for doesn't exist.

+ Go to Homepage + + +``` + +**Testing:** +```bash +curl https://yourdomain.com/nonexistent-page +# Should show your custom 404 page +``` + +#### Inline Messages + +**What it does:** Shows simple text message. + +```apache +ErrorDocument 403 "Access Denied - Contact Administrator" +ErrorDocument 404 "Page Not Found - Please check the URL" +``` + +#### WordPress-Friendly Error Pages + +**What it does:** Routes errors through WordPress. + +```apache +# Let WordPress handle 404s +ErrorDocument 404 /index.php?error=404 +``` + +**WordPress theme (404.php):** +```php + +

Page Not Found

+

Sorry, this page doesn't exist.

+ +``` + +### CyberPanel Integration + +#### Setup Custom Error Pages + +**Step 1:** Create error directory +```bash +cd /home/yourdomain.com/public_html +mkdir errors +cd errors +``` + +**Step 2:** Create error page files +```bash +nano 404.html +# Add custom HTML +# Save (Ctrl+X, Y, Enter) + +nano 500.html +# Add custom HTML +# Save +``` + +**Step 3:** Configure .htaccess +```apache +# In /home/yourdomain.com/public_html/.htaccess +ErrorDocument 404 /errors/404.html +ErrorDocument 500 /errors/500.html +ErrorDocument 403 /errors/403.html +``` + +**Step 4:** Test +```bash +curl https://yourdomain.com/test-404 +``` + +#### Maintenance Page + +```apache +# During maintenance +ErrorDocument 503 /maintenance.html +``` + +**maintenance.html:** +```html + + + + Maintenance + + + + +

We'll be right back!

+

Our site is undergoing maintenance.

+

Expected completion: 2 hours

+ + +``` + +**Trigger maintenance mode:** +```bash +# Temporarily disable PHP +mv index.php index.php.bak +# Site will show 503 +``` + +### Common Use Cases + +#### Professional 404 Page with Search + +**404.html:** +```html + + + + Page Not Found + + +

404 - Page Not Found

+

Try searching:

+
+ + +
+

Return to Homepage

+ + +``` + +#### Branded Error Pages + +```apache +ErrorDocument 400 /errors/400.html +ErrorDocument 401 /errors/401.html +ErrorDocument 403 /errors/403.html +ErrorDocument 404 /errors/404.html +ErrorDocument 500 /errors/500.html +ErrorDocument 502 /errors/502.html +ErrorDocument 503 /errors/503.html +``` + +Each page styled with your brand colors, logo, navigation. + +--- + +## 7. FilesMatch Directives + +### What is FilesMatch? + +FilesMatch applies directives only to files matching a regex pattern. Perfect for caching strategies, security headers per file type. + +### Syntax + +```apache + + # Directives here apply only to matching files + Header set Name "Value" + +``` + +### Common File Patterns + +| Pattern | Matches | +|---------|---------| +| `\.(jpg\|png\|gif)$` | Images | +| `\.(css\|js)$` | Stylesheets and JavaScript | +| `\.(woff2?\|ttf\|eot)$` | Fonts | +| `\.(pdf\|doc\|docx)$` | Documents | +| `\.(html\|php)$` | Dynamic pages | +| `\.json$` | JSON files | + +### How to Use + +#### Cache Static Assets (Performance Boost) + +**What it does:** Tells browsers to cache images/fonts for a long time. + +```apache +# Images - Cache for 1 year + + Header set Cache-Control "max-age=31536000, public, immutable" + Header unset ETag + Header unset Last-Modified + + +# Fonts - Cache for 1 year + + Header set Cache-Control "max-age=31536000, public, immutable" + Header set Access-Control-Allow-Origin "*" + + +# CSS/JS - Cache for 1 week (you update these more often) + + Header set Cache-Control "max-age=604800, public" + +``` + +**Testing:** +```bash +curl -I https://yourdomain.com/logo.png | grep Cache-Control +# Should show: Cache-Control: max-age=31536000, public, immutable +``` + +**Performance Impact:** +- First visit: Downloads all files +- Return visits: Loads from browser cache (instant!) +- Page load time: -50% to -80% + +#### Security Headers for HTML/PHP + +**What it does:** Applies security headers only to pages (not images). + +```apache + + Header set X-Frame-Options "SAMEORIGIN" + Header set X-Content-Type-Options "nosniff" + Header set X-XSS-Protection "1; mode=block" + Header set Referrer-Policy "strict-origin-when-cross-origin" + +``` + +#### Prevent Caching of Dynamic Content + +**What it does:** Ensures dynamic pages are never cached. + +```apache + + Header set Cache-Control "no-cache, no-store, must-revalidate" + Header set Pragma "no-cache" + Header set Expires "0" + +``` + +#### CORS for Fonts (Fix Font Loading) + +**What it does:** Allows fonts to load from CDN or different domain. + +```apache + + Header set Access-Control-Allow-Origin "*" + +``` + +**Use case:** Fixes "Font from origin has been blocked by CORS policy" errors. + +#### Download Headers for Files + +**What it does:** Forces download instead of displaying in browser. + +```apache + + Header set Content-Disposition "attachment" + Header set X-Content-Type-Options "nosniff" + +``` + +### CyberPanel Integration + +#### WordPress Performance Optimization + +```apache +# In /home/yourdomain.com/public_html/.htaccess + +# Cache WordPress static assets + + Header set Cache-Control "max-age=31536000, public, immutable" + + +# Cache CSS/JS (with version strings in WordPress) + + Header set Cache-Control "max-age=2592000, public" + + +# Don't cache WordPress admin + + Header set Cache-Control "no-cache, no-store, must-revalidate" + +``` + +**Result:** PageSpeed score +20-30 points + +#### WooCommerce Security + +```apache +# Protect sensitive files + + Order deny,allow + Deny from all + + +# JSON API security + + Header set X-Content-Type-Options "nosniff" + Header set Content-Type "application/json; charset=utf-8" + +``` + +### Common Use Cases + +#### Complete Caching Strategy + +```apache +# Aggressive caching for static assets (1 year) + + Header set Cache-Control "max-age=31536000, public, immutable" + Header unset ETag + + +# Moderate caching for CSS/JS (1 month) + + Header set Cache-Control "max-age=2592000, public" + + +# Short caching for HTML (1 hour) + + Header set Cache-Control "max-age=3600, public" + + +# No caching for dynamic content + + Header set Cache-Control "no-cache, must-revalidate" + +``` + +#### Media Library Protection + +```apache +# Prevent hotlinking (bandwidth theft) + + SetEnvIf Referer "^https://yourdomain\.com" local_ref + SetEnvIf Referer "^$" local_ref + Order deny,allow + Deny from all + Allow from env=local_ref + +``` + +--- + +## 8. Expires Directives + +### What is mod_expires? + +Alternative syntax for setting cache expiration. More concise than Cache-Control headers. + +### Directives + +```apache +ExpiresActive On +ExpiresByType mime-type base+seconds +``` + +### Time Bases + +- **A** = Access time (when user requests file) +- **M** = Modification time (when file was last modified) + +### Common Durations + +| Duration | Seconds | Example | +|----------|---------|---------| +| 1 minute | 60 | `A60` | +| 1 hour | 3600 | `A3600` | +| 1 day | 86400 | `A86400` | +| 1 week | 604800 | `A604800` | +| 1 month | 2592000 | `A2592000` | +| 1 year | 31557600 | `A31557600` | + +### How to Use + +#### Complete Expiration Strategy + +```apache +# Enable module +ExpiresActive On + +# Images - 1 year +ExpiresByType image/jpeg A31557600 +ExpiresByType image/png A31557600 +ExpiresByType image/gif A31557600 +ExpiresByType image/webp A31557600 +ExpiresByType image/svg+xml A31557600 +ExpiresByType image/x-icon A31557600 + +# CSS and JavaScript - 1 month +ExpiresByType text/css A2592000 +ExpiresByType application/javascript A2592000 +ExpiresByType application/x-javascript A2592000 +ExpiresByType text/javascript A2592000 + +# Fonts - 1 year +ExpiresByType font/ttf A31557600 +ExpiresByType font/woff A31557600 +ExpiresByType font/woff2 A31557600 +ExpiresByType application/font-woff A31557600 +ExpiresByType application/font-woff2 A31557600 + +# HTML - no cache +ExpiresByType text/html A0 + +# PDF - 1 month +ExpiresByType application/pdf A2592000 + +# JSON/XML - 1 hour +ExpiresByType application/json A3600 +ExpiresByType application/xml A3600 +``` + +**Testing:** +```bash +curl -I https://yourdomain.com/image.jpg | grep -E "Expires|Cache-Control" +``` + +### CyberPanel Integration + +#### WordPress Caching + +```apache +# In /home/yourdomain.com/public_html/.htaccess + +ExpiresActive On + +# WordPress uploads (images in wp-content/uploads) +ExpiresByType image/jpeg A31557600 +ExpiresByType image/png A31557600 +ExpiresByType image/gif A31557600 + +# WordPress theme assets +ExpiresByType text/css A2592000 +ExpiresByType application/javascript A2592000 + +# WordPress HTML (dynamic, don't cache) +ExpiresByType text/html A0 +``` + +### FilesMatch vs Expires + +**Use FilesMatch when:** +- Need multiple headers per file type +- Need complex regex patterns +- Want more control + +**Use Expires when:** +- Only setting cache expiration +- Want concise syntax +- Working with MIME types + +**Both together:** +```apache +ExpiresActive On + + + ExpiresByType image/jpeg A31557600 + Header set Cache-Control "public, immutable" + Header unset ETag + +``` + +--- + +## 9. PHP Directives + +### What Are PHP Directives? + +Change PHP configuration per-directory without editing php.ini. + +### Directives + +| Directive | Syntax | Purpose | +|-----------|--------|---------| +| **php_value** | `php_value name value` | Set numeric/string values | +| **php_flag** | `php_flag name on/off` | Set boolean (on/off) values | + +### Requirements + +- Must use **LSPHP** (not PHP-FPM) +- Must be **PHP_INI_ALL** or **PHP_INI_PERDIR** directive +- CyberPanel uses LSPHP by default ✅ + +### How to Use + +#### Memory and Execution Limits + +**What it does:** Allows scripts to use more memory/time. + +```apache +# Increase memory (default 128M) +php_value memory_limit 256M + +# Increase execution time (default 30s) +php_value max_execution_time 300 + +# Increase input time (default 60s) +php_value max_input_time 300 + +# Increase max input variables (default 1000) +php_value max_input_vars 5000 +``` + +**Use case:** WordPress imports, WooCommerce bulk operations, data processing. + +**Testing:** +```php + +``` + +#### Upload Limits + +**What it does:** Allows larger file uploads. + +```apache +# Allow 100MB uploads (default 2M) +php_value upload_max_filesize 100M +php_value post_max_size 100M + +# Increase max file uploads (default 20) +php_value max_file_uploads 50 +``` + +**Use case:** Media uploads, plugin/theme installation, backup uploads. + +**Testing:** +```php + +``` + +#### Error Handling + +**What it does:** Controls error display and logging. + +```apache +# Production (hide errors) +php_flag display_errors off +php_flag log_errors on +php_value error_log /home/yourdomain.com/logs/php_errors.log + +# Development (show errors) +php_flag display_errors on +php_value error_reporting 32767 +``` + +**Use case:** Debugging vs production security. + +#### Session Configuration + +**What it does:** Configures PHP sessions. + +```apache +# Session lifetime (1 hour) +php_value session.gc_maxlifetime 3600 + +# Session cookie (close browser = logout) +php_value session.cookie_lifetime 0 + +# Session security +php_flag session.cookie_httponly on +php_flag session.cookie_secure on +php_value session.cookie_samesite Strict +``` + +**Use case:** Login session duration, security. + +#### Timezone + +**What it does:** Sets server timezone. + +```apache +php_value date.timezone "America/New_York" +php_value date.timezone "Europe/London" +php_value date.timezone "Asia/Tokyo" +``` + +**Use case:** Correct timestamps in logs, posts, events. + +**Testing:** +```php + +``` + +### CyberPanel Integration + +#### WordPress Performance Tuning + +```apache +# In /home/yourdomain.com/public_html/.htaccess + +# WordPress recommended settings +php_value memory_limit 256M +php_value max_execution_time 300 +php_value max_input_time 300 +php_value max_input_vars 5000 +php_value upload_max_filesize 64M +php_value post_max_size 64M + +# Production error handling +php_flag display_errors off +php_flag log_errors on +php_value error_log /home/yourdomain.com/logs/php_errors.log + +# Session security +php_flag session.cookie_httponly on +php_flag session.cookie_secure on +``` + +#### WooCommerce Optimization + +```apache +# WooCommerce needs more resources +php_value memory_limit 512M +php_value max_execution_time 600 +php_value max_input_vars 10000 +php_value upload_max_filesize 128M +php_value post_max_size 128M +``` + +#### Development vs Production + +**Development .htaccess:** +```apache +php_flag display_errors on +php_value error_reporting 32767 +php_flag display_startup_errors on +php_value memory_limit 512M +``` + +**Production .htaccess:** +```apache +php_flag display_errors off +php_flag log_errors on +php_value error_log /home/yourdomain.com/logs/php_errors.log +php_value memory_limit 256M +``` + +### Common Use Cases + +#### Fix "Memory Exhausted" Error + +```apache +php_value memory_limit 512M +``` + +#### Fix "Maximum Execution Time Exceeded" + +```apache +php_value max_execution_time 300 +``` + +#### Fix "Upload Failed" (File Too Large) + +```apache +php_value upload_max_filesize 100M +php_value post_max_size 100M +``` + +#### Fix "Maximum Input Vars Exceeded" (WordPress Theme Options) + +```apache +php_value max_input_vars 10000 +``` + +### Supported Directives + +Most PHP ini settings can be changed: + +**✅ Supported:** +- memory_limit +- max_execution_time +- max_input_time +- max_input_vars +- upload_max_filesize +- post_max_size +- display_errors +- log_errors +- error_log +- error_reporting +- session.* (all session directives) +- date.timezone +- default_charset +- output_buffering + +**❌ Not Supported:** +- enable_dl (PHP_INI_SYSTEM only) +- safe_mode (deprecated) +- open_basedir (security setting) + +--- + +## 10. Brute Force Protection + +### What is Brute Force Protection? + +Built-in WordPress login protection. Limits POST requests to wp-login.php and xmlrpc.php to stop password guessing attacks. + +### Quick Start + +```apache +BruteForceProtection On +``` + +That's it! Default settings: 10 attempts per 5 minutes. + +### How It Works + +1. Tracks POST requests to `/wp-login.php` and `/xmlrpc.php` +2. Counts requests per IP address +3. Uses time-window quota system (e.g., 10 requests per 300 seconds) +4. When quota exhausted, applies action (block, log, or throttle) +5. Quota resets after time window expires + +### Phase 1 Directives (Basic) + +| Directive | Values | Default | Description | +|-----------|--------|---------|-------------| +| **BruteForceProtection** | On/Off | Off | Enable protection | +| **BruteForceAllowedAttempts** | 1-1000 | 10 | Max POST requests per window | +| **BruteForceWindow** | 60-86400 | 300 | Time window (seconds) | +| **BruteForceAction** | block/log/throttle | block | Action when limit exceeded | + +### Phase 2 Directives (Advanced) + +| Directive | Values | Default | Description | +|-----------|--------|---------|-------------| +| **BruteForceXForwardedFor** | On/Off | Off | Use X-Forwarded-For for real IP | +| **BruteForceWhitelist** | IP list | (empty) | Bypass protection for these IPs | +| **BruteForceProtectPath** | path | (none) | Additional paths to protect | + +### Actions Explained + +#### block (Recommended) + +**What it does:** Immediately returns 403 Forbidden. + +```apache +BruteForceAction block +``` + +**Response:** +``` +HTTP/1.1 403 Forbidden +Content-Type: text/html + + +403 Forbidden + +

Access Denied

+

Too many login attempts. Please try again later.

+ + +``` + +**Use case:** Production sites, maximum security. + +#### log (Monitoring) + +**What it does:** Allows request but logs to error.log. + +```apache +BruteForceAction log +``` + +**Use case:** Testing, monitoring before enabling blocking. + +**Check logs:** +```bash +grep BruteForce /usr/local/lsws/logs/error.log +``` + +#### throttle (New in v2.2.0) + +**What it does:** Applies progressive delays before responding. + +```apache +BruteForceAction throttle +``` + +**Throttle levels:** + +| Over-Limit Attempts | Level | Delay | HTTP Response | +|---------------------|-------|-------|---------------| +| 1-2 | Soft | 2 seconds | 429 Too Many Requests | +| 3-5 | Medium | 5 seconds | 429 Too Many Requests | +| 6+ | Hard | 15 seconds | 429 Too Many Requests | + +**Response includes:** +``` +HTTP/1.1 429 Too Many Requests +Retry-After: 15 +``` + +**Use case:** Slows down attackers while allowing legitimate users who forgot password. + +### How to Use + +#### Basic Protection (Small Site) + +```apache +# Simple protection +BruteForceProtection On +``` + +**Result:** Default 10 attempts per 5 minutes, then block. + +#### Strict Protection (High Security) + +```apache +# Only 3 attempts per 15 minutes +BruteForceProtection On +BruteForceAllowedAttempts 3 +BruteForceWindow 900 +BruteForceAction block +``` + +**Result:** Very strict, good for high-value targets. + +#### Moderate Protection with Throttle (Recommended) + +```apache +# 5 attempts per 5 minutes, then progressive throttle +BruteForceProtection On +BruteForceAllowedAttempts 5 +BruteForceWindow 300 +BruteForceAction throttle +``` + +**Result:** Legitimate users can still login (slowly), attackers waste time. + +#### Behind Cloudflare/Proxy + +**Problem:** All requests appear to come from proxy IP. + +**Solution:** Use X-Forwarded-For to get real client IP. + +```apache +BruteForceProtection On +BruteForceAllowedAttempts 5 +BruteForceWindow 300 +BruteForceAction throttle +BruteForceXForwardedFor On +``` + +**Important:** Only enable if behind trusted proxy (Cloudflare, nginx). + +#### With IP Whitelist + +**What it does:** Allows unlimited attempts from trusted IPs. + +```apache +BruteForceProtection On +BruteForceAllowedAttempts 3 +BruteForceWindow 900 +BruteForceAction block +BruteForceWhitelist 203.0.113.50, 192.168.1.0/24, 10.0.0.0/8 +``` + +**Use case:** Whitelist office IP, admin home IP, VPN range. + +#### Protect Custom Login Pages + +```apache +# Protect custom endpoints +BruteForceProtection On +BruteForceProtectPath /admin/login +BruteForceProtectPath /api/auth +BruteForceProtectPath /members/signin +``` + +**Default protected:** `/wp-login.php` and `/xmlrpc.php` + +### CyberPanel Integration + +#### WordPress Security Setup + +**Step 1:** Navigate to website .htaccess +```bash +cd /home/yourdomain.com/public_html +nano .htaccess +``` + +**Step 2:** Add protection +```apache +# At top of .htaccess +BruteForceProtection On +BruteForceAllowedAttempts 5 +BruteForceWindow 300 +BruteForceAction throttle +``` + +**Step 3:** Save and test +```bash +# Try multiple wrong passwords +# After 5 attempts, should get throttled +``` + +**Step 4:** Monitor logs +```bash +tail -f /usr/local/lsws/logs/error.log | grep BruteForce +``` + +#### WooCommerce + WordPress + +```apache +# Protect both WordPress and WooCommerce login +BruteForceProtection On +BruteForceAllowedAttempts 5 +BruteForceWindow 300 +BruteForceAction block +BruteForceProtectPath /my-account/ +BruteForceProtectPath /checkout/ +``` + +#### Multi-Site WordPress + +```apache +# Apply to all subsites +BruteForceProtection On +BruteForceAllowedAttempts 5 +BruteForceWindow 300 +BruteForceAction throttle +BruteForceXForwardedFor On +``` + +### Shared Memory Storage + +**Location:** `/dev/shm/ols/` + +```bash +ls -la /dev/shm/ols/ +# BFProt.shm - Stores IP quota data +# BFProt.lock - Synchronization lock +``` + +**Persistence:** Data survives OpenLiteSpeed restarts (stored in tmpfs). + +**Reset/Clear:** +```bash +# Clear all quota data +rm -f /dev/shm/ols/BFProt.* +/usr/local/lsws/bin/lswsctrl restart +``` + +**Use case:** Accidentally locked out, need to reset. + +### Monitoring and Logs + +#### View Brute Force Events + +```bash +grep BruteForce /usr/local/lsws/logs/error.log +``` + +**Sample log entries:** + +``` +[INFO] [BruteForce] Initialized: 10 attempts per 300s window, action: throttle +[WARN] [BruteForce] Warning: 192.168.1.50 has 2 attempts remaining for /wp-login.php +[NOTICE] [BruteForce] Blocked 192.168.1.50 - quota exhausted for /wp-login.php (10 attempts in 300s) +[NOTICE] [BruteForce] Throttling 192.168.1.50 (medium level, 5000ms delay) for /wp-login.php +``` + +#### Real-Time Monitoring + +```bash +# Watch in real-time +tail -f /usr/local/lsws/logs/error.log | grep BruteForce + +# Count blocked IPs today +grep "BruteForce.*Blocked" /usr/local/lsws/logs/error.log | grep "$(date +%Y-%m-%d)" | wc -l +``` + +#### Check Specific IP + +```bash +grep "BruteForce.*192.168.1.50" /usr/local/lsws/logs/error.log +``` + +### Testing Brute Force Protection + +#### Manual Test + +```bash +# Try multiple wrong passwords +for i in {1..15}; do + curl -X POST https://yourdomain.com/wp-login.php \ + -d "log=admin&pwd=wrong$i&wp-submit=Log+In" \ + -I | grep "HTTP" + sleep 1 +done + +# After BruteForceAllowedAttempts, should see: +# HTTP/1.1 403 Forbidden (if action=block) +# HTTP/1.1 429 Too Many Requests (if action=throttle) +``` + +#### Check Logs + +```bash +grep BruteForce /usr/local/lsws/logs/error.log | tail -20 +``` + +### Common Use Cases + +#### Production WordPress + +```apache +BruteForceProtection On +BruteForceAllowedAttempts 5 +BruteForceWindow 300 +BruteForceAction block +``` + +#### Behind Cloudflare + +```apache +BruteForceProtection On +BruteForceAllowedAttempts 5 +BruteForceWindow 300 +BruteForceAction throttle +BruteForceXForwardedFor On +``` + +#### Enterprise with Whitelist + +```apache +BruteForceProtection On +BruteForceAllowedAttempts 3 +BruteForceWindow 900 +BruteForceAction block +BruteForceXForwardedFor On +BruteForceWhitelist 10.0.0.0/8, 192.168.1.0/24, 203.0.113.100 +BruteForceProtectPath /admin/ +BruteForceProtectPath /api/login +``` + +### Troubleshooting + +**Problem:** Legitimate users getting blocked + +**Solution:** +```apache +# Increase allowed attempts +BruteForceAllowedAttempts 10 + +# Or use throttle instead of block +BruteForceAction throttle + +# Or whitelist their IP +BruteForceWhitelist 203.0.113.50 +``` + +**Problem:** Protection not working + +**Solution:** +```bash +# Check module loaded +ls -la /usr/local/lsws/modules/cyberpanel_ols.so + +# Check .htaccess syntax +cat /home/yourdomain.com/public_html/.htaccess | grep BruteForce + +# Check logs +grep BruteForce /usr/local/lsws/logs/error.log + +# Restart OpenLiteSpeed +/usr/local/lsws/bin/lswsctrl restart +``` + +**Problem:** Shared memory errors + +**Solution:** +```bash +# Create directory if missing +mkdir -p /dev/shm/ols + +# Set permissions +chmod 755 /dev/shm/ols + +# Restart +/usr/local/lsws/bin/lswsctrl restart +``` + +--- + +## CyberPanel Integration + +### Accessing Website Files + +#### Via CyberPanel File Manager + +1. Log into **CyberPanel** (https://yourserver:8090) +2. Click **File Manager** +3. Navigate to `/home/yourdomain.com/public_html` +4. Create or edit `.htaccess` +5. Add directives from this guide +6. Click **Save** + +#### Via SSH + +```bash +# Log in via SSH +ssh root@yourserver + +# Navigate to website +cd /home/yourdomain.com/public_html + +# Edit .htaccess +nano .htaccess + +# Add directives +# Save: Ctrl+X, Y, Enter +``` + +#### Via FTP (FileZilla) + +1. Connect via FTP +2. Navigate to `/home/yourdomain.com/public_html` +3. Download `.htaccess` +4. Edit locally +5. Upload back + +### Creating New Website + +1. **Create Website** in CyberPanel +2. **Navigate to directory:** + ```bash + cd /home/newsite.com/public_html + ``` +3. **Create .htaccess:** + ```bash + nano .htaccess + ``` +4. **Add base configuration:** + ```apache + # Security headers + Header set X-Frame-Options "SAMEORIGIN" + Header set X-Content-Type-Options "nosniff" + + # Brute force protection + BruteForceProtection On + + # Cache static assets + + Header set Cache-Control "max-age=31536000, public" + + ``` + +### WordPress on CyberPanel + +#### Complete WordPress .htaccess + +```apache +# Security Headers +Header set X-Frame-Options "SAMEORIGIN" +Header set X-Content-Type-Options "nosniff" +Header set X-XSS-Protection "1; mode=block" +Header unset X-Powered-By + +# Brute Force Protection +BruteForceProtection On +BruteForceAllowedAttempts 5 +BruteForceWindow 300 +BruteForceAction throttle + +# Performance - Cache Static Assets + + Header set Cache-Control "max-age=31536000, public, immutable" + + + + Header set Cache-Control "max-age=2592000, public" + + +# PHP Configuration +php_value memory_limit 256M +php_value upload_max_filesize 64M +php_value post_max_size 64M +php_value max_execution_time 300 +php_flag display_errors off + +# WordPress Rewrite Rules (leave as-is) +# BEGIN WordPress + +RewriteEngine On +RewriteBase / +RewriteRule ^index\.php$ - [L] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule . /index.php [L] + +# END WordPress +``` + +### Staging Environment + +```apache +# Staging site - restrict access +Order deny,allow +Deny from all +Allow from YOUR.OFFICE.IP +Allow from YOUR.HOME.IP + +# No search engine indexing +Header set X-Robots-Tag "noindex, nofollow" + +# Show errors (development) +php_flag display_errors on +php_value error_reporting 32767 +``` + +### Testing After Configuration + +```bash +# Test headers +curl -I https://yourdomain.com | grep -E "X-Frame|Cache-Control|X-Content" + +# Test specific file +curl -I https://yourdomain.com/wp-content/uploads/2024/12/image.jpg | grep Cache + +# Test PHP settings +echo '' > /home/yourdomain.com/public_html/info.php +curl https://yourdomain.com/info.php | grep memory_limit + +# Clean up +rm /home/yourdomain.com/public_html/info.php +``` + +--- + +## Real-World Examples + +### Example 1: High-Performance WordPress + +```apache +# Security +Header set X-Frame-Options "SAMEORIGIN" +Header set X-Content-Type-Options "nosniff" +Header set X-XSS-Protection "1; mode=block" +Header set Referrer-Policy "strict-origin-when-cross-origin" +Header unset Server +Header unset X-Powered-By + +# Brute Force Protection +BruteForceProtection On +BruteForceAllowedAttempts 5 +BruteForceWindow 300 +BruteForceAction throttle +BruteForceXForwardedFor On + +# Aggressive Caching + + Header set Cache-Control "max-age=31536000, public, immutable" + Header unset ETag + + + + Header set Cache-Control "max-age=31536000, public, immutable" + Header set Access-Control-Allow-Origin "*" + + + + Header set Cache-Control "max-age=2592000, public" + + +# PHP Optimization +php_value memory_limit 256M +php_value max_execution_time 300 +php_value upload_max_filesize 64M +php_value post_max_size 64M +php_flag display_errors off +php_flag log_errors on +``` + +### Example 2: WooCommerce E-commerce + +```apache +# Security Headers +Header set X-Frame-Options "SAMEORIGIN" +Header set X-Content-Type-Options "nosniff" +Header set X-XSS-Protection "1; mode=block" + +# Strict Brute Force Protection +BruteForceProtection On +BruteForceAllowedAttempts 3 +BruteForceWindow 900 +BruteForceAction block +BruteForceProtectPath /my-account/ +BruteForceProtectPath /checkout/ + +# Product Image Caching + + Header set Cache-Control "max-age=31536000, public, immutable" + + +# Don't Cache Checkout/Cart + + Header set Cache-Control "no-cache, no-store, must-revalidate" + + +# PHP for WooCommerce +php_value memory_limit 512M +php_value max_execution_time 600 +php_value max_input_vars 10000 +php_value upload_max_filesize 128M +php_value post_max_size 128M +``` + +### Example 3: API Server + +```apache +# CORS for API +Header set Access-Control-Allow-Origin "*" +Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" +Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-API-Key" +Header set Access-Control-Max-Age "86400" + +# JSON Response Headers + + Header set Content-Type "application/json; charset=utf-8" + Header set X-Content-Type-Options "nosniff" + Header set Cache-Control "no-cache, must-revalidate" + + +# API Rate Limiting +BruteForceProtection On +BruteForceAllowedAttempts 100 +BruteForceWindow 60 +BruteForceAction throttle +BruteForceProtectPath /api/ + +# Environment +SetEnv API_VERSION v2 +SetEnv API_ENVIRONMENT production +``` + +### Example 4: Static Site with CDN + +```apache +# Aggressive Caching +ExpiresActive On +ExpiresByType image/jpeg A31557600 +ExpiresByType image/png A31557600 +ExpiresByType image/gif A31557600 +ExpiresByType text/css A31557600 +ExpiresByType application/javascript A31557600 +ExpiresByType text/html A3600 + +# CORS for CDN +Header set Access-Control-Allow-Origin "*" + +# Security Headers +Header set X-Frame-Options "SAMEORIGIN" +Header set X-Content-Type-Options "nosniff" +Header set Content-Security-Policy "default-src 'self' https://cdn.example.com" + +# Remove Server Info +Header unset Server +Header unset X-Powered-By +``` + +### Example 5: Multi-Environment Setup + +**Production (.htaccess):** +```apache +SetEnv APPLICATION_ENV production +php_flag display_errors off +php_flag log_errors on +BruteForceProtection On +BruteForceAction block +Header set X-Robots-Tag "index, follow" +``` + +**Staging (staging.example.com/.htaccess):** +```apache +SetEnv APPLICATION_ENV staging +php_flag display_errors on +BruteForceProtection On +BruteForceAction log +Header set X-Robots-Tag "noindex, nofollow" + +# IP Restriction +Order deny,allow +Deny from all +Allow from 203.0.113.50 +``` + +**Development (dev.example.com/.htaccess):** +```apache +SetEnv APPLICATION_ENV development +php_flag display_errors on +php_value error_reporting 32767 +BruteForceProtection Off +Header set X-Robots-Tag "noindex, nofollow" +``` + +--- + +## Troubleshooting + +### Common Issues + +#### 1. Directives Not Working + +**Symptoms:** Headers not appearing, PHP settings not applied. + +**Solutions:** + +```bash +# Check module is installed +ls -la /usr/local/lsws/modules/cyberpanel_ols.so +# Should show 147KB file + +# Check module is loaded in config +grep cyberpanel_ols /usr/local/lsws/conf/httpd_config.conf +# Should show: module cyberpanel_ols { + +# Restart OpenLiteSpeed +/usr/local/lsws/bin/lswsctrl restart + +# Check logs for errors +tail -50 /usr/local/lsws/logs/error.log +``` + +#### 2. .htaccess File Permissions + +**Symptoms:** 500 Internal Server Error + +**Solutions:** + +```bash +# Set correct permissions +chmod 644 /home/yourdomain.com/public_html/.htaccess + +# Set correct ownership +chown nobody:nogroup /home/yourdomain.com/public_html/.htaccess + +# Verify +ls -la /home/yourdomain.com/public_html/.htaccess +# Should show: -rw-r--r-- nobody nogroup +``` + +#### 3. Headers Not Showing + +**Symptoms:** `curl -I` doesn't show custom headers + +**Solutions:** + +```bash +# Clear browser cache +# Some headers are cached aggressively + +# Test with curl (bypasses cache) +curl -I https://yourdomain.com + +# Test specific file +curl -I https://yourdomain.com/test.jpg + +# Check if file exists +ls -la /home/yourdomain.com/public_html/test.jpg + +# Verify .htaccess syntax +cat /home/yourdomain.com/public_html/.htaccess +``` + +#### 4. PHP Directives Not Applied + +**Symptoms:** `phpinfo()` shows old values + +**Solutions:** + +```bash +# Verify using LSPHP (not PHP-FPM) +# CyberPanel uses LSPHP by default + +# Check if directive is allowed +# Some directives are PHP_INI_SYSTEM only + +# Create test file +echo '' > /home/yourdomain.com/public_html/info.php + +# Check value +curl https://yourdomain.com/info.php | grep memory_limit + +# Delete test file +rm /home/yourdomain.com/public_html/info.php +``` + +#### 5. Brute Force Protection Not Triggering + +**Symptoms:** Can submit unlimited login attempts + +**Solutions:** + +```bash +# Check shared memory directory +ls -la /dev/shm/ols/ +# Should show BFProt.shm and BFProt.lock + +# Create if missing +mkdir -p /dev/shm/ols +chmod 755 /dev/shm/ols + +# Check .htaccess syntax +grep BruteForce /home/yourdomain.com/public_html/.htaccess + +# Must be POST request to protected path +curl -X POST https://yourdomain.com/wp-login.php -d "log=test&pwd=test" + +# Check logs +grep BruteForce /usr/local/lsws/logs/error.log + +# Restart +/usr/local/lsws/bin/lswsctrl restart +``` + +#### 6. Access Control Allowing All + +**Symptoms:** IP restrictions not working + +**Solutions:** + +```bash +# Verify your actual IP +curl ifconfig.me + +# Check CIDR syntax +# 192.168.1.0/24 = 192.168.1.1 to 192.168.1.254 +# 10.0.0.0/8 = 10.0.0.0 to 10.255.255.255 + +# Check logs for access decisions +grep "cyberpanel_access" /usr/local/lsws/logs/error.log + +# Test with curl from different IP +curl -I https://yourdomain.com +# Should get 403 if not allowed +``` + +#### 7. Redirect Loop + +**Symptoms:** ERR_TOO_MANY_REDIRECTS + +**Solutions:** + +```bash +# Check for conflicting redirects +grep Redirect /home/yourdomain.com/public_html/.htaccess + +# Common mistake: +# BAD: Both redirects active +# Redirect 301 / https://example.com/ +# Redirect 301 / https://www.example.com/ + +# GOOD: Only one +Redirect 301 / https://www.example.com/ + +# Check WordPress settings +# wp-admin > Settings > General +# WordPress Address and Site Address must match +``` + +### Getting Help + +#### Enable Debug Logging + +```bash +# Edit OpenLiteSpeed config +nano /usr/local/lsws/conf/httpd_config.conf + +# Change Log Level to DEBUG +# Restart +/usr/local/lsws/bin/lswsctrl restart + +# Monitor logs +tail -f /usr/local/lsws/logs/error.log +``` + +#### Collect Information + +```bash +# Module version +ls -lh /usr/local/lsws/modules/cyberpanel_ols.so + +# OpenLiteSpeed version +/usr/local/lsws/bin/openlitespeed -v + +# Check .htaccess +cat /home/yourdomain.com/public_html/.htaccess + +# Recent logs +tail -100 /usr/local/lsws/logs/error.log + +# Test headers +curl -I https://yourdomain.com +``` + +#### Report Issue + +When reporting issues, include: + +1. **What you're trying to do** (which feature) +2. **.htaccess content** (sanitized) +3. **Expected behavior** vs **actual behavior** +4. **Error logs** (last 50 lines) +5. **Test results** (curl output) +6. **Module version** and **OpenLiteSpeed version** + +--- + +## Performance Optimization + +### Best Practices + +1. **Minimize .htaccess size** - Only include necessary directives +2. **Use FilesMatch carefully** - Each pattern adds regex overhead +3. **Prefer block over throttle** - Throttle holds connections longer +4. **Whitelist known IPs** - Skips brute force checks entirely +5. **Set long cache times** - Reduce server load + +### Benchmarks + +| Metric | Value | +|--------|-------| +| Overhead per request | < 1ms | +| Memory per cached .htaccess | ~2KB | +| Memory per tracked IP (brute force) | ~64 bytes | +| Cache invalidation | mtime-based (instant) | + +### Optimization Examples + +**Before (Slow):** +```apache +# Every request checks all patterns +Header set X-Custom "Value" +Header set X-Another "Value" +Header set X-More "Value" + + + Header set Cache-Control "max-age=3600" + +``` + +**After (Fast):** +```apache +# Only static assets checked + + Header set Cache-Control "max-age=31536000, public, immutable" + +``` + +--- + +## Appendix + +### Quick Reference + +#### Headers +```apache +Header set Name "Value" +Header unset Name +Header append Name "Value" +``` + +#### Access Control +```apache +Order deny,allow +Deny from all +Allow from 192.168.1.0/24 +``` + +#### Redirects +```apache +Redirect 301 /old /new +RedirectMatch 301 ^/blog/(.*)$ /news/$1 +``` + +#### PHP +```apache +php_value memory_limit 256M +php_flag display_errors off +``` + +#### Brute Force +```apache +BruteForceProtection On +BruteForceAllowedAttempts 5 +BruteForceWindow 300 +BruteForceAction throttle +``` + +### Common MIME Types + +``` +image/jpeg, image/png, image/gif, image/webp, image/svg+xml +text/css, text/html, text/javascript, text/plain +application/javascript, application/json, application/xml, application/pdf +font/ttf, font/woff, font/woff2 +``` + +### Time Duration Reference + +``` +1 minute = 60 +5 minutes = 300 +15 minutes = 900 +1 hour = 3600 +1 day = 86400 +1 week = 604800 +1 month = 2592000 +1 year = 31557600 +``` + +### IP CIDR Cheat Sheet + +``` +/32 = 1 IP (255.255.255.255) +/24 = 256 IPs (255.255.255.0) +/16 = 65,536 IPs (255.255.0.0) +/8 = 16,777,216 IPs (255.0.0.0) +``` + +--- + +## Support + +- **GitHub:** [github.com/usmannasir/cyberpanel_ols](https://github.com/usmannasir/cyberpanel_ols) +- **Community:** [community.cyberpanel.net](https://community.cyberpanel.net) + +--- + +**Document Version:** 1.0 +**Module Version:** 2.2.0 +**Last Updated:** December 28, 2025 + +--- + +*Thank you for using the CyberPanel OpenLiteSpeed Module!* diff --git a/install/OpenLiteSpeed_htaccess_Module_Documentation.md b/install/OpenLiteSpeed_htaccess_Module_Documentation.md deleted file mode 100644 index e69de29bb..000000000 From aa7779083437b4d81e6705cb641dfed737e91da6 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 4 Jan 2026 00:13:27 +0100 Subject: [PATCH 083/129] Remove pagination from SSH Logins and SSH Logs tables, display all results directly --- .../baseTemplate/custom-js/system-status.js | 263 ++++++++++- .../templates/baseTemplate/homePage.html | 420 +++++++++++++++++- 2 files changed, 656 insertions(+), 27 deletions(-) diff --git a/baseTemplate/static/baseTemplate/custom-js/system-status.js b/baseTemplate/static/baseTemplate/custom-js/system-status.js index 63c4e4e42..b1f6e34b1 100644 --- a/baseTemplate/static/baseTemplate/custom-js/system-status.js +++ b/baseTemplate/static/baseTemplate/custom-js/system-status.js @@ -936,43 +936,187 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { // SSH Logins $scope.sshLogins = []; + $scope.sshLoginsPaginated = []; + $scope.sshLoginsCurrentPage = 1; + $scope.sshLoginsPerPage = 10; + $scope.sshLoginsGoToPage = 1; $scope.loadingSSHLogins = true; $scope.errorSSHLogins = ''; + + $scope.getSSHLoginsTotalPages = function() { + return Math.ceil($scope.sshLogins.length / $scope.sshLoginsPerPage); + }; + + $scope.getSSHLoginsStart = function() { + if (!$scope.sshLogins || $scope.sshLogins.length === 0) { + return 0; + } + return ($scope.sshLoginsCurrentPage - 1) * $scope.sshLoginsPerPage + 1; + }; + + $scope.getSSHLoginsEnd = function() { + if (!$scope.sshLogins || $scope.sshLogins.length === 0) { + return 0; + } + var end = $scope.sshLoginsCurrentPage * $scope.sshLoginsPerPage; + return Math.min(end, $scope.sshLogins.length); + }; + + $scope.updateSSHLoginsPaginated = function() { + if (!$scope.sshLogins || $scope.sshLogins.length === 0) { + $scope.sshLoginsPaginated = []; + console.log('updateSSHLoginsPaginated: No data, cleared paginated array'); + return; + } + var start = ($scope.sshLoginsCurrentPage - 1) * $scope.sshLoginsPerPage; + var end = start + $scope.sshLoginsPerPage; + $scope.sshLoginsPaginated = $scope.sshLogins.slice(start, end); + console.log('updateSSHLoginsPaginated: start=', start, 'end=', end, 'total=', $scope.sshLogins.length, 'paginated=', $scope.sshLoginsPaginated.length); + }; + + $scope.sshLoginsPrevPage = function() { + if ($scope.sshLoginsCurrentPage > 1) { + $scope.sshLoginsCurrentPage--; + $scope.updateSSHLoginsPaginated(); + } + }; + + $scope.sshLoginsNextPage = function() { + if ($scope.sshLoginsCurrentPage < $scope.getSSHLoginsTotalPages()) { + $scope.sshLoginsCurrentPage++; + $scope.updateSSHLoginsPaginated(); + } + }; + + $scope.sshLoginsGoToPageNumber = function() { + var page = parseInt($scope.sshLoginsGoToPage); + var totalPages = $scope.getSSHLoginsTotalPages(); + if (page >= 1 && page <= totalPages) { + $scope.sshLoginsCurrentPage = page; + $scope.updateSSHLoginsPaginated(); + } else { + $scope.sshLoginsGoToPage = $scope.sshLoginsCurrentPage; + } + }; + $scope.refreshSSHLogins = function() { $scope.loadingSSHLogins = true; $http.get('/base/getRecentSSHLogins').then(function (response) { $scope.loadingSSHLogins = false; - if (response.data && response.data.logins) { + console.log('SSH Logins response:', response.data); + if (response.data && response.data.logins && Array.isArray(response.data.logins)) { $scope.sshLogins = response.data.logins; + $scope.sshLoginsCurrentPage = 1; + $scope.sshLoginsGoToPage = 1; + console.log('SSH Logins loaded:', $scope.sshLogins.length, 'items'); + $scope.updateSSHLoginsPaginated(); + console.log('SSH Logins paginated:', $scope.sshLoginsPaginated.length, 'items'); } else { + console.warn('SSH Logins: No data or invalid format', response.data); $scope.sshLogins = []; + $scope.sshLoginsPaginated = []; } }, function (err) { $scope.loadingSSHLogins = false; + console.error('SSH Logins error:', err); $scope.errorSSHLogins = 'Failed to load SSH logins.'; + $scope.sshLogins = []; + $scope.sshLoginsPaginated = []; }); }; // SSH Logs $scope.sshLogs = []; + $scope.sshLogsPaginated = []; + $scope.sshLogsCurrentPage = 1; + $scope.sshLogsPerPage = 10; + $scope.sshLogsGoToPage = 1; $scope.loadingSSHLogs = true; $scope.errorSSHLogs = ''; $scope.securityAlerts = []; $scope.loadingSecurityAnalysis = false; + + $scope.getSSHLogsTotalPages = function() { + return Math.ceil($scope.sshLogs.length / $scope.sshLogsPerPage); + }; + + $scope.getSSHLogsStart = function() { + if (!$scope.sshLogs || $scope.sshLogs.length === 0) { + return 0; + } + return ($scope.sshLogsCurrentPage - 1) * $scope.sshLogsPerPage + 1; + }; + + $scope.getSSHLogsEnd = function() { + if (!$scope.sshLogs || $scope.sshLogs.length === 0) { + return 0; + } + var end = $scope.sshLogsCurrentPage * $scope.sshLogsPerPage; + return Math.min(end, $scope.sshLogs.length); + }; + + $scope.updateSSHLogsPaginated = function() { + if (!$scope.sshLogs || $scope.sshLogs.length === 0) { + $scope.sshLogsPaginated = []; + console.log('updateSSHLogsPaginated: No data, cleared paginated array'); + return; + } + var start = ($scope.sshLogsCurrentPage - 1) * $scope.sshLogsPerPage; + var end = start + $scope.sshLogsPerPage; + $scope.sshLogsPaginated = $scope.sshLogs.slice(start, end); + console.log('updateSSHLogsPaginated: start=', start, 'end=', end, 'total=', $scope.sshLogs.length, 'paginated=', $scope.sshLogsPaginated.length); + }; + + $scope.sshLogsPrevPage = function() { + if ($scope.sshLogsCurrentPage > 1) { + $scope.sshLogsCurrentPage--; + $scope.updateSSHLogsPaginated(); + } + }; + + $scope.sshLogsNextPage = function() { + if ($scope.sshLogsCurrentPage < $scope.getSSHLogsTotalPages()) { + $scope.sshLogsCurrentPage++; + $scope.updateSSHLogsPaginated(); + } + }; + + $scope.sshLogsGoToPageNumber = function() { + var page = parseInt($scope.sshLogsGoToPage); + var totalPages = $scope.getSSHLogsTotalPages(); + if (page >= 1 && page <= totalPages) { + $scope.sshLogsCurrentPage = page; + $scope.updateSSHLogsPaginated(); + } else { + $scope.sshLogsGoToPage = $scope.sshLogsCurrentPage; + } + }; + $scope.refreshSSHLogs = function() { $scope.loadingSSHLogs = true; $http.get('/base/getRecentSSHLogs').then(function (response) { $scope.loadingSSHLogs = false; - if (response.data && response.data.logs) { + console.log('SSH Logs response:', response.data); + if (response.data && response.data.logs && Array.isArray(response.data.logs)) { $scope.sshLogs = response.data.logs; + $scope.sshLogsCurrentPage = 1; + $scope.sshLogsGoToPage = 1; + console.log('SSH Logs loaded:', $scope.sshLogs.length, 'items'); + $scope.updateSSHLogsPaginated(); + console.log('SSH Logs paginated:', $scope.sshLogsPaginated.length, 'items'); // Analyze logs for security issues $scope.analyzeSSHSecurity(); } else { + console.warn('SSH Logs: No data or invalid format', response.data); $scope.sshLogs = []; + $scope.sshLogsPaginated = []; } }, function (err) { $scope.loadingSSHLogs = false; + console.error('SSH Logs error:', err); $scope.errorSSHLogs = 'Failed to load SSH logs.'; + $scope.sshLogs = []; + $scope.sshLogsPaginated = []; }); }; @@ -1499,16 +1643,48 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { var match = login.raw.match(/(pts\/[0-9]+)/); if (match) tty = match[1]; } - $http.post('/base/getSSHUserActivity', { user: login.user, tty: tty }).then(function(response) { + console.log('Fetching SSH activity for user:', login.user, 'IP:', login.ip, 'TTY:', tty); + $http.post('/base/getSSHUserActivity', { user: login.user, tty: tty, ip: login.ip || '' }, { + timeout: 30000 + }).then(function(response) { + console.log('SSH Activity response received:', response); $scope.loadingSSHActivity = false; - if (response.data) { + if (response.data && response.data.error) { + console.error('SSH Activity error:', response.data.error); + $scope.errorSSHActivity = response.data.error; + $scope.sshActivity = { processes: [], w: [], shell_history: [], geoip: {}, disk_usage: '' }; + } else if (response.data) { + console.log('SSH Activity data:', response.data); $scope.sshActivity = response.data; + $scope.errorSSHActivity = ''; } else { - $scope.sshActivity = { processes: [], w: [] }; + console.warn('SSH Activity: No data in response'); + $scope.sshActivity = { processes: [], w: [], shell_history: [], geoip: {}, disk_usage: '' }; + $scope.errorSSHActivity = 'No data received from server.'; } }, function(err) { $scope.loadingSSHActivity = false; - $scope.errorSSHActivity = (err.data && err.data.error) ? err.data.error : 'Failed to fetch activity.'; + console.error('Error fetching SSH activity:', err); + console.error('Error status:', err.status); + console.error('Error data:', err.data); + if (err.status === 0) { + $scope.errorSSHActivity = 'Network error: Unable to connect to server. Please check your connection.'; + } else if (err.status === -1) { + $scope.errorSSHActivity = 'Request timeout. The server took too long to respond.'; + } else if (err.data && err.data.error) { + $scope.errorSSHActivity = err.data.error; + } else if (err.data && err.data.errorMessage) { + $scope.errorSSHActivity = err.data.errorMessage; + } else if (err.status === 403) { + $scope.errorSSHActivity = 'Access denied. Admin privileges required.'; + } else if (err.status === 400) { + $scope.errorSSHActivity = 'Invalid request. Please try again.'; + } else if (err.status === 500) { + $scope.errorSSHActivity = 'Server error. Please try again later.'; + } else { + $scope.errorSSHActivity = 'Failed to fetch activity. Status: ' + (err.status || 'Unknown') + '. Please check your connection and try again.'; + } + $scope.sshActivity = { processes: [], w: [], shell_history: [], geoip: {}, disk_usage: '' }; }); }; @@ -1526,4 +1702,79 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.closeSSHActivityModal(); } }; + + // Kill a specific process + $scope.killProcess = function(pid, user) { + if (!confirm('Are you sure you want to kill process ' + pid + '? This action cannot be undone.')) { + return; + } + + console.log('Killing process:', pid, 'for user:', user); + $http.post('/base/killSSHProcess', { pid: pid, user: user }, { timeout: 10000 }).then(function(response) { + if (response.data && response.data.success) { + alert('Process ' + pid + ' killed successfully.'); + // Reload activity data + if ($scope.sshActivityUser) { + var login = { user: $scope.sshActivityUser, ip: '', tty: '' }; + $scope.viewSSHActivity(login); + } + } else if (response.data && response.data.error) { + alert('Error: ' + response.data.error); + } else { + alert('Unknown error occurred.'); + } + }, function(err) { + console.error('Error killing process:', err); + var errorMsg = 'Failed to kill process. '; + if (err.data && err.data.error) { + errorMsg += err.data.error; + } else if (err.status === 403) { + errorMsg += 'Access denied.'; + } else if (err.status === 404) { + errorMsg += 'Process not found.'; + } else { + errorMsg += 'Please try again.'; + } + alert(errorMsg); + }); + }; + + // Kill all sessions for a user + $scope.killSession = function(user) { + if (!confirm('WARNING: This will force close ALL active sessions for user "' + user + '". This action cannot be undone.\n\nAre you sure you want to continue?')) { + return; + } + + if (!confirm('Final confirmation: Kill all sessions for user "' + user + '"?')) { + return; + } + + console.log('Killing session for user:', user); + $http.post('/base/killSSHSession', { user: user }, { timeout: 10000 }).then(function(response) { + if (response.data && response.data.success) { + alert('All sessions for user ' + user + ' have been terminated successfully.'); + // Close modal and refresh SSH logins + $scope.closeSSHActivityModal(); + // Refresh SSH logins list + if (typeof $scope.loadSSHLogins === 'function') { + $scope.loadSSHLogins(); + } + } else if (response.data && response.data.error) { + alert('Error: ' + response.data.error); + } else { + alert('Unknown error occurred.'); + } + }, function(err) { + console.error('Error killing session:', err); + var errorMsg = 'Failed to kill session. '; + if (err.data && err.data.error) { + errorMsg += err.data.error; + } else if (err.status === 403) { + errorMsg += 'Access denied.'; + } else { + errorMsg += 'Please try again.'; + } + alert(errorMsg); + }); + }; }); \ No newline at end of file diff --git a/baseTemplate/templates/baseTemplate/homePage.html b/baseTemplate/templates/baseTemplate/homePage.html index d6b4409c2..96c50990d 100644 --- a/baseTemplate/templates/baseTemplate/homePage.html +++ b/baseTemplate/templates/baseTemplate/homePage.html @@ -234,25 +234,161 @@ width: 100%; border-collapse: collapse; margin-top: 15px; + display: table; + } + + .activity-table thead { + display: table-header-group; + } + + .activity-table tbody { + display: table-row-group; + } + + .activity-table tr { + display: table-row; + } + + .activity-table th, + .activity-table td { + display: table-cell !important; + } + + .activity-table tr { + display: table-row !important; + } + + .activity-table thead { + display: table-header-group !important; + } + + .activity-table tbody { + display: table-row-group !important; } .activity-table th { text-align: left; - padding: 12px 15px; + padding: 14px 12px; font-size: 11px; font-weight: 700; - color: var(--text-secondary, #64748b); + color: #ffffff; text-transform: uppercase; letter-spacing: 0.8px; - border-bottom: 2px solid var(--border-color, #e8e9ff); + border-bottom: 2px solid rgba(91, 95, 207, 0.3); + background: linear-gradient(135deg, #5b5fcf 0%, #4a4fc7 100%); + position: sticky; + top: 0; + z-index: 10; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + vertical-align: middle; + white-space: nowrap; + } + + .activity-table thead { + background: linear-gradient(135deg, #5b5fcf 0%, #4a4fc7 100%); + } + + .activity-table tbody tr { + border-bottom: 1px solid var(--border-color, #f0f0ff); + } + + .activity-table tbody tr:hover { background: var(--bg-hover, #f8f9ff); } .activity-table td { - padding: 12px 15px; + padding: 12px 12px; font-size: 13px; color: var(--text-primary, #2f3640); border-bottom: 1px solid var(--border-color, #f0f0ff); + vertical-align: middle; + word-wrap: break-word; + overflow-wrap: break-word; + } + + .activity-table td:nth-child(1) { + min-width: 80px; + max-width: 120px; + } + + .activity-table td:nth-child(2) { + min-width: 120px; + max-width: 180px; + font-family: monospace; + font-size: 12px; + } + + .activity-table td:nth-child(3) { + min-width: 80px; + max-width: 120px; + } + + .activity-table td:nth-child(4) { + min-width: 140px; + max-width: 200px; + white-space: nowrap; + } + + .activity-table td:nth-child(5) { + min-width: 150px; + max-width: 300px; + } + + .activity-table td:nth-child(6) { + min-width: 120px; + text-align: center; + } + + /* SSH Logs table specific column widths */ + .ssh-logs-table th:nth-child(1), + .ssh-logs-table td:nth-child(1) { + min-width: 180px; + max-width: 220px; + white-space: nowrap; + } + + .ssh-logs-table th:nth-child(2), + .ssh-logs-table td:nth-child(2) { + min-width: 300px; + word-break: break-word; + overflow-wrap: break-word; + } + + /* Process table specific column widths (for modal only) */ + .process-table thead th:nth-child(1), + .process-table tbody td:nth-child(1) { + min-width: 70px; + max-width: 90px; + } + + .process-table thead th:nth-child(2), + .process-table tbody td:nth-child(2) { + min-width: 80px; + max-width: 100px; + } + + .process-table thead th:nth-child(3), + .process-table tbody td:nth-child(3) { + min-width: 90px; + max-width: 120px; + } + + .process-table thead th:nth-child(4), + .process-table tbody td:nth-child(4) { + min-width: 200px; + } + + .process-table thead th:nth-child(5), + .process-table tbody td:nth-child(5) { + min-width: 150px; + max-width: 250px; + } + + .process-table thead th:nth-child(6), + .process-table tbody td:nth-child(6) { + min-width: 100px; + max-width: 120px; + text-align: center; } .activity-table tr:hover { @@ -278,6 +414,87 @@ box-shadow: 0 2px 4px rgba(91,95,207,0.3); } + /* Pagination Styles */ + .pagination-container { + margin-top: 20px; + padding: 16px 20px; + background: #ffffff; + border: 1px solid #e8e9ff; + border-radius: 12px; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 12px; + } + + .pagination-info { + color: #64748b; + font-size: 13px; + font-weight: 500; + } + + .pagination-controls { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + } + + .pagination-btn { + background: #f8f9ff; + border: 1px solid #e8e9ff; + color: #5b5fcf; + padding: 8px 16px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 6px; + } + + .pagination-btn:hover:not(:disabled) { + background: #5b5fcf; + color: #ffffff; + border-color: #5b5fcf; + } + + .pagination-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .pagination-page-info { + color: #2f3640; + font-size: 13px; + font-weight: 600; + padding: 0 8px; + } + + .pagination-goto { + display: flex; + align-items: center; + gap: 6px; + } + + .pagination-goto input { + width: 80px; + padding: 6px 8px; + border: 1px solid #e8e9ff; + border-radius: 6px; + font-size: 12px; + margin-right: 6px; + } + + .pagination-goto input:focus { + outline: none; + border-color: #5b5fcf; + box-shadow: 0 0 0 3px rgba(91, 95, 207, 0.1); + } + .chart-container { height: 280px; position: relative; @@ -297,17 +514,21 @@ height: 100vh; background: rgba(0,0,0,0.5); z-index: 10000; - display: flex; + display: none; align-items: center; justify-content: center; padding: 20px; backdrop-filter: blur(2px); - /* Initially hidden to prevent flicker on page load */ - display: none; + overflow-y: auto; } .modal-backdrop.show { - display: flex; + display: flex !important; + } + + /* Ensure modal is hidden when ng-show is false */ + .modal-backdrop.ng-hide { + display: none !important; } .modal-content { @@ -321,6 +542,31 @@ position: relative; overflow-y: auto; animation: modalFadeIn 0.3s ease-out; + margin: auto; + align-self: center; + } + + /* Ensure tables inside modal render correctly */ + .modal-content table { + display: table !important; + width: 100% !important; + } + + .modal-content table thead { + display: table-header-group !important; + } + + .modal-content table tbody { + display: table-row-group !important; + } + + .modal-content table tr { + display: table-row !important; + } + + .modal-content table th, + .modal-content table td { + display: table-cell !important; } @keyframes modalFadeIn { @@ -545,11 +791,14 @@ - {$ login.user $} - {$ login.ip $} - {$ login.country $} + {$ login.user $} + {$ login.ip $} + + + {$ login.country || 'N/A' $} + {$ login.date $} - {$ login.session $} + {$ login.session $} @@ -681,7 +930,7 @@
No recent SSH logs found.
- +
@@ -690,8 +939,8 @@ - - + +
TIMESTAMP
{$ log.timestamp $}{$ log.message $}{$ log.timestamp $}{$ log.message $}
@@ -754,7 +1003,7 @@
-
- + {% endblock %} From 5da5021675aa1afb02e9dbd0403c326af7061920 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Thu, 5 Mar 2026 05:34:32 +0500 Subject: [PATCH 108/129] Fix account switcher: use currentEmail as ng-model so display updates immediately --- webmail/static/webmail/webmail.js | 12 ++++++------ webmail/templates/webmail/index.html | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/webmail/static/webmail/webmail.js b/webmail/static/webmail/webmail.js index 64c154de6..0899eda74 100644 --- a/webmail/static/webmail/webmail.js +++ b/webmail/static/webmail/webmail.js @@ -109,7 +109,6 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', function($ // ── State ──────────────────────────────────────────────── $scope.currentEmail = ''; $scope.managedAccounts = []; - $scope.switchEmail = ''; $scope.folders = []; $scope.currentFolder = 'INBOX'; $scope.messages = []; @@ -169,7 +168,6 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', function($ if (data.status === 1) { $scope.currentEmail = data.email; $scope.managedAccounts = data.accounts || []; - $scope.switchEmail = data.email; $scope.loadFolders(); $scope.loadSettings(); } else { @@ -180,10 +178,10 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', function($ // ── Account Switching ──────────────────────────────────── $scope.switchAccount = function() { - if (!$scope.switchEmail || $scope.switchEmail === $scope.currentEmail) return; - apiCall('/webmail/api/switchAccount', {email: $scope.switchEmail}, function(data) { + var newEmail = $scope.currentEmail; + if (!newEmail) return; + apiCall('/webmail/api/switchAccount', {email: newEmail}, function(data) { if (data.status === 1) { - $scope.currentEmail = data.email; $scope.currentFolder = 'INBOX'; $scope.currentPage = 1; $scope.openMsg = null; @@ -195,8 +193,10 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', function($ $scope.loadFolders(); $scope.loadSettings(); } else { - notify(data.error_message, 'error'); + notify(data.error_message || 'Failed to switch account', 'error'); } + }, function() { + notify('Failed to switch account', 'error'); }); }; diff --git a/webmail/templates/webmail/index.html b/webmail/templates/webmail/index.html index bb9027296..e684057c7 100644 --- a/webmail/templates/webmail/index.html +++ b/webmail/templates/webmail/index.html @@ -15,7 +15,7 @@ {$ currentEmail $}
@@ -435,6 +435,6 @@
- + {% endblock %} From df7b1d3a6415795e4692e18d70e1d5df1bc4fa56 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Thu, 5 Mar 2026 05:39:55 +0500 Subject: [PATCH 109/129] Fix account switcher: send fromAccount with every API call instead of relying solely on session --- webmail/static/webmail/webmail.js | 35 ++++++++++++++++++---------- webmail/templates/webmail/index.html | 11 ++++++++- webmail/webmailManager.py | 22 ++++++++++++++++- 3 files changed, 54 insertions(+), 14 deletions(-) diff --git a/webmail/static/webmail/webmail.js b/webmail/static/webmail/webmail.js index 0899eda74..b1d3bb9a5 100644 --- a/webmail/static/webmail/webmail.js +++ b/webmail/static/webmail/webmail.js @@ -149,7 +149,12 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', function($ // ── Helper ─────────────────────────────────────────────── function apiCall(url, data, callback, errback) { var config = {headers: {'X-CSRFToken': getCookie('csrftoken')}}; - $http.post(url, data || {}, config).then(function(resp) { + var payload = data || {}; + // Always send current account so backend uses the right email + if ($scope.currentEmail && !payload.fromAccount) { + payload.fromAccount = $scope.currentEmail; + } + $http.post(url, payload, config).then(function(resp) { if (callback) callback(resp.data); }, function(err) { console.error('API error:', url, err); @@ -180,23 +185,28 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', function($ $scope.switchAccount = function() { var newEmail = $scope.currentEmail; if (!newEmail) return; + + // Reset view state immediately + $scope.currentFolder = 'INBOX'; + $scope.currentPage = 1; + $scope.openMsg = null; + $scope.viewMode = 'list'; + $scope.messages = []; + $scope.contacts = []; + $scope.filteredContacts = []; + $scope.sieveRules = []; + apiCall('/webmail/api/switchAccount', {email: newEmail}, function(data) { if (data.status === 1) { - $scope.currentFolder = 'INBOX'; - $scope.currentPage = 1; - $scope.openMsg = null; - $scope.viewMode = 'list'; - $scope.messages = []; - $scope.contacts = []; - $scope.filteredContacts = []; - $scope.sieveRules = []; $scope.loadFolders(); $scope.loadSettings(); } else { notify(data.error_message || 'Failed to switch account', 'error'); + console.error('switchAccount failed:', data); } - }, function() { - notify('Failed to switch account', 'error'); + }, function(err) { + notify('Failed to switch account: ' + (err.status || 'unknown error'), 'error'); + console.error('switchAccount HTTP error:', err); }); }; @@ -488,6 +498,7 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', function($ stopDraftAutoSave(); var fd = new FormData(); + fd.append('fromAccount', $scope.currentEmail || ''); fd.append('to', $scope.compose.to); fd.append('cc', $scope.compose.cc || ''); fd.append('bcc', $scope.compose.bcc || ''); @@ -508,7 +519,7 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', function($ }).then(function(resp) { $scope.sending = false; if (resp.data.status === 1) { - notify('Message sent.'); + notify('Message sent from ' + (resp.data.sentFrom || 'unknown')); $scope.viewMode = 'list'; $scope.loadMessages(); } else { diff --git a/webmail/templates/webmail/index.html b/webmail/templates/webmail/index.html index e684057c7..ab5b137e6 100644 --- a/webmail/templates/webmail/index.html +++ b/webmail/templates/webmail/index.html @@ -175,6 +175,15 @@

{% trans "Compose" %}

+
+ + + +
- + {% endblock %} diff --git a/webmail/webmailManager.py b/webmail/webmailManager.py index 3895c519d..78e380c60 100644 --- a/webmail/webmailManager.py +++ b/webmail/webmailManager.py @@ -46,6 +46,18 @@ class WebmailManager: return self.request.POST.dict() def _get_email(self): + # Check for explicit email in POST body (from account switcher) + # This ensures the correct account is used even if session is stale + try: + data = json.loads(self.request.body) + explicit = data.get('fromAccount', '') + if explicit: + accounts = self._get_managed_accounts() + if explicit in accounts: + self.request.session['webmail_email'] = explicit + return explicit + except Exception: + pass return self.request.session.get('webmail_email') def _get_master_config(self): @@ -324,6 +336,14 @@ class WebmailManager: def apiSendMessage(self): try: + # For multipart forms, check fromAccount in POST data + if self.request.content_type and 'multipart' in self.request.content_type: + from_account = self.request.POST.get('fromAccount', '') + if from_account: + accounts = self._get_managed_accounts() + if from_account in accounts: + self.request.session['webmail_email'] = from_account + email_addr = self._get_email() if not email_addr: return self._error('Not logged in.') @@ -390,7 +410,7 @@ class WebmailManager: except Exception: pass - return self._success({'messageId': result['message_id']}) + return self._success({'messageId': result['message_id'], 'sentFrom': email_addr}) except Exception as e: return self._error(str(e)) From a8ba63311c05719c505369f2ccc8494a88d87178 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Thu, 5 Mar 2026 05:42:44 +0500 Subject: [PATCH 110/129] Fix account switcher: ng-if creates child scope breaking ng-model binding, use ng-show instead --- webmail/templates/webmail/index.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/webmail/templates/webmail/index.html b/webmail/templates/webmail/index.html index ab5b137e6..d552a3164 100644 --- a/webmail/templates/webmail/index.html +++ b/webmail/templates/webmail/index.html @@ -9,7 +9,7 @@
-