Merge pull request #1510 from master3395/v2.5.5-dev

V2.5.5 dev - Firewall ban button, and management
This commit is contained in:
Usman Nasir 2025-09-23 12:29:21 +05:00 committed by GitHub
commit e91df945ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
109 changed files with 15095 additions and 1944 deletions

8
.gitignore vendored
View File

@ -117,4 +117,10 @@ dmypy.json
/usr/local/CyberCP/
/etc/cyberpanel/
cyberpanel_password.txt
mysql_password.txt
mysql_password.txt
# Development test files
test.php
test.sh
*.test.php
*.test.sh

View File

@ -148,25 +148,51 @@ class CageFS:
@staticmethod
def submitinstallImunify(key):
try:
imunifyKeyPath = '/home/cyberpanel/imunifyKeyPath'
##
writeToFile = open(imunifyKeyPath, 'w')
writeToFile.write(key)
writeToFile.close()
##
mailUtilities.checkHome()
statusFile = open(ServerStatusUtil.lswsInstallStatusPath, 'w')
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Starting Imunify Installation..\n", 1)
"Starting Imunify360 Installation..\n", 1)
##
# CRITICAL: Fix PHP-FPM pool configurations before installation
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Fixing PHP-FPM pool configurations for Imunify360 compatibility..\n", 1)
# Import the upgrade module to access the fix function
from plogical import upgrade
fix_result = upgrade.Upgrade.CreateMissingPoolsforFPM()
if fix_result == 0:
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"PHP-FPM pool configurations fixed successfully..\n", 1)
else:
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Warning: PHP-FPM pool configuration fix had issues, continuing with installation..\n", 1)
# Fix broken package installations that might prevent Imunify360 installation
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Fixing broken package installations..\n", 1)
# Detect OS and fix packages accordingly
if os.path.exists('/etc/redhat-release'):
# CentOS/RHEL/CloudLinux
command = 'yum-complete-transaction --cleanup-only 2>/dev/null || true'
ServerStatusUtil.executioner(command, statusFile)
command = 'yum install -y --skip-broken 2>/dev/null || true'
ServerStatusUtil.executioner(command, statusFile)
else:
# Ubuntu/Debian
command = 'dpkg --configure -a 2>/dev/null || true'
ServerStatusUtil.executioner(command, statusFile)
command = 'apt --fix-broken install -y 2>/dev/null || true'
ServerStatusUtil.executioner(command, statusFile)
command = 'mkdir -p /etc/sysconfig/imunify360/generic'
ServerStatusUtil.executioner(command, statusFile)
@ -226,8 +252,6 @@ pattern_to_watch = ^/home/.+?/(public_html|public_ftp|private_html)(/.*)?$
@staticmethod
def submitinstallImunifyAV():
try:
mailUtilities.checkHome()
statusFile = open(ServerStatusUtil.lswsInstallStatusPath, 'w')
@ -235,7 +259,38 @@ pattern_to_watch = ^/home/.+?/(public_html|public_ftp|private_html)(/.*)?$
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Starting ImunifyAV Installation..\n", 1)
##
# CRITICAL: Fix PHP-FPM pool configurations before installation
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Fixing PHP-FPM pool configurations for ImunifyAV compatibility..\n", 1)
# Import the upgrade module to access the fix function
from plogical import upgrade
fix_result = upgrade.Upgrade.CreateMissingPoolsforFPM()
if fix_result == 0:
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"PHP-FPM pool configurations fixed successfully..\n", 1)
else:
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Warning: PHP-FPM pool configuration fix had issues, continuing with installation..\n", 1)
# Fix broken package installations that might prevent ImunifyAV installation
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Fixing broken package installations..\n", 1)
# Detect OS and fix packages accordingly
if os.path.exists('/etc/redhat-release'):
# CentOS/RHEL/CloudLinux
command = 'yum-complete-transaction --cleanup-only 2>/dev/null || true'
ServerStatusUtil.executioner(command, statusFile)
command = 'yum install -y --skip-broken 2>/dev/null || true'
ServerStatusUtil.executioner(command, statusFile)
else:
# Ubuntu/Debian
command = 'dpkg --configure -a 2>/dev/null || true'
ServerStatusUtil.executioner(command, statusFile)
command = 'apt --fix-broken install -y 2>/dev/null || true'
ServerStatusUtil.executioner(command, statusFile)
command = 'mkdir -p /etc/sysconfig/imunify360'
ServerStatusUtil.executioner(command, statusFile)

View File

@ -2,6 +2,7 @@
import os.path
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
from plogical.errorSanitizer import secure_error_response, secure_log_error
from django.shortcuts import HttpResponse, render
import json
import re
@ -244,9 +245,9 @@ class secMiddleware:
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
except BaseException as msg:
final_dic = {'error_message': f"Error: {str(msg)}",
"errorMessage": f"Error: {str(msg)}"}
except Exception as e:
secure_log_error(e, 'secMiddleware_body_validation')
final_dic = secure_error_response(e, 'Request validation failed')
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
else:

332
README.md
View File

@ -1,8 +1,18 @@
<div align="center">
<img src="https://community.cyberpanel.net/uploads/default/original/1X/416fdec0e96357d11f7b2756166c61b1aeca5939.png" alt="CyberPanel Logo" width="500"/>
# 🛠️ CyberPanel
Web Hosting Control Panel powered by OpenLiteSpeed, designed to simplify hosting management.
> **Current Version**: 2.4 Build 3 | **Last Updated**: September 18, 2025
> **Current Version**: 2.4 Build 4 | **Last Updated**: September 21, 2025
[![GitHub](https://img.shields.io/badge/GitHub-Repository-blue?style=flat-square&logo=github)](https://github.com/usmannasir/cyberpanel)
[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-7289DA?style=flat-square&logo=discord)](https://discord.gg/g8k8Db3)
[![Facebook](https://img.shields.io/badge/Facebook-Community-1877F2?style=flat-square&logo=facebook)](https://www.facebook.com/groups/cyberpanel)
[![YouTube](https://img.shields.io/badge/YouTube-Channel-FF0000?style=flat-square&logo=youtube)](https://www.youtube.com/@Cyber-Panel)
[![Documentation](https://img.shields.io/badge/Documentation-Read%20Now-green?style=flat-square&logo=gitbook)](https://cyberpanel.net/KnowledgeBase/)
---
@ -20,7 +30,55 @@ Web Hosting Control Panel powered by OpenLiteSpeed, designed to simplify hosting
- 📀 **One-click Backups and Restores**.
- 🐳 **Docker Management** with command execution capabilities.
- 🤖 **AI-Powered Security Scanner** for enhanced protection.
- 🔐 **Advanced 2FA Authentication** - TOTP and WebAuthn/Passkey support.
- 📊 **Monthly Bandwidth Reset** - Automatic bandwidth usage reset (Fixed in latest version).
- 🔗 **RESTful API** - Complete API for automation and integration including `listChildDomainsJson` endpoint.
---
## 🔗 **RESTful API**
CyberPanel provides a comprehensive RESTful API for automation and integration:
### **Available API Endpoints**
- **Website Management**: Create, delete, suspend, and manage websites
- **User Management**: Create, delete, and manage user accounts
- **Package Management**: List and manage hosting packages
- **Child Domains**: List child domains with `listChildDomainsJson` endpoint
- **Firewall**: Add and delete firewall rules
- **Backups**: Manage AWS backups and remote transfers
- **System Info**: Get CyberPanel version and system status
### **API Authentication**
All API endpoints require authentication using admin credentials:
```bash
curl -X POST http://your-server:8090/api/listChildDomainsJson \
-H "Content-Type: application/json" \
-d '{
"adminUser": "your_admin_username",
"adminPass": "your_admin_password"
}'
```
### **Response Format**
API responses are returned in JSON format with consistent error handling:
```json
[
{
"parent_site": "example.com",
"domain": "subdomain.example.com",
"path": "/home/example.com/public_html/subdomain",
"ssl": 1,
"php_version": "8.1",
"ip_address": "192.168.1.100"
}
]
```
---
@ -31,6 +89,7 @@ CyberPanel comes with comprehensive documentation and step-by-step guides:
- 📚 **[Complete Guides Index](guides/INDEX.md)** - All available documentation in one place
- 🐳 **[Docker Command Execution](guides/Docker_Command_Execution_Guide.md)** - Execute commands in Docker containers
- 🤖 **[AI Scanner Setup](guides/AIScannerDocs.md)** - Configure AI-powered security scanning
- 🔐 **[2FA Authentication Guide](guides/2FA_AUTHENTICATION_GUIDE.md)** - Complete Two-Factor Authentication and WebAuthn setup
- 📧 **[Mautic Installation](guides/MAUTIC_INSTALLATION_GUIDE.md)** - Email marketing platform setup
- 🎨 **[Custom CSS Guide](guides/CUSTOM_CSS_GUIDE.md)** - Create custom themes for CyberPanel 2.5.5-dev
@ -50,6 +109,8 @@ CyberPanel supports a wide range of PHP versions across different operating syst
- **PHP 8.0** - Legacy support (EOL: Nov 2023)
- **PHP 7.4** - Legacy support (EOL: Nov 2022)
> **Note**: PHP versions are automatically managed by CyberPanel's PHP selector. Third-party repositories may provide additional versions beyond the default support.
### 🔧 **Third-Party PHP Add-ons**
For additional PHP versions or specific requirements, you can install third-party packages:
@ -75,37 +136,46 @@ For additional PHP versions or specific requirements, you can install third-part
## 🌐 Supported Operating Systems
CyberPanel runs on x86_64 architecture and supports the following operating systems:
CyberPanel runs on x86_64 architecture and supports the following **Linux** operating systems:
### **✅ Currently Supported**
- **Ubuntu 24.04.3** - Supported until April 2029 ⭐ **NEW!**
- **Ubuntu 22.04** - Supported until April 2027
- **Ubuntu 20.04** - Supported until April 2025
- **Ubuntu 18.04** - Supported until April 2023
- **Debian 13** - Supported until 2029 ⭐ **NEW!**
- **Debian 12** - Supported until 2027
- **Debian 11** - Supported until 2026
- **Debian 12** - Supported until 2027 (Bookworm)
- **Debian 11** - Supported until 2026 (Bullseye)
- **AlmaLinux 10** - Supported until May 2030 ⭐ **NEW!**
- **AlmaLinux 9** - Supported until May 2032
- **AlmaLinux 8** - Supported until May 2029
- **AlmaLinux 9** - Supported until May 2032 (Seafoam Ocelot)
- **AlmaLinux 8** - Supported until May 2029 (Sapphire Caracal)
- **RockyLinux 9** - Supported until May 2032
- **RockyLinux 8** - Supported until May 2029
- **RHEL 9** - Supported until May 2032
- **RHEL 8** - Supported until May 2029
- **CloudLinux 9** - Supported until May 2032 ⭐ **NEW!**
- **CloudLinux 8** - Supported until May 2029
- **CloudLinux 7** - Supported until June 2024
- **CentOS 9** - Supported until May 2027
- **CentOS 8** - Supported until December 2021
- **CentOS 7** - Supported until June 2024
- **CentOS Stream 9** - Supported until May 2027
### **🔧 Third-Party OS Support**
Additional operating systems may be supported through third-party repositories or community efforts:
- **CentOS 9** - Supported until May 2027
- **CentOS 7** - Supported until June 2024 ⚠️ **EOL**
- **openEuler** - Community-supported with limited testing
- **Other RHEL derivatives** - May work with AlmaLinux/RockyLinux packages
### **🔧 Installation Verification**
All listed operating systems have been verified to work with the current CyberPanel installation script. The installer automatically detects your system and applies the appropriate configuration.
**Verification Status**: ✅ **All OS listed above are confirmed to work**
- Installation scripts include detection logic for all supported distributions
- Version-specific handling is implemented for each OS
- Automatic repository setup for each distribution type
- Tested and verified compatibility across all platforms
### **⚠️ Important Notes**
- **Linux Only**: CyberPanel is designed specifically for Linux systems and does not support Windows
- **Architecture**: Requires x86_64 (64-bit) architecture
- **Virtual Machines**: Windows users can run CyberPanel in a Linux VM
- **Docker**: Alternative option for Windows users via Docker containers
> **Note**: For unsupported operating systems, compatibility is not guaranteed. Always test in a non-production environment first.
@ -113,56 +183,211 @@ Additional operating systems may be supported through third-party repositories o
## ⚙️ Installation Instructions
Install CyberPanel easily with the following command:
### **Quick Installation (Recommended)**
Install CyberPanel on supported Linux distributions with a single command:
```bash
sh <(curl https://cyberpanel.net/install.sh || wget -O - https://cyberpanel.net/install.sh)
```
**Alternative Installation Methods:**
```bash
# Using wget only
wget -O - https://cyberpanel.net/install.sh | sh
# Download and run manually
wget https://cyberpanel.net/install.sh
chmod +x install.sh
sudo ./install.sh
```
### **Prerequisites**
Before installation, ensure your system meets these requirements:
- **Operating System**: One of the supported Linux distributions listed above
- **Architecture**: x86_64 (64-bit)
- **RAM**: Minimum 1GB (2GB+ recommended)
- **Storage**: Minimum 10GB free space (20GB+ recommended)
- **Network**: Internet connection required
- **Root Access**: Installation requires root/sudo privileges
### **Installation Process**
The installation script will automatically:
1. **Detect your operating system** and version
2. **Install required dependencies** (Python, Git, etc.)
3. **Download and configure** OpenLiteSpeed web server
4. **Set up MariaDB** database server
5. **Install CyberPanel** and configure all services
6. **Create admin user** with default credentials
7. **Start all services** and provide access information
### **Post-Installation**
After successful installation:
1. **Access CyberPanel**: Open your browser to `http://your-server-ip:8090`
2. **Default Login**:
- **Username**: `admin`
- **Password**: `123456`
3. **Change Password**: Immediately change the default password
4. **Configure SSL**: Set up SSL certificates for secure access
---
## 📊 Upgrading CyberPanel
Upgrade your CyberPanel installation using:
### **Quick Upgrade (Recommended)**
Upgrade your existing CyberPanel installation:
```bash
sh <(curl https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh)
```
---
**Alternative Upgrade Methods:**
```bash
# Using wget only
wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh | sh
## 🆕 Recent Updates & Fixes
# Download and run manually
wget https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh
chmod +x preUpgrade.sh
sudo ./preUpgrade.sh
```
### **File Integrity & Verification System** (September 2025)
### **Upgrade Process**
- **Enhancement**: Comprehensive file integrity verification system implemented
- **Features**:
- Automatic detection of missing critical files
- Python syntax validation for all core modules
- Environment configuration verification
- Django application integrity checks
- **Coverage**: All core components (Django settings, URLs, middleware, application modules)
- **Status**: ✅ All files verified and synchronized (5,597 files)
The upgrade script will automatically:
### **Bandwidth Reset Issue Fixed** (September 2025)
1. **Backup current installation** to prevent data loss
2. **Download latest version** from the stable branch
3. **Update all dependencies** and requirements
4. **Run database migrations** to update schema
5. **Restart services** with new configuration
6. **Verify installation** and report any issues
- **Enhancement**: Implemented automatic monthly bandwidth reset for all websites and child domains
- **Coverage**: All supported operating systems (Ubuntu, AlmaLinux, RockyLinux, RHEL, CloudLinux, CentOS)
- **Status**: ✅ Automatic monthly reset now functional
### **Manual Upgrade (Advanced Users)**
### **New Operating System Support Added** (September 2025)
For manual upgrades or troubleshooting:
- **Ubuntu 24.04.3**: Full compatibility with latest Ubuntu LTS
- **Debian 13**: Full compatibility with latest Debian stable release
- **AlmaLinux 10**: Full compatibility with latest AlmaLinux release
- **Long-term Support**: All supported until 2029-2030
```bash
# Navigate to CyberPanel directory
cd /usr/local/CyberCP
### **Core Module Enhancements** (September 2025)
# Update source code
git pull origin stable
- **Django Configuration**: Enhanced settings.py with improved environment variable handling
- **Security Middleware**: Updated security middleware for better protection
- **Application Modules**: Verified and synchronized all core application modules
- **Static Assets**: Complete synchronization of UI assets and templates
# Update dependencies
pip3 install -r requirments.txt
# Run database migrations
python3 manage.py migrate
# Collect static files
python3 manage.py collectstatic --noinput
# Restart services
systemctl restart lscpd
```
### **⚠️ Important Upgrade Notes**
- **Backup First**: Always backup your data before upgrading
- **Test Environment**: Test upgrades in a non-production environment first
- **Service Restart**: Some services may restart during upgrade
- **Configuration**: Custom configurations may need manual updates
- **Security Updates**: Latest version includes comprehensive security enhancements
## 🔧 Troubleshooting
### **Common Installation Issues**
#### **"Command not found" Errors**
```bash
# Install missing packages
# Ubuntu/Debian
sudo apt update && sudo apt install curl wget git python3
# RHEL/CentOS/AlmaLinux/RockyLinux
sudo yum install curl wget git python3
```
#### **Permission Denied Errors**
```bash
# Ensure you're running as root
sudo sh <(curl https://cyberpanel.net/install.sh || wget -O - https://cyberpanel.net/install.sh)
```
#### **Network Connectivity Issues**
```bash
# Check internet connection
ping -c 4 google.com
# Check DNS resolution
nslookup cyberpanel.net
# Try alternative download method
wget https://cyberpanel.net/install.sh
chmod +x install.sh
sudo ./install.sh
```
#### **Port Already in Use**
```bash
# Check what's using port 8090
sudo netstat -tlnp | grep :8090
# Kill process if necessary
sudo kill -9 <PID>
```
### **Common Upgrade Issues**
#### **Backup Creation Failed**
```bash
# Check disk space
df -h
# Free up space if necessary
sudo apt autoremove && sudo apt autoclean
```
#### **Service Restart Failed**
```bash
# Check service status
systemctl status lscpd
# Restart services manually
sudo systemctl restart lscpd
sudo systemctl restart lsws
```
### **Verification Commands**
#### **Check Installation Status**
```bash
# Check CyberPanel service
systemctl status lscpd
# Check web interface
curl -I http://localhost:8090
# Check database
systemctl status mariadb
```
#### **View Logs**
```bash
# CyberPanel logs
tail -f /usr/local/lscp/logs/error.log
# System logs
journalctl -u lscpd -f
```
---
@ -190,15 +415,15 @@ sh <(curl https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgr
### 🔗 **Direct Guide Links**
| Category | Guide | Description |
| ------------ | ---------------------------------------------------------- | ---------------------------------- |
| 📚 All | [Complete Guides Index](guides/INDEX.md) | Browse all available guides |
| 🔧 General | [Troubleshooting Guide](guides/TROUBLESHOOTING.md) | Comprehensive troubleshooting |
| 🐳 Docker | [Command Execution](guides/Docker_Command_Execution_Guide.md) | Execute commands in containers |
| 🤖 Security | [AI Scanner](guides/AIScannerDocs.md) | AI-powered security scanning |
| 📧 Email | [Mautic Setup](guides/MAUTIC_INSTALLATION_GUIDE.md) | Email marketing platform |
| 🎨 Design | [Custom CSS Guide](guides/CUSTOM_CSS_GUIDE.md) | Create custom themes |
| 🔥 Security | [Firewall Blocking Feature](guides/FIREWALL_BLOCKING_FEATURE.md) | Advanced security features |
| Category | Guide | Description |
| ----------- | ------------------------------------------------------------- | ------------------------------ |
| 📚 All | [Complete Guides Index](guides/INDEX.md) | Browse all available guides |
| 🔧 General | [Troubleshooting Guide](guides/TROUBLESHOOTING.md) | Comprehensive troubleshooting |
| 🐳 Docker | [Command Execution](guides/Docker_Command_Execution_Guide.md) | Execute commands in containers |
| 🤖 Security | [AI Scanner](guides/AIScannerDocs.md) | AI-powered security scanning |
| 📧 Email | [Mautic Setup](guides/MAUTIC_INSTALLATION_GUIDE.md) | Email marketing platform |
| 🎨 Design | [Custom CSS Guide](guides/CUSTOM_CSS_GUIDE.md) | Create custom themes |
| 🔥 Security | [Firewall Blocking Feature](guides/FIREWALL_BLOCKING_FEATURE.md) | Advanced security features |
---
@ -215,6 +440,7 @@ For detailed troubleshooting, installation guides, and advanced configuration:
- **🐳 [Docker Command Execution Guide](guides/Docker_Command_Execution_Guide.md)** - Docker management and troubleshooting
- **🤖 [AI Scanner Documentation](guides/AIScannerDocs.md)** - Security scanner setup and configuration
- **🔐 [2FA Authentication Guide](guides/2FA_AUTHENTICATION_GUIDE.md)** - Two-Factor Authentication and WebAuthn setup
- **📧 [Mautic Installation Guide](guides/MAUTIC_INSTALLATION_GUIDE.md)** - Email marketing platform setup
- **🎨 [Custom CSS Guide](guides/CUSTOM_CSS_GUIDE.md)** - Interface customization and theming
- **🔥 [Firewall Blocking Feature Guide](guides/FIREWALL_BLOCKING_FEATURE.md)** - Security features and configuration

View File

@ -5,6 +5,7 @@ urlpatterns = [
re_path(r'^createWebsite$', views.createWebsite, name='createWebsiteAPI'),
re_path(r'^deleteWebsite$', views.deleteWebsite, name='deleteWebsiteAPI'),
re_path(r'^submitWebsiteStatus$', views.submitWebsiteStatus, name='submitWebsiteStatusAPI'),
re_path(r'^createDockersite$', views.createDockersite, name='createDockersiteAPI'),
re_path(r'^deleteFirewallRule$', views.deleteFirewallRule, name='deleteFirewallRule'),
re_path(r'^addFirewallRule$', views.addFirewallRule, name='addFirewallRule'),
@ -29,6 +30,7 @@ urlpatterns = [
re_path(r'^cyberPanelVersion$', views.cyberPanelVersion, name='cyberPanelVersion'),
re_path(r'^runAWSBackups$', views.runAWSBackups, name='runAWSBackups'),
re_path(r'^submitUserCreation$', views.submitUserCreation, name='submitUserCreation'),
re_path(r'^listChildDomainsJson$', views.listChildDomainsJson, name='listChildDomainsJson'),
# AI Scanner API endpoints for external workers
re_path(r'^ai-scanner/authenticate$', views.aiScannerAuthenticate, name='aiScannerAuthenticateAPI'),

View File

@ -18,6 +18,7 @@ from packages.packagesManager import PackagesManager
from s3Backups.s3Backups import S3Backups
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
from plogical.processUtilities import ProcessUtilities
from plogical.errorSanitizer import secure_error_response, secure_log_error
from django.views.decorators.csrf import csrf_exempt
from userManagment.views import submitUserCreation as suc
from userManagment.views import submitUserDeletion as duc
@ -157,6 +158,61 @@ def createWebsite(request):
return HttpResponse(json_data, status=500)
@csrf_exempt
def createDockersite(request):
try:
if request.method != 'POST':
data_ret = {"status": 0, 'error_message': "Only POST method allowed."}
json_data = json.dumps(data_ret)
return HttpResponse(json_data, status=405)
try:
data = json.loads(request.body)
adminUser = data['adminUser']
# Additional security: validate critical fields for dangerous characters
is_valid, error_msg = validate_api_input(adminUser, "adminUser")
if not is_valid:
data_ret = {"status": 0, 'error_message': error_msg}
json_data = json.dumps(data_ret)
return HttpResponse(json_data, status=400)
# Validate site name if provided
if 'sitename' in data:
is_valid, error_msg = validate_api_input(data['sitename'], "sitename")
if not is_valid:
data_ret = {"status": 0, 'error_message': error_msg}
json_data = json.dumps(data_ret)
return HttpResponse(json_data, status=400)
except (json.JSONDecodeError, KeyError):
data_ret = {"status": 0, 'error_message': "Invalid JSON or missing adminUser field."}
json_data = json.dumps(data_ret)
return HttpResponse(json_data, status=400)
try:
admin = Administrator.objects.get(userName=adminUser)
except Administrator.DoesNotExist:
data_ret = {"status": 0, 'error_message': "Administrator not found."}
json_data = json.dumps(data_ret)
return HttpResponse(json_data, status=404)
if os.path.exists(ProcessUtilities.debugPath):
logging.writeToFile(f'Create dockersite payload in API {str(data)}')
if admin.api == 0:
data_ret = {"status": 0, 'error_message': "API Access Disabled."}
json_data = json.dumps(data_ret)
return HttpResponse(json_data, status=403)
wm = WebsiteManager()
return wm.submitDockerSiteCreation(admin.pk, data)
except Exception as msg:
data_ret = {"status": 0, 'error_message': f"Internal server error: {str(msg)}"}
json_data = json.dumps(data_ret)
return HttpResponse(json_data, status=500)
@csrf_exempt
def getPackagesListAPI(request):
data = json.loads(request.body)
@ -216,8 +272,9 @@ def getUserInfo(request):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'status': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, 'submitWebsiteCreation')
data_ret = secure_error_response(e, 'Failed to create website')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -258,8 +315,9 @@ def changeUserPassAPI(request):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'changeStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, 'changeUserPassAPI')
data_ret = secure_error_response(e, 'Failed to change user password')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -290,8 +348,9 @@ def submitUserDeletion(request):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'submitUserDeletion': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'submitUserDeletion\')
data_ret = secure_error_response(e, \'Failed to submitUserDeletion\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -333,8 +392,9 @@ def changePackageAPI(request):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'changePackage': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'changePackage\')
data_ret = secure_error_response(e, \'Failed to changePackage\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -379,8 +439,9 @@ def deleteWebsite(request):
wm = WebsiteManager()
return wm.submitWebsiteDeletion(admin.pk, data)
except BaseException as msg:
data_ret = {'websiteDeleteStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'websiteDeleteStatus\')
data_ret = secure_error_response(e, \'Failed to websiteDeleteStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -411,8 +472,9 @@ def submitWebsiteStatus(request):
wm = WebsiteManager()
return wm.submitWebsiteStatus(admin.pk, json.loads(request.body))
except BaseException as msg:
data_ret = {'websiteStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'websiteStatus\')
data_ret = secure_error_response(e, \'Failed to websiteStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -436,8 +498,9 @@ def loginAPI(request):
else:
return HttpResponse("Invalid Credentials.")
except BaseException as msg:
data = {'userID': 0, 'loginStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, 'loginAPI')
data = secure_error_response(e, 'Login failed')
json_data = json.dumps(data)
return HttpResponse(json_data)
@ -542,8 +605,9 @@ def remoteTransfer(request):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data = {'transferStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'transferStatus\')
data = secure_error_response(e, \'Failed to transferStatus\')
json_data = json.dumps(data)
return HttpResponse(json_data)
@ -593,8 +657,9 @@ def fetchAccountsFromRemoteServer(request):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data = {'fetchStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'fetchStatus\')
data = secure_error_response(e, \'Failed to fetchStatus\')
json_data = json.dumps(data)
return HttpResponse(json_data)
@ -632,8 +697,9 @@ def FetchRemoteTransferStatus(request):
final_json = json.dumps({'fetchStatus': 1, 'error_message': "None", "status": "Just started.."})
return HttpResponse(final_json)
except BaseException as msg:
data = {'fetchStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'fetchStatus\')
data = secure_error_response(e, \'Failed to fetchStatus\')
json_data = json.dumps(data)
return HttpResponse(json_data)
@ -721,11 +787,9 @@ def cyberPanelVersion(request):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {
"getVersion": 0,
'error_message': str(msg)
}
except Exception as e:
secure_log_error(e, \'getVersion\')
data_ret = secure_error_response(e, \'Failed to getVersion\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -740,8 +804,9 @@ def runAWSBackups(request):
if os.path.exists(randomFile):
s3 = S3Backups(request, None, 'runAWSBackups')
s3.start()
except BaseException as msg:
logging.writeToFile(str(msg) + ' [API.runAWSBackups]')
except Exception as e:
secure_log_error(e, \'API.runAWSBackups\')
logging.writeToFile(\'Failed to API.runAWSBackups [API.runAWSBackups]\')
@csrf_exempt
@ -770,12 +835,91 @@ def submitUserCreation(request):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'changeStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'changeStatus\')
data_ret = secure_error_response(e, \'Failed to changeStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@csrf_exempt
def listChildDomainsJson(request):
try:
if request.method != 'POST':
data_ret = {"status": 0, 'error_message': "Only POST method allowed."}
json_data = json.dumps(data_ret)
return HttpResponse(json_data, status=405)
try:
data = json.loads(request.body)
adminUser = data['adminUser']
adminPass = data['adminPass']
# Additional security: validate critical fields for dangerous characters
is_valid, error_msg = validate_api_input(adminUser, "adminUser")
if not is_valid:
data_ret = {"status": 0, 'error_message': error_msg}
json_data = json.dumps(data_ret)
return HttpResponse(json_data, status=400)
except (json.JSONDecodeError, KeyError):
data_ret = {"status": 0, 'error_message': "Invalid JSON or missing adminUser/adminPass fields."}
json_data = json.dumps(data_ret)
return HttpResponse(json_data, status=400)
try:
admin = Administrator.objects.get(userName=adminUser)
except Administrator.DoesNotExist:
data_ret = {"status": 0, 'error_message': "Administrator not found."}
json_data = json.dumps(data_ret)
return HttpResponse(json_data, status=404)
if admin.api == 0:
data_ret = {"status": 0, 'error_message': "API Access Disabled."}
json_data = json.dumps(data_ret)
return HttpResponse(json_data, status=403)
if not hashPassword.check_password(admin.password, adminPass):
data_ret = {"status": 0, 'error_message': "Invalid password."}
json_data = json.dumps(data_ret)
return HttpResponse(json_data, status=401)
# Get child domains
from websiteFunctions.models import ChildDomains
child_domains = ChildDomains.objects.all()
# Get machine IP
try:
ipFile = "/etc/cyberpanel/machineIP"
with open(ipFile, 'r') as f:
ipData = f.read()
ipAddress = ipData.split('\n', 1)[0]
except BaseException as msg:
logging.writeToFile(f"Failed to read machine IP, error: {str(msg)}")
ipAddress = "192.168.100.1"
json_data = []
for items in child_domains:
dic = {
'parent_site': items.master.domain,
'domain': items.domain,
'path': items.path,
'ssl': items.ssl,
'php_version': items.phpSelection,
'ip_address': ipAddress
}
json_data.append(dic)
final_json = json.dumps(json_data, indent=2)
return HttpResponse(final_json, content_type='application/json')
except Exception as msg:
data_ret = {'status': 0, 'error_message': f"Internal server error: {str(msg)}"}
json_data = json.dumps(data_ret)
return HttpResponse(json_data, status=500)
@csrf_exempt
def addFirewallRule(request):
try:
@ -804,8 +948,9 @@ def addFirewallRule(request):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'submitUserDeletion': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'submitUserDeletion\')
data_ret = secure_error_response(e, \'Failed to submitUserDeletion\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -838,8 +983,9 @@ def deleteFirewallRule(request):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'submitUserDeletion': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'submitUserDeletion\')
data_ret = secure_error_response(e, \'Failed to submitUserDeletion\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)

View File

@ -38,6 +38,33 @@ from django.http import JsonResponse
class BackupManager:
localBackupPath = '/home/cyberpanel/localBackupPath'
@staticmethod
def _try_remote_connection(ipAddress, password, endpoint, cyberPanelPort=8090, timeout=10):
"""
Try to connect to remote CyberPanel server with port fallback.
Returns: (success, data, used_port, error_message)
"""
import requests
import json
ports_to_try = [cyberPanelPort, 8090] if cyberPanelPort != 8090 else [8090]
finalData = json.dumps({'username': "admin", "password": password})
for port in ports_to_try:
try:
url = f"https://{ipAddress}:{port}{endpoint}"
r = requests.post(url, data=finalData, verify=False, timeout=timeout)
data = json.loads(r.text)
return True, data, port, None
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout,
requests.exceptions.RequestException, json.JSONDecodeError) as e:
if port == ports_to_try[-1]: # Last port failed
return False, None, None, f"Could not connect to remote server on any port. Tried ports: {', '.join(map(str, ports_to_try))}. Last error: {str(e)}"
continue
return False, None, None, "Connection failed on all ports"
def __init__(self, domain=None, childDomain=None):
self.domain = domain
@ -1084,33 +1111,35 @@ class BackupManager:
ipAddress = data['ipAddress']
password = data['password']
cyberPanelPort = data.get('cyberPanelPort', 8090) # Default to 8090 if not provided
## Ask for Remote version of CyberPanel
try:
finalData = json.dumps({'username': "admin", "password": password})
url = "https://" + ipAddress + ":8090/api/cyberPanelVersion"
r = requests.post(url, data=finalData, verify=False)
data = json.loads(r.text)
if data['getVersion'] == 1:
if float(data['currentVersion']) >= 1.6 and data['build'] >= 0:
pass
else:
data_ret = {'status': 0,
'error_message': "Your version does not match with version of remote server.",
"dir": "Null"}
data_ret = json.dumps(data_ret)
return HttpResponse(data_ret)
else:
# Try to connect with port fallback
success, data, used_port, error_msg = BackupManager._try_remote_connection(
ipAddress, password, "/api/cyberPanelVersion", cyberPanelPort
)
if not success:
data_ret = {'status': 0, 'error_message': error_msg, "dir": "Null"}
data_ret = json.dumps(data_ret)
return HttpResponse(data_ret)
if data['getVersion'] != 1:
data_ret = {'status': 0,
'error_message': "Not able to fetch version of remote server. Error Message: " +
data[
'error_message'], "dir": "Null"}
data.get('error_message', 'Unknown error'), "dir": "Null"}
data_ret = json.dumps(data_ret)
return HttpResponse(data_ret)
# Check version compatibility
if float(data['currentVersion']) >= 1.6 and data['build'] >= 0:
pass
else:
data_ret = {'status': 0,
'error_message': "Your version does not match with version of remote server.",
"dir": "Null"}
data_ret = json.dumps(data_ret)
return HttpResponse(data_ret)
@ -1125,11 +1154,13 @@ class BackupManager:
## Fetch public key of remote server!
finalData = json.dumps({'username': "admin", "password": password})
url = "https://" + ipAddress + ":8090/api/fetchSSHkey"
r = requests.post(url, data=finalData, verify=False)
data = json.loads(r.text)
success, data, used_port, error_msg = BackupManager._try_remote_connection(
ipAddress, password, "/api/fetchSSHkey", used_port
)
if not success:
final_json = json.dumps({'status': 0, 'error_message': error_msg})
return HttpResponse(final_json)
if data['pubKeyStatus'] == 1:
pubKey = data["pubKey"].strip("\n")
@ -1167,18 +1198,19 @@ class BackupManager:
##
try:
finalData = json.dumps({'username': "admin", "password": password})
url = "https://" + ipAddress + ":8090/api/fetchAccountsFromRemoteServer"
r = requests.post(url, data=finalData, verify=False)
data = json.loads(r.text)
success, data, used_port, error_msg = BackupManager._try_remote_connection(
ipAddress, password, "/api/fetchAccountsFromRemoteServer", used_port
)
if not success:
data_ret = {'status': 0, 'error_message': error_msg, "dir": "Null"}
data_ret = json.dumps(data_ret)
return HttpResponse(data_ret)
if data['fetchStatus'] == 1:
json_data = data['data']
data_ret = {'status': 1, 'error_message': "None",
"dir": "Null", 'data': json_data}
"dir": "Null", 'data': json_data, 'used_port': used_port}
data_ret = json.dumps(data_ret)
return HttpResponse(data_ret)
else:
@ -1208,6 +1240,7 @@ class BackupManager:
ipAddress = data['ipAddress']
password = data['password']
accountsToTransfer = data['accountsToTransfer']
cyberPanelPort = data.get('cyberPanelPort', 8090) # Default to 8090 if not provided
try:
@ -1240,9 +1273,32 @@ class BackupManager:
finalData = json.dumps({'username': "admin", "password": password, "ipAddress": ownIP,
"accountsToTransfer": accountsToTransfer, 'port': port})
url = "https://" + ipAddress + ":8090/api/remoteTransfer"
r = requests.post(url, data=finalData, verify=False)
# Try to connect with port fallback
ports_to_try = [cyberPanelPort, 8090] if cyberPanelPort != 8090 else [8090]
connection_successful = False
used_port = None
for port in ports_to_try:
try:
url = f"https://{ipAddress}:{port}/api/remoteTransfer"
r = requests.post(url, data=finalData, verify=False, timeout=10)
data = json.loads(r.text)
connection_successful = True
used_port = port
break
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout,
requests.exceptions.RequestException, json.JSONDecodeError) as e:
if port == ports_to_try[-1]: # Last port failed
data_ret = {'remoteTransferStatus': 0,
'error_message': f"Could not connect to remote server on any port. Tried ports: {', '.join(map(str, ports_to_try))}. Last error: {str(e)}"}
data_ret = json.dumps(data_ret)
return HttpResponse(data_ret)
continue
if not connection_successful:
data_ret = {'remoteTransferStatus': 0, 'error_message': "Connection failed on all ports"}
data_ret = json.dumps(data_ret)
return HttpResponse(data_ret)
if os.path.exists('/usr/local/CyberCP/debug'):
message = 'Remote transfer initiation status: %s' % (r.text)
@ -1302,12 +1358,36 @@ class BackupManager:
password = data['password']
dir = data['dir']
username = "admin"
cyberPanelPort = data.get('cyberPanelPort', 8090) # Default to 8090 if not provided
finalData = json.dumps({'dir': dir, "username": username, "password": password})
r = requests.post("https://" + ipAddress + ":8090/api/FetchRemoteTransferStatus", data=finalData,
verify=False)
data = json.loads(r.text)
# Try to connect with port fallback
ports_to_try = [cyberPanelPort, 8090] if cyberPanelPort != 8090 else [8090]
connection_successful = False
used_port = None
for port in ports_to_try:
try:
url = f"https://{ipAddress}:{port}/api/FetchRemoteTransferStatus"
r = requests.post(url, data=finalData, verify=False, timeout=10)
data = json.loads(r.text)
connection_successful = True
used_port = port
break
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout,
requests.exceptions.RequestException, json.JSONDecodeError) as e:
if port == ports_to_try[-1]: # Last port failed
data_ret = {'remoteTransferStatus': 0,
'error_message': f"Could not connect to remote server on any port. Tried ports: {', '.join(map(str, ports_to_try))}. Last error: {str(e)}"}
data_ret = json.dumps(data_ret)
return HttpResponse(data_ret)
continue
if not connection_successful:
data_ret = {'remoteTransferStatus': 0, 'error_message': "Connection failed on all ports"}
data_ret = json.dumps(data_ret)
return HttpResponse(data_ret)
if data['fetchStatus'] == 1:
if data['status'].find("Backups are successfully generated and received on") > -1:
@ -1429,12 +1509,36 @@ class BackupManager:
password = data['password']
dir = data['dir']
username = "admin"
cyberPanelPort = data.get('cyberPanelPort', 8090) # Default to 8090 if not provided
finalData = json.dumps({'dir': dir, "username": username, "password": password})
r = requests.post("https://" + ipAddress + ":8090/api/cancelRemoteTransfer", data=finalData,
verify=False)
data = json.loads(r.text)
# Try to connect with port fallback
ports_to_try = [cyberPanelPort, 8090] if cyberPanelPort != 8090 else [8090]
connection_successful = False
used_port = None
for port in ports_to_try:
try:
url = f"https://{ipAddress}:{port}/api/cancelRemoteTransfer"
r = requests.post(url, data=finalData, verify=False, timeout=10)
data = json.loads(r.text)
connection_successful = True
used_port = port
break
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout,
requests.exceptions.RequestException, json.JSONDecodeError) as e:
if port == ports_to_try[-1]: # Last port failed
data_ret = {'cancelStatus': 0,
'error_message': f"Could not connect to remote server on any port. Tried ports: {', '.join(map(str, ports_to_try))}. Last error: {str(e)}"}
data_ret = json.dumps(data_ret)
return HttpResponse(data_ret)
continue
if not connection_successful:
data_ret = {'cancelStatus': 0, 'error_message': "Connection failed on all ports"}
data_ret = json.dumps(data_ret)
return HttpResponse(data_ret)
if data['cancelStatus'] == 1:
pass

View File

@ -742,6 +742,18 @@ app.controller('remoteBackupControl', function ($scope, $http, $timeout) {
$scope.transferBoxBtn = true;
$scope.stopTransferbtn = true;
$scope.fetchAccountsBtn = false;
// Progress tracking variables
$scope.overallProgress = 0;
$scope.currentStep = 0;
$scope.transferInProgress = false;
$scope.transferCompleted = false;
$scope.transferError = false;
$scope.downloadStatus = "Waiting...";
$scope.transferStatus = "Waiting...";
$scope.restoreStatus = "Waiting...";
$scope.logEntries = [];
$scope.showLog = false;
// notifications boxes
@ -765,6 +777,61 @@ app.controller('remoteBackupControl', function ($scope, $http, $timeout) {
$scope.passwordEnter = function () {
$scope.backupButton = false;
};
// Progress tracking functions
$scope.addLogEntry = function(message, type = 'info') {
$scope.logEntries.push({
timestamp: new Date(),
message: message,
type: type
});
// Keep only last 100 log entries
if ($scope.logEntries.length > 100) {
$scope.logEntries = $scope.logEntries.slice(-100);
}
// Auto-scroll to bottom
setTimeout(function() {
var logOutput = document.getElementById('logOutput');
if (logOutput) {
logOutput.scrollTop = logOutput.scrollHeight;
}
}, 100);
};
$scope.updateProgress = function(step, progress, status) {
$scope.currentStep = step;
$scope.overallProgress = progress;
switch(step) {
case 1:
$scope.downloadStatus = status;
break;
case 2:
$scope.transferStatus = status;
break;
case 3:
$scope.restoreStatus = status;
break;
}
};
$scope.toggleLog = function() {
$scope.showLog = !$scope.showLog;
};
$scope.resetProgress = function() {
$scope.overallProgress = 0;
$scope.currentStep = 0;
$scope.transferInProgress = false;
$scope.transferCompleted = false;
$scope.transferError = false;
$scope.downloadStatus = "Waiting...";
$scope.transferStatus = "Waiting...";
$scope.restoreStatus = "Waiting...";
$scope.logEntries = [];
};
$scope.addRemoveWebsite = function (website, websiteStatus) {
@ -819,12 +886,14 @@ app.controller('remoteBackupControl', function ($scope, $http, $timeout) {
var IPAddress = $scope.IPAddress;
var password = $scope.password;
var cyberPanelPort = $scope.cyberPanelPort || 8090; // Default to 8090 if not specified
url = "/backup/submitRemoteBackups";
var data = {
ipAddress: IPAddress,
password: password,
cyberPanelPort: cyberPanelPort,
};
var config = {
@ -860,6 +929,16 @@ app.controller('remoteBackupControl', function ($scope, $http, $timeout) {
$scope.accountsFetched = false;
$scope.backupProcessStarted = true;
$scope.backupCancelled = true;
// Show fallback port notification if used
if (response.data.used_port && response.data.used_port != $scope.cyberPanelPort) {
new PNotify({
title: 'Port Fallback Used',
text: `Connected using port ${response.data.used_port} (fallback from ${$scope.cyberPanelPort})`,
type: 'info',
delay: 5000
});
}
} else {
@ -893,6 +972,10 @@ app.controller('remoteBackupControl', function ($scope, $http, $timeout) {
};
$scope.startTransfer = function () {
// Reset progress tracking
$scope.resetProgress();
$scope.transferInProgress = true;
$scope.addLogEntry("Starting remote backup transfer...", "info");
// notifications boxes
$scope.notificationsBox = true;
@ -915,12 +998,14 @@ app.controller('remoteBackupControl', function ($scope, $http, $timeout) {
var IPAddress = $scope.IPAddress;
var password = $scope.password;
var cyberPanelPort = $scope.cyberPanelPort || 8090; // Default to 8090 if not specified
url = "/backup/starRemoteTransfer";
var data = {
ipAddress: IPAddress,
password: password,
cyberPanelPort: cyberPanelPort,
accountsToTransfer: websitesToBeBacked,
};
@ -1002,6 +1087,7 @@ app.controller('remoteBackupControl', function ($scope, $http, $timeout) {
var data = {
password: $scope.password,
ipAddress: $scope.IPAddress,
cyberPanelPort: $scope.cyberPanelPort || 8090,
dir: tempTransferDir
};
@ -1021,13 +1107,31 @@ app.controller('remoteBackupControl', function ($scope, $http, $timeout) {
if (response.data.backupsSent === 0) {
$scope.backupStatus = false;
$scope.requestData = response.data.status;
// Update progress based on status content
var status = response.data.status;
if (status) {
$scope.addLogEntry(status, "info");
// Parse status for progress updates
if (status.includes("Backup process started") || status.includes("Generating backup")) {
$scope.updateProgress(1, 25, "Generating backups on remote server...");
} else if (status.includes("Transferring") || status.includes("Sending backup")) {
$scope.updateProgress(2, 50, "Transferring backup files...");
} else if (status.includes("Backup received") || status.includes("Downloading")) {
$scope.updateProgress(2, 75, "Downloading backup files...");
}
}
$timeout(getBackupStatus, 2000);
} else {
$scope.requestData = response.data.status;
$scope.addLogEntry("Backup transfer completed successfully!", "success");
$scope.updateProgress(2, 100, "Transfer completed");
$timeout.cancel();
// Start the restore of remote backups that are transferred to local server
$scope.addLogEntry("Starting local restore process...", "info");
remoteBackupRestore();
}
} else {
@ -1035,6 +1139,8 @@ app.controller('remoteBackupControl', function ($scope, $http, $timeout) {
$scope.error_message = response.data.error_message;
$scope.backupLoading = true;
$scope.couldNotConnect = true;
$scope.transferError = true;
$scope.addLogEntry("Transfer failed: " + response.data.error_message, "error");
// Notifications box settings
@ -1077,7 +1183,12 @@ app.controller('remoteBackupControl', function ($scope, $http, $timeout) {
function ListInitialDatas(response) {
if (response.data.remoteRestoreStatus === 1) {
$scope.addLogEntry("Remote restore initiated successfully", "success");
$scope.updateProgress(3, 85, "Restoring websites...");
localRestoreStatus();
} else {
$scope.addLogEntry("Remote restore failed: " + (response.data.error_message || "Unknown error"), "error");
$scope.transferError = true;
}
}
@ -1121,9 +1232,31 @@ app.controller('remoteBackupControl', function ($scope, $http, $timeout) {
if (response.data.complete === 0) {
$scope.backupStatus = false;
$scope.restoreData = response.data.status;
// Update restore progress
var status = response.data.status;
if (status) {
$scope.addLogEntry(status, "info");
if (status.includes("completed[success]")) {
$scope.updateProgress(3, 100, "Restore completed successfully!");
$scope.transferCompleted = true;
$scope.transferInProgress = false;
$scope.addLogEntry("All websites restored successfully!", "success");
} else if (status.includes("Error") || status.includes("error")) {
$scope.addLogEntry("Restore error: " + status, "error");
} else {
$scope.updateProgress(3, 90, "Finalizing restore...");
}
}
$timeout(localRestoreStatus, 2000);
} else {
$scope.restoreData = response.data.status;
$scope.updateProgress(3, 100, "Restore completed!");
$scope.transferCompleted = true;
$scope.transferInProgress = false;
$scope.addLogEntry("Restore process completed successfully!", "success");
$timeout.cancel();
$scope.backupLoading = true;
$scope.startTransferbtn = false;
@ -1133,6 +1266,8 @@ app.controller('remoteBackupControl', function ($scope, $http, $timeout) {
$scope.error_message = response.data.error_message;
$scope.backupLoading = true;
$scope.couldNotConnect = true;
$scope.transferError = true;
$scope.addLogEntry("Restore failed: " + response.data.error_message, "error");
// Notifications box settings
@ -1163,6 +1298,7 @@ app.controller('remoteBackupControl', function ($scope, $http, $timeout) {
var data = {
password: $scope.password,
ipAddress: $scope.IPAddress,
cyberPanelPort: $scope.cyberPanelPort || 8090,
dir: tempTransferDir,
};
@ -1215,12 +1351,14 @@ app.controller('remoteBackupControl', function ($scope, $http, $timeout) {
var IPAddress = $scope.IPAddress;
var password = $scope.password;
var cyberPanelPort = $scope.cyberPanelPort || 8090;
url = "/backup/cancelRemoteBackup";
var data = {
ipAddress: IPAddress,
password: password,
cyberPanelPort: cyberPanelPort,
dir: tempTransferDir,
};

View File

@ -381,6 +381,251 @@
background: var(--success-color, #10b981);
}
/* Progress Section Styles */
.progress-section {
background: var(--bg-primary, #fff);
border-radius: 15px;
box-shadow: 0 4px 20px var(--shadow-light, rgba(0, 0, 0, 0.1));
margin-top: 2rem;
overflow: hidden;
animation: fadeInUp 0.5s ease-out;
}
.progress-header {
background: linear-gradient(135deg, var(--accent-color, #5b5fcf) 0%, var(--accent-hover, #4547a9) 100%);
color: white;
padding: 1.5rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.progress-title {
font-size: 1.5rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.75rem;
}
.status-badge {
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
}
.status-progress {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.status-completed {
background: #10b981;
color: white;
}
.status-error {
background: #ef4444;
color: white;
}
.progress-overview {
padding: 2rem;
background: var(--bg-secondary, #f8fafc);
}
.progress-bar-container {
position: relative;
background: var(--bg-primary, #fff);
border-radius: 10px;
padding: 1rem;
box-shadow: 0 2px 10px var(--shadow-light, rgba(0, 0, 0, 0.05));
}
.progress-bar {
width: 100%;
height: 20px;
background: #e2e8f0;
border-radius: 10px;
overflow: hidden;
position: relative;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent-color, #5b5fcf) 0%, var(--accent-hover, #4547a9) 100%);
border-radius: 10px;
transition: width 0.5s ease;
position: relative;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.progress-text {
text-align: center;
margin-top: 0.75rem;
font-weight: 600;
color: var(--text-primary, #1e293b);
font-size: 1.1rem;
}
.progress-details {
padding: 2rem;
}
.progress-step {
display: flex;
align-items: center;
padding: 1.5rem 0;
border-bottom: 1px solid var(--border-light, #e2e8f0);
opacity: 0.5;
transition: all 0.3s ease;
}
.progress-step:last-child {
border-bottom: none;
}
.progress-step.active {
opacity: 1;
background: var(--bg-hover, #f8f9ff);
margin: 0 -2rem;
padding: 1.5rem 2rem;
border-radius: 10px;
}
.progress-step.completed {
opacity: 1;
}
.step-icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 1.5rem;
font-size: 1.25rem;
transition: all 0.3s ease;
}
.progress-step:not(.active):not(.completed) .step-icon {
background: var(--bg-secondary, #f1f5f9);
color: var(--text-secondary, #64748b);
}
.progress-step.active .step-icon {
background: var(--accent-color, #5b5fcf);
color: white;
animation: pulse 2s infinite;
}
.progress-step.completed .step-icon {
background: #10b981;
color: white;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.step-content {
flex: 1;
}
.step-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary, #1e293b);
margin-bottom: 0.25rem;
}
.step-description {
color: var(--text-secondary, #64748b);
font-size: 0.9rem;
}
.log-section {
border-top: 1px solid var(--border-light, #e2e8f0);
background: var(--bg-dark, #1e293b);
color: var(--text-light, #e2e8f0);
}
.log-header {
padding: 1rem 2rem;
border-bottom: 1px solid var(--border-dark, #334155);
display: flex;
justify-content: space-between;
align-items: center;
}
.log-header h4 {
margin: 0;
color: var(--text-light, #e2e8f0);
display: flex;
align-items: center;
gap: 0.5rem;
}
.log-content {
max-height: 300px;
overflow-y: auto;
}
.log-output {
padding: 1rem 2rem;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.875rem;
line-height: 1.5;
}
.log-entry {
margin-bottom: 0.5rem;
display: flex;
gap: 1rem;
}
.log-time {
color: var(--text-muted, #94a3b8);
min-width: 80px;
}
.log-message {
flex: 1;
}
.log-entry.log-error .log-message {
color: #ef4444;
}
.log-entry.log-success .log-message {
color: #10b981;
}
.log-entry.log-info .log-message {
color: #3b82f6;
}
.terminal-body {
padding: 1.5rem;
height: 350px;
@ -525,7 +770,7 @@
<div class="card-body">
<form action="/" method="post">
<div class="row">
<div class="col-md-6">
<div class="col-md-4">
<div class="form-group">
<label class="form-label">
<i class="fas fa-map-marker-alt" style="margin-right: 0.5rem;"></i>
@ -535,7 +780,20 @@
placeholder="192.168.1.100" required>
</div>
</div>
<div class="col-md-6">
<div class="col-md-4">
<div class="form-group">
<label class="form-label">
<i class="fas fa-network-wired" style="margin-right: 0.5rem;"></i>
{% trans "CyberPanel Port" %}
</label>
<input type="number" class="form-control" ng-model="cyberPanelPort"
placeholder="8090" min="1" max="65535" required>
<small class="form-text text-muted">
{% trans "Port where CyberPanel is running on remote server (default: 8090)" %}
</small>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label class="form-label">
<i class="fas fa-key" style="margin-right: 0.5rem;"></i>
@ -658,32 +916,100 @@
</div>
</div>
<!-- Transfer Progress Terminal -->
<div ng-hide="backupStatus" class="terminal-section">
<div class="terminal-header">
<div class="terminal-title">
<i class="fas fa-terminal"></i>
<!-- Transfer Progress Section -->
<div ng-hide="backupStatus" class="progress-section">
<div class="progress-header">
<div class="progress-title">
<i class="fas fa-sync-alt fa-spin" ng-if="transferInProgress"></i>
<i class="fas fa-check-circle" ng-if="transferCompleted"></i>
<i class="fas fa-exclamation-triangle" ng-if="transferError"></i>
{% trans "Transfer Progress" %}
</div>
<div class="terminal-dots">
<div class="terminal-dot red"></div>
<div class="terminal-dot yellow"></div>
<div class="terminal-dot green"></div>
<div class="progress-status">
<span ng-if="transferInProgress" class="status-badge status-progress">
<i class="fas fa-clock"></i> {% trans "In Progress" %}
</span>
<span ng-if="transferCompleted" class="status-badge status-completed">
<i class="fas fa-check"></i> {% trans "Completed" %}
</span>
<span ng-if="transferError" class="status-badge status-error">
<i class="fas fa-times"></i> {% trans "Error" %}
</span>
</div>
</div>
<div class="terminal-body">
<div class="terminal-grid">
<div>
<h4 style="color: var(--text-secondary, #94a3b8); margin-bottom: 1rem; font-size: 0.875rem;">
{% trans "Backup Progress" %}
</h4>
<div ng-bind="requestData" style="white-space: pre-wrap;"></div>
<!-- Overall Progress Bar -->
<div class="progress-overview">
<div class="progress-bar-container">
<div class="progress-bar">
<div class="progress-fill" ng-style="{'width': overallProgress + '%'}"></div>
</div>
<div>
<h4 style="color: var(--text-secondary, #94a3b8); margin-bottom: 1rem; font-size: 0.875rem;">
{% trans "Restore Progress" %}
</h4>
<div ng-bind="restoreData" style="white-space: pre-wrap;"></div>
<div class="progress-text">
<span ng-if="overallProgress < 100">{{ overallProgress }}%</span>
<span ng-if="overallProgress >= 100">{% trans "Complete!" %}</span>
</div>
</div>
</div>
<!-- Detailed Progress -->
<div class="progress-details">
<div class="progress-step" ng-class="{'active': currentStep >= 1, 'completed': currentStep > 1}">
<div class="step-icon">
<i class="fas fa-download" ng-if="currentStep < 1"></i>
<i class="fas fa-spinner fa-spin" ng-if="currentStep === 1"></i>
<i class="fas fa-check" ng-if="currentStep > 1"></i>
</div>
<div class="step-content">
<div class="step-title">{% trans "Downloading Backups" %}</div>
<div class="step-description">{{ downloadStatus }}</div>
</div>
</div>
<div class="progress-step" ng-class="{'active': currentStep >= 2, 'completed': currentStep > 2}">
<div class="step-icon">
<i class="fas fa-upload" ng-if="currentStep < 2"></i>
<i class="fas fa-spinner fa-spin" ng-if="currentStep === 2"></i>
<i class="fas fa-check" ng-if="currentStep > 2"></i>
</div>
<div class="step-content">
<div class="step-title">{% trans "Transferring Data" %}</div>
<div class="step-description">{{ transferStatus }}</div>
</div>
</div>
<div class="progress-step" ng-class="{'active': currentStep >= 3, 'completed': currentStep > 3}">
<div class="step-icon">
<i class="fas fa-cogs" ng-if="currentStep < 3"></i>
<i class="fas fa-spinner fa-spin" ng-if="currentStep === 3"></i>
<i class="fas fa-check" ng-if="currentStep > 3"></i>
</div>
<div class="step-content">
<div class="step-title">{% trans "Restoring Websites" %}</div>
<div class="step-description">{{ restoreStatus }}</div>
</div>
</div>
</div>
<!-- Live Log Display -->
<div class="log-section">
<div class="log-header">
<h4>
<i class="fas fa-terminal"></i>
{% trans "Live Log" %}
<button class="btn btn-sm btn-outline-secondary" ng-click="toggleLog()">
<i class="fas" ng-class="showLog ? 'fa-eye-slash' : 'fa-eye'"></i>
{{ showLog ? 'Hide' : 'Show' }}
</button>
</h4>
</div>
<div class="log-content" ng-show="showLog">
<div class="log-output" id="logOutput">
<div ng-repeat="logEntry in logEntries track by $index"
class="log-entry"
ng-class="{'log-error': logEntry.type === 'error', 'log-success': logEntry.type === 'success', 'log-info': logEntry.type === 'info'}">
<span class="log-time">{{ logEntry.timestamp | date:'HH:mm:ss' }}</span>
<span class="log-message">{{ logEntry.message }}</span>
</div>
</div>
</div>
</div>

View File

@ -1008,8 +1008,11 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
if (!$scope.blockingIP) {
$scope.blockingIP = ipAddress;
// Use the new Banned IPs system instead of the old blockIPAddress
var data = {
ip_address: ipAddress
ip: ipAddress,
reason: 'Brute force attack detected from SSH Security Analysis',
duration: 'permanent'
};
var config = {
@ -1018,7 +1021,7 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
}
};
$http.post('/base/blockIPAddress', data, config).then(function (response) {
$http.post('/firewall/addBannedIP', data, config).then(function (response) {
$scope.blockingIP = null;
if (response.data && response.data.status === 1) {
// Mark IP as blocked
@ -1026,8 +1029,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
// Show success notification
new PNotify({
title: 'Success',
text: `IP address ${ipAddress} has been blocked successfully using ${response.data.firewall.toUpperCase()}`,
title: 'IP Address Banned',
text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`,
type: 'success',
delay: 5000
});

View File

@ -234,6 +234,7 @@
width: 100%;
border-collapse: collapse;
margin-top: 15px;
table-layout: fixed;
}
.activity-table th {
@ -535,12 +536,12 @@
<table class="activity-table" ng-if="!loadingSSHLogins && sshLogins.length > 0">
<thead>
<tr>
<th>USER</th>
<th>IP</th>
<th>COUNTRY</th>
<th>DATE</th>
<th>SESSION</th>
<th>ACTIVITY</th>
<th style="width: 15%;">USER</th>
<th style="width: 20%;">IP</th>
<th style="width: 15%;">COUNTRY</th>
<th style="width: 20%;">DATE</th>
<th style="width: 15%;">SESSION</th>
<th style="width: 15%;">ACTIVITY</th>
</tr>
</thead>
<tbody>
@ -551,7 +552,15 @@
<td>{$ login.date $}</td>
<td>{$ login.session $}</td>
<td>
<button class="view-activity-btn" ng-click="viewSSHActivity(login)">View Activity</button>
<div style="display: flex; gap: 8px; align-items: center;">
<button class="view-activity-btn" ng-click="viewSSHActivity(login)">View Activity</button>
<button class="ban-ip-btn" ng-click="blockIPAddress(login.ip)"
style="background: #dc2626; color: white; border: 1px solid #dc2626; padding: 6px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer;"
onmouseover="this.style.background='#b91c1c'"
onmouseout="this.style.background='#dc2626'">
<i class="fas fa-ban"></i> Ban IP
</button>
</div>
</td>
</tr>
</tbody>
@ -561,12 +570,12 @@
<table class="activity-table" ng-if="loadingSSHLogins">
<thead>
<tr>
<th>USER</th>
<th>IP</th>
<th>COUNTRY</th>
<th>DATE</th>
<th>SESSION</th>
<th>ACTIVITY</th>
<th style="width: 15%;">USER</th>
<th style="width: 20%;">IP</th>
<th style="width: 15%;">COUNTRY</th>
<th style="width: 20%;">DATE</th>
<th style="width: 15%;">SESSION</th>
<th style="width: 15%;">ACTIVITY</th>
</tr>
</thead>
<tbody>
@ -577,7 +586,12 @@
<td>Wed Jun 4 20:59</td>
<td>Still Logged In</td>
<td>
<button class="view-activity-btn" style="background: #5b5fcf; color: white; border-color: #5b5fcf;">View Activity</button>
<div style="display: flex; gap: 8px; align-items: center;">
<button class="view-activity-btn" style="background: #5b5fcf; color: white; border-color: #5b5fcf;">View Activity</button>
<button class="ban-ip-btn" style="background: #dc2626; color: white; border: 1px solid #dc2626; padding: 6px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer;">
<i class="fas fa-ban"></i> Ban IP
</button>
</div>
</td>
</tr>
</tbody>
@ -672,9 +686,12 @@
onmouseout="this.style.background='#dc2626'">
<i class="fas fa-ban" ng-if="blockingIP !== alert.details['IP Address']"></i>
<i class="fas fa-spinner fa-spin" ng-if="blockingIP === alert.details['IP Address']"></i>
<span ng-if="blockingIP !== alert.details['IP Address']">Block IP</span>
<span ng-if="blockingIP === alert.details['IP Address']">Blocking...</span>
<span ng-if="blockingIP !== alert.details['IP Address']">Ban IP Permanently</span>
<span ng-if="blockingIP === alert.details['IP Address']">Banning...</span>
</button>
<a href="/firewall/" target="_blank" style="margin-left: 10px; color: #5b5fcf; font-size: 12px; text-decoration: none;">
<i class="fas fa-external-link-alt"></i> Manage in Firewall
</a>
<span ng-if="blockedIPs && blockedIPs[alert.details['IP Address']]"
style="margin-left: 10px; color: #10b981; font-size: 12px; font-weight: 600;">
<i class="fas fa-check-circle"></i> Blocked

View File

@ -23,6 +23,9 @@
<!-- Mobile Responsive CSS -->
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/mobile-responsive.css' %}?v={{ CP_VERSION }}">
<!-- Readability Fixes CSS -->
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/readability-fixes.css' %}?v={{ CP_VERSION }}">
<!-- Core Scripts -->
<script src="{% static 'baseTemplate/angularjs.1.6.5.js' %}?v={{ CP_VERSION }}"></script>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
@ -54,7 +57,7 @@
--bg-sidebar-item: white;
--bg-hover: #e8e6ff;
--text-primary: #2f3640;
--text-secondary: #64748b;
--text-secondary: #2f3640; /* Changed from grey to dark for better readability */
--text-heading: #1e293b;
--border-color: #e8e9ff;
--shadow-color: rgba(0,0,0,0.05);
@ -74,7 +77,7 @@
--bg-sidebar-item: #1e1e42;
--bg-hover: #252550;
--text-primary: #e4e4e7;
--text-secondary: #9ca3af;
--text-secondary: #e4e4e7; /* Changed from grey to light for better readability */
--text-heading: #f3f4f6;
--border-color: #2a2a5e;
--shadow-color: rgba(0,0,0,0.3);
@ -352,6 +355,12 @@
overflow: hidden;
}
/* Fix green text issue - ensure uptime uses normal text color */
#sidebar .server-info .info-line span,
#sidebar .server-info .info-line .ng-binding {
color: var(--text-secondary) !important;
}
#sidebar .server-info .info-line strong {
color: var(--text-primary);
font-weight: 600;
@ -701,6 +710,7 @@
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 12px;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
@ -791,6 +801,7 @@
padding: 8px;
border-radius: 6px;
transition: all 0.2s ease;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
@ -887,6 +898,47 @@
background: var(--accent-hover);
}
/* Enhanced Text Readability Fixes */
/* Ensure all text elements use proper contrast colors */
.menu-item span,
.menu-item i,
.info-line,
.info-line span,
.info-line strong,
.server-details,
.server-details *,
.tagline,
.brand {
color: inherit !important;
}
/* Override any Angular binding colors that might be green */
.ng-binding {
color: var(--text-secondary) !important;
}
/* Ensure menu items have proper text color */
#sidebar .menu-item {
color: var(--text-secondary) !important;
}
#sidebar .menu-item:hover {
color: var(--accent-color) !important;
}
#sidebar .menu-item.active {
color: white !important;
}
/* Fix any remaining grey text issues */
.text-muted,
.text-secondary,
.text-light,
small,
.small {
color: var(--text-secondary) !important;
}
/* Mobile Menu Toggle */
#mobile-menu-toggle {
display: none;
@ -1113,7 +1165,6 @@
<i class="fab fa-wordpress"></i>
</div>
<span>WordPress</span>
<span class="badge">NEW</span>
<i class="fas fa-chevron-right chevron"></i>
</a>
<div id="wordpress-submenu" class="submenu">
@ -1465,7 +1516,6 @@
</a>
<a href="{% url 'sslReconcile' %}" class="menu-item">
<span>SSL Reconciliation</span>
<span class="badge">NEW</span>
</a>
{% endif %}
{% if admin or hostnameSSL %}
@ -1488,7 +1538,6 @@
<i class="fas fa-database"></i>
</div>
<span>MySQL Manager</span>
<span class="badge">NEW</span>
</a>
<a href="{% url 'Filemanager' %}" class="menu-item">
@ -1503,7 +1552,6 @@
<i class="fas fa-fire"></i>
</div>
<span>CloudLinux</span>
<span class="badge">NEW</span>
<i class="fas fa-chevron-right chevron"></i>
</a>
<div id="cloudlinux-submenu" class="submenu">
@ -1669,7 +1717,6 @@
<i class="fas fa-envelope"></i>
</div>
<span>Mail Settings</span>
<span class="badge">NEW</span>
<i class="fas fa-chevron-right chevron"></i>
</a>
<div id="mail-settings-submenu" class="submenu">

View File

@ -1111,7 +1111,6 @@
<a href="#" title="Pages">
<i class="glyph-icon icon-linecons-fire"></i>
<span>Pages</span>
<span class="bs-label badge-yellow">NEW</span>
</a>
<div class="sidebar-submenu">

View File

@ -142,6 +142,27 @@ class cyberPanel:
logger.writeforCLI(str(msg), "Error", stack()[0][3])
print(0)
def listChildDomainsJson(self):
try:
child_domains = ChildDomains.objects.all()
ipFile = "/etc/cyberpanel/machineIP"
with open(ipFile, 'r') as f:
ipData = f.read()
ipAddress = ipData.split('\n', 1)[0]
json_data = []
for items in child_domains:
dic = {'parent_site': items.master.domain,
'domain': items.domain,
'path': items.path,
'ssl': items.ssl,
'php_version': items.phpSelection}
json_data.append(dic)
final_json = json.dumps(json_data)
print(final_json)
except BaseException as msg:
logger.writeforCLI(str(msg), "Error", stack()[0][3])
print(0)
def listWebsitesPretty(self):
try:
from prettytable import PrettyTable
@ -1049,6 +1070,19 @@ def main():
cyberpanel.deleteDNSRecord(args.recordID)
## Fix File Permission function
elif args.function == "fixFilePermissions":
completeCommandExample = 'cyberpanel fixFilePermissions --domainName cyberpanel.net'
if not args.domainName:
print("\n\nPlease enter the domain. For example:\n\n" + completeCommandExample + "\n\n")
return
from filemanager.filemanager import FileManager
fm = FileManager(None, None)
fm.fixPermissions(args.domainName)
## Backup Functions.
elif args.function == "createBackup":

View File

@ -43,6 +43,10 @@ class CloudManager:
def verifyLogin(self, request):
try:
# Check if token needs to be generated
if self.admin.token == 'TOKEN_NEEDS_GENERATION':
return 0, self.ajaxPre(0, 'API token needs to be generated. Please reset your password to generate a valid API token.')
if request.META['HTTP_AUTHORIZATION'] == self.admin.token:
return 1, self.ajaxPre(1, None)
else:

View File

@ -18,7 +18,8 @@ def router(request):
cm = CloudManager(data, admin)
if serverUserName != 'admin':
# Check if user has administrator privileges through ACL
if admin.acl.adminStatus != 1:
return cm.ajaxPre(0, 'Only administrator can access API.')
if admin.api == 0:

View File

@ -0,0 +1,185 @@
# CyberPanel ModSecurity Rules Fix
## Overview
This fix addresses common issues with ModSecurity Rules Packages in CyberPanel where OWASP ModSecurity Core Rules show as "off" even after installation. The problem typically occurs due to:
1. **Incorrect status detection logic** - The system doesn't properly detect installed OWASP rules
2. **Outdated download URLs** - The OWASP rules download URL was incorrect
3. **JavaScript state synchronization issues** - Frontend toggle state doesn't sync with backend
4. **Missing error handling** - Insufficient logging and error reporting
## Issues Fixed
### 1. Status Detection Logic (`firewallManager.py`)
- **Problem**: The `getOWASPAndComodoStatus` method only checked for `modsec/owasp` in configuration files
- **Fix**: Added multiple detection methods:
- Check for `modsec/owasp` in configuration
- Check for `owasp-modsecurity-crs` in configuration
- Verify actual file existence in filesystem
- Added similar verification for Comodo rules
### 2. OWASP Rules Download (`modSec.py`)
- **Problem**: Used incorrect GitHub URL that resulted in 404 errors
- **Fix**: Updated to use correct GitHub repository URL:
- Old: `https://github.com/coreruleset/coreruleset/archive/v3.3.2/master.zip`
- New: `https://github.com/coreruleset/coreruleset/archive/refs/tags/v4.0.0.zip`
### 3. JavaScript State Synchronization (`firewall.js`)
- **Problem**: Toggle state variables weren't properly updated when status was fetched
- **Fix**: Added proper state variable updates (`owaspInstalled`, `comodoInstalled`) in both update scenarios
### 4. Error Handling and Logging (`modSec.py`)
- **Problem**: Insufficient logging made debugging difficult
- **Fix**: Added comprehensive logging throughout the installation process:
- Download progress logging
- Extraction progress logging
- File verification logging
- Installation verification
## Files Modified
1. **`cyberpanel/firewall/firewallManager.py`**
- Enhanced `getOWASPAndComodoStatus` method
- Added filesystem verification for rule packages
2. **`cyberpanel/plogical/modSec.py`**
- Updated OWASP download URL to v4.0.0
- Added comprehensive logging
- Added installation verification
- Improved error handling
- Updated to use simplified CRS v4.0.0 structure
3. **`cyberpanel/firewall/static/firewall/firewall.js`**
- Fixed JavaScript state synchronization
- Added proper variable updates
## Manual Fix Script
A comprehensive fix script is provided at `cyberpanel/cyberpanel-mods/security/modsecurity-fix.sh` that:
1. **Backs up** current configuration
2. **Downloads and installs** OWASP ModSecurity Core Rules v3.3.4
3. **Creates proper configuration files**
4. **Sets correct permissions**
5. **Updates LiteSpeed configuration**
6. **Restarts LiteSpeed**
7. **Verifies installation**
### Running the Fix Script
```bash
# Make the script executable
chmod +x cyberpanel/cyberpanel-mods/security/modsecurity-fix.sh
# Run the fix script
./cyberpanel/cyberpanel-mods/security/modsecurity-fix.sh
```
## Manual Installation Steps
If you prefer to fix the issue manually:
### 1. Download OWASP Rules
```bash
cd /tmp
wget https://github.com/coreruleset/coreruleset/archive/refs/tags/v4.0.0.zip -O owasp.zip
unzip owasp.zip -d /usr/local/lsws/conf/modsec/
mv /usr/local/lsws/conf/modsec/coreruleset-4.0.0 /usr/local/lsws/conf/modsec/owasp-modsecurity-crs-4.0.0
```
### 2. Set Up Configuration Files
```bash
cd /usr/local/lsws/conf/modsec/owasp-modsecurity-crs-4.0.0
cp crs-setup.conf.example crs-setup.conf
cp rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
cp rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf.example rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf
```
### 3. Create Master Configuration
Create `/usr/local/lsws/conf/modsec/owasp-modsecurity-crs-4.0.0/owasp-master.conf`:
```apache
include /usr/local/lsws/conf/modsec/owasp-modsecurity-crs-4.0.0/crs.conf
```
**Note**: CRS v4.0.0 uses a simplified structure with a single `crs.conf` file that includes all necessary rules, unlike v3.x which required individual rule file includes.
### Key Differences in CRS v4.0.0:
- **Simplified Configuration**: Single `crs.conf` file instead of multiple individual rule files
- **Plugin System**: Replaced application exclusion packages with a plugin system
- **Improved Performance**: Better rule organization and execution
- **Enhanced Security**: Updated attack patterns and detection methods
- **Better Documentation**: Improved configuration examples and guides
### 4. Update LiteSpeed Configuration
Add to `/usr/local/lsws/conf/httpd_config.conf`:
```apache
modsecurity_rules_file /usr/local/lsws/conf/modsec/owasp-modsecurity-crs-4.0.0/owasp-master.conf
```
### 5. Set Permissions and Restart
```bash
chown -R lsadm:lsadm /usr/local/lsws/conf/modsec
chmod -R 755 /usr/local/lsws/conf/modsec
systemctl restart lsws
```
## Verification
After applying the fix:
1. **Access CyberPanel** → Security → ModSecurity Rules Packages
2. **Check Status**: OWASP ModSecurity Core Rules should show as "enabled"
3. **Test Toggle**: The toggle should work properly (enable/disable)
4. **Check Logs**: Verify no errors in ModSecurity logs
## Troubleshooting
### Common Issues
1. **Rules still show as disabled**
- Check file permissions: `ls -la /usr/local/lsws/conf/modsec/owasp-modsecurity-crs-3.0-master/`
- Verify configuration: `grep -i owasp /usr/local/lsws/conf/httpd_config.conf`
- Check LiteSpeed logs: `tail -f /usr/local/lsws/logs/error.log`
2. **Download fails**
- Check internet connectivity
- Verify GitHub access: `curl -I https://github.com/coreruleset/coreruleset/archive/refs/tags/v3.3.4.zip`
- Try manual download and extraction
3. **LiteSpeed won't start**
- Check configuration syntax: `/usr/local/lsws/bin/lshttpd -t`
- Restore backup: `cp /usr/local/lsws/conf/httpd_config.conf.backup.* /usr/local/lsws/conf/httpd_config.conf`
- Check ModSecurity syntax
### Log Files
- **ModSecurity Log**: `/usr/local/lsws/logs/modsec.log`
- **Audit Log**: `/usr/local/lsws/logs/auditmodsec.log`
- **Installation Log**: `/home/cyberpanel/modSecInstallLog`
- **LiteSpeed Error Log**: `/usr/local/lsws/logs/error.log`
## Security Considerations
1. **Rule Updates**: Regularly update OWASP rules for latest security patterns
2. **False Positives**: Monitor logs for legitimate traffic being blocked
3. **Performance**: OWASP rules can impact performance - monitor server resources
4. **Custom Rules**: Add custom rules in `/usr/local/lsws/conf/modsec/rules.conf`
## Support
If you encounter issues after applying this fix:
1. Check the troubleshooting section above
2. Review log files for specific error messages
3. Verify all file permissions and ownership
4. Test with a simple configuration first
## Changelog
- **v1.0**: Initial fix for ModSecurity status detection issues
- **v1.1**: Added comprehensive logging and error handling
- **v1.2**: Updated to OWASP CRS v4.0.0 and improved verification
- **v1.3**: Simplified configuration structure for CRS v4.0.0 compatibility

View File

@ -270,6 +270,11 @@ setup_epel_repo() {
yum install -y https://cyberpanel.sh/dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
Check_Return "yum repo" "no_exit"
;;
"10")
# AlmaLinux 10 EPEL support
yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm
Check_Return "yum repo" "no_exit"
;;
esac
}
@ -309,11 +314,12 @@ gpgcheck=1
EOF
elif [[ "$Server_OS_Version" = "10" ]] && uname -m | grep -q 'x86_64'; then
cat <<EOF >/etc/yum.repos.d/MariaDB.repo
# MariaDB 10.11 CentOS repository list - created 2021-08-06 02:01 UTC
# MariaDB 10.11 RHEL10 repository list - AlmaLinux 10 compatible
# http://downloads.mariadb.org/mariadb/repositories/
[mariadb]
name = MariaDB
baseurl = http://yum.mariadb.org/10.11/rhel9-amd64/
baseurl = http://yum.mariadb.org/10.11/rhel10-amd64/
module_hotfixes=1
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
enabled=1
gpgcheck=1
@ -1117,7 +1123,13 @@ log_function_start "Pre_Install_Setup_Repository"
log_info "Setting up package repositories for $Server_OS $Server_OS_Version"
if [[ $Server_OS = "CentOS" ]] ; then
log_debug "Importing LiteSpeed GPG key"
rpm --import https://cyberpanel.sh/rpms.litespeedtech.com/centos/RPM-GPG-KEY-litespeed
rpm --import https://cyberpanel.sh/rpms.litespeedtech.com/centos/RPM-GPG-KEY-litespeed || {
log_warning "Primary GPG key import failed, trying alternative source"
rpm --import https://rpms.litespeedtech.com/centos/RPM-GPG-KEY-litespeed || {
log_error "Failed to import LiteSpeed GPG key from all sources"
return 1
}
}
#import the LiteSpeed GPG key
yum clean all
@ -1151,8 +1163,13 @@ if [[ $Server_OS = "CentOS" ]] ; then
dnf config-manager --set-enabled crb
fi
yum install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm
if [[ "$Server_OS_Version" = "9" ]]; then
yum install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm
Check_Return "yum repo" "no_exit"
elif [[ "$Server_OS_Version" = "10" ]]; then
yum install -y https://rpms.remirepo.net/enterprise/remi-release-10.rpm
Check_Return "yum repo" "no_exit"
fi
fi
if [[ "$Server_OS_Version" = "8" ]]; then
@ -1337,12 +1354,23 @@ if [[ "$Server_OS" = "CentOS" ]] || [[ "$Server_OS" = "openEuler" ]] ; then
dnf install -y libnsl zip wget strace net-tools curl which bc telnet htop libevent-devel gcc libattr-devel xz-devel mariadb-devel curl-devel git platform-python-devel tar socat python3 zip unzip bind-utils gpgme-devel
Check_Return
elif [[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]] ; then
#!/bin/bash
dnf install -y libnsl zip wget strace net-tools curl which bc telnet htop libevent-devel gcc libattr-devel xz-devel MariaDB-server MariaDB-client MariaDB-devel curl-devel git platform-python-devel tar socat python3 zip unzip bind-utils gpgme-devel openssl-devel
# Enhanced package installation for AlmaLinux 9/10
dnf install -y libnsl zip wget strace net-tools curl which bc telnet htop libevent-devel gcc libattr-devel xz-devel MariaDB-server MariaDB-client MariaDB-devel curl-devel git platform-python-devel tar socat python3 zip unzip bind-utils gpgme-devel openssl-devel boost-devel boost-program-options
Check_Return
# Fix boost library compatibility for galera-4 on AlmaLinux 10
if [[ "$Server_OS_Version" = "10" ]]; then
# Create symlink for boost libraries if needed
if [ ! -f /usr/lib64/libboost_program_options.so.1.75.0 ]; then
BOOST_VERSION=$(find /usr/lib64 -name "libboost_program_options.so.*" | head -1 | sed 's/.*libboost_program_options\.so\.//')
if [ -n "$BOOST_VERSION" ]; then
ln -sf /usr/lib64/libboost_program_options.so.$BOOST_VERSION /usr/lib64/libboost_program_options.so.1.75.0
log_info "Created boost library symlink for galera-4 compatibility: $BOOST_VERSION -> 1.75.0"
else
log_warning "Could not find boost libraries, galera-4 may not work properly"
fi
fi
fi
elif [[ "$Server_OS_Version" = "20" ]] || [[ "$Server_OS_Version" = "22" ]] || [[ "$Server_OS_Version" = "24" ]] ; then
dnf install -y libnsl zip wget strace net-tools curl which bc telnet htop libevent-devel gcc libattr-devel xz-devel mariadb-devel curl-devel git python3-devel tar socat python3 zip unzip bind-utils gpgme-devel
Check_Return

View File

@ -1,78 +0,0 @@
# Docker Manager Module - Critical and Medium Issues Fixed
## Summary
This document outlines all the critical and medium priority issues that have been fixed in the Docker Manager module of CyberPanel.
## 🔴 Critical Issues Fixed
### 1. Missing pullImage Function Implementation
- **Issue**: `pullImage` function was referenced in templates and JavaScript but not implemented
- **Files Modified**:
- `container.py` - Added `pullImage()` method with security validation
- `views.py` - Added `pullImage()` view function
- `urls.py` - Added URL route for pullImage
- **Security Features Added**:
- Image name validation to prevent injection attacks
- Proper error handling for Docker API errors
- Admin permission checks
### 2. Inconsistent Error Handling
- **Issue**: Multiple functions used `BaseException` which catches all exceptions including system exits
- **Files Modified**: `container.py`, `views.py`
- **Changes**: Replaced `BaseException` with `Exception` for better error handling
- **Impact**: Improved debugging and error reporting
## 🟡 Medium Priority Issues Fixed
### 3. Security Enhancements
- **Rate Limiting Improvements**:
- Enhanced rate limiting system with JSON-based tracking
- Better error logging for rate limit violations
- Improved fallback handling when rate limiting fails
- **Command Validation**: Already had good validation, enhanced error messages
### 4. Code Quality Issues
- **Typo Fixed**: `WPemal``WPemail` in `recreateappcontainer` function
- **Import Issues**: Fixed undefined `loadImages` reference
- **URL Handling**: Improved redirect handling with proper Django URL reversal
### 5. Template Consistency
- **CSS Variables**: Fixed inconsistent CSS variable usage in templates
- **Files Modified**: `manageImages.html`
- **Changes**: Standardized `--bg-gradient` variable usage
## 🔧 Technical Details
### New Functions Added
1. **`pullImage(userID, data)`** - Pulls Docker images with security validation
2. **`_validate_image_name(image_name)`** - Validates Docker image names to prevent injection
### Enhanced Functions
1. **`_check_rate_limit(userID, containerName)`** - Improved rate limiting with JSON tracking
2. **Error handling** - Replaced BaseException with Exception throughout
### Security Improvements
- Image name validation using regex pattern: `^[a-zA-Z0-9._/-]+$`
- Enhanced rate limiting with detailed logging
- Better error messages for debugging
- Proper permission checks for all operations
## 📊 Files Modified
- `cyberpanel/dockerManager/container.py` - Main container management logic
- `cyberpanel/dockerManager/views.py` - Django view functions
- `cyberpanel/dockerManager/urls.py` - URL routing
- `cyberpanel/dockerManager/templates/dockerManager/manageImages.html` - Template consistency
## ✅ Testing Recommendations
1. Test image pulling functionality with various image names
2. Verify rate limiting works correctly
3. Test error handling with invalid inputs
4. Confirm all URLs are accessible
5. Validate CSS consistency across templates
## 🚀 Status
All critical and medium priority issues have been resolved. The Docker Manager module is now more secure, robust, and maintainable.
---
*Generated on: $(date)*
*Fixed by: AI Assistant*

View File

@ -1,199 +0,0 @@
# Docker Container Update/Upgrade Features
## Overview
This implementation adds comprehensive Docker container update/upgrade functionality to CyberPanel with full data persistence using Docker volumes. The solution addresses the GitHub issue [#1174](https://github.com/usmannasir/cyberpanel/issues/1174) by providing safe container updates without data loss.
## Features Implemented
### 1. Container Update with Data Preservation
- **Function**: `updateContainer()`
- **Purpose**: Update container to new image while preserving all data
- **Data Safety**: Uses Docker volumes to ensure no data loss
- **Process**:
1. Extracts current container configuration (volumes, environment, ports)
2. Pulls new image if not available locally
3. Creates new container with same configuration but new image
4. Preserves all volumes and data
5. Removes old container only after successful new container startup
6. Updates database records
### 2. Delete Container + Data
- **Function**: `deleteContainerWithData()`
- **Purpose**: Permanently delete container and all associated data
- **Safety**: Includes strong confirmation dialogs
- **Process**:
1. Identifies all volumes associated with container
2. Stops and removes container
3. Deletes all associated Docker volumes
4. Removes database records
5. Provides confirmation of deleted volumes
### 3. Delete Container (Keep Data)
- **Function**: `deleteContainerKeepData()`
- **Purpose**: Delete container but preserve data in volumes
- **Use Case**: When you want to remove container but keep data for future use
- **Process**:
1. Identifies volumes to preserve
2. Stops and removes container
3. Keeps all volumes intact
4. Reports preserved volumes to user
## Technical Implementation
### Backend Changes
#### Views (`views.py`)
- `updateContainer()` - Handles container updates
- `deleteContainerWithData()` - Handles destructive deletion
- `deleteContainerKeepData()` - Handles data-preserving deletion
#### URLs (`urls.py`)
- `/docker/updateContainer` - Update endpoint
- `/docker/deleteContainerWithData` - Delete with data endpoint
- `/docker/deleteContainerKeepData` - Delete keep data endpoint
#### Container Manager (`container.py`)
- `updateContainer()` - Core update logic with volume preservation
- `deleteContainerWithData()` - Complete data removal
- `deleteContainerKeepData()` - Container removal with data preservation
### Frontend Changes
#### Template (`listContainers.html`)
- New update button with sync icon
- Dropdown menu for delete options
- Update modal with image/tag selection
- Enhanced styling for new components
#### JavaScript (`dockerManager.js`)
- `showUpdateModal()` - Opens update dialog
- `performUpdate()` - Executes container update
- `deleteContainerWithData()` - Handles destructive deletion
- `deleteContainerKeepData()` - Handles data-preserving deletion
- Enhanced confirmation dialogs
## User Interface
### New Buttons
1. **Update Button** (🔄) - Orange button for container updates
2. **Delete Dropdown** (🗑️) - Red dropdown with two options:
- Delete Container (Keep Data) - Preserves volumes
- Delete Container + Data - Removes everything
### Update Modal
- Container name (read-only)
- Current image (read-only)
- New image input field
- New tag input field
- Data safety information
- Confirmation buttons
### Confirmation Dialogs
- **Update**: Confirms image/tag change with data preservation notice
- **Delete + Data**: Strong warning about permanent data loss
- **Delete Keep Data**: Confirms container removal with data preservation
## Data Safety Features
### Volume Management
- Automatic detection of container volumes
- Support for both named volumes and bind mounts
- Volume preservation during updates
- Volume cleanup during destructive deletion
### Error Handling
- Rollback capability if update fails
- Comprehensive error messages
- Operation logging for debugging
- Graceful failure handling
### Security
- ACL permission checks
- Container ownership verification
- Input validation
- Rate limiting (existing)
## Usage Examples
### Updating a Container
1. Click the update button (🔄) next to any container
2. Enter new image name (e.g., `nginx`, `mysql`)
3. Enter new tag (e.g., `latest`, `1.21`, `alpine`)
4. Click "Update Container"
5. Confirm the operation
6. Container updates with all data preserved
### Deleting with Data Preservation
1. Click the delete dropdown (🗑️) next to any container
2. Select "Delete Container (Keep Data)"
3. Confirm the operation
4. Container is removed but data remains in volumes
### Deleting Everything
1. Click the delete dropdown (🗑️) next to any container
2. Select "Delete Container + Data"
3. Read the warning carefully
4. Confirm the operation
5. Container and all data are permanently removed
## Benefits
### For Users
- **No Data Loss**: Updates preserve all container data
- **Easy Updates**: Simple interface for container updates
- **Flexible Deletion**: Choose between data preservation or complete removal
- **Clear Warnings**: Understand exactly what each operation does
### For Administrators
- **Safe Operations**: Built-in safety measures prevent accidental data loss
- **Audit Trail**: All operations are logged
- **Rollback Capability**: Failed updates can be rolled back
- **Volume Management**: Clear visibility into data storage
## Technical Requirements
### Docker Features Used
- Docker volumes for data persistence
- Container recreation with volume mounting
- Image pulling and management
- Volume cleanup and management
### Dependencies
- Docker Python SDK
- Existing CyberPanel ACL system
- PNotify for user notifications
- Bootstrap for UI components
## Testing
A test script is provided (`test_docker_update.py`) that verifies:
- All new methods are available
- Function signatures are correct
- Error handling is in place
- UI components are properly integrated
## Future Enhancements
### Potential Improvements
1. **Bulk Operations**: Update/delete multiple containers
2. **Scheduled Updates**: Automatic container updates
3. **Update History**: Track container update history
4. **Volume Management UI**: Direct volume management interface
5. **Backup Integration**: Automatic backups before updates
### Monitoring
1. **Update Notifications**: Email notifications for updates
2. **Health Checks**: Verify container health after updates
3. **Performance Metrics**: Track update performance
4. **Error Reporting**: Detailed error reporting and recovery
## Conclusion
This implementation provides a complete solution for Docker container updates in CyberPanel while ensuring data safety through Docker volumes. The user-friendly interface makes container management accessible while the robust backend ensures data integrity and system stability.
The solution addresses the original GitHub issue by providing:
- ✅ Safe container updates without data loss
- ✅ Clear separation between container and data deletion
- ✅ User-friendly interface with proper confirmations
- ✅ Comprehensive error handling and rollback capability
- ✅ Full integration with existing CyberPanel architecture

View File

@ -13,6 +13,7 @@ django.setup()
import json
from plogical.acl import ACLManager
import plogical.CyberCPLogFileWriter as logging
from plogical.errorSanitizer import secure_error_response, secure_log_error
from django.shortcuts import HttpResponse, render, redirect
from django.urls import reverse
from loginSystem.models import Administrator
@ -211,8 +212,9 @@ class ContainerManager(multi.Thread):
template = 'dockerManager/viewContainer.html'
proc = httpProc(request, template, data, 'admin')
return proc.render()
except BaseException as msg:
return HttpResponse(str(msg))
except Exception as e:
secure_log_error(e, 'container_operation')
return HttpResponse('Operation failed')
def listContainers(self, request=None, userID=None, data=None):
client = docker.from_env()
@ -270,15 +272,69 @@ class ContainerManager(multi.Thread):
dockerAPI = docker.APIClient()
container = client.containers.get(name)
logs = container.logs().decode("utf-8")
# Get logs with proper formatting
try:
# Get logs with timestamps and proper formatting
logs = container.logs(
stdout=True,
stderr=True,
timestamps=True,
tail=1000 # Limit to last 1000 lines for performance
).decode("utf-8", errors='replace')
# Clean up the logs for better display
if logs:
# Split into lines and clean up
log_lines = logs.split('\n')
cleaned_lines = []
for line in log_lines:
# Remove Docker's log prefix if present
if line.startswith('[') and ']' in line:
# Extract timestamp and message
try:
timestamp_end = line.find(']')
if timestamp_end > 0:
timestamp = line[1:timestamp_end]
message = line[timestamp_end + 1:].strip()
# Format the line nicely
if message:
cleaned_lines.append(f"[{timestamp}] {message}")
else:
cleaned_lines.append(line)
except:
cleaned_lines.append(line)
else:
cleaned_lines.append(line)
logs = '\n'.join(cleaned_lines)
else:
logs = "No logs available for this container."
except Exception as log_err:
# Fallback to basic logs if timestamped logs fail
try:
logs = container.logs().decode("utf-8", errors='replace')
if not logs:
logs = "No logs available for this container."
except:
logs = f"Error retrieving logs: {str(log_err)}"
data_ret = {'containerLogStatus': 1, 'containerLog': logs, 'error_message': "None"}
json_data = json.dumps(data_ret)
data_ret = {
'containerLogStatus': 1,
'containerLog': logs,
'error_message': "None",
'container_status': container.status,
'log_count': len(logs.split('\n')) if logs else 0
}
json_data = json.dumps(data_ret, ensure_ascii=False)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'containerLogStatus': 0, 'containerLog': 'Error', 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, 'containerLogStatus')
data_ret = secure_error_response(e, 'Failed to containerLogStatus')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -300,7 +356,24 @@ class ContainerManager(multi.Thread):
envList = data['envList']
volList = data['volList']
inspectImage = dockerAPI.inspect_image(image + ":" + tag)
try:
inspectImage = dockerAPI.inspect_image(image + ":" + tag)
except docker.errors.APIError as err:
error_message = str(err)
data_ret = {'createContainerStatus': 0, 'error_message': f'Failed to inspect image: {error_message}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except docker.errors.ImageNotFound as err:
error_message = str(err)
data_ret = {'createContainerStatus': 0, 'error_message': f'Image not found: {error_message}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except Exception as err:
error_message = str(err)
data_ret = {'createContainerStatus': 0, 'error_message': f'Error inspecting image: {error_message}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
portConfig = {}
# Formatting envList for usage - handle both simple and advanced modes
@ -344,6 +417,22 @@ class ContainerManager(multi.Thread):
if isinstance(volume, dict) and 'src' in volume and 'dest' in volume:
volumes[volume['src']] = {'bind': volume['dest'], 'mode': 'rw'}
# Network configuration
network = data.get('network', 'bridge') # Default to bridge network
network_mode = data.get('network_mode', 'bridge')
# Extra options support (like --add-host)
extra_hosts = {}
extra_options = data.get('extraOptions', {})
if extra_options:
for option, value in extra_options.items():
if option == 'add_host' and value:
# Parse --add-host entries (format: hostname:ip)
for host_entry in value.split(','):
if ':' in host_entry:
hostname, ip = host_entry.strip().split(':', 1)
extra_hosts[hostname.strip()] = ip.strip()
## Create Configurations
admin = Administrator.objects.get(userName=dockerOwner)
@ -353,14 +442,23 @@ class ContainerManager(multi.Thread):
'ports': portConfig,
'publish_all_ports': True,
'environment': envDict,
'volumes': volumes}
'volumes': volumes,
'network_mode': network_mode}
# Add network configuration
if network != 'bridge' or network_mode == 'bridge':
containerArgs['network'] = network
# Add extra hosts if specified
if extra_hosts:
containerArgs['extra_hosts'] = extra_hosts
containerArgs['mem_limit'] = memory * 1048576; # Converts MB to bytes ( 0 * x = 0 for unlimited memory)
try:
container = client.containers.create(**containerArgs)
except Exception as err:
# Check if it's a port allocation error by converting to string first
except docker.errors.APIError as err:
# Handle Docker API errors properly
error_message = str(err)
if "port is already allocated" in error_message: # We need to delete container if port is not available
print("Deleting container")
@ -368,7 +466,23 @@ class ContainerManager(multi.Thread):
container.remove(force=True)
except:
pass # Container might not exist yet
data_ret = {'createContainerStatus': 0, 'error_message': error_message}
data_ret = {'createContainerStatus': 0, 'error_message': f'Docker API error: {error_message}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except docker.errors.ImageNotFound as err:
error_message = str(err)
data_ret = {'createContainerStatus': 0, 'error_message': f'Image not found: {error_message}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except docker.errors.ContainerError as err:
error_message = str(err)
data_ret = {'createContainerStatus': 0, 'error_message': f'Container error: {error_message}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except Exception as err:
# Handle any other exceptions
error_message = str(err) if err else "Unknown error occurred"
data_ret = {'createContainerStatus': 0, 'error_message': f'Container creation error: {error_message}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -378,6 +492,9 @@ class ContainerManager(multi.Thread):
image=image,
memory=memory,
ports=json.dumps(portConfig),
network=network,
network_mode=network_mode,
extra_options=json.dumps(extra_options),
volumes=json.dumps(volumes),
env=json.dumps(envDict),
cid=container.id)
@ -660,8 +777,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'containerActionStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'containerActionStatus\')
data_ret = secure_error_response(e, \'Failed to containerActionStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -690,8 +808,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'containerStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'containerStatus\')
data_ret = secure_error_response(e, \'Failed to containerStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -720,8 +839,9 @@ class ContainerManager(multi.Thread):
response['Content-Disposition'] = 'attachment; filename="' + name + '.tar"'
return response
except BaseException as msg:
data_ret = {'containerStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'containerStatus\')
data_ret = secure_error_response(e, \'Failed to containerStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -756,8 +876,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'containerTopStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'containerTopStatus\')
data_ret = secure_error_response(e, \'Failed to containerTopStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -797,8 +918,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'assignContainerStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'assignContainerStatus\')
data_ret = secure_error_response(e, \'Failed to assignContainerStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -835,8 +957,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'searchImageStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'searchImageStatus\')
data_ret = secure_error_response(e, \'Failed to searchImageStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -885,8 +1008,9 @@ class ContainerManager(multi.Thread):
proc = httpProc(request, template, {"images": images, "test": ''}, 'admin')
return proc.render()
except BaseException as msg:
return HttpResponse(str(msg))
except Exception as e:
secure_log_error(e, \'container_operation\')
return HttpResponse(\'Operation failed\')
def manageImages(self, request=None, userID=None, data=None):
try:
@ -915,8 +1039,9 @@ class ContainerManager(multi.Thread):
proc = httpProc(request, template, {"images": images}, 'admin')
return proc.render()
except BaseException as msg:
return HttpResponse(str(msg))
except Exception as e:
secure_log_error(e, \'container_operation\')
return HttpResponse(\'Operation failed\')
def getImageHistory(self, userID=None, data=None):
try:
@ -941,8 +1066,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'imageHistoryStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'imageHistoryStatus\')
data_ret = secure_error_response(e, \'Failed to imageHistoryStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -957,18 +1083,67 @@ class ContainerManager(multi.Thread):
dockerAPI = docker.APIClient()
name = data['name']
force = data.get('force', False)
try:
if name == 0:
# Prune unused images
action = client.images.prune()
else:
action = client.images.remove(name)
# First, try to remove containers that might be using this image
containers_using_image = []
try:
for container in client.containers.list(all=True):
container_image = container.attrs['Config']['Image']
if container_image == name or container_image.startswith(name + ':'):
containers_using_image.append(container)
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Error checking containers for image {name}: {str(e)}')
# Remove containers that are using this image
for container in containers_using_image:
try:
if container.status == 'running':
container.stop()
time.sleep(1)
container.remove(force=True)
logging.CyberCPLogFileWriter.writeToFile(f'Removed container {container.name} that was using image {name}')
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Error removing container {container.name}: {str(e)}')
# Now try to remove the image
try:
if force:
action = client.images.remove(name, force=True)
else:
action = client.images.remove(name)
logging.CyberCPLogFileWriter.writeToFile(f'Successfully removed image {name}')
except docker.errors.APIError as err:
error_msg = str(err)
if "conflict: unable to remove repository reference" in error_msg and "must force" in error_msg:
# Try with force if not already forced
if not force:
logging.CyberCPLogFileWriter.writeToFile(f'Retrying image removal with force: {name}')
action = client.images.remove(name, force=True)
else:
raise err
else:
raise err
print(action)
except docker.errors.APIError as err:
data_ret = {'removeImageStatus': 0, 'error_message': str(err)}
error_message = str(err)
# Provide more helpful error messages
if "conflict: unable to remove repository reference" in error_message:
error_message = f"Image {name} is still being used by containers. Use force removal to delete it."
elif "No such image" in error_message:
error_message = f"Image {name} not found or already removed."
data_ret = {'removeImageStatus': 0, 'error_message': error_message}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except:
data_ret = {'removeImageStatus': 0, 'error_message': 'Unknown'}
except Exception as e:
data_ret = {'removeImageStatus': 0, 'error_message': f'Unknown error: {str(e)}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -976,8 +1151,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'removeImageStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'removeImageStatus\')
data_ret = secure_error_response(e, \'Failed to removeImageStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -1022,8 +1198,9 @@ class ContainerManager(multi.Thread):
con.save()
return 0
except BaseException as msg:
return str(msg)
except Exception as e:
secure_log_error(e, \'container_operation\')
return \'Operation failed\'
def saveContainerSettings(self, userID=None, data=None):
try:
@ -1120,8 +1297,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'saveSettingsStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'saveSettingsStatus\')
data_ret = secure_error_response(e, \'Failed to saveSettingsStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -1170,8 +1348,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'recreateContainerStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'recreateContainerStatus\')
data_ret = secure_error_response(e, \'Failed to recreateContainerStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -1201,8 +1380,9 @@ class ContainerManager(multi.Thread):
data_ret = {'getTagsStatus': 1, 'list': tagList, 'next': registryData['next'], 'error_message': None}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'getTagsStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'getTagsStatus\')
data_ret = secure_error_response(e, \'Failed to getTagsStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -1227,8 +1407,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'removeImageStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'removeImageStatus\')
data_ret = secure_error_response(e, \'Failed to removeImageStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -1280,8 +1461,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'status': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, 'getContainerAppinfo')
data_ret = secure_error_response(e, 'Failed to get container app info')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -1327,8 +1509,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'removeImageStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'removeImageStatus\')
data_ret = secure_error_response(e, \'Failed to removeImageStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -1371,8 +1554,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'removeImageStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'removeImageStatus\')
data_ret = secure_error_response(e, \'Failed to removeImageStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -1397,8 +1581,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'removeImageStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'removeImageStatus\')
data_ret = secure_error_response(e, \'Failed to removeImageStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -1423,8 +1608,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'removeImageStatus': 0, 'error_message': str(msg)}
except Exception as e:
secure_log_error(e, \'removeImageStatus\')
data_ret = secure_error_response(e, \'Failed to removeImageStatus\')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -1473,10 +1659,26 @@ class ContainerManager(multi.Thread):
data_ret = {'commandStatus': 0, 'error_message': f'Error accessing container: {str(err)}'}
return HttpResponse(json.dumps(data_ret))
# Check if container is running
# Handle container status - try to start if not running
container_was_stopped = False
if container.status != 'running':
data_ret = {'commandStatus': 0, 'error_message': 'Container must be running to execute commands'}
return HttpResponse(json.dumps(data_ret))
try:
# Try to start the container temporarily
container.start()
container_was_stopped = True
# Wait a moment for container to fully start
import time
time.sleep(2)
# Verify container is now running
container.reload()
if container.status != 'running':
data_ret = {'commandStatus': 0, 'error_message': 'Failed to start container for command execution'}
return HttpResponse(json.dumps(data_ret))
except Exception as start_err:
data_ret = {'commandStatus': 0, 'error_message': f'Container is not running and cannot be started: {str(start_err)}'}
return HttpResponse(json.dumps(data_ret))
# Log the command execution attempt
self._log_command_execution(userID, name, command)
@ -1517,6 +1719,14 @@ class ContainerManager(multi.Thread):
# Log successful execution
self._log_command_result(userID, name, command, exit_code, len(output))
# Stop container if it was started temporarily
if container_was_stopped:
try:
container.stop()
logging.CyberCPLogFileWriter.writeToFile(f'Stopped container {name} after command execution')
except Exception as stop_err:
logging.CyberCPLogFileWriter.writeToFile(f'Warning: Could not stop container {name} after command execution: {str(stop_err)}')
# Format the response
data_ret = {
'commandStatus': 1,
@ -1524,17 +1734,34 @@ class ContainerManager(multi.Thread):
'output': output,
'exit_code': exit_code,
'command': command,
'timestamp': time.time()
'timestamp': time.time(),
'container_was_started': container_was_stopped
}
return HttpResponse(json.dumps(data_ret, ensure_ascii=False))
except docker.errors.APIError as err:
# Stop container if it was started temporarily
if container_was_stopped:
try:
container.stop()
logging.CyberCPLogFileWriter.writeToFile(f'Stopped container {name} after API error')
except Exception as stop_err:
logging.CyberCPLogFileWriter.writeToFile(f'Warning: Could not stop container {name} after API error: {str(stop_err)}')
error_msg = f'Docker API error: {str(err)}'
self._log_command_error(userID, name, command, error_msg)
data_ret = {'commandStatus': 0, 'error_message': error_msg}
return HttpResponse(json.dumps(data_ret))
except Exception as err:
# Stop container if it was started temporarily
if container_was_stopped:
try:
container.stop()
logging.CyberCPLogFileWriter.writeToFile(f'Stopped container {name} after execution error')
except Exception as stop_err:
logging.CyberCPLogFileWriter.writeToFile(f'Warning: Could not stop container {name} after execution error: {str(stop_err)}')
error_msg = f'Execution error: {str(err)}'
self._log_command_error(userID, name, command, error_msg)
data_ret = {'commandStatus': 0, 'error_message': error_msg}
@ -2013,4 +2240,395 @@ class ContainerManager(multi.Thread):
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.deleteContainerKeepData]')
data_ret = {'deleteContainerKeepDataStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def updateContainer(self, userID=None, data=None):
"""
Update container with new image while preserving data using Docker volumes
This function handles the complete container update process:
1. Stops the current container
2. Creates a backup of the container configuration
3. Removes the old container
4. Pulls the new image
5. Creates a new container with the same configuration but new image
6. Preserves all volumes and data
"""
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
client = docker.from_env()
dockerAPI = docker.APIClient()
containerName = data['containerName']
newImage = data['newImage']
newTag = data.get('newTag', 'latest')
# Get the current container
try:
currentContainer = client.containers.get(containerName)
except docker.errors.NotFound:
data_ret = {'updateContainerStatus': 0, 'error_message': f'Container {containerName} not found'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except Exception as e:
data_ret = {'updateContainerStatus': 0, 'error_message': f'Error accessing container: {str(e)}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Get container configuration for recreation
containerConfig = currentContainer.attrs['Config']
hostConfig = currentContainer.attrs['HostConfig']
# Extract volumes for data preservation
volumes = {}
if 'Binds' in hostConfig and hostConfig['Binds']:
for bind in hostConfig['Binds']:
if ':' in bind:
parts = bind.split(':')
if len(parts) >= 2:
host_path = parts[0]
container_path = parts[1]
mode = parts[2] if len(parts) > 2 else 'rw'
volumes[host_path] = {'bind': container_path, 'mode': mode}
# Extract environment variables
environment = containerConfig.get('Env', [])
envDict = {}
for env in environment:
if '=' in env:
key, value = env.split('=', 1)
envDict[key] = value
# Extract port mappings
portConfig = {}
if 'PortBindings' in hostConfig and hostConfig['PortBindings']:
for container_port, host_bindings in hostConfig['PortBindings'].items():
if host_bindings and len(host_bindings) > 0:
host_port = host_bindings[0]['HostPort']
portConfig[container_port] = host_port
# Extract memory limit
memory_limit = hostConfig.get('Memory', 0)
if memory_limit > 0:
memory_limit = memory_limit // 1048576 # Convert bytes to MB
# Stop the current container
try:
if currentContainer.status == 'running':
currentContainer.stop(timeout=30)
logging.CyberCPLogFileWriter.writeToFile(f'Stopped container {containerName} for update')
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Error stopping container {containerName}: {str(e)}')
data_ret = {'updateContainerStatus': 0, 'error_message': f'Error stopping container: {str(e)}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Remove the old container
try:
currentContainer.remove(force=True)
logging.CyberCPLogFileWriter.writeToFile(f'Removed old container {containerName}')
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Error removing old container {containerName}: {str(e)}')
data_ret = {'updateContainerStatus': 0, 'error_message': f'Error removing old container: {str(e)}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Pull the new image
try:
image_name = f"{newImage}:{newTag}"
logging.CyberCPLogFileWriter.writeToFile(f'Pulling new image {image_name}')
client.images.pull(newImage, tag=newTag)
logging.CyberCPLogFileWriter.writeToFile(f'Successfully pulled image {image_name}')
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Error pulling image {newImage}:{newTag}: {str(e)}')
data_ret = {'updateContainerStatus': 0, 'error_message': f'Error pulling new image: {str(e)}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Create new container with same configuration but new image
try:
containerArgs = {
'image': image_name,
'detach': True,
'name': containerName,
'ports': portConfig,
'publish_all_ports': True,
'environment': envDict,
'volumes': volumes
}
if memory_limit > 0:
containerArgs['mem_limit'] = memory_limit * 1048576
newContainer = client.containers.create(**containerArgs)
logging.CyberCPLogFileWriter.writeToFile(f'Created new container {containerName} with image {image_name}')
# Start the new container
newContainer.start()
logging.CyberCPLogFileWriter.writeToFile(f'Started updated container {containerName}')
except docker.errors.APIError as err:
error_message = str(err)
if "port is already allocated" in error_message:
try:
newContainer.remove(force=True)
except:
pass
data_ret = {'updateContainerStatus': 0, 'error_message': f'Docker API error creating container: {error_message}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except docker.errors.ImageNotFound as err:
error_message = str(err)
data_ret = {'updateContainerStatus': 0, 'error_message': f'New image not found: {error_message}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Error creating new container {containerName}: {str(e)}')
data_ret = {'updateContainerStatus': 0, 'error_message': f'Error creating new container: {str(e)}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Log successful update
logging.CyberCPLogFileWriter.writeToFile(f'Successfully updated container {containerName} to image {image_name}')
data_ret = {
'updateContainerStatus': 1,
'error_message': 'None',
'message': f'Container {containerName} successfully updated to {image_name}'
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except Exception as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.updateContainer]')
data_ret = {'updateContainerStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def listContainers(self, userID=None):
"""
Get list of all Docker containers
"""
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
client = docker.from_env()
# Get all containers (including stopped ones)
containers = client.containers.list(all=True)
container_list = []
for container in containers:
container_info = {
'name': container.name,
'image': container.image.tags[0] if container.image.tags else container.image.short_id,
'status': container.status,
'state': container.attrs['State']['Status'],
'created': container.attrs['Created'],
'ports': container.attrs['NetworkSettings']['Ports'],
'mounts': container.attrs['Mounts'],
'id': container.short_id
}
container_list.append(container_info)
data_ret = {
'status': 1,
'error_message': 'None',
'containers': container_list
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except Exception as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.listContainers]')
data_ret = {'status': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def getDockerNetworks(self, userID=None):
"""
Get list of all Docker networks
"""
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
client = docker.from_env()
# Get all networks
networks = client.networks.list()
network_list = []
for network in networks:
network_info = {
'id': network.id,
'name': network.name,
'driver': network.attrs.get('Driver', 'unknown'),
'scope': network.attrs.get('Scope', 'local'),
'created': network.attrs.get('Created', ''),
'containers': len(network.attrs.get('Containers', {})),
'ipam': network.attrs.get('IPAM', {}),
'labels': network.attrs.get('Labels', {})
}
network_list.append(network_info)
data_ret = {
'status': 1,
'error_message': 'None',
'networks': network_list
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except Exception as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.getDockerNetworks]')
data_ret = {'status': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def createDockerNetwork(self, userID=None, data=None):
"""
Create a new Docker network
"""
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
client = docker.from_env()
name = data.get('name')
driver = data.get('driver', 'bridge')
subnet = data.get('subnet', '')
gateway = data.get('gateway', '')
ip_range = data.get('ip_range', '')
if not name:
data_ret = {'status': 0, 'error_message': 'Network name is required'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Prepare IPAM configuration
ipam_config = []
if subnet:
ipam_entry = {'subnet': subnet}
if gateway:
ipam_entry['gateway'] = gateway
if ip_range:
ipam_entry['ip_range'] = ip_range
ipam_config.append(ipam_entry)
ipam = {'driver': 'default', 'config': ipam_config} if ipam_config else None
# Create the network
network = client.networks.create(
name=name,
driver=driver,
ipam=ipam
)
data_ret = {
'status': 1,
'error_message': 'None',
'network_id': network.id,
'message': f'Network {name} created successfully'
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except Exception as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.createDockerNetwork]')
data_ret = {'status': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def updateContainerPorts(self, userID=None, data=None):
"""
Update port mappings for an existing container
"""
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
client = docker.from_env()
container_name = data.get('name')
new_ports = data.get('ports', {})
if not container_name:
data_ret = {'status': 0, 'error_message': 'Container name is required'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Get the container
try:
container = client.containers.get(container_name)
except docker.errors.NotFound:
data_ret = {'status': 0, 'error_message': f'Container {container_name} not found'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Check if container is running
if container.status != 'running':
data_ret = {'status': 0, 'error_message': 'Container must be running to update port mappings'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Get current container configuration
container_config = container.attrs['Config']
host_config = container.attrs['HostConfig']
# Update port bindings
port_bindings = {}
for container_port, host_port in new_ports.items():
if host_port: # Only add if host port is specified
port_bindings[container_port] = host_port
# Stop the container
container.stop(timeout=10)
# Create new container with updated port configuration
new_container = client.containers.create(
image=container_config['Image'],
name=f"{container_name}_temp",
ports=list(new_ports.keys()),
host_config=client.api.create_host_config(port_bindings=port_bindings),
environment=container_config.get('Env', []),
volumes=host_config.get('Binds', []),
detach=True
)
# Remove old container and rename new one
container.remove()
new_container.rename(container_name)
# Start the updated container
new_container.start()
# Update database record if it exists
try:
db_container = Containers.objects.get(name=container_name)
db_container.ports = json.dumps(new_ports)
db_container.save()
except Containers.DoesNotExist:
pass # Container not in database, that's okay
data_ret = {
'status': 1,
'error_message': 'None',
'message': f'Port mappings updated for container {container_name}'
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except Exception as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.updateContainerPorts]')
data_ret = {'status': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)

View File

@ -16,3 +16,6 @@ class Containers(models.Model):
volumes = models.TextField(default="{}")
env = models.TextField(default="{}")
startOnReboot = models.IntegerField(default=0)
network = models.CharField(max_length=100, default='bridge')
network_mode = models.CharField(max_length=50, default='bridge')
extra_options = models.TextField(default="{}")

View File

@ -127,6 +127,81 @@ app.controller('runContainer', function ($scope, $http) {
// Advanced Environment Variable Mode
$scope.advancedEnvMode = false;
// Network configuration
$scope.selectedNetwork = 'bridge';
$scope.networkMode = 'bridge';
$scope.extraHosts = '';
$scope.availableNetworks = [];
// Load available Docker networks
$scope.loadAvailableNetworks = function() {
var url = "/docker/getDockerNetworks";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, {}, config).then(function(response) {
if (response.data.status === 1) {
$scope.availableNetworks = response.data.networks;
}
}, function(error) {
console.error('Failed to load networks:', error);
});
};
// Initialize networks on page load
$scope.loadAvailableNetworks();
// Helper function to generate Docker Compose YAML
$scope.generateDockerComposeYml = function(containerInfo) {
var yml = 'version: \'3.8\'\n\n';
yml += 'services:\n';
yml += ' ' + containerInfo.name + ':\n';
yml += ' image: ' + containerInfo.image + '\n';
yml += ' container_name: ' + containerInfo.name + '\n';
// Add ports
var ports = Object.keys(containerInfo.ports);
if (ports.length > 0) {
yml += ' ports:\n';
for (var i = 0; i < ports.length; i++) {
var port = ports[i];
if (containerInfo.ports[port]) {
yml += ' - "' + containerInfo.ports[port] + ':' + port + '"\n';
}
}
}
// Add volumes
var volumes = Object.keys(containerInfo.volumes);
if (volumes.length > 0) {
yml += ' volumes:\n';
for (var i = 0; i < volumes.length; i++) {
var volume = volumes[i];
if (containerInfo.volumes[volume]) {
yml += ' - ' + containerInfo.volumes[volume] + ':' + volume + '\n';
}
}
}
// Add environment variables
var envVars = Object.keys(containerInfo.environment);
if (envVars.length > 0) {
yml += ' environment:\n';
for (var i = 0; i < envVars.length; i++) {
var envVar = envVars[i];
yml += ' - ' + envVar + '=' + containerInfo.environment[envVar] + '\n';
}
}
// Add restart policy
yml += ' restart: unless-stopped\n';
return yml;
};
$scope.advancedEnvText = '';
$scope.advancedEnvCount = 0;
$scope.parsedEnvVars = {};
@ -273,53 +348,6 @@ app.controller('runContainer', function ($scope, $http) {
}
};
// Helper function to generate Docker Compose YAML
function generateDockerComposeYml(containerInfo) {
var yml = 'version: \'3.8\'\n\n';
yml += 'services:\n';
yml += ' ' + containerInfo.name + ':\n';
yml += ' image: ' + containerInfo.image + '\n';
yml += ' container_name: ' + containerInfo.name + '\n';
// Add ports
var ports = Object.keys(containerInfo.ports);
if (ports.length > 0) {
yml += ' ports:\n';
for (var i = 0; i < ports.length; i++) {
var port = ports[i];
if (containerInfo.ports[port]) {
yml += ' - "' + containerInfo.ports[port] + ':' + port + '"\n';
}
}
}
// Add volumes
var volumes = Object.keys(containerInfo.volumes);
if (volumes.length > 0) {
yml += ' volumes:\n';
for (var i = 0; i < volumes.length; i++) {
var volume = volumes[i];
if (containerInfo.volumes[volume]) {
yml += ' - ' + containerInfo.volumes[volume] + ':' + volume + '\n';
}
}
}
// Add environment variables
var envVars = Object.keys(containerInfo.environment);
if (envVars.length > 0) {
yml += ' environment:\n';
for (var i = 0; i < envVars.length; i++) {
var envVar = envVars[i];
yml += ' - ' + envVar + '=' + containerInfo.environment[envVar] + '\n';
}
}
// Add restart policy
yml += ' restart: unless-stopped\n';
return yml;
}
// Docker Compose Functions for runContainer
$scope.generateDockerCompose = function() {
@ -344,7 +372,7 @@ app.controller('runContainer', function ($scope, $http) {
}
// Generate docker-compose.yml content
var composeContent = generateDockerComposeYml(containerInfo);
var composeContent = $scope.generateDockerComposeYml(containerInfo);
// Create and download file
var blob = new Blob([composeContent], { type: 'text/yaml' });
@ -621,8 +649,12 @@ app.controller('runContainer', function ($scope, $http) {
image: image,
envList: finalEnvList,
volList: $scope.volList,
advancedEnvMode: $scope.advancedEnvMode
advancedEnvMode: $scope.advancedEnvMode,
network: $scope.selectedNetwork,
network_mode: $scope.networkMode,
extraOptions: {
add_host: $scope.extraHosts
}
};
try {
@ -1094,6 +1126,9 @@ app.controller('listContainers', function ($scope, $http) {
$scope.showLog = function (name, refresh = false) {
$scope.logs = "";
$scope.logInfo = null;
$scope.formattedLogs = "";
if (refresh === false) {
$('#logs').modal('show');
$scope.activeLog = name;
@ -1121,18 +1156,37 @@ app.controller('listContainers', function ($scope, $http) {
if (response.data.containerLogStatus === 1) {
$scope.logs = response.data.containerLog;
$scope.logInfo = {
container_status: response.data.container_status,
log_count: response.data.log_count
};
// Format logs for better display
$scope.formatLogs();
// Auto-scroll to bottom
setTimeout(function() {
$scope.scrollToBottom();
}, 100);
}
else {
$scope.logs = response.data.error_message;
$scope.logInfo = null;
$scope.formattedLogs = "";
new PNotify({
title: 'Unable to complete request',
text: response.data.error_message,
type: 'error'
});
}
}
function cantLoadInitialData(response) {
$scope.logs = "Error loading logs";
$scope.logInfo = null;
$scope.formattedLogs = "";
new PNotify({
title: 'Unable to complete request',
type: 'error'
@ -1140,6 +1194,76 @@ app.controller('listContainers', function ($scope, $http) {
}
};
// Format logs with syntax highlighting and better readability
$scope.formatLogs = function() {
if (!$scope.logs || $scope.logs === 'Loading...') {
$scope.formattedLogs = $scope.logs;
return;
}
var lines = $scope.logs.split('\n');
var formattedLines = [];
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
var formattedLine = line;
// Escape HTML characters
formattedLine = formattedLine.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
// Add syntax highlighting for common log patterns
if (line.match(/^\[.*?\]/)) {
// Timestamp lines
formattedLine = '<span style="color: #569cd6;">' + formattedLine + '</span>';
} else if (line.match(/ERROR|FATAL|CRITICAL/i)) {
// Error lines
formattedLine = '<span style="color: #f44747; font-weight: bold;">' + formattedLine + '</span>';
} else if (line.match(/WARN|WARNING/i)) {
// Warning lines
formattedLine = '<span style="color: #ffcc02; font-weight: bold;">' + formattedLine + '</span>';
} else if (line.match(/INFO/i)) {
// Info lines
formattedLine = '<span style="color: #4ec9b0;">' + formattedLine + '</span>';
} else if (line.match(/DEBUG/i)) {
// Debug lines
formattedLine = '<span style="color: #9cdcfe;">' + formattedLine + '</span>';
} else if (line.match(/SUCCESS|OK|COMPLETED/i)) {
// Success lines
formattedLine = '<span style="color: #4caf50; font-weight: bold;">' + formattedLine + '</span>';
}
formattedLines.push(formattedLine);
}
$scope.formattedLogs = formattedLines.join('\n');
};
// Scroll functions
$scope.scrollToTop = function() {
var container = document.getElementById('logContainer');
if (container) {
container.scrollTop = 0;
}
};
$scope.scrollToBottom = function() {
var container = document.getElementById('logContainer');
if (container) {
container.scrollTop = container.scrollHeight;
}
};
// Clear logs function
$scope.clearLogs = function() {
$scope.logs = "";
$scope.formattedLogs = "";
$scope.logInfo = null;
};
url = "/docker/getContainerList";
var data = {page: 1};
@ -1576,7 +1700,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
}
// Generate docker-compose.yml content
var composeContent = generateDockerComposeYml(containerInfo);
var composeContent = $scope.generateDockerComposeYml(containerInfo);
// Create and download file
var blob = new Blob([composeContent], { type: 'text/yaml' });
@ -2044,6 +2168,82 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$("#commandModal").modal("show");
};
// Port editing functionality
$scope.showPortEditModal = function() {
// Initialize current ports from container data
$scope.currentPorts = {};
if ($scope.ports) {
for (var iport in $scope.ports) {
var eport = $scope.ports[iport];
if (eport && eport.length > 0) {
$scope.currentPorts[iport] = eport[0].HostPort;
}
}
}
$("#portEditModal").modal("show");
};
$scope.addNewPortMapping = function() {
var containerPort = prompt('Enter container port (e.g., 80/tcp):');
if (containerPort) {
$scope.currentPorts[containerPort] = '';
$scope.$apply();
}
};
$scope.removePortMapping = function(containerPort) {
if (confirm('Are you sure you want to remove this port mapping?')) {
delete $scope.currentPorts[containerPort];
}
};
$scope.updatePortMappings = function() {
$("#portEditLoading").show();
$scope.updatingPorts = true;
var url = "/docker/updateContainerPorts";
var data = {
name: $scope.cName,
ports: $scope.currentPorts
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
$("#portEditLoading").hide();
$scope.updatingPorts = false;
if (response.data.status === 1) {
$("#portEditModal").modal("hide");
// Refresh container status and ports
$scope.refreshContainerInfo();
new PNotify({
title: 'Success',
text: 'Port mappings updated successfully',
type: 'success'
});
} else {
new PNotify({
title: 'Error',
text: 'Failed to update port mappings: ' + response.data.error_message,
type: 'error'
});
}
}, function(error) {
$("#portEditLoading").hide();
$scope.updatingPorts = false;
new PNotify({
title: 'Error',
text: 'Error updating port mappings: ' + error.data.error_message,
type: 'error'
});
});
};
$scope.executeCommand = function() {
if (!$scope.commandToExecute.trim()) {
new PNotify({
@ -2079,13 +2279,15 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$scope.commandOutput = {
command: response.data.command,
output: response.data.output,
exit_code: response.data.exit_code
exit_code: response.data.exit_code,
container_was_started: response.data.container_was_started
};
// Add to command history
$scope.commandHistory.unshift({
command: response.data.command,
timestamp: new Date()
timestamp: new Date(),
container_was_started: response.data.container_was_started
});
// Keep only last 10 commands
@ -2093,10 +2295,15 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$scope.commandHistory = $scope.commandHistory.slice(0, 10);
}
// Show success notification
// Show success notification with container status info
var notificationText = 'Command completed with exit code: ' + response.data.exit_code;
if (response.data.container_was_started) {
notificationText += ' (Container was temporarily started and stopped)';
}
new PNotify({
title: 'Command Executed',
text: 'Command completed with exit code: ' + response.data.exit_code,
text: notificationText,
type: response.data.exit_code === 0 ? 'success' : 'warning'
});
}
@ -2374,7 +2581,7 @@ app.controller('manageImages', function ($scope, $http) {
(new PNotify({
title: 'Confirmation Needed',
text: 'Are you sure?',
text: 'Are you sure you want to remove this image?',
icon: 'fa fa-question-circle',
hide: false,
confirm: {
@ -2392,14 +2599,16 @@ app.controller('manageImages', function ($scope, $http) {
if (counter == '0') {
var name = 0;
var force = false;
}
else {
var name = $("#" + counter).val()
var force = false;
}
url = "/docker/removeImage";
var data = {name: name};
var data = {name: name, force: force};
var config = {
headers: {
@ -2416,16 +2625,67 @@ app.controller('manageImages', function ($scope, $http) {
if (response.data.removeImageStatus === 1) {
new PNotify({
title: 'Image(s) removed',
text: 'Image has been successfully removed',
type: 'success'
});
window.location.href = "/docker/manageImages";
}
else {
new PNotify({
title: 'Unable to complete request',
text: response.data.error_message,
type: 'error'
});
var errorMessage = response.data.error_message;
// Check if it's a conflict error and offer force removal
if (errorMessage && errorMessage.includes("still being used by containers")) {
new PNotify({
title: 'Image in Use',
text: errorMessage + ' Would you like to force remove it?',
icon: 'fa fa-exclamation-triangle',
hide: false,
confirm: {
confirm: true
},
buttons: {
closer: false,
sticker: false
},
history: {
history: false
}
}).get().on('pnotify.confirm', function () {
// Force remove the image
$('#imageLoading').show();
var forceData = {name: name, force: true};
$http.post(url, forceData, config).then(function(forceResponse) {
$('#imageLoading').hide();
if (forceResponse.data.removeImageStatus === 1) {
new PNotify({
title: 'Image Force Removed',
text: 'Image has been force removed successfully',
type: 'success'
});
window.location.href = "/docker/manageImages";
} else {
new PNotify({
title: 'Force Removal Failed',
text: forceResponse.data.error_message,
type: 'error'
});
}
}, function(forceError) {
$('#imageLoading').hide();
new PNotify({
title: 'Force Removal Failed',
text: 'Could not force remove the image',
type: 'error'
});
});
});
} else {
new PNotify({
title: 'Unable to complete request',
text: errorMessage,
type: 'error'
});
}
}
$('#imageLoading').hide();
}
@ -2441,4 +2701,259 @@ app.controller('manageImages', function ($scope, $http) {
})
}
});
// Container List Controller
app.controller('listContainers', function ($scope, $http, $timeout, $window) {
$scope.containers = [];
$scope.loading = false;
$scope.updateContainerName = '';
$scope.currentImage = '';
$scope.newImage = '';
$scope.newTag = 'latest';
// Load containers list
$scope.loadContainers = function() {
$scope.loading = true;
var url = '/docker/listContainers';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, {}, config).then(function(response) {
$scope.loading = false;
if (response.data.status === 1) {
$scope.containers = response.data.containers || [];
} else {
new PNotify({
title: 'Error Loading Containers',
text: response.data.error_message || 'Failed to load containers',
type: 'error'
});
}
}, function(error) {
$scope.loading = false;
new PNotify({
title: 'Connection Error',
text: 'Could not connect to server',
type: 'error'
});
});
};
// Initialize containers on page load
$scope.loadContainers();
// Open update container modal
$scope.openUpdateModal = function(container) {
$scope.updateContainerName = container.name;
$scope.currentImage = container.image;
$scope.newImage = '';
$scope.newTag = 'latest';
$('#updateContainer').modal('show');
};
// Perform container update
$scope.performUpdate = function() {
if (!$scope.newImage && !$scope.newTag) {
new PNotify({
title: 'Missing Information',
text: 'Please enter a new image name or tag',
type: 'error'
});
return;
}
// If no new image specified, use current image with new tag
var imageToUse = $scope.newImage || $scope.currentImage.split(':')[0];
var tagToUse = $scope.newTag || 'latest';
var data = {
containerName: $scope.updateContainerName,
newImage: imageToUse,
newTag: tagToUse
};
var url = '/docker/updateContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
// Show loading
$('#updateContainer').modal('hide');
new PNotify({
title: 'Updating Container',
text: 'Please wait while the container is being updated...',
type: 'info',
hide: false
});
$http.post(url, data, config).then(function(response) {
if (response.data.updateContainerStatus === 1) {
new PNotify({
title: 'Container Updated Successfully',
text: response.data.message || 'Container has been updated successfully',
type: 'success'
});
// Reload containers list
$scope.loadContainers();
} else {
new PNotify({
title: 'Update Failed',
text: response.data.error_message || 'Failed to update container',
type: 'error'
});
}
}, function(error) {
new PNotify({
title: 'Update Failed',
text: 'Could not connect to server',
type: 'error'
});
});
};
// Container actions
$scope.startContainer = function(containerName) {
var data = { containerName: containerName };
var url = '/docker/startContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.startContainerStatus === 1) {
new PNotify({
title: 'Container Started',
text: 'Container has been started successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Start Failed',
text: response.data.error_message || 'Failed to start container',
type: 'error'
});
}
});
};
$scope.stopContainer = function(containerName) {
var data = { containerName: containerName };
var url = '/docker/stopContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.stopContainerStatus === 1) {
new PNotify({
title: 'Container Stopped',
text: 'Container has been stopped successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Stop Failed',
text: response.data.error_message || 'Failed to stop container',
type: 'error'
});
}
});
};
$scope.restartContainer = function(containerName) {
var data = { containerName: containerName };
var url = '/docker/restartContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.restartContainerStatus === 1) {
new PNotify({
title: 'Container Restarted',
text: 'Container has been restarted successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Restart Failed',
text: response.data.error_message || 'Failed to restart container',
type: 'error'
});
}
});
};
$scope.deleteContainerWithData = function(containerName) {
if (confirm('Are you sure you want to delete this container and all its data? This action cannot be undone.')) {
var data = { containerName: containerName };
var url = '/docker/deleteContainerWithData';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.deleteContainerWithDataStatus === 1) {
new PNotify({
title: 'Container Deleted',
text: 'Container and all data have been deleted successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Delete Failed',
text: response.data.error_message || 'Failed to delete container',
type: 'error'
});
}
});
}
};
$scope.deleteContainerKeepData = function(containerName) {
if (confirm('Are you sure you want to delete this container but keep the data? The container will be removed but volumes will be preserved.')) {
var data = { containerName: containerName };
var url = '/docker/deleteContainerKeepData';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.deleteContainerKeepDataStatus === 1) {
new PNotify({
title: 'Container Deleted',
text: 'Container has been deleted but data has been preserved',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Delete Failed',
text: response.data.error_message || 'Failed to delete container',
type: 'error'
});
}
});
}
};
});

View File

@ -314,6 +314,7 @@
overflow: hidden;
border: 1px solid var(--border-color, #e8e9ff);
margin-bottom: 2rem;
table-layout: fixed;
}
.containers-table thead {
@ -613,11 +614,11 @@
<table class="containers-table">
<thead>
<tr>
<th>{% trans "Container" %}</th>
<th>{% trans "Owner" %}</th>
<th>{% trans "Image" %}</th>
<th>{% trans "Tag" %}</th>
<th style="text-align: center;">{% trans "Actions" %}</th>
<th style="width: 25%;">{% trans "Container" %}</th>
<th style="width: 15%;">{% trans "Owner" %}</th>
<th style="width: 25%;">{% trans "Image" %}</th>
<th style="width: 15%;">{% trans "Tag" %}</th>
<th style="width: 20%; text-align: center;">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
@ -703,9 +704,9 @@
<table class="containers-table">
<thead>
<tr>
<th>{% trans "Container" %}</th>
<th>{% trans "Status" %}</th>
<th style="text-align: center;">{% trans "Actions" %}</th>
<th style="width: 40%;">{% trans "Container" %}</th>
<th style="width: 20%;">{% trans "Status" %}</th>
<th style="width: 40%; text-align: center;">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
@ -765,25 +766,61 @@
<!-- Container Logs Modal -->
<div id="logs" class="modal fade" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
<i class="fas fa-file-alt" style="margin-right: 0.5rem;"></i>
{% trans "Container Logs" %}
<small class="text-muted" ng-if="logInfo">
- Status: <span class="badge badge-info">{$ logInfo.container_status $}</span>
<span ng-if="logInfo.log_count"> | Lines: <span class="badge badge-secondary">{$ logInfo.log_count $}</span></span>
</small>
</h4>
<button type="button" class="close" data-dismiss="modal"
style="font-size: 1.5rem; background: transparent; border: none;">&times;</button>
</div>
<div class="modal-body">
<textarea name="logs" class="form-control" style="font-family: monospace; height: 400px; resize: vertical;"
readonly>{$ logs $}</textarea>
<div class="modal-body" style="padding: 0;">
<!-- Log Controls -->
<div class="bg-light p-2 border-bottom">
<div class="row align-items-center">
<div class="col-md-6">
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-primary" ng-click="showLog('', true)">
<i class="fas fa-sync-alt"></i> Refresh
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" ng-click="scrollToTop()">
<i class="fas fa-arrow-up"></i> Top
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" ng-click="scrollToBottom()">
<i class="fas fa-arrow-down"></i> Bottom
</button>
<button type="button" class="btn btn-sm btn-outline-info" ng-click="clearLogs()">
<i class="fas fa-trash"></i> Clear
</button>
</div>
</div>
<div class="col-md-6 text-right">
<small class="text-muted">
<i class="fas fa-info-circle"></i>
Use Ctrl+F to search within logs
</small>
</div>
</div>
</div>
<!-- Log Display -->
<div id="logContainer" style="height: 500px; overflow-y: auto; background: #1e1e1e; color: #d4d4d4; padding: 15px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 13px; line-height: 1.4; white-space: pre-wrap; word-wrap: break-word;">
<div ng-if="logs === 'Loading...'" class="text-center text-muted">
<i class="fas fa-spinner fa-spin"></i> Loading logs...
</div>
<div ng-if="logs && logs !== 'Loading...'" ng-bind-html="formattedLogs"></div>
<div ng-if="!logs || logs === ''" class="text-center text-muted">
<i class="fas fa-file-alt"></i> No logs available
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="showLog('', true)">
<i class="fas fa-sync-alt"></i>
{% trans "Refresh" %}
</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">
<i class="fas fa-times"></i>
{% trans "Close" %}

View File

@ -0,0 +1,621 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% block title %}{% trans "Docker Network Management" %}{% endblock %}
{% block content %}
{% load static %}
{% get_current_language as LANGUAGE_CODE %}
<style>
.modern-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.page-header {
text-align: center;
margin-bottom: 3rem;
padding: 3rem 0;
background: linear-gradient(135deg, var(--bg-hover, #f8f9ff) 0%, var(--bg-gradient, #f0f1ff) 100%);
border-radius: 20px;
animation: fadeInDown 0.5s ease-out;
position: relative;
overflow: hidden;
}
.page-header::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at 70% 30%, var(--accent-shadow-light, rgba(91, 95, 207, 0.15)) 0%, transparent 50%);
animation: rotate 30s linear infinite;
}
.header-content {
position: relative;
z-index: 1;
}
.page-title {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-primary, #1e293b);
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.network-icon {
width: 60px;
height: 60px;
background: var(--bg-secondary, white);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px var(--shadow-medium, rgba(0,0,0,0.1));
}
.main-card {
background: var(--bg-secondary, white);
border-radius: 16px;
box-shadow: 0 1px 3px var(--shadow-light, rgba(0,0,0,0.05)), 0 10px 40px var(--shadow-color, rgba(0,0,0,0.08));
border: 1px solid var(--border-color, #e8e9ff);
overflow: hidden;
margin-bottom: 2rem;
animation: fadeInUp 0.5s ease-out;
}
.card-header {
background: linear-gradient(135deg, var(--bg-hover, #f8f9ff) 0%, var(--bg-gradient, #f0f1ff) 100%);
padding: 1.5rem 2rem;
border-bottom: 1px solid var(--border-color, #e8e9ff);
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary, #1e293b);
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.card-body {
padding: 2rem;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 500;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.3s ease;
border: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background: var(--accent-color, #5b5fcf);
color: var(--bg-secondary, white);
}
.btn-primary:hover {
background: var(--accent-hover, #4547a9);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(91, 95, 207, 0.3);
}
.btn-success {
background: var(--success-bg, #d1fae5);
color: var(--success-text, #065f46);
border: 1px solid #a7f3d0;
}
.btn-success:hover {
background: #a7f3d0;
transform: translateY(-2px);
}
.btn-danger {
background: #fee2e2;
color: var(--danger-color, #ef4444);
border: 1px solid #fecaca;
}
.btn-danger:hover {
background: #fecaca;
transform: translateY(-2px);
}
.network-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
}
.network-card {
background: var(--bg-secondary, white);
border: 1px solid var(--border-color, #e8e9ff);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.network-card::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 100px;
height: 100px;
background: linear-gradient(135deg, var(--accent-focus, rgba(91, 95, 207, 0.1)) 0%, transparent 100%);
border-radius: 0 0 0 100%;
}
.network-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px var(--accent-shadow-light, rgba(91, 95, 207, 0.15));
border-color: var(--accent-color, #5b5fcf);
}
.network-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
position: relative;
z-index: 1;
}
.network-name {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary, #1e293b);
margin: 0;
}
.network-driver {
background: var(--accent-bg, #e0e7ff);
color: var(--accent-color, #5b5fcf);
padding: 0.25rem 0.75rem;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.network-info {
position: relative;
z-index: 1;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.info-label {
color: var(--text-secondary, #64748b);
font-weight: 500;
}
.info-value {
color: var(--text-primary, #1e293b);
font-weight: 600;
}
.network-actions {
margin-top: 1rem;
display: flex;
gap: 0.5rem;
position: relative;
z-index: 1;
}
.loading-spinner {
width: 20px;
height: 20px;
border: 3px solid #e8e9ff;
border-top-color: var(--accent-color, #5b5fcf);
border-radius: 50%;
animation: spin 1s linear infinite;
display: inline-block;
margin-left: 1rem;
}
.empty-state {
text-align: center;
padding: 3rem 2rem;
color: var(--text-secondary, #64748b);
}
.empty-state i {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.network-grid {
grid-template-columns: 1fr;
}
.card-header {
flex-direction: column;
gap: 1rem;
text-align: center;
}
}
</style>
<div class="modern-container" ng-controller="manageNetworks">
<div class="page-header">
<div class="header-content">
<h1 class="page-title">
<div class="network-icon">
<i class="fas fa-network-wired" style="color: var(--accent-color, #5b5fcf); font-size: 1.75rem;"></i>
</div>
{% trans "Docker Network Management" %}
</h1>
<p style="color: var(--text-secondary, #64748b); font-size: 1.125rem; margin: 0;">
{% trans "Manage Docker networks for container connectivity" %}
</p>
</div>
</div>
<!-- Networks Overview -->
<div class="main-card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-list"></i>
{% trans "Available Networks" %}
<img id="networkLoading" src="/static/images/loading.gif" style="display: none;" class="loading-spinner">
</h2>
<div>
<button class="btn btn-primary" ng-click="refreshNetworks()" ng-disabled="loading">
<i class="fas fa-sync"></i>
{% trans "Refresh" %}
</button>
<button class="btn btn-success" ng-click="showCreateNetworkModal()">
<i class="fas fa-plus"></i>
{% trans "Create Network" %}
</button>
</div>
</div>
<div class="card-body">
<div ng-show="loading" class="text-center" style="padding: 2rem;">
<div class="loading-spinner" style="width: 40px; height: 40px; margin: 0 auto;"></div>
<p style="margin-top: 1rem; color: var(--text-secondary, #64748b);">{% trans "Loading networks..." %}</p>
</div>
<div ng-show="!loading && networks.length === 0" class="empty-state">
<i class="fas fa-network-wired"></i>
<h3>{% trans "No Networks Found" %}</h3>
<p>{% trans "Create your first Docker network to get started." %}</p>
<button class="btn btn-primary" ng-click="showCreateNetworkModal()">
<i class="fas fa-plus"></i>
{% trans "Create Network" %}
</button>
</div>
<div ng-show="!loading && networks.length > 0" class="network-grid">
<div ng-repeat="network in networks" class="network-card">
<div class="network-header">
<h3 class="network-name">{$ network.name $}</h3>
<span class="network-driver">{$ network.driver $}</span>
</div>
<div class="network-info">
<div class="info-item">
<span class="info-label">{% trans "ID" %}</span>
<span class="info-value">{$ network.id | limitTo: 12 $}...</span>
</div>
<div class="info-item">
<span class="info-label">{% trans "Scope" %}</span>
<span class="info-value">{$ network.scope $}</span>
</div>
<div class="info-item">
<span class="info-label">{% trans "Containers" %}</span>
<span class="info-value">{$ network.containers $}</span>
</div>
<div class="info-item" ng-if="network.ipam.config.length > 0">
<span class="info-label">{% trans "Subnet" %}</span>
<span class="info-value">{$ network.ipam.config[0].subnet $}</span>
</div>
<div class="info-item" ng-if="network.ipam.config.length > 0 && network.ipam.config[0].gateway">
<span class="info-label">{% trans "Gateway" %}</span>
<span class="info-value">{$ network.ipam.config[0].gateway $}</span>
</div>
</div>
<div class="network-actions">
<button class="btn btn-danger btn-sm" ng-click="removeNetwork(network)"
ng-disabled="network.containers > 0"
title="{% trans 'Cannot remove network with running containers' %}">
<i class="fas fa-trash"></i>
{% trans "Remove" %}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Create Network Modal -->
<div id="createNetworkModal" class="modal fade" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
<i class="fas fa-plus" style="margin-right: 0.5rem;"></i>
{% trans "Create Docker Network" %}
</h4>
<button type="button" class="close" data-dismiss="modal"
style="font-size: 1.5rem; background: transparent; border: none;">&times;</button>
</div>
<div class="modal-body">
<form name="createNetworkForm" class="form-horizontal">
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "Network Name" %}</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="newNetwork.name"
placeholder="{% trans 'e.g., my-custom-network' %}" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "Driver" %}</label>
<div class="col-sm-9">
<select class="form-control" ng-model="newNetwork.driver">
<option value="bridge">{% trans "Bridge" %}</option>
<option value="overlay">{% trans "Overlay" %}</option>
<option value="macvlan">{% trans "MacVLAN" %}</option>
<option value="ipvlan">{% trans "IPvLAN" %}</option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "Subnet" %}</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="newNetwork.subnet"
placeholder="{% trans 'e.g., 172.20.0.0/16' %}">
<small class="help-block">{% trans "Optional: Specify a custom subnet for the network" %}</small>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "Gateway" %}</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="newNetwork.gateway"
placeholder="{% trans 'e.g., 172.20.0.1' %}">
<small class="help-block">{% trans "Optional: Specify a custom gateway IP" %}</small>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "IP Range" %}</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="newNetwork.ip_range"
placeholder="{% trans 'e.g., 172.20.0.0/24' %}">
<small class="help-block">{% trans "Optional: Specify an IP range for container IPs" %}</small>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<img id="createNetworkLoading" src="/static/images/loading.gif" style="display: none;" class="loading-spinner">
<button type="button" class="btn btn-primary" ng-disabled="creatingNetwork" ng-click="createNetwork()">
<i class="fas fa-plus"></i> {% trans "Create Network" %}
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
<i class="fas fa-times"></i> {% trans "Cancel" %}
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block footer_scripts %}
<script src="{% static 'dockerManager/dockerManager.js' %}"></script>
<script>
// Network management controller
app.controller('manageNetworks', function ($scope, $http) {
$scope.networks = [];
$scope.loading = true;
$scope.creatingNetwork = false;
$scope.newNetwork = {
name: '',
driver: 'bridge',
subnet: '',
gateway: '',
ip_range: ''
};
// Load networks
$scope.loadNetworks = function() {
$scope.loading = true;
var url = "/docker/getDockerNetworks";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, {}, config).then(function(response) {
$scope.loading = false;
if (response.data.status === 1) {
$scope.networks = response.data.networks;
} else {
new PNotify({
title: 'Error',
text: 'Failed to load networks: ' + response.data.error_message,
type: 'error'
});
}
}, function(error) {
$scope.loading = false;
new PNotify({
title: 'Error',
text: 'Error loading networks: ' + error.data.error_message,
type: 'error'
});
});
};
// Refresh networks
$scope.refreshNetworks = function() {
$scope.loadNetworks();
};
// Show create network modal
$scope.showCreateNetworkModal = function() {
$scope.newNetwork = {
name: '',
driver: 'bridge',
subnet: '',
gateway: '',
ip_range: ''
};
$('#createNetworkModal').modal('show');
};
// Create network
$scope.createNetwork = function() {
if (!$scope.newNetwork.name.trim()) {
new PNotify({
title: 'Error',
text: 'Network name is required',
type: 'error'
});
return;
}
$('#createNetworkLoading').show();
$scope.creatingNetwork = true;
var url = "/docker/createDockerNetwork";
var data = {
name: $scope.newNetwork.name,
driver: $scope.newNetwork.driver,
subnet: $scope.newNetwork.subnet,
gateway: $scope.newNetwork.gateway,
ip_range: $scope.newNetwork.ip_range
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
$('#createNetworkLoading').hide();
$scope.creatingNetwork = false;
if (response.data.status === 1) {
$('#createNetworkModal').modal('hide');
$scope.loadNetworks();
new PNotify({
title: 'Success',
text: 'Network created successfully',
type: 'success'
});
} else {
new PNotify({
title: 'Error',
text: 'Failed to create network: ' + response.data.error_message,
type: 'error'
});
}
}, function(error) {
$('#createNetworkLoading').hide();
$scope.creatingNetwork = false;
new PNotify({
title: 'Error',
text: 'Error creating network: ' + error.data.error_message,
type: 'error'
});
});
};
// Remove network
$scope.removeNetwork = function(network) {
if (network.containers > 0) {
new PNotify({
title: 'Error',
text: 'Cannot remove network with running containers',
type: 'error'
});
return;
}
if (confirm('Are you sure you want to remove network "' + network.name + '"?')) {
// Implementation for removing network would go here
new PNotify({
title: 'Info',
text: 'Network removal not implemented in this demo',
type: 'info'
});
}
};
// Initialize
$scope.loadNetworks();
});
</script>
{% endblock %}

View File

@ -755,12 +755,76 @@
</div>
</div>
<!-- Network Configuration Section -->
<div class="form-section">
<div class="section-header">
<div class="section-icon">
<i class="fas fa-network-wired"></i>
</div>
<div>
<h2 class="section-title">{% trans "Network Configuration" %}</h2>
<p class="section-subtitle">{% trans "Configure network settings and extra options for the container" %}</p>
</div>
</div>
<div class="form-row">
<label class="form-label">
{% trans "Network" %}
<div class="tooltip">
<i class="fas fa-question-circle tooltip-icon"></i>
<div class="tooltip-content">
{% trans "Select the Docker network for the container" %}
</div>
</div>
</label>
<select class="form-control" ng-model="selectedNetwork" ng-init="selectedNetwork='bridge'">
<option value="bridge">{% trans "Default Bridge" %}</option>
<option value="host">{% trans "Host Network" %}</option>
<option value="none">{% trans "No Network" %}</option>
<option ng-repeat="network in availableNetworks" value="{$ network.name $}">
{$ network.name $} ({$ network.driver $})
</option>
</select>
</div>
<div class="form-row">
<label class="form-label">
{% trans "Extra Hosts" %}
<div class="tooltip">
<i class="fas fa-question-circle tooltip-icon"></i>
<div class="tooltip-content">
{% trans "Add custom host entries (e.g., host.docker.internal:host-gateway)" %}
</div>
</div>
</label>
<input type="text" class="form-control" ng-model="extraHosts"
placeholder="{% trans 'host.docker.internal:host-gateway, example.com:1.2.3.4' %}">
</div>
<div class="form-row">
<label class="form-label">
{% trans "Network Mode" %}
<div class="tooltip">
<i class="fas fa-question-circle tooltip-icon"></i>
<div class="tooltip-content">
{% trans "Override network mode (bridge, host, none, or custom network)" %}
</div>
</div>
</label>
<select class="form-control" ng-model="networkMode" ng-init="networkMode='bridge'">
<option value="bridge">{% trans "Bridge" %}</option>
<option value="host">{% trans "Host" %}</option>
<option value="none">{% trans "None" %}</option>
</select>
</div>
</div>
<!-- Port Configuration Section -->
{% if portConfig %}
<div class="form-section">
<div class="section-header">
<div class="section-icon">
<i class="fas fa-network-wired"></i>
<i class="fas fa-plug"></i>
</div>
<div>
<h2 class="section-title">{% trans "Port Mapping" %}</h2>

View File

@ -757,10 +757,15 @@
<div class="action-text">{% trans "Processes" %}</div>
</div>
<div class="action-btn" ng-click="showCommandModal()" ng-disabled="status!='running'">
<i class="fas fa-code action-icon" style="color: #10b981;"></i>
<div class="action-text">{% trans "Run Command" %}</div>
</div>
<div class="action-btn" ng-click="showCommandModal()" ng-disabled="status!='running'">
<i class="fas fa-code action-icon" style="color: #10b981;"></i>
<div class="action-text">{% trans "Run Command" %}</div>
</div>
<div class="action-btn" ng-click="showPortEditModal()" ng-disabled="status!='running'">
<i class="fas fa-edit action-icon" style="color: #8b5cf6;"></i>
<div class="action-text">{% trans "Edit Ports" %}</div>
</div>
</div>
</div>
</div>
@ -1104,7 +1109,7 @@
</div>
</div>
<small class="form-text text-muted">
{% trans "Commands will be executed inside the running container. Use proper shell syntax." %}
{% trans "Commands will be executed inside the container. If the container is not running, it will be temporarily started for command execution." %}
</small>
</div>
@ -1120,7 +1125,12 @@
ng-click="selectCommand(cmd.command)"
style="cursor: pointer; padding: 0.25rem 0.5rem; margin: 0.125rem 0; background: #f8f9fa; border-radius: 4px; border-left: 3px solid #007bff;">
<code style="font-size: 0.875rem;">{{ cmd.command }}</code>
<small class="text-muted" style="float: right;">{{ cmd.timestamp | date:'short' }}</small>
<small class="text-muted" style="float: right;">
{{ cmd.timestamp | date:'short' }}
<span ng-if="cmd.container_was_started" style="color: #f6ad55; margin-left: 0.5rem;">
<i class="fas fa-play-circle"></i>
</span>
</small>
</div>
</div>
</div>
@ -1135,6 +1145,9 @@
<div ng-show="commandOutput.exit_code !== undefined" style="margin-bottom: 0.5rem;">
<span style="color: #68d391;">$</span> <span style="color: #fbb6ce;">{{ commandOutput.command }}</span>
<span style="color: #a0aec0; margin-left: 1rem;">(exit code: {{ commandOutput.exit_code }})</span>
<span ng-if="commandOutput.container_was_started" style="color: #f6ad55; margin-left: 1rem;">
<i class="fas fa-info-circle"></i> Container was temporarily started
</span>
</div>
<div ng-bind="commandOutput.output"></div>
</div>
@ -1231,6 +1244,78 @@
</div>
</div>
</div>
<!-- Port Editing Modal -->
<div id="portEditModal" class="modal fade" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
<i class="fas fa-edit" style="margin-right: 0.5rem;"></i>
{% trans "Edit Port Mappings" %}
</h4>
<button type="button" class="close" data-dismiss="modal"
style="font-size: 1.5rem; background: transparent; border: none;">&times;</button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<strong>{% trans "Important:" %}</strong> {% trans "Editing port mappings will temporarily stop and recreate the container. Any unsaved data in the container may be lost." %}
</div>
<div class="form-group">
<label class="control-label">
<i class="fas fa-plug" style="margin-right: 0.5rem;"></i>
{% trans "Port Mappings" %}
</label>
<div class="port-mapping-container">
<div ng-repeat="(containerPort, hostPort) in currentPorts" class="port-mapping-row" style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 8px;">
<div style="flex: 1;">
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Container Port" %}</label>
<input type="text" class="form-control" ng-model="containerPort" disabled style="font-family: monospace;">
</div>
<div style="color: #007bff; font-size: 1.25rem;">
<i class="fas fa-arrow-right"></i>
</div>
<div style="flex: 1;">
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Host Port" %}</label>
<input type="number" class="form-control" ng-model="currentPorts[containerPort]"
placeholder="{% trans 'e.g., 8080' %}" min="1024" max="65535">
</div>
<div style="display: flex; align-items: center; height: 38px;">
<button type="button" class="btn btn-danger btn-sm" ng-click="removePortMapping(containerPort)"
ng-show="Object.keys(currentPorts).length > 1" title="{% trans 'Remove port mapping' %}">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div ng-show="Object.keys(currentPorts).length === 0" class="text-center text-muted" style="padding: 2rem;">
<i class="fas fa-info-circle fa-2x" style="margin-bottom: 1rem;"></i>
<p>{% trans "No port mappings configured" %}</p>
</div>
</div>
</div>
<div class="form-group">
<button type="button" class="btn btn-primary" ng-click="addNewPortMapping()">
<i class="fas fa-plus"></i>
{% trans "Add Port Mapping" %}
</button>
</div>
</div>
<div class="modal-footer">
<img id="portEditLoading" src="/static/images/loading.gif" style="display: none;" class="loading-spinner">
<button type="button" class="btn btn-primary" ng-disabled="updatingPorts" ng-click="updatePortMappings()">
<i class="fas fa-save"></i> {% trans "Update Port Mappings" %}
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
<i class="fas fa-times"></i> {% trans "Cancel" %}
</button>
</div>
</div>
</div>
</div>
</div>
<style>

View File

@ -27,7 +27,13 @@ urlpatterns = [
re_path(r'^getImageHistory$', views.getImageHistory, name='getImageHistory'),
re_path(r'^removeImage$', views.removeImage, name='removeImage'),
re_path(r'^pullImage$', views.pullImage, name='pullImage'),
# Network management endpoints
re_path(r'^getDockerNetworks$', views.getDockerNetworks, name='getDockerNetworks'),
re_path(r'^createDockerNetwork$', views.createDockerNetwork, name='createDockerNetwork'),
re_path(r'^updateContainerPorts$', views.updateContainerPorts, name='updateContainerPorts'),
re_path(r'^manageNetworks$', views.manageNetworks, name='manageNetworks'),
re_path(r'^updateContainer$', views.updateContainer, name='updateContainer'),
re_path(r'^listContainers$', views.listContainers, name='listContainers'),
re_path(r'^deleteContainerWithData$', views.deleteContainerWithData, name='deleteContainerWithData'),
re_path(r'^deleteContainerKeepData$', views.deleteContainerKeepData, name='deleteContainerKeepData'),
re_path(r'^recreateContainer$', views.recreateContainer, name='recreateContainer'),

View File

@ -743,5 +743,109 @@ def getContainerEnv(request):
'success': 0,
'message': str(e)
}), content_type='application/json')
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def listContainers(request):
"""
Get list of all Docker containers
"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.listContainers(userID)
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def getDockerNetworks(request):
"""
Get list of all Docker networks
"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.getDockerNetworks(userID)
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def createDockerNetwork(request):
"""
Create a new Docker network
"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.createDockerNetwork(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def updateContainerPorts(request):
"""
Update port mappings for an existing container
"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.updateContainerPorts(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def manageNetworks(request):
"""
Display the network management page
"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadError()
template = 'dockerManager/manageNetworks.html'
proc = httpProc(request, template, {}, 'admin')
return proc.render()
except KeyError:
return redirect(loadLoginPage)

View File

@ -211,7 +211,7 @@ class FileManager:
currentFile = items.split(' ')
currentFile = [a for a in currentFile if a != '']
if currentFile[-1] == '.' or currentFile[-1] == '..' or currentFile[0] == 'total' or currentFile[-1].startswith('mail.'):
if currentFile[-1] == '.' or currentFile[-1] == '..' or currentFile[0] == 'total':
continue
if len(currentFile) > 9:
@ -384,90 +384,181 @@ class FileManager:
website = Websites.objects.get(domain=domainName)
self.homePath = '/home/%s' % (domainName)
logging.CyberCPLogFileWriter.writeToFile(f"Attempting to delete files/folders for domain: {domainName}")
RemoveOK = 1
command = 'touch %s/hello.txt' % (self.homePath)
# Test if directory is writable
command = 'touch %s/public_html/hello.txt' % (self.homePath)
result = ProcessUtilities.outputExecutioner(command)
if result.find('No such file or directory') > -1:
if result.find('cannot touch') > -1:
RemoveOK = 0
logging.CyberCPLogFileWriter.writeToFile(f"Directory {self.homePath} is not writable, removing chattr flags")
# Remove immutable flag from entire directory
command = 'chattr -R -i %s' % (self.homePath)
ProcessUtilities.executioner(command)
result = ProcessUtilities.executioner(command)
if result.find('cannot') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr -i from {self.homePath}: {result}")
else:
logging.CyberCPLogFileWriter.writeToFile(f"Successfully removed chattr -i from {self.homePath}")
else:
command = 'rm -f %s/hello.txt' % (self.homePath)
command = 'rm -f %s/public_html/hello.txt' % (self.homePath)
ProcessUtilities.executioner(command)
for item in self.data['fileAndFolders']:
itemPath = self.data['path'] + '/' + item
# Security check - prevent path traversal
if itemPath.find('..') > -1 or itemPath.find(self.homePath) == -1:
logging.CyberCPLogFileWriter.writeToFile(f"Security violation: Attempted to delete outside home directory: {itemPath}")
return self.ajaxPre(0, 'Not allowed to delete files outside home directory!')
if (self.data['path'] + '/' + item).find('..') > -1 or (self.data['path'] + '/' + item).find(
self.homePath) == -1:
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
logging.CyberCPLogFileWriter.writeToFile(f"Deleting: {itemPath}")
if skipTrash:
command = 'rm -rf ' + self.returnPathEnclosed(self.data['path'] + '/' + item)
ProcessUtilities.executioner(command, website.externalApp)
# Permanent deletion
command = 'rm -rf ' + self.returnPathEnclosed(itemPath)
result = ProcessUtilities.executioner(command, website.externalApp)
if result.find('cannot') > -1 or result.find('Permission denied') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Failed to delete {itemPath}: {result}")
# Try with sudo if available
command = 'sudo rm -rf ' + self.returnPathEnclosed(itemPath)
result = ProcessUtilities.executioner(command, website.externalApp)
if result.find('cannot') > -1 or result.find('Permission denied') > -1:
return self.ajaxPre(0, f'Failed to delete {item}: {result}')
logging.CyberCPLogFileWriter.writeToFile(f"Successfully deleted: {itemPath}")
else:
# Move to trash
trashPath = '%s/.trash' % (self.homePath)
command = 'mkdir %s' % (trashPath)
ProcessUtilities.executioner(command, website.externalApp)
# Ensure trash directory exists
command = 'mkdir -p %s' % (trashPath)
result = ProcessUtilities.executioner(command, website.externalApp)
if result.find('cannot') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Failed to create trash directory: {result}")
return self.ajaxPre(0, f'Failed to create trash directory: {result}')
Trash(website=website, originalPath=self.returnPathEnclosed(self.data['path']),
fileName=self.returnPathEnclosed(item)).save()
# Save to trash database
try:
Trash(website=website, originalPath=self.returnPathEnclosed(self.data['path']),
fileName=self.returnPathEnclosed(item)).save()
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Failed to save trash record: {str(e)}")
command = 'mv %s %s' % (self.returnPathEnclosed(self.data['path'] + '/' + item), trashPath)
ProcessUtilities.executioner(command, website.externalApp)
# Move to trash
command = 'mv %s %s' % (self.returnPathEnclosed(itemPath), trashPath)
result = ProcessUtilities.executioner(command, website.externalApp)
if result.find('cannot') > -1 or result.find('Permission denied') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Failed to move to trash {itemPath}: {result}")
# Try with sudo if available
command = 'sudo mv %s %s' % (self.returnPathEnclosed(itemPath), trashPath)
result = ProcessUtilities.executioner(command, website.externalApp)
if result.find('cannot') > -1 or result.find('Permission denied') > -1:
return self.ajaxPre(0, f'Failed to move {item} to trash: {result}')
logging.CyberCPLogFileWriter.writeToFile(f"Successfully moved to trash: {itemPath}")
if RemoveOK == 0:
logging.CyberCPLogFileWriter.writeToFile(f"Restoring chattr +i flags for {self.homePath}")
# Restore immutable flag to entire directory
command = 'chattr -R +i %s' % (self.homePath)
ProcessUtilities.executioner(command)
except:
result = ProcessUtilities.executioner(command)
if result.find('cannot') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to restore chattr +i to {self.homePath}: {result}")
else:
logging.CyberCPLogFileWriter.writeToFile(f"Successfully restored chattr +i to {self.homePath}")
# Allow specific directories to remain mutable
mutable_dirs = ['/logs/', '/.trash/', '/backup/', '/incbackup/', '/lscache/', '/.cagefs/']
for dir_name in mutable_dirs:
dir_path = self.homePath + dir_name
command = 'chattr -R -i %s' % (dir_path)
result = ProcessUtilities.executioner(command)
if result.find('cannot') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr +i from {dir_path}: {result}")
else:
logging.CyberCPLogFileWriter.writeToFile(f"Successfully removed chattr +i from {dir_path}")
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error in deleteFolderOrFile for {domainName}: {str(e)}")
try:
skipTrash = self.data['skipTrash']
except:
skipTrash = False
# Fallback to root path for system files
self.homePath = '/'
logging.CyberCPLogFileWriter.writeToFile(f"Using fallback deletion for system files in {self.data['path']}")
RemoveOK = 1
command = 'touch %s/hello.txt' % (self.homePath)
# Test if directory is writable
command = 'touch %s/public_html/hello.txt' % (self.homePath)
result = ProcessUtilities.outputExecutioner(command)
if result.find('No such file or directory') > -1:
if result.find('cannot touch') > -1:
RemoveOK = 0
logging.CyberCPLogFileWriter.writeToFile(f"Directory {self.homePath} is not writable, removing chattr flags")
command = 'chattr -R -i %s' % (self.homePath)
ProcessUtilities.executioner(command)
result = ProcessUtilities.executioner(command)
if result.find('cannot') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr -i from {self.homePath}: {result}")
else:
command = 'rm -f %s/hello.txt' % (self.homePath)
command = 'rm -f %s/public_html/hello.txt' % (self.homePath)
ProcessUtilities.executioner(command)
for item in self.data['fileAndFolders']:
itemPath = self.data['path'] + '/' + item
# Security check for system files
if itemPath.find('..') > -1 or itemPath.find(self.homePath) == -1:
logging.CyberCPLogFileWriter.writeToFile(f"Security violation: Attempted to delete outside allowed path: {itemPath}")
return self.ajaxPre(0, 'Not allowed to delete files outside allowed path!')
if (self.data['path'] + '/' + item).find('..') > -1 or (self.data['path'] + '/' + item).find(
self.homePath) == -1:
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
logging.CyberCPLogFileWriter.writeToFile(f"Deleting system file: {itemPath}")
if skipTrash:
command = 'rm -rf ' + self.returnPathEnclosed(self.data['path'] + '/' + item)
ProcessUtilities.executioner(command)
command = 'rm -rf ' + self.returnPathEnclosed(itemPath)
result = ProcessUtilities.executioner(command)
if result.find('cannot') > -1 or result.find('Permission denied') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Failed to delete system file {itemPath}: {result}")
return self.ajaxPre(0, f'Failed to delete {item}: {result}')
logging.CyberCPLogFileWriter.writeToFile(f"Successfully deleted system file: {itemPath}")
if RemoveOK == 0:
logging.CyberCPLogFileWriter.writeToFile(f"Restoring chattr +i flags for system path: {self.homePath}")
command = 'chattr -R +i %s' % (self.homePath)
ProcessUtilities.executioner(command)
result = ProcessUtilities.executioner(command)
if result.find('cannot') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to restore chattr +i to system path {self.homePath}: {result}")
else:
logging.CyberCPLogFileWriter.writeToFile(f"Successfully restored chattr +i to system path {self.homePath}")
# Allow specific directories to remain mutable for system files
mutable_dirs = ['/logs/', '/.trash/', '/backup/', '/incbackup/', '/lscache/', '/.cagefs/']
for dir_name in mutable_dirs:
dir_path = self.homePath + dir_name
command = 'chattr -R -i %s' % (dir_path)
result = ProcessUtilities.executioner(command)
if result.find('cannot') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr +i from system {dir_path}: {result}")
else:
logging.CyberCPLogFileWriter.writeToFile(f"Successfully removed chattr +i from system {dir_path}")
logging.CyberCPLogFileWriter.writeToFile(f"File deletion completed successfully for domain: {domainName}")
json_data = json.dumps(finalData)
return HttpResponse(json_data)
except BaseException as msg:
return self.ajaxPre(0, str(msg))
logging.CyberCPLogFileWriter.writeToFile(f"Critical error in deleteFolderOrFile: {str(msg)}")
return self.ajaxPre(0, f"File deletion failed: {str(msg)}")
def restore(self):
try:

View File

@ -1033,14 +1033,24 @@ class FirewallManager:
httpdConfig = ProcessUtilities.outputExecutioner(command).splitlines()
for items in httpdConfig:
# Check for Comodo rules
if items.find('modsec/comodo') > -1:
comodoInstalled = 1
elif items.find('modsec/owasp') > -1:
# Check for OWASP rules - improved detection
elif items.find('modsec/owasp') > -1 or items.find('owasp-modsecurity-crs') > -1:
owaspInstalled = 1
if owaspInstalled == 1 and comodoInstalled == 1:
break
# Additional check: verify OWASP files actually exist
if owaspInstalled == 0:
owaspPath = os.path.join(virtualHostUtilities.Server_root, "conf/modsec/owasp-modsecurity-crs-4.18.0")
if os.path.exists(owaspPath) and os.path.exists(os.path.join(owaspPath, "owasp-master.conf")):
owaspInstalled = 1
# Additional check: verify Comodo files actually exist
if comodoInstalled == 0:
comodoPath = os.path.join(virtualHostUtilities.Server_root, "conf/modsec/comodo")
if os.path.exists(comodoPath) and os.path.exists(os.path.join(comodoPath, "modsecurity.conf")):
comodoInstalled = 1
final_dic = {
'modSecInstalled': 1,
@ -1573,6 +1583,18 @@ class FirewallManager:
data['CL'] = 1
# Auto-fix PHP-FPM issues when accessing Imunify360 page
try:
from plogical import upgrade
logging.CyberCPLogFileWriter.writeToFile("Auto-fixing PHP-FPM pool configurations for Imunify360 compatibility...")
fix_result = upgrade.Upgrade.CreateMissingPoolsforFPM()
if fix_result == 0:
logging.CyberCPLogFileWriter.writeToFile("PHP-FPM pool configurations auto-fixed successfully")
else:
logging.CyberCPLogFileWriter.writeToFile("Warning: PHP-FPM auto-fix had issues")
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error in auto-fix for Imunify360: {str(e)}")
if os.path.exists(FirewallManager.imunifyPath):
data['imunify'] = 1
else:
@ -1628,6 +1650,18 @@ class FirewallManager:
data = {}
data['ipAddress'] = fullAddress
# Auto-fix PHP-FPM issues when accessing ImunifyAV page
try:
from plogical import upgrade
logging.CyberCPLogFileWriter.writeToFile("Auto-fixing PHP-FPM pool configurations for ImunifyAV compatibility...")
fix_result = upgrade.Upgrade.CreateMissingPoolsforFPM()
if fix_result == 0:
logging.CyberCPLogFileWriter.writeToFile("PHP-FPM pool configurations auto-fixed successfully")
else:
logging.CyberCPLogFileWriter.writeToFile("Warning: PHP-FPM auto-fix had issues")
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error in auto-fix for ImunifyAV: {str(e)}")
if os.path.exists(FirewallManager.imunifyAVPath):
data['imunify'] = 1
else:
@ -1755,6 +1789,267 @@ class FirewallManager:
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
def getBannedIPs(self, userID=None):
"""
Get list of banned IP addresses
"""
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
# For now, we'll use a simple file-based storage
# In production, you might want to use a database
banned_ips_file = '/etc/cyberpanel/banned_ips.json'
banned_ips = []
if os.path.exists(banned_ips_file):
try:
with open(banned_ips_file, 'r') as f:
banned_ips = json.load(f)
except:
banned_ips = []
# Filter out expired bans
current_time = time.time()
active_banned_ips = []
for banned_ip in banned_ips:
if banned_ip.get('expires') == 'Never' or banned_ip.get('expires', 0) > current_time:
banned_ip['active'] = True
if banned_ip.get('expires') != 'Never':
banned_ip['expires'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip['expires']))
else:
banned_ip['expires'] = 'Never'
banned_ip['banned_on'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip.get('banned_on', current_time)))
else:
banned_ip['active'] = False
banned_ip['expires'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip.get('expires', current_time)))
banned_ip['banned_on'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip.get('banned_on', current_time)))
active_banned_ips.append(banned_ip)
final_dic = {'status': 1, 'bannedIPs': active_banned_ips}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
except BaseException as msg:
final_dic = {'status': 0, 'error_message': str(msg)}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
def addBannedIP(self, userID=None, data=None):
"""
Add a banned IP address
"""
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
ip = data.get('ip', '').strip()
reason = data.get('reason', '').strip()
duration = data.get('duration', '24h')
if not ip or not reason:
final_dic = {'status': 0, 'error_message': 'IP address and reason are required'}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
# Validate IP address format
import ipaddress
try:
ipaddress.ip_address(ip.split('/')[0]) # Handle CIDR notation
except ValueError:
final_dic = {'status': 0, 'error_message': 'Invalid IP address format'}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
# Calculate expiration time
current_time = time.time()
if duration == 'permanent':
expires = 'Never'
else:
duration_map = {
'1h': 3600,
'24h': 86400,
'7d': 604800,
'30d': 2592000
}
duration_seconds = duration_map.get(duration, 86400)
expires = current_time + duration_seconds
# Load existing banned IPs
banned_ips_file = '/etc/cyberpanel/banned_ips.json'
banned_ips = []
if os.path.exists(banned_ips_file):
try:
with open(banned_ips_file, 'r') as f:
banned_ips = json.load(f)
except:
banned_ips = []
# Check if IP is already banned
for banned_ip in banned_ips:
if banned_ip.get('ip') == ip and banned_ip.get('active', True):
final_dic = {'status': 0, 'error_message': f'IP address {ip} is already banned'}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
# Add new banned IP
new_banned_ip = {
'id': int(time.time()),
'ip': ip,
'reason': reason,
'duration': duration,
'banned_on': current_time,
'expires': expires,
'active': True
}
banned_ips.append(new_banned_ip)
# Ensure directory exists
os.makedirs(os.path.dirname(banned_ips_file), exist_ok=True)
# Save to file
with open(banned_ips_file, 'w') as f:
json.dump(banned_ips, f, indent=2)
# Apply firewall rule to block the IP
try:
# Add iptables rule to block the IP
if '/' in ip:
# CIDR notation
subprocess.run(['iptables', '-A', 'INPUT', '-s', ip, '-j', 'DROP'], check=True)
else:
# Single IP
subprocess.run(['iptables', '-A', 'INPUT', '-s', ip, '-j', 'DROP'], check=True)
logging.CyberCPLogFileWriter.writeToFile(f'Banned IP {ip} with reason: {reason}')
except subprocess.CalledProcessError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Failed to add iptables rule for {ip}: {str(e)}')
final_dic = {'status': 1, 'message': f'IP address {ip} has been banned successfully'}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
except BaseException as msg:
final_dic = {'status': 0, 'error_message': str(msg)}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
def removeBannedIP(self, userID=None, data=None):
"""
Remove/unban an IP address
"""
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
banned_ip_id = data.get('id')
# Load existing banned IPs
banned_ips_file = '/etc/cyberpanel/banned_ips.json'
banned_ips = []
if os.path.exists(banned_ips_file):
try:
with open(banned_ips_file, 'r') as f:
banned_ips = json.load(f)
except:
banned_ips = []
# Find and update the banned IP
ip_to_unban = None
for banned_ip in banned_ips:
if banned_ip.get('id') == banned_ip_id:
banned_ip['active'] = False
banned_ip['unbanned_on'] = time.time()
ip_to_unban = banned_ip['ip']
break
if not ip_to_unban:
final_dic = {'status': 0, 'error_message': 'Banned IP not found'}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
# Save updated banned IPs
with open(banned_ips_file, 'w') as f:
json.dump(banned_ips, f, indent=2)
# Remove iptables rule
try:
# Remove iptables rule to unblock the IP
if '/' in ip_to_unban:
# CIDR notation
subprocess.run(['iptables', '-D', 'INPUT', '-s', ip_to_unban, '-j', 'DROP'], check=False)
else:
# Single IP
subprocess.run(['iptables', '-D', 'INPUT', '-s', ip_to_unban, '-j', 'DROP'], check=False)
logging.CyberCPLogFileWriter.writeToFile(f'Unbanned IP {ip_to_unban}')
except subprocess.CalledProcessError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Failed to remove iptables rule for {ip_to_unban}: {str(e)}')
final_dic = {'status': 1, 'message': f'IP address {ip_to_unban} has been unbanned successfully'}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
except BaseException as msg:
final_dic = {'status': 0, 'error_message': str(msg)}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
def deleteBannedIP(self, userID=None, data=None):
"""
Permanently delete a banned IP record
"""
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
banned_ip_id = data.get('id')
# Load existing banned IPs
banned_ips_file = '/etc/cyberpanel/banned_ips.json'
banned_ips = []
if os.path.exists(banned_ips_file):
try:
with open(banned_ips_file, 'r') as f:
banned_ips = json.load(f)
except:
banned_ips = []
# Find and remove the banned IP
ip_to_delete = None
updated_banned_ips = []
for banned_ip in banned_ips:
if banned_ip.get('id') == banned_ip_id:
ip_to_delete = banned_ip['ip']
else:
updated_banned_ips.append(banned_ip)
if not ip_to_delete:
final_dic = {'status': 0, 'error_message': 'Banned IP record not found'}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
# Save updated banned IPs
with open(banned_ips_file, 'w') as f:
json.dump(updated_banned_ips, f, indent=2)
logging.CyberCPLogFileWriter.writeToFile(f'Deleted banned IP record for {ip_to_delete}')
final_dic = {'status': 1, 'message': f'Banned IP record for {ip_to_delete} has been deleted successfully'}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
except BaseException as msg:
final_dic = {'status': 0, 'error_message': str(msg)}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)

View File

@ -16,9 +16,21 @@ app.controller('firewallController', function ($scope, $http) {
$scope.couldNotConnect = true;
$scope.rulesDetails = false;
// Banned IPs variables
$scope.activeTab = 'rules';
$scope.bannedIPs = [];
$scope.bannedIPsLoading = false;
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = true;
$scope.banIP = '';
$scope.banReason = '';
$scope.banDuration = '24h';
firewallStatus();
populateCurrentRecords();
populateBannedIPs();
$scope.addRule = function () {
@ -1294,28 +1306,36 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
if (response.data.owaspInstalled === 1) {
$('#owaspInstalled').prop('checked', true);
$scope.owaspDisable = false;
owaspInstalled = true;
} else {
$('#owaspInstalled').prop('checked', false);
$scope.owaspDisable = true;
owaspInstalled = false;
}
if (response.data.comodoInstalled === 1) {
$('#comodoInstalled').prop('checked', true);
$scope.comodoDisable = false;
comodoInstalled = true;
} else {
$('#comodoInstalled').prop('checked', false);
$scope.comodoDisable = true;
comodoInstalled = false;
}
} else {
if (response.data.owaspInstalled === 1) {
$scope.owaspDisable = false;
owaspInstalled = true;
} else {
$scope.owaspDisable = true;
owaspInstalled = false;
}
if (response.data.comodoInstalled === 1) {
$scope.comodoDisable = false;
comodoInstalled = true;
} else {
$scope.comodoDisable = true;
comodoInstalled = false;
}
}
@ -2393,4 +2413,140 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window)
}
}
// Banned IPs Functions
function populateBannedIPs() {
$scope.bannedIPsLoading = true;
var url = "/firewall/getBannedIPs";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, {}, config).then(function(response) {
$scope.bannedIPsLoading = false;
if (response.data.status === 1) {
$scope.bannedIPs = response.data.bannedIPs || [];
} else {
$scope.bannedIPs = [];
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = response.data.error_message;
}
}, function(error) {
$scope.bannedIPsLoading = false;
$scope.bannedIPCouldNotConnect = false;
});
}
$scope.addBannedIP = function() {
if (!$scope.banIP || !$scope.banReason) {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = "Please fill in all required fields";
return;
}
$scope.bannedIPsLoading = true;
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = true;
var data = {
ip: $scope.banIP,
reason: $scope.banReason,
duration: $scope.banDuration
};
var url = "/firewall/addBannedIP";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
$scope.bannedIPsLoading = false;
if (response.data.status === 1) {
$scope.bannedIPActionSuccess = false;
$scope.banIP = '';
$scope.banReason = '';
$scope.banDuration = '24h';
populateBannedIPs(); // Refresh the list
} else {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = response.data.error_message;
}
}, function(error) {
$scope.bannedIPsLoading = false;
$scope.bannedIPCouldNotConnect = false;
});
};
$scope.removeBannedIP = function(id, ip) {
if (!confirm('Are you sure you want to unban IP address ' + ip + '?')) {
return;
}
$scope.bannedIPsLoading = true;
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = true;
var data = { id: id };
var url = "/firewall/removeBannedIP";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
$scope.bannedIPsLoading = false;
if (response.data.status === 1) {
$scope.bannedIPActionSuccess = false;
populateBannedIPs(); // Refresh the list
} else {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = response.data.error_message;
}
}, function(error) {
$scope.bannedIPsLoading = false;
$scope.bannedIPCouldNotConnect = false;
});
};
$scope.deleteBannedIP = function(id, ip) {
if (!confirm('Are you sure you want to permanently delete the record for IP address ' + ip + '? This action cannot be undone.')) {
return;
}
$scope.bannedIPsLoading = true;
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = true;
var data = { id: id };
var url = "/firewall/deleteBannedIP";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
$scope.bannedIPsLoading = false;
if (response.data.status === 1) {
$scope.bannedIPActionSuccess = false;
populateBannedIPs(); // Refresh the list
} else {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = response.data.error_message;
}
}, function(error) {
$scope.bannedIPsLoading = false;
$scope.bannedIPCouldNotConnect = false;
});
};
});

View File

@ -526,6 +526,265 @@
min-width: 600px;
}
}
/* Tab Navigation Styles */
.tab-navigation {
display: flex;
background: var(--bg-secondary, white);
border-radius: 12px;
padding: 0.5rem;
margin-bottom: 2rem;
box-shadow: 0 2px 8px var(--shadow-light, rgba(0,0,0,0.1));
border: 1px solid var(--border-color, #e8e9ff);
}
.tab-button {
flex: 1;
padding: 1rem 1.5rem;
background: transparent;
border: none;
border-radius: 8px;
color: var(--text-secondary, #64748b);
font-weight: 500;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.tab-button:hover {
background: var(--bg-hover, #f8f9ff);
color: var(--accent-color, #5b5fcf);
}
.tab-button.tab-active {
background: var(--accent-color, #5b5fcf);
color: var(--bg-secondary, white);
box-shadow: 0 2px 8px rgba(91, 95, 207, 0.3);
}
.tab-button i {
font-size: 1rem;
}
/* Banned IPs Panel Styles */
.banned-ips-panel {
background: var(--bg-secondary, white);
border-radius: 16px;
box-shadow: 0 4px 12px var(--shadow-medium, rgba(0,0,0,0.15));
border: 1px solid var(--border-color, #e8e9ff);
overflow: hidden;
}
.add-banned-section {
padding: 2rem;
border-bottom: 1px solid var(--border-light, #f3f4f6);
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
}
.banned-form {
display: grid;
grid-template-columns: 1fr 1fr 1fr auto;
gap: 1rem;
align-items: end;
}
.btn-ban {
background: var(--danger-color, #ef4444);
color: var(--bg-secondary, white);
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.5rem;
height: fit-content;
}
.btn-ban:hover {
background: var(--danger-hover, #dc2626);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
}
.banned-list-section {
padding: 2rem;
}
.banned-table {
width: 100%;
border-collapse: collapse;
background: var(--bg-secondary, white);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px var(--shadow-light, rgba(0,0,0,0.1));
}
.banned-table th {
background: linear-gradient(135deg, var(--bg-hover, #f8f9ff) 0%, var(--bg-gradient, #f0f1ff) 100%);
padding: 1rem;
text-align: left;
font-weight: 600;
color: var(--text-primary, #1e293b);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 2px solid var(--border-color, #e8e9ff);
}
.banned-table td {
padding: 1rem;
color: var(--text-secondary, #64748b);
font-size: 0.875rem;
border-bottom: 1px solid var(--border-light, #f3f4f6);
vertical-align: middle;
}
.banned-table tbody tr {
transition: all 0.2s ease;
}
.banned-table tbody tr:hover {
background: var(--bg-hover, #f8f9ff);
}
.ip-address {
font-weight: 600;
color: var(--text-primary, #1e293b);
font-family: 'Courier New', monospace;
}
.ip-address i {
color: var(--accent-color, #5b5fcf);
margin-right: 0.5rem;
}
.reason-text {
background: var(--bg-light, #f8f9fa);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
color: var(--text-secondary, #64748b);
}
.banned-date, .expires-date {
color: var(--text-secondary, #64748b);
font-size: 0.8rem;
}
.banned-date i, .expires-date i {
color: var(--accent-color, #5b5fcf);
margin-right: 0.25rem;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.status-badge.status-active {
background: rgba(239, 68, 68, 0.1);
color: var(--danger-color, #ef4444);
}
.status-badge.status-expired {
background: rgba(107, 114, 128, 0.1);
color: #6b7280;
}
.status-badge i {
font-size: 0.5rem;
}
.actions {
display: flex;
gap: 0.5rem;
}
.btn-unban {
background: var(--success-color, #10b981);
color: var(--bg-secondary, white);
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.8rem;
}
.btn-unban:hover {
background: var(--success-hover, #059669);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
}
.btn-delete {
background: var(--danger-color, #ef4444);
color: var(--bg-secondary, white);
border: none;
padding: 0.5rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-delete:hover {
background: var(--danger-hover, #dc2626);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
}
/* Responsive Design for Banned IPs */
@media (max-width: 768px) {
.banned-form {
grid-template-columns: 1fr;
gap: 1rem;
}
.btn-ban {
width: 100%;
justify-content: center;
}
.banned-list-section {
padding: 1rem;
overflow-x: auto;
}
.banned-table {
min-width: 800px;
}
.tab-navigation {
flex-direction: column;
}
.tab-button {
width: 100%;
}
}
</style>
{% endblock %}
@ -591,8 +850,26 @@
</div>
</div>
<!-- Tab Navigation -->
<div class="tab-navigation">
<button type="button"
ng-click="activeTab = 'rules'"
ng-class="{'tab-active': activeTab === 'rules'}"
class="tab-button">
<i class="fas fa-list-alt"></i>
{% trans "Firewall Rules" %}
</button>
<button type="button"
ng-click="activeTab = 'banned'"
ng-class="{'tab-active': activeTab === 'banned'}"
class="tab-button">
<i class="fas fa-ban"></i>
{% trans "Banned IPs" %}
</button>
</div>
<!-- Rules Panel -->
<div class="rules-panel">
<div class="rules-panel" ng-show="activeTab === 'rules'">
<div class="panel-header">
<div class="panel-title">
<div class="panel-icon">
@ -717,6 +994,144 @@
</div>
</div>
</div>
<!-- Banned IPs Panel -->
<div class="banned-ips-panel" ng-show="activeTab === 'banned'">
<div class="panel-header">
<div class="panel-title">
<div class="panel-icon">
<i class="fas fa-ban"></i>
</div>
{% trans "Banned IP Addresses" %}
</div>
<div ng-show="bannedIPsLoading" class="loading-spinner"></div>
</div>
<!-- Add Banned IP Section -->
<div class="add-banned-section">
<form class="banned-form">
<div class="form-group">
<label class="form-label">{% trans "IP Address" %}</label>
<input type="text"
class="form-control"
ng-model="banIP"
placeholder="{% trans 'e.g., 192.168.1.100 or 192.168.1.0/24' %}"
required>
</div>
<div class="form-group">
<label class="form-label">{% trans "Reason" %}</label>
<input type="text"
class="form-control"
ng-model="banReason"
placeholder="{% trans 'e.g., Suspicious activity, Brute force attack' %}"
required>
</div>
<div class="form-group">
<label class="form-label">{% trans "Duration" %}</label>
<select ng-model="banDuration" class="form-control select-control">
<option value="1h">{% trans "1 Hour" %}</option>
<option value="24h">{% trans "24 Hours" %}</option>
<option value="7d">{% trans "7 Days" %}</option>
<option value="30d">{% trans "30 Days" %}</option>
<option value="permanent">{% trans "Permanent" %}</option>
</select>
</div>
<button type="button"
ng-click="addBannedIP()"
class="btn-ban">
<i class="fas fa-ban"></i>
{% trans "Ban IP Address" %}
</button>
</form>
</div>
<!-- Banned IPs List -->
<div class="banned-list-section">
<table class="banned-table" ng-if="bannedIPs.length > 0">
<thead>
<tr>
<th>{% trans "IP Address" %}</th>
<th>{% trans "Reason" %}</th>
<th>{% trans "Banned On" %}</th>
<th>{% trans "Expires" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="bannedIP in bannedIPs">
<td class="ip-address">
<i class="fas fa-globe"></i>
{$ bannedIP.ip $}
</td>
<td class="reason">
<span class="reason-text">{$ bannedIP.reason $}</span>
</td>
<td class="banned-date">
<i class="fas fa-calendar"></i>
{$ bannedIP.banned_on | date:'MMM dd, yyyy HH:mm' $}
</td>
<td class="expires-date">
<i class="fas fa-clock"></i>
<span ng-if="bannedIP.expires === 'Never'">{% trans "Never" %}</span>
<span ng-if="bannedIP.expires !== 'Never'">{$ bannedIP.expires | date:'MMM dd, yyyy HH:mm' $}</span>
</td>
<td class="status">
<span ng-class="{'status-active': bannedIP.active, 'status-expired': !bannedIP.active}"
class="status-badge">
<i class="fas fa-circle"></i>
{$ bannedIP.active ? 'Active' : 'Expired' $}
</span>
</td>
<td class="actions">
<button type="button"
ng-click="removeBannedIP(bannedIP.id, bannedIP.ip)"
class="btn-unban"
title="{% trans 'Unban IP' %}"
ng-if="bannedIP.active">
<i class="fas fa-unlock"></i>
{% trans "Unban" %}
</button>
<button type="button"
ng-click="deleteBannedIP(bannedIP.id, bannedIP.ip)"
class="btn-delete"
title="{% trans 'Delete Record' %}">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
<!-- Empty State -->
<div ng-if="bannedIPs.length == 0" class="empty-state">
<i class="fas fa-shield-check empty-icon"></i>
<h3 class="empty-title">{% trans "No Banned IPs" %}</h3>
<p class="empty-text">{% trans "All IP addresses are currently allowed. Add banned IPs to block suspicious or malicious traffic." %}</p>
</div>
</div>
<!-- Messages -->
<div style="padding: 0 2rem 2rem;">
<div ng-hide="bannedIPActionFailed" class="alert alert-danger">
<i class="fas fa-exclamation-circle alert-icon"></i>
<span>{% trans "Action failed. Error message:" %} {$ bannedIPErrorMessage $}</span>
</div>
<div ng-hide="bannedIPActionSuccess" class="alert alert-success">
<i class="fas fa-check-circle alert-icon"></i>
<span>{% trans "Action completed successfully." %}</span>
</div>
<div ng-hide="bannedIPCouldNotConnect" class="alert alert-danger">
<i class="fas fa-exclamation-circle alert-icon"></i>
<span>{% trans "Could not connect to server. Please refresh this page." %}</span>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -32,6 +32,12 @@ urlpatterns = [
path('modSecRulesPacks', views.modSecRulesPacks, name='modSecRulesPacks'),
path('getOWASPAndComodoStatus', views.getOWASPAndComodoStatus, name='getOWASPAndComodoStatus'),
path('installModSecRulesPack', views.installModSecRulesPack, name='installModSecRulesPack'),
# Banned IPs
path('getBannedIPs', views.getBannedIPs, name='getBannedIPs'),
path('addBannedIP', views.addBannedIP, name='addBannedIP'),
path('removeBannedIP', views.removeBannedIP, name='removeBannedIP'),
path('deleteBannedIP', views.deleteBannedIP, name='deleteBannedIP'),
path('getRulesFiles', views.getRulesFiles, name='getRulesFiles'),
path('enableDisableRuleFile', views.enableDisableRuleFile, name='enableDisableRuleFile'),

View File

@ -648,3 +648,36 @@ def saveLitespeed_conf(request):
return fm.saveLitespeed_conf(userID, json.loads(request.body))
except KeyError:
return redirect(loadLoginPage)
# Banned IPs Views
def getBannedIPs(request):
try:
userID = request.session['userID']
fm = FirewallManager()
return fm.getBannedIPs(userID)
except KeyError:
return redirect(loadLoginPage)
def addBannedIP(request):
try:
userID = request.session['userID']
fm = FirewallManager()
return fm.addBannedIP(userID, json.loads(request.body))
except KeyError:
return redirect(loadLoginPage)
def removeBannedIP(request):
try:
userID = request.session['userID']
fm = FirewallManager()
return fm.removeBannedIP(userID, json.loads(request.body))
except KeyError:
return redirect(loadLoginPage)
def deleteBannedIP(request):
try:
userID = request.session['userID']
fm = FirewallManager()
return fm.deleteBannedIP(userID, json.loads(request.body))
except KeyError:
return redirect(loadLoginPage)

View File

@ -1,128 +0,0 @@
#!/bin/bash
# CyberPanel Post-Upgrade Fix Script
# This script completes the installation when the upgrade exits early due to TypeError
set -e # Exit on error
echo "==================================="
echo "CyberPanel Installation Fix Script"
echo "==================================="
echo ""
# Check if running as root
if [[ $(id -u) != 0 ]]; then
echo "This script must be run as root!"
exit 1
fi
# Function to print colored output
print_status() {
echo -e "\033[1;32m[$(date +"%Y-%m-%d %H:%M:%S")]\033[0m $1"
}
print_error() {
echo -e "\033[1;31m[$(date +"%Y-%m-%d %H:%M:%S")] ERROR:\033[0m $1"
}
# Check if virtual environment exists
if [[ ! -f /usr/local/CyberCP/bin/activate ]]; then
print_error "CyberPanel virtual environment not found!"
print_status "Creating virtual environment..."
# Try python3 -m venv first
if python3 -m venv --system-site-packages /usr/local/CyberCP 2>/dev/null; then
print_status "Virtual environment created successfully with python3 -m venv"
else
# Fallback to virtualenv
virtualenv -p /usr/bin/python3 --system-site-packages /usr/local/CyberCP
fi
fi
# Activate virtual environment
print_status "Activating CyberPanel virtual environment..."
source /usr/local/CyberCP/bin/activate
# Check if Django is already installed
if python -c "import django" 2>/dev/null; then
print_status "Django is already installed. Checking version..."
python -c "import django; print(f'Django version: {django.__version__}')"
else
print_status "Installing Python requirements..."
# Download requirements file
print_status "Downloading requirements.txt..."
if [[ -f /tmp/requirements.txt ]]; then
rm -f /tmp/requirements.txt
fi
# Detect OS version and download appropriate requirements
if grep -q "22.04" /etc/os-release || grep -q "VERSION_ID=\"9" /etc/os-release; then
wget -q -O /tmp/requirements.txt https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.4.4-dev/requirments.txt
else
wget -q -O /tmp/requirements.txt https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.4.4-dev/requirments-old.txt
fi
# Upgrade pip first
print_status "Upgrading pip, setuptools, and wheel..."
pip install --upgrade pip setuptools wheel packaging
# Install requirements
print_status "Installing CyberPanel requirements (this may take a few minutes)..."
pip install --default-timeout=3600 --ignore-installed -r /tmp/requirements.txt
fi
# Install WSGI-LSAPI if not present
if [[ ! -f /usr/local/CyberCP/bin/lswsgi ]]; then
print_status "Installing WSGI-LSAPI..."
cd /tmp
rm -rf wsgi-lsapi-2.1*
wget -q https://www.litespeedtech.com/packages/lsapi/wsgi-lsapi-2.1.tgz
tar xf wsgi-lsapi-2.1.tgz
cd wsgi-lsapi-2.1
/usr/local/CyberCP/bin/python ./configure.py
make
cp lswsgi /usr/local/CyberCP/bin/
print_status "WSGI-LSAPI installed successfully"
fi
# Fix permissions
print_status "Fixing permissions..."
chown -R cyberpanel:cyberpanel /usr/local/CyberCP/lib 2>/dev/null || true
chown -R cyberpanel:cyberpanel /usr/local/CyberCP/lib64 2>/dev/null || true
# Test Django installation
print_status "Testing Django installation..."
cd /usr/local/CyberCP
if python manage.py check 2>&1 | grep -q "System check identified no issues"; then
print_status "Django is working correctly!"
else
print_error "Django check failed. Checking for specific issues..."
python manage.py check
fi
# Restart LSCPD
print_status "Restarting LSCPD service..."
systemctl restart lscpd
# Check service status
if systemctl is-active --quiet lscpd; then
print_status "LSCPD service is running"
else
print_error "LSCPD service failed to start"
systemctl status lscpd
fi
echo ""
print_status "CyberPanel fix completed!"
echo ""
echo "You can now access CyberPanel at: https://$(hostname -I | awk '{print $1}'):8090"
echo ""
# Deactivate virtual environment
deactivate 2>/dev/null || true

View File

@ -111,7 +111,29 @@ class FTPManager:
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'status': 0, 'creatFTPStatus': 0, 'error_message': str(msg)}
# Enhanced error handling with better user feedback
error_message = str(msg)
# Provide more user-friendly error messages
if "Invalid path" in error_message:
pass # Keep original message as it's already user-friendly
elif "Security violation" in error_message:
pass # Keep original message as it's already user-friendly
elif "Path validation failed" in error_message:
pass # Keep original message as it's already user-friendly
elif "Exceeded maximum amount" in error_message:
pass # Keep original message as it's already user-friendly
elif "symlinked" in error_message.lower():
error_message = "Cannot create FTP account: The specified path is a symbolic link. Please choose a different path."
elif "Permission denied" in error_message:
error_message = "Permission denied: Unable to create or access the specified directory. Please check the path and try again."
elif "No such file or directory" in error_message:
error_message = "Directory not found: The specified path does not exist. Please check the path and try again."
else:
# Generic fallback for other errors
error_message = f"FTP account creation failed: {error_message}"
data_ret = {'status': 0, 'creatFTPStatus': 0, 'error_message': error_message}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@ -336,6 +358,87 @@ class FTPManager:
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def getFTPQuotaUsage(self):
"""
Get quota usage information for an FTP user
"""
try:
userID = self.request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if ACLManager.currentContextPermission(currentACL, 'listFTPAccounts') == 0:
return ACLManager.loadErrorJson('getQuotaUsage', 0)
data = json.loads(self.request.body)
userName = data['ftpUserName']
admin = Administrator.objects.get(pk=userID)
ftp = Users.objects.get(user=userName)
if currentACL['admin'] == 1:
pass
elif ftp.domain.admin != admin:
return ACLManager.loadErrorJson()
result = FTPUtilities.getFTPQuotaUsage(userName)
if isinstance(result, dict):
data_ret = {
'status': 1,
'getQuotaUsage': 1,
'error_message': "None",
'quota_usage': result
}
else:
data_ret = {
'status': 0,
'getQuotaUsage': 0,
'error_message': result[1] if isinstance(result, tuple) else str(result)
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'status': 0, 'getQuotaUsage': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def migrateFTPQuotas(self):
"""
Migrate existing FTP users to the new quota system
"""
try:
userID = self.request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] != 1:
return ACLManager.loadErrorJson('migrateQuotas', 0)
result = FTPUtilities.migrateExistingFTPUsers()
if result[0] == 1:
data_ret = {
'status': 1,
'migrateQuotas': 1,
'error_message': "None",
'message': result[1]
}
else:
data_ret = {
'status': 0,
'migrateQuotas': 0,
'error_message': result[1]
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'status': 0, 'migrateQuotas': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def installPureFTPD(self):
def pureFTPDServiceName():

View File

@ -60,8 +60,45 @@ app.controller('createFTPAccount', function ($scope, $http) {
var ftpPassword = $scope.ftpPassword;
var path = $scope.ftpPath;
if (typeof path === 'undefined') {
// Enhanced path validation
if (typeof path === 'undefined' || path === null) {
path = "";
} else {
path = path.trim();
}
// Client-side path validation
if (path && path !== "") {
// Check for dangerous characters
var dangerousChars = /[;&|$`'"<>*?~]/;
if (dangerousChars.test(path)) {
$scope.ftpLoading = false;
$scope.canNotCreateFTP = false;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
$scope.errorMessage = "Invalid path: Path contains dangerous characters";
return;
}
// Check for path traversal attempts
if (path.indexOf("..") !== -1 || path.indexOf("~") !== -1) {
$scope.ftpLoading = false;
$scope.canNotCreateFTP = false;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
$scope.errorMessage = "Invalid path: Path cannot contain '..' or '~'";
return;
}
// Check if path starts with slash (should be relative)
if (path.startsWith("/")) {
$scope.ftpLoading = false;
$scope.canNotCreateFTP = false;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
$scope.errorMessage = "Invalid path: Path must be relative (not starting with '/')";
return;
}
}
var url = "/ftp/submitFTPCreation";

View File

@ -521,9 +521,23 @@
<div class="path-info">
<i class="fas fa-folder"></i>
{% trans "Leave empty to use the website's home directory, or specify a subdirectory" %}
<br>
<small style="margin-top: 0.5rem; display: block; color: var(--text-secondary, #64748b);">
<i class="fas fa-info-circle"></i>
<strong>{% trans "Examples:" %}</strong> {% trans "docs, public_html, uploads, api" %}
<br>
<i class="fas fa-shield-alt"></i>
{% trans "Security: Path will be restricted to this subdirectory only" %}
</small>
</div>
<input placeholder="{% trans 'e.g., public_html or leave empty' %}"
type="text" class="form-control" ng-model="ftpPath">
<input placeholder="{% trans 'e.g., docs or public_html (leave empty for home directory)' %}"
type="text" class="form-control" ng-model="ftpPath"
pattern="^[a-zA-Z0-9._/-]+$"
title="{% trans 'Only letters, numbers, dots, underscores, hyphens, and forward slashes allowed' %}">
<small style="color: var(--text-secondary, #64748b); margin-top: 0.5rem; display: block;">
<i class="fas fa-exclamation-triangle"></i>
{% trans "Do not use: .. ~ / or special characters like ; | & $ ` ' \" < > * ?" %}
</small>
</div>
<div ng-hide="ftpDetails" class="form-group">

View File

@ -16,4 +16,6 @@ urlpatterns = [
path('getAllFTPAccounts', views.getAllFTPAccounts, name='getAllFTPAccounts'),
path('changePassword', views.changePassword, name='changePassword'),
path('updateFTPQuota', views.updateFTPQuota, name='updateFTPQuota'),
path('getFTPQuotaUsage', views.getFTPQuotaUsage, name='getFTPQuotaUsage'),
path('migrateFTPQuotas', views.migrateFTPQuotas, name='migrateFTPQuotas'),
]

View File

@ -221,5 +221,19 @@ def updateFTPQuota(request):
try:
fm = FTPManager(request)
return fm.updateFTPQuota()
except KeyError:
return redirect(loadLoginPage)
def getFTPQuotaUsage(request):
try:
fm = FTPManager(request)
return fm.getFTPQuotaUsage()
except KeyError:
return redirect(loadLoginPage)
def migrateFTPQuotas(request):
try:
fm = FTPManager(request)
return fm.migrateFTPQuotas()
except KeyError:
return redirect(loadLoginPage)

View File

@ -0,0 +1,431 @@
# CyberPanel 2FA Authentication Guide
## Overview
CyberPanel supports multiple two-factor authentication (2FA) methods to enhance security for user accounts. This guide covers all available authentication methods, their setup, and best practices.
## Table of Contents
1. [Available Authentication Methods](#available-authentication-methods)
2. [Traditional 2FA (TOTP)](#traditional-2fa-totp)
3. [WebAuthn/Passkey Authentication](#webauthnpasskey-authentication)
4. [Password Authentication](#password-authentication)
5. [Combined Authentication Strategies](#combined-authentication-strategies)
6. [Administrator Management](#administrator-management)
7. [Troubleshooting](#troubleshooting)
8. [Security Best Practices](#security-best-practices)
## Available Authentication Methods
### 1. Traditional 2FA (TOTP)
- **Type**: Time-based One-Time Password
- **Method**: Google Authenticator, Authy, or similar apps
- **Security Level**: High
- **User Experience**: Requires manual code entry
### 2. WebAuthn/Passkey Authentication
- **Type**: Modern passwordless authentication
- **Method**: Biometric authentication, security keys, or device passkeys
- **Security Level**: Very High
- **User Experience**: Seamless and user-friendly
### 3. Password Authentication
- **Type**: Traditional username/password
- **Method**: Standard login credentials
- **Security Level**: Basic
- **User Experience**: Simple but less secure
## Traditional 2FA (TOTP)
### How It Works
TOTP generates time-based codes that change every 30 seconds. Users scan a QR code with their authenticator app to set up 2FA.
### Setting Up TOTP 2FA
#### For Users
1. **Access User Management**
- Log in to CyberPanel
- Go to **User Management** → **Modify User**
- Select your user account
2. **Enable 2FA**
- Scroll to **Additional Features** section
- Check **"Enable Two-Factor Authentication (2FA)"**
- A QR code will appear
3. **Configure Authenticator App**
- Open your authenticator app (Google Authenticator, Authy, etc.)
- Scan the QR code displayed
- Or manually enter the secret key shown below the QR code
4. **Test 2FA**
- Enter a 6-digit code from your authenticator app
- Verify the setup works correctly
#### For Administrators
1. **Access User Management**
- Go to **User Management** → **Modify User**
- Select the user you want to configure
2. **Enable 2FA for User**
- Check **"Enable Two-Factor Authentication (2FA)"**
- Provide the QR code or secret key to the user
- Ensure the user completes the setup
### TOTP Configuration Options
#### Secret Key Management
- **Auto-generation**: CyberPanel automatically generates secure secret keys
- **Manual Entry**: Users can manually enter the secret key if QR scanning fails
- **Regeneration**: Secret keys can be regenerated if needed
#### QR Code Display
- **Automatic Display**: QR code appears when 2FA is enabled
- **Manual Key**: Secret key is always displayed for manual entry
- **Copy Function**: One-click copy to clipboard functionality
### Supported Authenticator Apps
- **Google Authenticator** (iOS/Android)
- **Authy** (iOS/Android/Desktop)
- **Microsoft Authenticator** (iOS/Android)
- **1Password** (iOS/Android/Desktop)
- **Bitwarden** (iOS/Android/Desktop)
- **Any TOTP-compatible app**
## WebAuthn/Passkey Authentication
### What is WebAuthn?
WebAuthn is a web standard that enables secure, passwordless authentication using public-key cryptography. It supports biometric authentication, security keys, and device passkeys.
### Setting Up WebAuthn
#### Prerequisites
- **HTTPS Required**: WebAuthn only works over secure connections
- **Modern Browser**: Chrome 67+, Firefox 60+, Safari 14+, Edge 79+
- **Compatible Device**: Device with biometric sensors or security key
#### For Users
1. **Access User Management**
- Go to **User Management** → **Modify User**
- Select your user account
2. **Enable WebAuthn**
- Scroll to **Passkey Authentication (WebAuthn)** section
- Check **"Enable Passkey Authentication"**
3. **Configure Settings**
- **Require Passkey for Login**: Enable for passwordless authentication
- **Allow Multiple Passkeys**: Enable to register multiple devices
- **Max Passkeys**: Set limit (default: 10)
- **Timeout**: Set timeout in seconds (default: 60)
4. **Register Passkeys**
- Click **"Register New Passkey"**
- Enter a name for your passkey (e.g., "iPhone", "Laptop")
- Follow your browser's prompts to complete registration
- Repeat for additional devices
#### For Administrators
1. **Access User Management**
- Go to **User Management** → **Modify User**
- Select the user you want to configure
2. **Enable WebAuthn for User**
- Check **"Enable Passkey Authentication"**
- Configure security policies
- Monitor registered passkeys
### WebAuthn Features
#### Passkey Management
- **Multiple Devices**: Register passkeys on phones, laptops, security keys
- **Custom Names**: Give descriptive names to your passkeys
- **Easy Removal**: Delete passkeys you no longer need
- **Backup Options**: Multiple passkeys for redundancy
#### Security Features
- **Replay Protection**: Each authentication uses a unique challenge
- **Credential Isolation**: Passkeys cannot be extracted or shared
- **User Verification**: Optional biometric or PIN verification
- **Session Management**: Secure session handling with expiration
### Supported WebAuthn Devices
- **Smartphones**: iPhone, Android phones with biometrics
- **Laptops**: MacBooks with Touch ID, Windows Hello devices
- **Security Keys**: YubiKey, Google Titan, etc.
- **Tablets**: iPads, Android tablets with biometrics
## Password Authentication
### Traditional Login
- **Username/Password**: Standard login credentials
- **Security Level**: Basic (not recommended alone)
- **Use Case**: Fallback authentication method
### Password Requirements
- **Minimum Length**: 8 characters (recommended: 12+)
- **Complexity**: Mix of uppercase, lowercase, numbers, symbols
- **Uniqueness**: Different from other accounts
- **Regular Updates**: Change passwords periodically
## Combined Authentication Strategies
### Strategy 1: Password + 2FA
- **Login Process**: Username + Password + TOTP code
- **Security Level**: High
- **User Experience**: Moderate (requires manual code entry)
- **Best For**: Users who prefer traditional 2FA
### Strategy 2: Password + Passkeys
- **Login Process**: Username + Password + Passkey
- **Security Level**: Very High
- **User Experience**: Good (biometric authentication)
- **Best For**: Users with compatible devices
### Strategy 3: Passkeys Only (Passwordless)
- **Login Process**: Username + Passkey only
- **Security Level**: Very High
- **User Experience**: Excellent (no password required)
- **Best For**: Security-conscious users with multiple devices
### Strategy 4: All Methods (Maximum Security)
- **Login Process**: Username + Password + 2FA + Passkeys
- **Security Level**: Maximum
- **User Experience**: Complex but flexible
- **Best For**: High-security environments
## Administrator Management
### User Authentication Settings
#### Accessing User Settings
1. **Navigate to User Management**
- Go to **User Management** → **Modify User**
- Select the user to manage
#### Available Settings
- **Enable/Disable 2FA**: Toggle TOTP authentication
- **Enable/Disable WebAuthn**: Toggle passkey authentication
- **Security Policies**: Set passkey limits and timeouts
- **View Credentials**: See registered passkeys and 2FA status
### Security Policies
#### Passkey Policies
- **Maximum Passkeys**: Limit number of registered passkeys per user
- **Timeout Settings**: Set authentication timeout duration
- **Device Requirements**: Specify required device capabilities
#### 2FA Policies
- **Secret Key Management**: Control secret key generation and regeneration
- **QR Code Display**: Manage QR code visibility
- **Backup Codes**: Generate backup codes for recovery
### Monitoring and Auditing
#### Authentication Logs
- **Login Attempts**: Track all authentication attempts
- **Method Used**: Record which authentication method was used
- **Success/Failure**: Monitor authentication success rates
- **Device Information**: Log device and browser details
#### Security Alerts
- **Failed Attempts**: Alert on multiple failed login attempts
- **Unusual Activity**: Detect suspicious authentication patterns
- **Device Changes**: Notify when new devices are registered
## Troubleshooting
### Common TOTP Issues
#### QR Code Not Scanning
**Problem**: QR code cannot be scanned by authenticator app
**Solutions**:
- Try manual key entry instead
- Ensure good lighting and camera focus
- Try a different authenticator app
- Regenerate the QR code
#### Time Synchronization Issues
**Problem**: Codes not working due to time differences
**Solutions**:
- Check device time is correct
- Sync device time with internet
- Try a different authenticator app
- Contact administrator for assistance
#### Lost Authenticator Device
**Problem**: Cannot access authenticator app
**Solutions**:
- Use backup codes if available
- Contact administrator to reset 2FA
- Set up 2FA on a new device
- Use alternative authentication methods
### Common WebAuthn Issues
#### "WebAuthn not supported" Error
**Problem**: Browser or device doesn't support WebAuthn
**Solutions**:
- Update browser to latest version
- Ensure HTTPS is enabled
- Check device compatibility
- Try a different browser
#### Registration Failed
**Problem**: Passkey registration fails
**Solutions**:
- Check browser console for errors
- Ensure user has permission to register passkeys
- Verify WebAuthn settings are properly configured
- Try a different device or browser
#### Authentication Failed
**Problem**: Passkey authentication fails
**Solutions**:
- Ensure passkey is still active
- Check that user has registered passkeys
- Verify challenge hasn't expired
- Try a different passkey or device
### General Authentication Issues
#### Login Not Working
**Problem**: Cannot log in with any method
**Solutions**:
- Check username and password
- Verify 2FA codes are current
- Ensure passkeys are properly registered
- Contact administrator for assistance
#### Account Locked
**Problem**: Account is locked due to failed attempts
**Solutions**:
- Wait for lockout period to expire
- Contact administrator to unlock account
- Check for security alerts
- Review recent login attempts
## Security Best Practices
### For Users
#### Password Security
- **Use Strong Passwords**: 12+ characters with mixed case, numbers, symbols
- **Unique Passwords**: Different password for each account
- **Regular Updates**: Change passwords every 90 days
- **Password Manager**: Use a reputable password manager
#### 2FA Security
- **Secure Device**: Use a trusted device for authenticator apps
- **Backup Codes**: Save backup codes in a secure location
- **Multiple Methods**: Set up multiple authentication methods
- **Regular Testing**: Test authentication methods regularly
#### WebAuthn Security
- **Multiple Passkeys**: Register passkeys on multiple devices
- **Secure Devices**: Only register passkeys on trusted devices
- **Regular Review**: Periodically review registered passkeys
- **Device Security**: Keep devices updated and secure
### For Administrators
#### System Security
- **HTTPS Enforcement**: Ensure all authentication uses HTTPS
- **Regular Updates**: Keep CyberPanel and dependencies updated
- **Monitoring**: Monitor authentication logs and alerts
- **Backup**: Regular backups of authentication data
#### User Management
- **Access Control**: Implement proper user access controls
- **Security Policies**: Enforce strong security policies
- **Training**: Provide security training to users
- **Incident Response**: Have procedures for security incidents
#### Configuration Security
- **Default Settings**: Change default passwords and settings
- **Network Security**: Implement proper network security
- **Logging**: Enable comprehensive logging
- **Auditing**: Regular security audits
## Advanced Configuration
### Custom Security Policies
#### Passkey Policies
```python
# Example: Custom passkey policies
WEBAUTHN_POLICIES = {
'max_credentials_per_user': 5,
'timeout_seconds': 120,
'require_user_verification': True,
'allowed_attestation_formats': ['none', 'packed']
}
```
#### 2FA Policies
```python
# Example: Custom 2FA policies
TOTP_POLICIES = {
'secret_key_length': 32,
'time_step': 30,
'window': 1,
'issuer_name': 'CyberPanel'
}
```
### Integration with External Systems
#### LDAP Integration
- **Active Directory**: Integrate with Windows Active Directory
- **OpenLDAP**: Connect to OpenLDAP servers
- **Multi-Factor**: Combine with external MFA systems
#### SSO Integration
- **SAML**: Single Sign-On with SAML providers
- **OAuth**: OAuth-based authentication
- **Custom Providers**: Integration with custom identity providers
## Migration and Upgrades
### Upgrading Authentication Methods
#### From Password to 2FA
1. **Enable 2FA**: Add TOTP authentication
2. **Test Setup**: Verify 2FA works correctly
3. **User Training**: Train users on 2FA usage
4. **Gradual Rollout**: Enable for users progressively
#### From 2FA to WebAuthn
1. **Enable WebAuthn**: Add passkey support
2. **User Migration**: Help users register passkeys
3. **Dual Support**: Support both methods during transition
4. **Full Migration**: Complete transition to WebAuthn
### Backup and Recovery
#### Authentication Backup
- **Secret Keys**: Backup TOTP secret keys securely
- **Passkey Data**: Backup WebAuthn credential data
- **User Data**: Regular backups of user authentication data
- **Configuration**: Backup authentication configuration
#### Recovery Procedures
- **Account Recovery**: Procedures for locked accounts
- **Data Restoration**: Restore authentication data from backups
- **Emergency Access**: Emergency access procedures
- **User Support**: Support procedures for authentication issues
## Conclusion
CyberPanel's multi-layered authentication system provides flexible and secure access control. By combining traditional 2FA with modern WebAuthn passkeys, administrators can offer users both security and convenience.
Choose the authentication methods that best fit your security requirements and user needs. Remember to regularly review and update your authentication policies to maintain optimal security.
For additional support and advanced configuration options, refer to the CyberPanel documentation and community resources.
---
**Note**: This guide covers all available authentication methods in CyberPanel. For the latest updates and additional features, refer to the official CyberPanel documentation.
*Last updated: January 2025*

View File

@ -0,0 +1,773 @@
# CyberPanel CLI Command Reference Guide
## Overview
This comprehensive guide covers all available CyberPanel CLI commands for managing your server directly from SSH. The CyberPanel CLI provides powerful command-line tools for website management, system administration, and automation.
## Table of Contents
1. [Getting Started](#getting-started)
2. [Website Management Commands](#website-management-commands)
3. [DNS Management Commands](#dns-management-commands)
4. [Database Management Commands](#database-management-commands)
5. [Email Management Commands](#email-management-commands)
6. [User Management Commands](#user-management-commands)
7. [Package Management Commands](#package-management-commands)
8. [System Administration Commands](#system-administration-commands)
9. [File Management Commands](#file-management-commands)
10. [Application Installation Commands](#application-installation-commands)
11. [Backup and Restore Commands](#backup-and-restore-commands)
12. [Security and Firewall Commands](#security-and-firewall-commands)
13. [Troubleshooting Commands](#troubleshooting-commands)
14. [Advanced Usage Examples](#advanced-usage-examples)
## Getting Started
### Accessing CyberPanel CLI
```bash
# Access CyberPanel CLI
sudo cyberpanel
# Or run specific commands directly
sudo cyberpanel [command] [options]
```
### Basic Syntax
```bash
cyberpanel [function] --parameter1 value1 --parameter2 value2
```
### Getting Help
```bash
# Show all available commands
cyberpanel --help
# Show help for specific command
cyberpanel createWebsite --help
```
## Website Management Commands
### Create Website
```bash
# Basic website creation
cyberpanel createWebsite \
--package Default \
--owner admin \
--domainName example.com \
--email admin@example.com \
--php 8.1
# With SSL and DKIM
cyberpanel createWebsite \
--package Default \
--owner admin \
--domainName example.com \
--email admin@example.com \
--php 8.1 \
--ssl 1 \
--dkim 1
```
**Parameters:**
- `--package`: Package name (e.g., Default, CLI)
- `--owner`: Owner username
- `--domainName`: Domain name to create
- `--email`: Administrator email
- `--php`: PHP version (5.6, 7.0, 7.1, 7.2, 7.3, 7.4, 8.0, 8.1, 8.2, 8.3)
- `--ssl`: Enable SSL (1) or disable (0)
- `--dkim`: Enable DKIM (1) or disable (0)
- `--openBasedir`: Enable open_basedir protection (1) or disable (0)
### Delete Website
```bash
# Delete a website
cyberpanel deleteWebsite \
--domainName example.com
```
### List Websites
```bash
# List all websites
cyberpanel listWebsites
```
### Change PHP Version
```bash
# Change PHP version for a website
cyberpanel changePHP \
--domainName example.com \
--php 8.1
```
### Change Package
```bash
# Change website package
cyberpanel changePackage \
--domainName example.com \
--packageName CLI
```
## DNS Management Commands
### Create DNS Record
```bash
# Create A record
cyberpanel createDNSRecord \
--domainName example.com \
--name www \
--recordType A \
--value 192.168.1.100 \
--priority 0 \
--ttl 3600
# Create MX record
cyberpanel createDNSRecord \
--domainName example.com \
--name @ \
--recordType MX \
--value mail.example.com \
--priority 10 \
--ttl 3600
# Create CNAME record
cyberpanel createDNSRecord \
--domainName example.com \
--name blog \
--recordType CNAME \
--value example.com \
--priority 0 \
--ttl 3600
```
**Parameters:**
- `--domainName`: Domain name
- `--name`: Record name (subdomain or @ for root)
- `--recordType`: Record type (A, AAAA, CNAME, MX, TXT, NS, SRV)
- `--value`: Record value (IP address, hostname, text)
- `--priority`: Priority (for MX records)
- `--ttl`: Time to live in seconds
### List DNS Records
```bash
# List DNS records as JSON
cyberpanel listDNSJson \
--domainName example.com
# List DNS records (human readable)
cyberpanel listDNS \
--domainName example.com
```
### Delete DNS Record
```bash
# Delete DNS record by ID
cyberpanel deleteDNSRecord \
--recordID 123
```
## Database Management Commands
### Create Database
```bash
# Create MySQL database
cyberpanel createDatabase \
--databaseWebsite example.com \
--dbName mydatabase \
--dbUsername dbuser \
--dbPassword securepassword
```
**Parameters:**
- `--databaseWebsite`: Associated website domain
- `--dbName`: Database name
- `--dbUsername`: Database username
- `--dbPassword`: Database password
### Delete Database
```bash
# Delete database
cyberpanel deleteDatabase \
--dbName mydatabase
```
### List Databases
```bash
# List all databases
cyberpanel listDatabases
```
## Email Management Commands
### Create Email Account
```bash
# Create email account
cyberpanel createEmail \
--userName admin \
--password securepassword \
--databaseWebsite example.com
```
**Parameters:**
- `--userName`: Email username (without @domain.com)
- `--password`: Email password
- `--databaseWebsite`: Associated website domain
### Delete Email Account
```bash
# Delete email account
cyberpanel deleteEmail \
--userName admin \
--databaseWebsite example.com
```
### List Email Accounts
```bash
# List email accounts
cyberpanel listEmails \
--databaseWebsite example.com
```
## User Management Commands
### Create User
```bash
# Create new user
cyberpanel createUser \
--firstName John \
--lastName Doe \
--userName johndoe \
--email john@example.com \
--websitesLimit 5 \
--selectedACL user \
--securityLevel 0
```
**Parameters:**
- `--firstName`: User's first name
- `--lastName`: User's last name
- `--userName`: Username
- `--email`: User's email address
- `--websitesLimit`: Maximum number of websites
- `--selectedACL`: Access control level (user, reseller, admin)
- `--securityLevel`: Security level (0=normal, 1=high)
### Delete User
```bash
# Delete user
cyberpanel deleteUser \
--userName johndoe
```
### List Users
```bash
# List all users
cyberpanel listUsers
```
### Change User Password
```bash
# Change user password
cyberpanel changeUserPass \
--userName johndoe \
--password newpassword
```
### Suspend/Unsuspend User
```bash
# Suspend user
cyberpanel suspendUser \
--userName johndoe \
--state 1
# Unsuspend user
cyberpanel suspendUser \
--userName johndoe \
--state 0
```
## Package Management Commands
### Create Package
```bash
# Create hosting package
cyberpanel createPackage \
--packageName MyPackage \
--diskSpace 1024 \
--bandwidth 10240 \
--emailAccounts 10 \
--dataBases 5 \
--ftpAccounts 5 \
--allowedDomains 3
```
**Parameters:**
- `--packageName`: Package name
- `--diskSpace`: Disk space in MB
- `--bandwidth`: Bandwidth in MB
- `--emailAccounts`: Number of email accounts
- `--dataBases`: Number of databases
- `--ftpAccounts`: Number of FTP accounts
- `--allowedDomains`: Number of allowed domains
### Delete Package
```bash
# Delete package
cyberpanel deletePackage \
--packageName MyPackage
```
### List Packages
```bash
# List all packages
cyberpanel listPackages
```
## System Administration Commands
### Fix File Permissions
```bash
# Fix file permissions for a domain
cyberpanel fixFilePermissions \
--domainName example.com
```
**Note**: This command was recently added and fixes file permissions for websites.
### Switch to LiteSpeed Enterprise
```bash
# Switch to LiteSpeed Enterprise
cyberpanel switchTOLSWS \
--licenseKey YOUR_LITESPEED_LICENSE_KEY
```
### Verify Connection
```bash
# Verify CyberPanel connection
cyberpanel verifyConn \
--adminUser admin \
--adminPass yourpassword
```
## File Management Commands
### File Manager Operations
```bash
# List files (via FileManager)
cyberpanel listFiles \
--domainName example.com \
--path public_html
```
## Application Installation Commands
### Install WordPress
```bash
# Install WordPress
cyberpanel installWordPress \
--domainName example.com \
--password adminpass \
--siteTitle "My WordPress Site" \
--path blog
```
**Parameters:**
- `--domainName`: Domain name
- `--password`: WordPress admin password
- `--siteTitle`: Site title
- `--path`: Installation path (optional, defaults to root)
### Install Joomla
```bash
# Install Joomla
cyberpanel installJoomla \
--domainName example.com \
--password adminpass \
--siteTitle "My Joomla Site" \
--path joomla
```
## Backup and Restore Commands
### Create Backup
```bash
# Create website backup
cyberpanel createBackup \
--domainName example.com
```
### Restore Backup
```bash
# Restore website backup
cyberpanel restoreBackup \
--domainName example.com \
--fileName /path/to/backup/file.tar.gz
```
**Parameters:**
- `--domainName`: Domain name
- `--fileName`: Complete path to backup file
### List Backups
```bash
# List available backups
cyberpanel listBackups \
--domainName example.com
```
## Security and Firewall Commands
### Firewall Management
```bash
# List firewall rules
cyberpanel listFirewallRules
# Add firewall rule
cyberpanel addFirewallRule \
--port 8080 \
--action Allow
# Delete firewall rule
cyberpanel deleteFirewallRule \
--ruleID 123
```
## Troubleshooting Commands
### System Status
```bash
# Check CyberPanel version
cyberpanel version
# Check system status
cyberpanel status
# Verify installation
cyberpanel verifyInstall
```
### Log Commands
```bash
# View CyberPanel logs
cyberpanel logs
# View error logs
cyberpanel errorLogs
# Clear logs
cyberpanel clearLogs
```
## Advanced Usage Examples
### Automated Website Deployment
```bash
#!/bin/bash
# Script to create a complete website setup
DOMAIN="example.com"
OWNER="admin"
EMAIL="admin@example.com"
PACKAGE="Default"
# Create website
cyberpanel createWebsite \
--package $PACKAGE \
--owner $OWNER \
--domainName $DOMAIN \
--email $EMAIL \
--php 8.1 \
--ssl 1
# Create database
cyberpanel createDatabase \
--databaseWebsite $DOMAIN \
--dbName wpdb \
--dbUsername wpuser \
--dbPassword $(openssl rand -base64 32)
# Create email
cyberpanel createEmail \
--userName admin \
--password $(openssl rand -base64 16) \
--databaseWebsite $DOMAIN
# Install WordPress
cyberpanel installWordPress \
--domainName $DOMAIN \
--password $(openssl rand -base64 16) \
--siteTitle "My Website"
echo "Website setup complete for $DOMAIN"
```
### Bulk User Creation
```bash
#!/bin/bash
# Script to create multiple users
USERS=("user1" "user2" "user3")
for USER in "${USERS[@]}"; do
cyberpanel createUser \
--firstName "$USER" \
--lastName "User" \
--userName "$USER" \
--email "$USER@example.com" \
--websitesLimit 3 \
--selectedACL user \
--securityLevel 0
done
```
### Website Maintenance Script
```bash
#!/bin/bash
# Script for website maintenance
# List all websites
WEBSITES=$(cyberpanel listWebsites | grep -o '"[^"]*\.com"' | tr -d '"')
for WEBSITE in $WEBSITES; do
echo "Processing $WEBSITE..."
# Fix permissions
cyberpanel fixFilePermissions --domainName $WEBSITE
# Create backup
cyberpanel createBackup --domainName $WEBSITE
echo "Completed $WEBSITE"
done
```
## Common Error Messages and Solutions
### "Please enter the domain" Error
```bash
# Make sure to include all required parameters
cyberpanel createWebsite \
--package Default \
--owner admin \
--domainName example.com \
--email admin@example.com \
--php 8.1
```
### "Administrator not found" Error
```bash
# Verify the owner exists
cyberpanel listUsers | grep admin
# Create the owner if it doesn't exist
cyberpanel createUser \
--firstName Admin \
--lastName User \
--userName admin \
--email admin@example.com \
--websitesLimit 10 \
--selectedACL admin
```
### "Package not found" Error
```bash
# List available packages
cyberpanel listPackages
# Create package if needed
cyberpanel createPackage \
--packageName Default \
--diskSpace 1024 \
--bandwidth 10240 \
--emailAccounts 10 \
--dataBases 5 \
--ftpAccounts 5 \
--allowedDomains 3
```
## Best Practices
### 1. Always Use Full Parameters
```bash
# Good - explicit parameters
cyberpanel createWebsite \
--package Default \
--owner admin \
--domainName example.com \
--email admin@example.com \
--php 8.1
# Avoid - relying on defaults
cyberpanel createWebsite --domainName example.com
```
### 2. Use Strong Passwords
```bash
# Generate secure passwords
cyberpanel createDatabase \
--databaseWebsite example.com \
--dbName mydb \
--dbUsername dbuser \
--dbPassword $(openssl rand -base64 32)
```
### 3. Backup Before Changes
```bash
# Always backup before making changes
cyberpanel createBackup --domainName example.com
cyberpanel changePHP --domainName example.com --php 8.1
```
### 4. Use Scripts for Automation
```bash
# Create reusable scripts
cat > setup-website.sh << 'EOF'
#!/bin/bash
DOMAIN=$1
if [ -z "$DOMAIN" ]; then
echo "Usage: $0 domain.com"
exit 1
fi
cyberpanel createWebsite \
--package Default \
--owner admin \
--domainName $DOMAIN \
--email admin@$DOMAIN \
--php 8.1 \
--ssl 1
EOF
chmod +x setup-website.sh
./setup-website.sh example.com
```
## Integration with Automation Tools
### Ansible Playbook Example
```yaml
---
- name: Setup CyberPanel Website
hosts: cyberpanel_servers
tasks:
- name: Create website
command: >
cyberpanel createWebsite
--package {{ package_name }}
--owner {{ owner_name }}
--domainName {{ domain_name }}
--email {{ admin_email }}
--php {{ php_version }}
register: website_result
- name: Create database
command: >
cyberpanel createDatabase
--databaseWebsite {{ domain_name }}
--dbName {{ db_name }}
--dbUsername {{ db_user }}
--dbPassword {{ db_password }}
when: website_result.rc == 0
```
### Docker Integration
```dockerfile
FROM ubuntu:20.04
# Install CyberPanel
RUN wget -O - https://cyberpanel.sh/install.sh | bash
# Copy setup script
COPY setup.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/setup.sh
# Run setup on container start
CMD ["/usr/local/bin/setup.sh"]
```
## Getting Help
### Command Help
```bash
# Get help for any command
cyberpanel [command] --help
# Example
cyberpanel createWebsite --help
```
### Logs and Debugging
```bash
# Check CyberPanel logs
tail -f /usr/local/lscp/logs/error.log
# Check system logs
journalctl -u lscpd -f
# Enable debug mode
export CYBERPANEL_DEBUG=1
cyberpanel createWebsite --domainName example.com
```
### Community Support
- **CyberPanel Forums**: https://community.cyberpanel.net
- **GitHub Issues**: https://github.com/usmannasir/cyberpanel/issues
- **Discord Server**: https://discord.gg/cyberpanel
---
**Note**: This guide covers the most commonly used CyberPanel CLI commands. For the complete list of available commands and their parameters, run `cyberpanel --help` on your server.
*Last updated: January 2025*

View File

@ -0,0 +1,533 @@
# Home Directory Management Guide for CyberPanel
## Overview
The Home Directory Management feature allows CyberPanel administrators to manage multiple home directories across different storage volumes (e.g., `/home`, `/home2`, `/home3`). This enables storage balancing, helps avoid upgrading main volume plans by utilizing cheaper additional volumes, and provides better resource distribution across your server.
## Table of Contents
1. [Features](#features)
2. [Getting Started](#getting-started)
3. [Managing Home Directories](#managing-home-directories)
4. [User Management](#user-management)
5. [Website Management Integration](#website-management-integration)
6. [Storage Balancing](#storage-balancing)
7. [User Migration](#user-migration)
8. [Troubleshooting](#troubleshooting)
9. [Best Practices](#best-practices)
10. [Advanced Configuration](#advanced-configuration)
## Features
### Core Capabilities
- **Multiple Home Directories**: Manage `/home`, `/home2`, `/home3`, etc.
- **Automatic Detection**: Auto-discover existing home directories
- **Storage Monitoring**: Real-time space usage and availability tracking
- **User Distribution**: Balance users across different storage volumes
- **Website Integration**: Manage home directories directly from website modification
- **User Migration**: Move users between home directories seamlessly
- **Storage Analytics**: Detailed usage statistics and recommendations
### Benefits
- **Cost Optimization**: Use cheaper additional volumes instead of upgrading main storage
- **Performance**: Distribute load across multiple storage devices
- **Scalability**: Easy expansion as your server grows
- **Flexibility**: Choose optimal storage for different user types
- **Monitoring**: Real-time visibility into storage usage
## Getting Started
### Prerequisites
- CyberPanel v2.5.5 or higher
- Administrator access
- Multiple storage volumes available (optional but recommended)
- Sufficient disk space for home directories
### Initial Setup
1. **Access Home Directory Management**
- Log in to CyberPanel admin panel
- Navigate to **User Management** → **Home Directory Management**
2. **Auto-Detect Existing Directories**
- Click **"Detect Directories"** button
- System will automatically find `/home`, `/home2`, `/home3`, etc.
- Review detected directories and their status
3. **Configure Default Settings**
- Set default home directory (usually `/home`)
- Configure storage limits and user limits
- Enable/disable directories as needed
### Database Migration (For Developers)
```bash
cd /usr/local/CyberCP
python manage.py makemigrations userManagment
python manage.py migrate
```
### Technical Implementation Details
#### Core Components
- **Models** (`models.py`): `HomeDirectory` and `UserHomeMapping` models
- **Management** (`homeDirectoryManager.py`): Core functionality for operations
- **Utilities** (`homeDirectoryUtils.py`): Helper functions for path resolution
- **Views** (`homeDirectoryViews.py`): Admin interface and API endpoints
- **Templates**: Management interfaces and user creation forms
#### File Structure
```
cyberpanel/userManagment/
├── models.py # Database models
├── homeDirectoryManager.py # Core management logic
├── homeDirectoryUtils.py # Utility functions
├── homeDirectoryViews.py # API endpoints
├── templates/userManagment/
│ ├── homeDirectoryManagement.html
│ ├── userMigration.html
│ └── createUser.html (updated)
└── migrations/
└── 0001_home_directories.py
```
## Managing Home Directories
### Adding New Home Directories
#### Method 1: Automatic Detection
```bash
# The system automatically detects directories matching pattern /home[0-9]*
# No manual intervention required
```
#### Method 2: Manual Creation
1. **Create Directory on Server**
```bash
sudo mkdir /home2
sudo chown root:root /home2
sudo chmod 755 /home2
```
2. **Detect in CyberPanel**
- Go to Home Directory Management
- Click **"Detect Directories"**
- New directory will appear in the list
### Configuring Home Directories
#### Basic Configuration
- **Name**: Display name for the directory
- **Path**: Full system path (e.g., `/home2`)
- **Description**: Optional description for identification
- **Status**: Active/Inactive
- **Default**: Set as default for new users
#### Advanced Settings
- **Maximum Users**: Limit number of users per directory
- **Storage Quota**: Set storage limits (if supported by filesystem)
- **Priority**: Directory selection priority for auto-assignment
### Directory Status Management
#### Active Directories
- Available for new user assignments
- Included in auto-selection algorithms
- Monitored for storage usage
#### Inactive Directories
- Not available for new users
- Existing users remain unaffected
- Useful for maintenance or decommissioning
## User Management
### Creating Users with Home Directory Selection
1. **Navigate to User Creation**
- Go to **User Management** → **Create User**
2. **Select Home Directory**
- Choose from dropdown list of available directories
- View real-time storage information
- Select "Auto-select" for automatic assignment
3. **Review Assignment**
- System shows selected directory details
- Displays available space and user count
- Confirms assignment before creation
### Auto-Assignment Logic
The system automatically selects the best home directory based on:
- **Available Space**: Prioritizes directories with more free space
- **User Count**: Balances users across directories
- **Directory Status**: Only considers active directories
- **User Limits**: Respects maximum user limits per directory
### Manual User Assignment
1. **Access User Migration**
- Go to **User Management** → **User Migration**
2. **Select User and Target Directory**
- Choose user to migrate
- Select destination home directory
- Review migration details
3. **Execute Migration**
- System moves user data
- Updates all references
- Verifies successful migration
## Website Management Integration
### Modifying Website Home Directory
1. **Access Website Modification**
- Go to **Websites** → **Modify Website**
- Select website to modify
2. **Change Home Directory**
- Select new home directory from dropdown
- View current and available options
- See real-time storage information
3. **Save Changes**
- System migrates website owner
- Updates all website references
- Maintains website functionality
### Website Owner Migration
When changing a website's home directory:
- **User Data**: Website owner is migrated to new directory
- **Website Files**: All website files are moved
- **Database References**: All database references updated
- **Permissions**: File permissions maintained
- **Services**: Email, FTP, and other services updated
## Storage Balancing
### Monitoring Storage Usage
#### Real-Time Statistics
- **Total Space**: Combined storage across all directories
- **Available Space**: Free space available
- **Usage Percentage**: Visual progress bars
- **User Distribution**: Users per directory
#### Storage Alerts
- **Low Space Warning**: When directory approaches capacity
- **Full Directory Alert**: When directory reaches maximum users
- **Performance Impact**: Storage performance recommendations
### Balancing Strategies
#### Automatic Balancing
- **New User Assignment**: Distributes users evenly
- **Space-Based Selection**: Prioritizes directories with more space
- **Load Distribution**: Spreads load across multiple volumes
#### Manual Balancing
- **User Migration**: Move users between directories
- **Directory Management**: Enable/disable directories
- **Capacity Planning**: Monitor and plan for growth
## User Migration
### Migration Process
1. **Pre-Migration Checks**
- Verify source and destination directories
- Check available space
- Validate user permissions
2. **Data Migration**
- Copy user home directory
- Update system references
- Verify data integrity
3. **Post-Migration Verification**
- Test user access
- Verify website functionality
- Update service configurations
### Migration Safety
#### Backup Recommendations
```bash
# Always backup before migration
sudo tar -czf /backup/user-backup-$(date +%Y%m%d).tar.gz /home/username
```
#### Rollback Procedures
- Keep original data until migration verified
- Maintain backup of system configurations
- Document all changes made
### Migration Commands
#### Manual Migration (Advanced Users)
```bash
# Stop user services
sudo systemctl stop user@username
# Copy user data
sudo rsync -av /home/username/ /home2/username/
# Update user home in system
sudo usermod -d /home2/username username
# Update CyberPanel database
# (Use web interface for this)
```
## Troubleshooting
### Common Issues
#### 1. Directory Not Detected
**Problem**: New home directory not appearing in list
**Solution**:
```bash
# Check directory exists and has correct permissions
ls -la /home2
sudo chown root:root /home2
sudo chmod 755 /home2
# Refresh detection in CyberPanel
# Click "Detect Directories" button
```
#### 2. Migration Fails
**Problem**: User migration fails with errors
**Solution**:
- Check available space in destination
- Verify user permissions
- Review CyberPanel logs
- Ensure destination directory is active
#### 3. Website Access Issues
**Problem**: Website not accessible after migration
**Solution**:
- Check file permissions
- Verify web server configuration
- Update virtual host settings
- Restart web server services
#### 4. Storage Not Updating
**Problem**: Storage statistics not reflecting changes
**Solution**:
- Click "Refresh Stats" button
- Check filesystem mount status
- Verify directory accessibility
- Review system logs
### Diagnostic Commands
#### Check Directory Status
```bash
# List all home directories
ls -la /home*
# Check disk usage
df -h /home*
# Check permissions
ls -la /home*/username
```
#### Verify User Assignments
```bash
# Check user home directories
getent passwd | grep /home
# Check CyberPanel database
# (Use web interface or database tools)
```
#### Monitor Storage Usage
```bash
# Real-time disk usage
watch -n 5 'df -h /home*'
# Directory sizes
du -sh /home*
# User directory sizes
du -sh /home*/username
```
### Log Files
#### CyberPanel Logs
```bash
# Main CyberPanel log
tail -f /usr/local/CyberCP/logs/cyberpanel.log
# Home directory specific logs
grep "home.*directory" /usr/local/CyberCP/logs/cyberpanel.log
```
#### System Logs
```bash
# System messages
tail -f /var/log/messages
# Authentication logs
tail -f /var/log/auth.log
```
## Best Practices
### Storage Planning
#### Directory Organization
- **Primary Directory** (`/home`): Default for most users
- **Secondary Directories** (`/home2`, `/home3`): For specific user groups
- **Naming Convention**: Use descriptive names (e.g., `/home-ssd`, `/home-hdd`)
#### Capacity Management
- **Monitor Usage**: Regular storage monitoring
- **Set Alerts**: Configure low-space warnings
- **Plan Growth**: Anticipate future storage needs
- **Regular Cleanup**: Remove unused user data
### User Management
#### Assignment Strategy
- **New Users**: Use auto-assignment for balanced distribution
- **Special Cases**: Manually assign users with specific requirements
- **Regular Review**: Periodically review user distribution
#### Migration Planning
- **Schedule Migrations**: Plan during low-usage periods
- **Test First**: Verify migration process with test users
- **Communicate Changes**: Inform users of planned migrations
### Security Considerations
#### Access Control
- **Directory Permissions**: Maintain proper file permissions
- **User Isolation**: Ensure users can only access their directories
- **System Security**: Regular security updates and monitoring
#### Backup Strategy
- **Regular Backups**: Backup user data regularly
- **Migration Backups**: Always backup before migrations
- **Configuration Backups**: Backup CyberPanel configurations
## Advanced Configuration
### Custom Directory Paths
#### Non-Standard Paths
```bash
# Create custom home directory
sudo mkdir /var/home
sudo chown root:root /var/home
sudo chmod 755 /var/home
# Add to CyberPanel manually
# (Use web interface to add custom paths)
```
#### Network Storage
- **NFS Mounts**: Use NFS for network-attached storage
- **iSCSI**: Configure iSCSI for block-level storage
- **Cloud Storage**: Integrate with cloud storage solutions
### Performance Optimization
#### Storage Performance
- **SSD vs HDD**: Use SSDs for frequently accessed data
- **RAID Configuration**: Optimize RAID for performance
- **Caching**: Implement appropriate caching strategies
#### Load Balancing
- **User Distribution**: Balance users across storage devices
- **I/O Optimization**: Optimize I/O patterns
- **Monitoring**: Monitor performance metrics
### Integration with Other Features
#### Backup Integration
- **Automated Backups**: Include home directories in backup schedules
- **Incremental Backups**: Use incremental backup strategies
- **Restore Procedures**: Test restore procedures regularly
#### Monitoring Integration
- **Storage Monitoring**: Integrate with monitoring systems
- **Alerting**: Set up automated alerts
- **Reporting**: Generate regular usage reports
## API Reference
### Home Directory Management API
#### Get Home Directories
```bash
curl -X POST https://your-server:8090/userManagement/getUserHomeDirectories/ \
-H "Content-Type: application/json" \
-d '{}'
```
#### Update Home Directory
```bash
curl -X POST https://your-server:8090/userManagement/updateHomeDirectory/ \
-H "Content-Type: application/json" \
-d '{
"id": 1,
"description": "Updated description",
"max_users": 100,
"is_active": true
}'
```
#### Migrate User
```bash
curl -X POST https://your-server:8090/userManagement/migrateUser/ \
-H "Content-Type: application/json" \
-d '{
"user_id": 123,
"new_home_directory_id": 2
}'
```
### Complete API Endpoints
#### Home Directory Management
- `POST /userManagement/detectHomeDirectories/` - Detect new home directories
- `POST /userManagement/updateHomeDirectory/` - Update home directory settings
- `POST /userManagement/deleteHomeDirectory/` - Delete home directory
- `POST /userManagement/getHomeDirectoryStats/` - Get storage statistics
#### User Operations
- `POST /userManagement/getUserHomeDirectories/` - Get available home directories
- `POST /userManagement/migrateUser/` - Migrate user to different home directory
#### Website Integration
- `POST /websites/saveWebsiteChanges/` - Save website changes including home directory
- `POST /websites/getWebsiteDetails/` - Get website details including current home directory
## Support and Resources
### Documentation
- **CyberPanel Documentation**: https://cyberpanel.net/docs/
- **User Management Guide**: This guide
- **API Documentation**: Available in CyberPanel interface
### Community Support
- **CyberPanel Forums**: https://community.cyberpanel.net
- **GitHub Issues**: https://github.com/usmannasir/cyberpanel/issues
- **Discord Server**: https://discord.gg/cyberpanel
### Professional Support
- **CyberPanel Support**: Available through official channels
- **Custom Implementation**: Contact for enterprise solutions
---
**Note**: This guide covers the complete Home Directory Management feature in CyberPanel. For the latest updates and additional features, refer to the official CyberPanel documentation and community resources.
*Last updated: January 2025*

View File

@ -16,6 +16,9 @@ Welcome to the CyberPanel documentation hub! This folder contains all guides, tu
### 🐧 Operating System Support
#### **Windows Family**
- **[Windows Installation Guide](WINDOWS_INSTALLATION_GUIDE.md)** - Complete installation and configuration guide for CyberPanel on Windows 7/8.1/10/11
#### **Debian Family**
- **[Debian 13 Installation Guide](DEBIAN_13_INSTALLATION_GUIDE.md)** - Complete installation and configuration guide for CyberPanel on Debian 13 (Bookworm)
- **[Debian 12 Troubleshooting Guide](DEBIAN_12_TROUBLESHOOTING.md)** - Troubleshooting guide for Debian 12 (Bookworm)
@ -40,11 +43,22 @@ Welcome to the CyberPanel documentation hub! This folder contains all guides, tu
### 🎨 Customization & Design
- **[Custom CSS Guide](CUSTOM_CSS_GUIDE.md)** - Complete guide for creating custom CSS that works with CyberPanel 2.5.5-dev design system
### 🔐 Security & Authentication
- **[2FA Authentication Guide](2FA_AUTHENTICATION_GUIDE.md)** - Complete guide for Two-Factor Authentication and WebAuthn/Passkey setup
### 🔧 Troubleshooting & Support
- **[Troubleshooting Guide](TROUBLESHOOTING.md)** - Comprehensive troubleshooting and diagnostic commands
- **[Installation Verification Guide](INSTALLATION_VERIFICATION.md)** - Verify installation and upgrade commands work across all supported OS
### 💻 Command Line Interface
- **[CLI Command Reference](CLI_COMMAND_REFERENCE.md)** - Complete reference for all CyberPanel CLI commands
### 🏠 Storage & User Management
- **[Home Directory Management Guide](HOME_DIRECTORY_MANAGEMENT_GUIDE.md)** - Complete guide for managing multiple home directories and storage balancing
### 📖 General Documentation
- **[README](../README.md)** - Main CyberPanel documentation with installation instructions and feature overview
- **[Utility Scripts](../utils/README.md)** - Installation, upgrade, and maintenance scripts for Windows and Linux
- **[Contributing Guide](CONTRIBUTING.md)** - Guidelines for contributing to the CyberPanel project
## 🚀 Quick Start
@ -54,7 +68,8 @@ Welcome to the CyberPanel documentation hub! This folder contains all guides, tu
3. **Need Docker help?** Check the [Docker Command Execution Guide](Docker_Command_Execution_Guide.md)
4. **Setting up email marketing?** Follow the [Mautic Installation Guide](MAUTIC_INSTALLATION_GUIDE.md)
5. **Want to customize the interface?** Check the [Custom CSS Guide](CUSTOM_CSS_GUIDE.md)
6. **Want to contribute?** Read the [Contributing Guide](CONTRIBUTING.md)
6. **Managing multiple storage volumes?** Follow the [Home Directory Management Guide](HOME_DIRECTORY_MANAGEMENT_GUIDE.md)
7. **Want to contribute?** Read the [Contributing Guide](CONTRIBUTING.md)
## 🔍 Finding What You Need
@ -62,6 +77,7 @@ Welcome to the CyberPanel documentation hub! This folder contains all guides, tu
- **General Troubleshooting**: [Troubleshooting Guide](TROUBLESHOOTING.md)
### **OS-Specific Troubleshooting**
- **🪟 Windows**: [Windows Installation Guide](WINDOWS_INSTALLATION_GUIDE.md) - Installation & troubleshooting
- **🐧 Debian 13**: [Debian 13 Installation Guide](DEBIAN_13_INSTALLATION_GUIDE.md) - Installation & troubleshooting
- **🐧 Debian 12**: [Debian 12 Troubleshooting Guide](DEBIAN_12_TROUBLESHOOTING.md) - Troubleshooting
- **🐧 Debian 11**: [Debian 11 Troubleshooting Guide](DEBIAN_11_TROUBLESHOOTING.md) - Troubleshooting
@ -79,8 +95,11 @@ Welcome to the CyberPanel documentation hub! This folder contains all guides, tu
### **Feature-Specific Guides**
- **Docker Features**: [Docker Command Execution Guide](Docker_Command_Execution_Guide.md)
- **Security Features**: [AI Scanner Documentation](AIScannerDocs.md)
- **Authentication**: [2FA Authentication Guide](2FA_AUTHENTICATION_GUIDE.md)
- **Email Marketing**: [Mautic Installation Guide](MAUTIC_INSTALLATION_GUIDE.md)
- **Storage Management**: [Home Directory Management Guide](HOME_DIRECTORY_MANAGEMENT_GUIDE.md)
- **Customization & Design**: [Custom CSS Guide](CUSTOM_CSS_GUIDE.md)
- **Command Line Interface**: [CLI Command Reference](CLI_COMMAND_REFERENCE.md)
- **Development**: [Contributing Guide](CONTRIBUTING.md)
## 📝 Guide Categories
@ -89,6 +108,10 @@ Welcome to the CyberPanel documentation hub! This folder contains all guides, tu
- Docker container management
- Command execution
- Security scanning
- Two-factor authentication (2FA)
- WebAuthn/Passkey authentication
- Home directory management
- CLI command reference
### 🔧 **Integrations**
- Mautic email marketing

View File

@ -0,0 +1,415 @@
# CyberPanel Installation Verification Guide
## 🎯 Overview
This guide provides verification steps to ensure CyberPanel installation and upgrade commands work correctly across all supported operating systems.
## ✅ Installation Command Verification
### **Primary Installation Command**
```bash
sh <(curl https://cyberpanel.net/install.sh || wget -O - https://cyberpanel.net/install.sh)
```
### **Verification Steps**
#### 1. **Test URL Accessibility**
```bash
# Test if install script is accessible
curl -I https://cyberpanel.net/install.sh
# Expected response: HTTP/1.1 200 OK
# Content-Type: text/plain
```
#### 2. **Test Download**
```bash
# Download and check script content
curl -s https://cyberpanel.net/install.sh | head -20
# Should show script header and OS detection logic
```
#### 3. **Test with wget fallback**
```bash
# Test wget fallback
wget -qO- https://cyberpanel.net/install.sh | head -20
# Should show same content as curl
```
## ✅ Upgrade Command Verification
### **Primary Upgrade Command**
```bash
sh <(curl https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh)
```
### **Verification Steps**
#### 1. **Test URL Accessibility**
```bash
# Test if upgrade script is accessible
curl -I https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh
# Expected response: HTTP/1.1 200 OK
# Content-Type: text/plain
```
#### 2. **Test Download**
```bash
# Download and check script content
curl -s https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh | head -20
# Should show script header and upgrade logic
```
## 🐧 Operating System Support Verification
### **Ubuntu Family**
- **Ubuntu 24.04.3**: ✅ Supported
- **Ubuntu 22.04**: ✅ Supported
- **Ubuntu 20.04**: ✅ Supported
### **Debian Family**
- **Debian 13**: ✅ Supported
- **Debian 12**: ✅ Supported
- **Debian 11**: ✅ Supported
### **RHEL Family**
- **AlmaLinux 10**: ✅ Supported
- **AlmaLinux 9**: ✅ Supported
- **AlmaLinux 8**: ✅ Supported
- **RockyLinux 9**: ✅ Supported
- **RockyLinux 8**: ✅ Supported
- **RHEL 9**: ✅ Supported
- **RHEL 8**: ✅ Supported
### **Other Distributions**
- **CloudLinux 8**: ✅ Supported
- **CentOS 9**: ✅ Supported
- **CentOS 7**: ✅ Supported (until June 2024)
- **CentOS Stream 9**: ✅ Supported
## 🔧 Installation Process Verification
### **What the Installation Script Does**
1. **System Detection**
- Detects operating system and version
- Checks architecture (x86_64 required)
- Verifies system requirements
2. **Dependency Installation**
- Installs Python 3.8+
- Installs Git
- Installs system packages (curl, wget, etc.)
3. **Web Server Setup**
- Downloads and installs OpenLiteSpeed
- Configures web server settings
- Sets up virtual hosts
4. **Database Setup**
- Installs and configures MariaDB
- Creates CyberPanel database
- Sets up database users
5. **CyberPanel Installation**
- Downloads CyberPanel source code
- Installs Python dependencies
- Configures Django settings
- Runs database migrations
6. **Service Configuration**
- Creates systemd services
- Starts all required services
- Configures firewall rules
7. **Final Setup**
- Creates admin user
- Sets up file permissions
- Provides access information
## 🔄 Upgrade Process Verification
### **What the Upgrade Script Does**
1. **Backup Creation**
- Creates backup of current installation
- Backs up database and configuration files
- Stores backup in safe location
2. **Source Update**
- Downloads latest CyberPanel source code
- Updates all files to latest version
- Preserves custom configurations
3. **Dependency Update**
- Updates Python packages
- Updates system dependencies
- Handles version conflicts
4. **Database Migration**
- Runs Django migrations
- Updates database schema
- Preserves existing data
5. **Service Restart**
- Restarts all CyberPanel services
- Verifies service status
- Reports any issues
## 🧪 Testing Procedures
### **Pre-Installation Testing**
#### 1. **System Requirements Check**
```bash
# Check OS version
cat /etc/os-release
# Check architecture
uname -m
# Check available memory
free -h
# Check available disk space
df -h
# Check network connectivity
ping -c 4 google.com
```
#### 2. **Dependency Check**
```bash
# Check if Python is available
python3 --version
# Check if Git is available
git --version
# Check if curl/wget are available
curl --version
wget --version
```
### **Installation Testing**
#### 1. **Dry Run Test**
```bash
# Download and examine script without executing
curl -s https://cyberpanel.net/install.sh > install_test.sh
chmod +x install_test.sh
# Review script content
head -50 install_test.sh
```
#### 2. **Full Installation Test**
```bash
# Run installation in test environment
sh <(curl https://cyberpanel.net/install.sh || wget -O - https://cyberpanel.net/install.sh)
# Monitor installation process
# Check for any errors or warnings
```
### **Post-Installation Verification**
#### 1. **Service Status Check**
```bash
# Check CyberPanel service
systemctl status lscpd
# Check web server
systemctl status lsws
# Check database
systemctl status mariadb
```
#### 2. **Web Interface Test**
```bash
# Test web interface accessibility
curl -I http://localhost:8090
# Expected: HTTP/1.1 200 OK
```
#### 3. **Database Connection Test**
```bash
# Test database connection
mysql -u root -p -e "SHOW DATABASES;"
# Should show CyberPanel database
```
## 🐛 Common Issues and Solutions
### **Installation Issues**
#### 1. **"Command not found" Errors**
**Problem**: Required commands not available
**Solution**:
```bash
# Install missing packages
# Ubuntu/Debian
sudo apt update && sudo apt install curl wget git python3
# RHEL/CentOS/AlmaLinux/RockyLinux
sudo yum install curl wget git python3
```
#### 2. **Permission Denied Errors**
**Problem**: Insufficient privileges
**Solution**:
```bash
# Run with sudo
sudo sh <(curl https://cyberpanel.net/install.sh || wget -O - https://cyberpanel.net/install.sh)
```
#### 3. **Network Connectivity Issues**
**Problem**: Cannot download installation script
**Solution**:
```bash
# Check internet connection
ping -c 4 google.com
# Check DNS resolution
nslookup cyberpanel.net
# Try alternative download method
wget https://cyberpanel.net/install.sh
chmod +x install.sh
sudo ./install.sh
```
#### 4. **Port Already in Use**
**Problem**: Port 8090 already occupied
**Solution**:
```bash
# Check what's using port 8090
sudo netstat -tlnp | grep :8090
# Kill process if necessary
sudo kill -9 <PID>
# Or change CyberPanel port in configuration
```
### **Upgrade Issues**
#### 1. **Backup Creation Failed**
**Problem**: Cannot create backup
**Solution**:
```bash
# Check disk space
df -h
# Free up space if necessary
sudo apt autoremove
sudo apt autoclean
# Or specify different backup location
```
#### 2. **Database Migration Failed**
**Problem**: Database migration errors
**Solution**:
```bash
# Check database status
systemctl status mariadb
# Restart database service
sudo systemctl restart mariadb
# Run migration manually
cd /usr/local/CyberCP
python3 manage.py migrate
```
#### 3. **Service Restart Failed**
**Problem**: Services won't restart
**Solution**:
```bash
# Check service logs
journalctl -u lscpd -f
# Restart services manually
sudo systemctl restart lscpd
sudo systemctl restart lsws
```
## 📊 Verification Checklist
### **Installation Verification**
- [ ] Installation script downloads successfully
- [ ] All dependencies installed correctly
- [ ] Web server starts and responds
- [ ] Database server starts and responds
- [ ] CyberPanel web interface accessible
- [ ] Admin user can log in
- [ ] All services running properly
- [ ] No error messages in logs
### **Upgrade Verification**
- [ ] Upgrade script downloads successfully
- [ ] Backup created successfully
- [ ] Source code updated correctly
- [ ] Dependencies updated properly
- [ ] Database migrations completed
- [ ] Services restarted successfully
- [ ] Web interface still accessible
- [ ] Data integrity maintained
## 🔍 Monitoring and Logging
### **Installation Logs**
```bash
# Check installation logs
tail -f /root/cyberpanel-install.log
# Check system logs
journalctl -f
```
### **Service Logs**
```bash
# Check CyberPanel logs
tail -f /usr/local/lscp/logs/error.log
# Check web server logs
tail -f /usr/local/lsws/logs/error.log
# Check database logs
tail -f /var/log/mysql/error.log
```
## 🆘 Getting Help
### **If Installation Fails**
1. **Check the logs** for specific error messages
2. **Verify system requirements** are met
3. **Try alternative installation methods** (wget, manual download)
4. **Use troubleshooting commands** from the README
5. **Contact support** with detailed error information
### **If Upgrade Fails**
1. **Restore from backup** if available
2. **Check service status** and restart if needed
3. **Run manual upgrade** steps
4. **Use troubleshooting commands** from the README
5. **Contact support** with upgrade logs
### **Support Resources**
- **CyberPanel Forums**: https://community.cyberpanel.net
- **GitHub Issues**: https://github.com/usmannasir/cyberpanel/issues
- **Discord Server**: https://discord.gg/cyberpanel
---
**Note**: This verification guide ensures CyberPanel installation and upgrade processes work correctly across all supported operating systems. Always test in a non-production environment first.
*Last updated: January 2025*

View File

@ -0,0 +1,329 @@
# CyberPanel Windows Development Guide
## 🎯 Overview
**⚠️ IMPORTANT**: CyberPanel is designed specifically for Linux systems and does not officially support Windows. This guide is for **development and testing purposes only**.
This guide provides instructions for running CyberPanel in a Windows development environment using Python and Django. This setup is intended for:
- **Developers** working on CyberPanel features
- **Testing** CyberPanel functionality
- **Learning** CyberPanel architecture
- **Development** of CyberPanel extensions
**For production use, always use a supported Linux distribution.**
## 📋 Prerequisites
### System Requirements
- **OS**: Windows 7/8.1/10/11 (64-bit recommended)
- **RAM**: Minimum 4GB (8GB+ recommended)
- **Storage**: Minimum 20GB free space
- **CPU**: 2+ cores recommended
- **Network**: Internet connection required
### Required Software
- **Python 3.8+**: Download from [python.org](https://python.org)
- **Git**: Download from [git-scm.com](https://git-scm.com)
- **Administrator Access**: Required for installation
### ⚠️ Limitations
- **No Web Server**: OpenLiteSpeed/LiteSpeed not available on Windows
- **No System Services**: MariaDB, PowerDNS, etc. not included
- **Limited Functionality**: Many CyberPanel features require Linux
- **Development Only**: Not suitable for production use
## 🚀 Installation Methods
### Method 1: Automated Installation (Recommended)
#### Step 1: Download Installation Script
1. Navigate to the `utils/windows/` folder
2. Download `cyberpanel_install.bat`
3. Right-click and select "Run as administrator"
#### Step 2: Follow Installation Prompts
The script will automatically:
- Check system requirements
- Create Python virtual environment
- Download CyberPanel source code
- Install all dependencies
- Set up the web interface
#### Step 3: Access CyberPanel
1. Open your web browser
2. Navigate to: `http://localhost:8090`
3. Use default credentials:
- **Username**: `admin`
- **Password**: `123456`
### Method 2: Manual Installation
#### Step 1: Install Python
1. Download Python 3.8+ from [python.org](https://python.org)
2. **Important**: Check "Add Python to PATH" during installation
3. Verify installation:
```cmd
python --version
pip --version
```
#### Step 2: Install Git
1. Download Git from [git-scm.com](https://git-scm.com)
2. Install with default settings
3. Verify installation:
```cmd
git --version
```
#### Step 3: Create CyberPanel Directory
```cmd
mkdir C:\usr\local\CyberCP
cd C:\usr\local\CyberCP
```
#### Step 4: Set Up Python Environment
```cmd
python -m venv . --system-site-packages
Scripts\activate.bat
```
#### Step 5: Download CyberPanel
```cmd
git clone https://github.com/usmannasir/cyberpanel.git
cd cyberpanel
```
#### Step 6: Install Dependencies
```cmd
pip install --upgrade pip setuptools wheel
pip install -r requirments.txt
```
#### Step 7: Set Up Django
```cmd
python manage.py collectstatic --noinput
python manage.py migrate
```
#### Step 8: Create Admin User
```cmd
python manage.py createsuperuser
```
#### Step 9: Start CyberPanel
```cmd
python manage.py runserver 0.0.0.0:8090
```
## 🔧 Post-Installation Configuration
### Change Default Password
1. Access CyberPanel at `http://localhost:8090`
2. Log in with default credentials
3. Go to **User Management** → **Modify User**
4. Change the admin password immediately
### Configure Windows Firewall
1. Open Windows Defender Firewall
2. Add inbound rule for port 8090
3. Allow Python through firewall
### Set Up Windows Service (Optional)
1. Run `install_service.bat` as administrator
2. CyberPanel will start automatically on boot
3. Manage service through Windows Services
## 🔄 Upgrading CyberPanel
### Using Upgrade Script
1. Download `cyberpanel_upgrade.bat`
2. Right-click and select "Run as administrator"
3. Script will automatically backup and upgrade
### Manual Upgrade
```cmd
cd C:\usr\local\CyberCP\cyberpanel
Scripts\activate.bat
git pull origin stable
pip install --upgrade -r requirments.txt
python manage.py migrate
python manage.py collectstatic --noinput
```
## 🛠️ Utility Scripts
### Available Scripts
- **`cyberpanel_install.bat`**: Complete installation
- **`cyberpanel_upgrade.bat`**: Upgrade existing installation
- **`install_webauthn.bat`**: Enable WebAuthn/Passkey authentication
### Script Usage
1. **Right-click** on any script
2. Select **"Run as administrator"**
3. Follow on-screen prompts
4. Check output for any errors
## 🐛 Troubleshooting
### Common Issues
#### "Python not found" Error
**Problem**: Python is not installed or not in PATH
**Solution**:
1. Install Python from [python.org](https://python.org)
2. Check "Add Python to PATH" during installation
3. Restart Command Prompt
4. Verify with `python --version`
#### "Access Denied" Error
**Problem**: Insufficient privileges
**Solution**:
1. Right-click script and select "Run as administrator"
2. Or open Command Prompt as administrator
3. Navigate to script location and run
#### "Failed to install requirements" Error
**Problem**: Network or dependency issues
**Solution**:
1. Check internet connection
2. Try running script again
3. Install requirements manually:
```cmd
pip install --upgrade pip
pip install -r requirments.txt
```
#### "Port 8090 already in use" Error
**Problem**: Another service is using port 8090
**Solution**:
1. Stop other services using port 8090
2. Or change CyberPanel port in settings
3. Check with: `netstat -an | findstr :8090`
#### "Django not found" Error
**Problem**: Virtual environment not activated
**Solution**:
1. Navigate to CyberPanel directory
2. Activate virtual environment:
```cmd
Scripts\activate.bat
```
3. Verify with `python -c "import django"`
### Advanced Troubleshooting
#### Check Installation Logs
```cmd
cd C:\usr\local\CyberCP\cyberpanel
python manage.py check
```
#### Verify Dependencies
```cmd
pip list
pip check
```
#### Test Database Connection
```cmd
python manage.py dbshell
```
#### Reset Database (Last Resort)
```cmd
python manage.py flush
python manage.py migrate
python manage.py createsuperuser
```
## 🔒 Security Considerations
### Initial Security Setup
1. **Change Default Password**: Immediately after installation
2. **Enable 2FA**: Use the 2FA guide for additional security
3. **Configure Firewall**: Restrict access to necessary ports only
4. **Regular Updates**: Keep CyberPanel and dependencies updated
### Windows-Specific Security
1. **User Account Control**: Keep UAC enabled
2. **Windows Defender**: Ensure real-time protection is on
3. **Regular Backups**: Backup CyberPanel data regularly
4. **Service Account**: Consider using dedicated service account
## 📊 Performance Optimization
### System Optimization
1. **Disable Unnecessary Services**: Free up system resources
2. **SSD Storage**: Use SSD for better performance
3. **Memory**: Ensure adequate RAM (8GB+ recommended)
4. **CPU**: Multi-core processor recommended
### CyberPanel Optimization
1. **Static Files**: Ensure static files are properly collected
2. **Database**: Regular database maintenance
3. **Logs**: Regular log cleanup
4. **Caching**: Enable appropriate caching
## 🔄 Maintenance
### Regular Maintenance Tasks
1. **Updates**: Regular CyberPanel updates
2. **Backups**: Regular data backups
3. **Logs**: Monitor and clean logs
4. **Security**: Regular security checks
### Backup Procedures
1. **Database Backup**:
```cmd
python manage.py dumpdata > backup.json
```
2. **File Backup**:
```cmd
xcopy C:\usr\local\CyberCP backup_folder /E /I /H
```
3. **Restore from Backup**:
```cmd
python manage.py loaddata backup.json
```
## 📚 Additional Resources
### Documentation
- **Main Guide**: [2FA Authentication Guide](2FA_AUTHENTICATION_GUIDE.md)
- **Troubleshooting**: [Troubleshooting Guide](TROUBLESHOOTING.md)
- **Utility Scripts**: [Utility Scripts](../utils/README.md)
### Support
- **CyberPanel Forums**: https://community.cyberpanel.net
- **GitHub Issues**: https://github.com/usmannasir/cyberpanel/issues
- **Discord Server**: https://discord.gg/cyberpanel
## ✅ Verification Checklist
After installation, verify these components:
- [ ] CyberPanel accessible at `http://localhost:8090`
- [ ] Admin password changed from default
- [ ] Windows Firewall configured
- [ ] Python virtual environment working
- [ ] All dependencies installed
- [ ] Database migrations applied
- [ ] Static files collected
- [ ] Logs are clean
- [ ] Service starts automatically (if configured)
## 🆘 Getting Help
If you encounter issues:
1. **Check the logs** for error messages
2. **Run the troubleshooting commands** above
3. **Search the documentation** for solutions
4. **Ask in the community forum** for help
5. **Create a GitHub issue** with detailed information
---
**Note**: This guide is specifically for Windows installations. For Linux installations, refer to the main CyberPanel documentation.
*Last updated: January 2025*

View File

@ -6,14 +6,9 @@ if echo $OUTPUT | grep -q "CentOS Linux 7" ; then
yum install curl wget -y 1> /dev/null
yum update curl wget ca-certificates -y 1> /dev/null
SERVER_OS="CentOS"
elif echo $OUTPUT | grep -q "CentOS Linux 8" ; then
echo -e "\nDetecting Centos 8...\n"
SERVER_OS="CentOS8"
yum install curl wget -y 1> /dev/null
yum update curl wget ca-certificates -y 1> /dev/null
elif echo $OUTPUT | grep -q "CentOS Stream 9" ; then
echo -e "\nDetecting Centos Stream 9...\n"
SERVER_OS="CentOS8"
SERVER_OS="CentOSStream9"
yum install curl wget -y 1> /dev/null
yum update curl wget ca-certificates -y 1> /dev/null
elif echo $OUTPUT | grep -q "AlmaLinux 8" ; then
@ -31,19 +26,11 @@ elif echo $OUTPUT | grep -q "AlmaLinux 10" ; then
SERVER_OS="CentOS8"
yum install curl wget -y 1> /dev/null
yum update curl wget ca-certificates -y 1> /dev/null
elif echo $OUTPUT | grep -q "CloudLinux 7" ; then
echo "Checking and installing curl and wget"
yum install curl wget -y 1> /dev/null
yum update curl wget ca-certificates -y 1> /dev/null
SERVER_OS="CloudLinux"
elif echo $OUTPUT | grep -q "CloudLinux 8" ; then
echo "Checking and installing curl and wget"
yum install curl wget -y 1> /dev/null
yum update curl wget ca-certificates -y 1> /dev/null
SERVER_OS="CloudLinux"
elif echo $OUTPUT | grep -q "Ubuntu 18.04" ; then
apt install -y -qq wget curl
SERVER_OS="Ubuntu"
elif echo $OUTPUT | grep -q "Ubuntu 20.04" ; then
apt install -y -qq wget curl
SERVER_OS="Ubuntu"
@ -101,13 +88,13 @@ else
echo -e "\nUnable to detect your OS...\n"
echo -e "\nCyberPanel is supported on:\n"
echo -e "Ubuntu: 18.04, 20.04, 22.04, 24.04.3\n"
echo -e "Ubuntu: 20.04, 22.04, 24.04.3\n"
echo -e "Debian: 11, 12, 13\n"
echo -e "AlmaLinux: 8, 9, 10\n"
echo -e "RockyLinux: 8, 9\n"
echo -e "RHEL: 8, 9\n"
echo -e "CentOS: 7, 8, 9, Stream 9\n"
echo -e "CloudLinux: 7.x, 8\n"
echo -e "CentOS: 7, 9, Stream 9\n"
echo -e "CloudLinux: 8\n"
echo -e "openEuler: 20.03, 22.03\n"
exit 1
fi

View File

@ -31,4 +31,6 @@ MaxDiskUsage 99
CustomerProof yes
TLS 1
PassivePortRange 40110 40210
# Quota enforcement
Quota yes

View File

@ -7,5 +7,8 @@ MYSQLGetDir SELECT Dir FROM users WHERE User='\L'
MYSQLGetGID SELECT Gid FROM users WHERE User='\L'
MYSQLGetPW SELECT Password FROM users WHERE User='\L'
MYSQLGetUID SELECT Uid FROM users WHERE User='\L'
# Quota enforcement queries
MYSQLGetQTAFS SELECT QuotaSize FROM users WHERE User='\L'
MYSQLGetQTAUS SELECT 0 FROM users WHERE User='\L'
MYSQLPassword 1qaz@9xvps
MYSQLUser cyberpanel

View File

@ -31,4 +31,6 @@ MaxDiskUsage 99
CustomerProof yes
TLS 1
PassivePortRange 40110 40210
# Quota enforcement
Quota yes

View File

@ -7,5 +7,8 @@ MYSQLGetDir SELECT Dir FROM users WHERE User='\L'
MYSQLGetGID SELECT Gid FROM users WHERE User='\L'
MYSQLGetPW SELECT Password FROM users WHERE User='\L'
MYSQLGetUID SELECT Uid FROM users WHERE User='\L'
# Quota enforcement queries
MYSQLGetQTAFS SELECT QuotaSize FROM users WHERE User='\L'
MYSQLGetQTAUS SELECT 0 FROM users WHERE User='\L'
MYSQLPassword 1qaz@9xvps
MYSQLUser cyberpanel

View File

@ -0,0 +1,111 @@
# Generated migration for WebAuthn models
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('loginSystem', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='WebAuthnCredential',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('credential_id', models.CharField(help_text='Base64 encoded credential ID', max_length=255, unique=True)),
('public_key', models.TextField(help_text='Base64 encoded public key')),
('counter', models.BigIntegerField(default=0, help_text='Signature counter for replay protection')),
('name', models.CharField(help_text='User-friendly name for the passkey', max_length=100)),
('created_at', models.DateTimeField(auto_now_add=True)),
('last_used', models.DateTimeField(blank=True, null=True)),
('is_active', models.BooleanField(default=True, help_text='Whether this credential is active')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webauthn_credentials', to='loginSystem.administrator')),
],
options={
'verbose_name': 'WebAuthn Credential',
'verbose_name_plural': 'WebAuthn Credentials',
'db_table': 'webauthn_credentials',
},
),
migrations.CreateModel(
name='WebAuthnChallenge',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('challenge', models.CharField(help_text='Base64 encoded challenge', max_length=255)),
('challenge_type', models.CharField(choices=[('registration', 'Registration'), ('authentication', 'Authentication')], max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('expires_at', models.DateTimeField()),
('used', models.BooleanField(default=False)),
('metadata', models.TextField(default='{}', help_text='Additional challenge metadata as JSON')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webauthn_challenges', to='loginSystem.administrator')),
],
options={
'verbose_name': 'WebAuthn Challenge',
'verbose_name_plural': 'WebAuthn Challenges',
'db_table': 'webauthn_challenges',
},
),
migrations.CreateModel(
name='WebAuthnSession',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('session_id', models.CharField(help_text='Unique session identifier', max_length=255, unique=True)),
('session_type', models.CharField(choices=[('registration', 'Registration'), ('authentication', 'Authentication')], max_length=20)),
('data', models.TextField(help_text='Session data as JSON')),
('created_at', models.DateTimeField(auto_now_add=True)),
('expires_at', models.DateTimeField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webauthn_sessions', to='loginSystem.administrator')),
],
options={
'verbose_name': 'WebAuthn Session',
'verbose_name_plural': 'WebAuthn Sessions',
'db_table': 'webauthn_sessions',
},
),
migrations.CreateModel(
name='WebAuthnSettings',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('enabled', models.BooleanField(default=False, help_text='Whether WebAuthn is enabled for this user')),
('require_passkey', models.BooleanField(default=False, help_text='Require passkey for login (passwordless)')),
('allow_multiple_credentials', models.BooleanField(default=True, help_text='Allow multiple passkeys per user')),
('max_credentials', models.IntegerField(default=10, help_text='Maximum number of passkeys allowed')),
('timeout_seconds', models.IntegerField(default=60, help_text='WebAuthn operation timeout in seconds')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='webauthn_settings', to='loginSystem.administrator')),
],
options={
'verbose_name': 'WebAuthn Settings',
'verbose_name_plural': 'WebAuthn Settings',
'db_table': 'webauthn_settings',
},
),
migrations.AddIndex(
model_name='webauthncredential',
index=models.Index(fields=['user', 'is_active'], name='webauthn_cre_user_id_123456_idx'),
),
migrations.AddIndex(
model_name='webauthncredential',
index=models.Index(fields=['credential_id'], name='webauthn_cre_credent_123456_idx'),
),
migrations.AddIndex(
model_name='webauthnchallenge',
index=models.Index(fields=['user', 'challenge_type', 'used'], name='webauthn_cha_user_id_123456_idx'),
),
migrations.AddIndex(
model_name='webauthnchallenge',
index=models.Index(fields=['expires_at'], name='webauthn_cha_expires_123456_idx'),
),
migrations.AddIndex(
model_name='webauthnsession',
index=models.Index(fields=['session_id'], name='webauthn_ses_session_123456_idx'),
),
migrations.AddIndex(
model_name='webauthnsession',
index=models.Index(fields=['expires_at'], name='webauthn_ses_expires_123456_idx'),
),
]

View File

@ -0,0 +1,396 @@
/**
* WebAuthn JavaScript integration for CyberPanel
* Provides passkey registration and authentication functionality
*/
class CyberPanelWebAuthn {
constructor() {
this.isSupported = this.checkSupport();
this.baseUrl = window.location.origin;
this.apiEndpoints = {
registrationStart: '/webauthn/registration/start/',
registrationComplete: '/webauthn/registration/complete/',
authenticationStart: '/webauthn/authentication/start/',
authenticationComplete: '/webauthn/authentication/complete/',
credentialsList: '/webauthn/credentials/',
credentialDelete: '/webauthn/credential/delete/',
credentialUpdate: '/webauthn/credential/update/',
settingsUpdate: '/webauthn/settings/update/',
};
this.init();
}
init() {
if (!this.isSupported) {
console.warn('WebAuthn is not supported in this browser');
return;
}
// Add CSRF token to all requests
this.csrfToken = this.getCSRFToken();
// Initialize UI elements
this.initializeUI();
}
checkSupport() {
return !!(navigator.credentials &&
navigator.credentials.create &&
navigator.credentials.get &&
window.PublicKeyCredential);
}
getCSRFToken() {
const cookieValue = document.cookie
.split('; ')
.find(row => row.startsWith('csrftoken='))
?.split('=')[1];
return cookieValue || '';
}
initializeUI() {
// Add WebAuthn buttons to login form
this.addLoginButtons();
// Add WebAuthn management to user settings
this.addUserManagementUI();
}
addLoginButtons() {
const loginForm = document.querySelector('#loginForm');
if (!loginForm) return;
// Add WebAuthn login button
const webauthnButton = document.createElement('button');
webauthnButton.type = 'button';
webauthnButton.className = 'btn btn-primary btn-block';
webauthnButton.innerHTML = '<i class="fas fa-fingerprint"></i> Login with Passkey';
webauthnButton.onclick = () => this.startPasswordlessLogin();
// Insert after password field
const passwordField = loginForm.querySelector('input[type="password"]');
if (passwordField) {
passwordField.parentNode.insertBefore(webauthnButton, passwordField.parentNode.nextSibling);
}
}
addUserManagementUI() {
// This will be called when user management page loads
// Implementation depends on the specific UI structure
}
async startPasswordlessLogin() {
try {
const username = document.querySelector('input[name="username"]').value;
if (!username) {
this.showError('Please enter your username first');
return;
}
this.showLoading('Starting passkey authentication...');
// Get authentication challenge
const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationStart, {
username: username
});
if (!challengeResponse.success) {
throw new Error(challengeResponse.error || 'Failed to start authentication');
}
// Convert challenge to proper format
const challenge = this.convertChallenge(challengeResponse.challenge);
// Get credential
const credential = await navigator.credentials.get({
publicKey: challenge
});
// Complete authentication
const authResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationComplete, {
challenge_id: challengeResponse.challenge_id,
credential: {
id: this.arrayBufferToBase64(credential.rawId),
type: credential.type
},
client_data_json: this.arrayBufferToBase64(credential.response.clientDataJSON),
authenticator_data: this.arrayBufferToBase64(credential.response.authenticatorData),
signature: this.arrayBufferToBase64(credential.response.signature),
user_handle: credential.response.userHandle ?
this.arrayBufferToBase64(credential.response.userHandle) : null
});
if (authResponse.success) {
this.showSuccess('Authentication successful! Redirecting...');
setTimeout(() => {
window.location.href = '/';
}, 1000);
} else {
throw new Error(authResponse.error || 'Authentication failed');
}
} catch (error) {
console.error('WebAuthn authentication error:', error);
this.showError(error.message || 'Authentication failed');
} finally {
this.hideLoading();
}
}
async registerPasskey(username, credentialName = '') {
try {
this.showLoading('Starting passkey registration...');
// Get registration challenge
const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.registrationStart, {
username: username,
credential_name: credentialName
});
if (!challengeResponse.success) {
throw new Error(challengeResponse.error || 'Failed to start registration');
}
// Convert challenge to proper format
const challenge = this.convertChallenge(challengeResponse.challenge);
// Create credential
const credential = await navigator.credentials.create({
publicKey: challenge
});
// Complete registration
const regResponse = await this.makeRequest('POST', this.apiEndpoints.registrationComplete, {
challenge_id: challengeResponse.challenge_id,
credential: {
id: this.arrayBufferToBase64(credential.rawId),
type: credential.type
},
client_data_json: this.arrayBufferToBase64(credential.response.clientDataJSON),
attestation_object: this.arrayBufferToBase64(credential.response.attestationObject)
});
if (regResponse.success) {
this.showSuccess('Passkey registered successfully!');
return regResponse;
} else {
throw new Error(regResponse.error || 'Registration failed');
}
} catch (error) {
console.error('WebAuthn registration error:', error);
this.showError(error.message || 'Registration failed');
throw error;
} finally {
this.hideLoading();
}
}
async listCredentials(username) {
try {
const response = await this.makeRequest('GET',
`${this.apiEndpoints.credentialsList}${username}/`);
if (response.success) {
return response.credentials;
} else {
throw new Error(response.error || 'Failed to list credentials');
}
} catch (error) {
console.error('Error listing credentials:', error);
throw error;
}
}
async deleteCredential(username, credentialId) {
try {
const response = await this.makeRequest('POST', this.apiEndpoints.credentialDelete, {
username: username,
credential_id: credentialId
});
if (response.success) {
this.showSuccess('Credential deleted successfully');
return response;
} else {
throw new Error(response.error || 'Failed to delete credential');
}
} catch (error) {
console.error('Error deleting credential:', error);
this.showError(error.message || 'Failed to delete credential');
throw error;
}
}
async updateCredentialName(username, credentialId, newName) {
try {
const response = await this.makeRequest('POST', this.apiEndpoints.credentialUpdate, {
username: username,
credential_id: credentialId,
new_name: newName
});
if (response.success) {
this.showSuccess('Credential name updated successfully');
return response;
} else {
throw new Error(response.error || 'Failed to update credential name');
}
} catch (error) {
console.error('Error updating credential name:', error);
this.showError(error.message || 'Failed to update credential name');
throw error;
}
}
async updateSettings(username, settings) {
try {
const response = await this.makeRequest('POST', this.apiEndpoints.settingsUpdate, {
username: username,
...settings
});
if (response.success) {
this.showSuccess('Settings updated successfully');
return response;
} else {
throw new Error(response.error || 'Failed to update settings');
}
} catch (error) {
console.error('Error updating settings:', error);
this.showError(error.message || 'Failed to update settings');
throw error;
}
}
convertChallenge(challenge) {
// Convert base64 challenge to ArrayBuffer
const challengeBytes = this.base64ToArrayBuffer(challenge.challenge);
return {
...challenge,
challenge: challengeBytes,
user: {
...challenge.user,
id: this.base64ToArrayBuffer(challenge.user.id)
},
excludeCredentials: challenge.excludeCredentials?.map(cred => ({
...cred,
id: this.base64ToArrayBuffer(cred.id)
})) || [],
allowCredentials: challenge.allowCredentials?.map(cred => ({
...cred,
id: this.base64ToArrayBuffer(cred.id)
})) || []
};
}
base64ToArrayBuffer(base64) {
const binaryString = window.atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
async makeRequest(method, url, data = null) {
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.csrfToken
}
};
if (data) {
options.body = JSON.stringify(data);
}
const response = await fetch(url, options);
return await response.json();
}
showLoading(message) {
// Create or update loading indicator
let loadingDiv = document.getElementById('webauthn-loading');
if (!loadingDiv) {
loadingDiv = document.createElement('div');
loadingDiv.id = 'webauthn-loading';
loadingDiv.className = 'alert alert-info';
loadingDiv.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + message;
document.body.appendChild(loadingDiv);
} else {
loadingDiv.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + message;
loadingDiv.style.display = 'block';
}
}
hideLoading() {
const loadingDiv = document.getElementById('webauthn-loading');
if (loadingDiv) {
loadingDiv.style.display = 'none';
}
}
showSuccess(message) {
this.showAlert('success', message);
}
showError(message) {
this.showAlert('danger', message);
}
showAlert(type, message) {
// Create alert element
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="close" data-dismiss="alert">
<span>&times;</span>
</button>
`;
// Insert at top of page
const container = document.querySelector('.container') || document.body;
container.insertBefore(alertDiv, container.firstChild);
// Auto-dismiss after 5 seconds
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
// Utility method to check if WebAuthn is available
static isSupported() {
return !!(navigator.credentials &&
navigator.credentials.create &&
navigator.credentials.get &&
window.PublicKeyCredential);
}
}
// Initialize WebAuthn when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
if (CyberPanelWebAuthn.isSupported()) {
window.cyberPanelWebAuthn = new CyberPanelWebAuthn();
}
});
// Export for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
module.exports = CyberPanelWebAuthn;
}

View File

@ -361,6 +361,18 @@
<button type="button" style="background-color: #33CCCC;" ng-click="verifyLoginCredentials()"
class="btn btn-success btn-block btn-login">Sign In
</button>
<!-- WebAuthn Passkey Login Button -->
<div id="webauthn-login-section" style="margin-top: 15px; display: none;">
<button type="button" id="webauthn-login-btn"
class="btn btn-primary btn-block btn-login"
style="background-color: #5b5fcf; border-color: #5b5fcf;">
<i class="fas fa-fingerprint"></i> Login with Passkey
</button>
<div class="text-center" style="margin-top: 10px;">
<small class="text-muted">or</small>
</div>
</div>
</div>
</div>
</div>
@ -374,6 +386,36 @@
<script src="https://code.angularjs.org/1.6.5/angular.min.js"></script>
<script src="https://code.angularjs.org/1.6.5/angular-route.min.js"></script>
<script src="{% static 'loginSystem/login-system.js' %}"></script>
<script src="{% static 'loginSystem/webauthn.js' %}"></script>
<script>
// Initialize WebAuthn login functionality
document.addEventListener('DOMContentLoaded', function() {
const webauthnSection = document.getElementById('webauthn-login-section');
const webauthnBtn = document.getElementById('webauthn-login-btn');
const usernameInput = document.querySelector('input[name="username"]');
// Show WebAuthn section if supported
if (window.cyberPanelWebAuthn && window.cyberPanelWebAuthn.isSupported()) {
webauthnSection.style.display = 'block';
// Add click handler for WebAuthn login
webauthnBtn.addEventListener('click', function() {
if (window.cyberPanelWebAuthn) {
window.cyberPanelWebAuthn.startPasswordlessLogin();
}
});
// Show/hide WebAuthn button based on username input
usernameInput.addEventListener('input', function() {
if (this.value.trim()) {
webauthnBtn.disabled = false;
} else {
webauthnBtn.disabled = true;
}
});
}
});
</script>
</body>

View File

@ -0,0 +1,452 @@
# -*- coding: utf-8 -*-
from django.test import TestCase, Client
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils import timezone
from datetime import datetime, timedelta
import json
import base64
from .models import Administrator
from .webauthn_models import WebAuthnCredential, WebAuthnChallenge, WebAuthnSettings
from .webauthn_backend import WebAuthnBackend
class WebAuthnTestCase(TestCase):
"""Test cases for WebAuthn functionality"""
def setUp(self):
"""Set up test data"""
self.client = Client()
# Create test user
self.user = Administrator.objects.create(
userName='testuser',
password='hashedpassword',
email='test@example.com',
firstName='Test',
lastName='User',
type=1,
acl_id=1
)
# Create WebAuthn settings
self.webauthn_settings = WebAuthnSettings.objects.create(
user=self.user,
enabled=True,
require_passkey=False,
allow_multiple_credentials=True,
max_credentials=10,
timeout_seconds=60
)
self.webauthn_backend = WebAuthnBackend()
def test_webauthn_models(self):
"""Test WebAuthn models"""
# Test WebAuthnCredential
credential = WebAuthnCredential.objects.create(
user=self.user,
credential_id='test_credential_id',
public_key='test_public_key',
name='Test Passkey',
counter=0
)
self.assertEqual(credential.user, self.user)
self.assertEqual(credential.name, 'Test Passkey')
self.assertTrue(credential.is_active)
# Test WebAuthnChallenge
challenge = WebAuthnChallenge.objects.create(
user=self.user,
challenge='test_challenge',
challenge_type='registration',
expires_at=timezone.now() + timedelta(minutes=5)
)
self.assertEqual(challenge.user, self.user)
self.assertEqual(challenge.challenge_type, 'registration')
self.assertFalse(challenge.is_expired())
# Test WebAuthnSettings
self.assertTrue(self.webauthn_settings.enabled)
self.assertTrue(self.webauthn_settings.can_add_credential())
def test_registration_challenge_creation(self):
"""Test WebAuthn registration challenge creation"""
result = self.webauthn_backend.create_registration_challenge(
self.user, 'Test Passkey'
)
self.assertTrue(result['success'])
self.assertIn('challenge', result)
self.assertIn('challenge_id', result)
# Verify challenge was stored in database
challenge_id = result['challenge_id']
challenge = WebAuthnChallenge.objects.get(id=challenge_id)
self.assertEqual(challenge.user, self.user)
self.assertEqual(challenge.challenge_type, 'registration')
def test_authentication_challenge_creation(self):
"""Test WebAuthn authentication challenge creation"""
result = self.webauthn_backend.create_authentication_challenge(self.user)
self.assertTrue(result['success'])
self.assertIn('challenge', result)
self.assertIn('challenge_id', result)
# Verify challenge was stored in database
challenge_id = result['challenge_id']
challenge = WebAuthnChallenge.objects.get(id=challenge_id)
self.assertEqual(challenge.user, self.user)
self.assertEqual(challenge.challenge_type, 'authentication')
def test_credential_management(self):
"""Test credential management functions"""
# Create test credential
credential = WebAuthnCredential.objects.create(
user=self.user,
credential_id='test_credential_id',
public_key='test_public_key',
name='Test Passkey',
counter=0
)
# Test get_user_credentials
credentials = self.webauthn_backend.get_user_credentials(self.user)
self.assertEqual(len(credentials), 1)
self.assertEqual(credentials[0]['name'], 'Test Passkey')
# Test delete_credential
result = self.webauthn_backend.delete_credential(self.user, credential.id)
self.assertTrue(result['success'])
# Verify credential is deactivated
credential.refresh_from_db()
self.assertFalse(credential.is_active)
# Test update_credential_name
credential.is_active = True
credential.save()
result = self.webauthn_backend.update_credential_name(
self.user, credential.id, 'Updated Name'
)
self.assertTrue(result['success'])
credential.refresh_from_db()
self.assertEqual(credential.name, 'Updated Name')
def test_webauthn_api_endpoints(self):
"""Test WebAuthn API endpoints"""
# Test registration start endpoint
response = self.client.post('/webauthn/registration/start/',
json.dumps({'username': 'testuser', 'credential_name': 'Test Passkey'}),
content_type='application/json')
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertTrue(data['success'])
# Test credentials list endpoint
response = self.client.get('/webauthn/credentials/testuser/')
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertTrue(data['success'])
self.assertIn('credentials', data)
self.assertIn('settings', data)
# Test settings update endpoint
response = self.client.post('/webauthn/settings/update/',
json.dumps({
'username': 'testuser',
'enabled': True,
'require_passkey': False
}),
content_type='application/json')
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertTrue(data['success'])
def test_webauthn_settings_validation(self):
"""Test WebAuthn settings validation"""
# Test max credentials limit
settings = WebAuthnSettings.objects.get(user=self.user)
settings.max_credentials = 1
settings.save()
# Create first credential
WebAuthnCredential.objects.create(
user=self.user,
credential_id='cred1',
public_key='key1',
name='First Passkey'
)
# Should not be able to add more credentials
self.assertFalse(settings.can_add_credential())
# Test multiple credentials disabled
settings.allow_multiple_credentials = False
settings.max_credentials = 10
settings.save()
# Should not be able to add more credentials
self.assertFalse(settings.can_add_credential())
def test_challenge_expiration(self):
"""Test challenge expiration handling"""
# Create expired challenge
expired_challenge = WebAuthnChallenge.objects.create(
user=self.user,
challenge='expired_challenge',
challenge_type='registration',
expires_at=timezone.now() - timedelta(minutes=1)
)
self.assertTrue(expired_challenge.is_expired())
# Test cleanup
self.webauthn_backend.cleanup_expired_challenges()
# Expired challenge should be deleted
with self.assertRaises(WebAuthnChallenge.DoesNotExist):
WebAuthnChallenge.objects.get(id=expired_challenge.id)
def test_webauthn_integration_with_existing_2fa(self):
"""Test WebAuthn integration with existing 2FA system"""
# Enable 2FA for user
self.user.twoFA = 1
self.user.secretKey = 'test_secret_key'
self.user.save()
# Enable WebAuthn
settings = WebAuthnSettings.objects.get(user=self.user)
settings.enabled = True
settings.save()
# Both should be enabled
self.assertTrue(self.user.twoFA)
self.assertTrue(settings.enabled)
# User should be able to use either authentication method
# (This would be tested in the actual authentication flow)
def test_webauthn_security_features(self):
"""Test WebAuthn security features"""
# Test credential counter update
credential = WebAuthnCredential.objects.create(
user=self.user,
credential_id='test_credential_id',
public_key='test_public_key',
name='Test Passkey',
counter=0
)
# Update counter
result = credential.update_counter(5)
self.assertTrue(result)
# Should not allow decreasing counter
result = credential.update_counter(3)
self.assertFalse(result)
# Test challenge uniqueness
challenge1 = self.webauthn_backend.generate_challenge()
challenge2 = self.webauthn_backend.generate_challenge()
self.assertNotEqual(challenge1, challenge2)
self.assertEqual(len(challenge1), 44) # Base64 encoded 32 bytes
def test_webauthn_error_handling(self):
"""Test WebAuthn error handling"""
# Test with non-existent user
result = self.webauthn_backend.create_registration_challenge(
None, 'Test Passkey'
)
self.assertFalse(result['success'])
self.assertIn('error', result)
# Test with disabled WebAuthn
settings = WebAuthnSettings.objects.get(user=self.user)
settings.enabled = False
settings.save()
result = self.webauthn_backend.create_authentication_challenge(self.user)
self.assertFalse(result['success'])
self.assertIn('error', result)
def test_webauthn_data_serialization(self):
"""Test WebAuthn data serialization"""
# Test challenge metadata
challenge = WebAuthnChallenge.objects.create(
user=self.user,
challenge='test_challenge',
challenge_type='registration',
expires_at=timezone.now() + timedelta(minutes=5)
)
# Set metadata
metadata = {'test_key': 'test_value', 'number': 123}
challenge.set_metadata(metadata)
challenge.save()
# Retrieve metadata
retrieved_metadata = challenge.get_metadata()
self.assertEqual(retrieved_metadata, metadata)
# Test session data
from .webauthn_models import WebAuthnSession
session = WebAuthnSession.create_session(
self.user, 'registration', {'test': 'data'}
)
session_data = session.get_data()
self.assertEqual(session_data, {'test': 'data'})
# Update session data
session.set_data({'updated': 'data'})
session.save()
updated_data = session.get_data()
self.assertEqual(updated_data, {'updated': 'data'})
class WebAuthnIntegrationTestCase(TestCase):
"""Integration tests for WebAuthn with CyberPanel"""
def setUp(self):
"""Set up integration test data"""
self.client = Client()
# Create admin user
self.admin = Administrator.objects.create(
userName='admin',
password='hashedpassword',
email='admin@example.com',
firstName='Admin',
lastName='User',
type=1,
acl_id=1
)
# Create regular user
self.user = Administrator.objects.create(
userName='testuser',
password='hashedpassword',
email='test@example.com',
firstName='Test',
lastName='User',
type=0,
acl_id=2,
owner=self.admin.pk
)
def test_webauthn_user_management_integration(self):
"""Test WebAuthn integration with user management"""
# Login as admin
self.client.force_login(self.admin)
# Test WebAuthn settings update through user management
response = self.client.post('/webauthn/settings/update/',
json.dumps({
'username': 'testuser',
'enabled': True,
'require_passkey': False,
'allow_multiple_credentials': True,
'max_credentials': 5
}),
content_type='application/json')
self.assertEqual(response.status_code, 200)
# Verify settings were updated
settings = WebAuthnSettings.get_or_create_settings(self.user)
self.assertTrue(settings.enabled)
self.assertEqual(settings.max_credentials, 5)
def test_webauthn_permissions(self):
"""Test WebAuthn permission system"""
# Test admin can manage any user's WebAuthn settings
self.client.force_login(self.admin)
response = self.client.get('/webauthn/credentials/testuser/')
self.assertEqual(response.status_code, 200)
# Test user can manage their own WebAuthn settings
self.client.force_login(self.user)
response = self.client.get('/webauthn/credentials/testuser/')
self.assertEqual(response.status_code, 200)
# Test user cannot manage other users' settings
other_user = Administrator.objects.create(
userName='otheruser',
password='hashedpassword',
email='other@example.com',
firstName='Other',
lastName='User',
type=0,
acl_id=2,
owner=self.admin.pk
)
response = self.client.get('/webauthn/credentials/otheruser/')
self.assertEqual(response.status_code, 403)
def test_webauthn_with_existing_authentication(self):
"""Test WebAuthn alongside existing authentication methods"""
# Enable 2FA for user
self.user.twoFA = 1
self.user.secretKey = 'test_secret_key'
self.user.save()
# Enable WebAuthn
settings = WebAuthnSettings.objects.create(
user=self.user,
enabled=True,
require_passkey=False
)
# Both authentication methods should be available
self.assertTrue(self.user.twoFA)
self.assertTrue(settings.enabled)
# User should be able to use either method
# (In practice, this would be handled in the login flow)
def test_webauthn_cleanup_maintenance(self):
"""Test WebAuthn cleanup and maintenance functions"""
# Create expired challenges and sessions
expired_challenge = WebAuthnChallenge.objects.create(
user=self.user,
challenge='expired_challenge',
challenge_type='registration',
expires_at=timezone.now() - timedelta(hours=1)
)
from .webauthn_models import WebAuthnSession
expired_session = WebAuthnSession.objects.create(
user=self.user,
session_id='expired_session',
session_type='registration',
data='{}',
expires_at=timezone.now() - timedelta(hours=1)
)
# Run cleanup
backend = WebAuthnBackend()
backend.cleanup_expired_challenges()
backend.cleanup_expired_sessions()
# Expired items should be deleted
with self.assertRaises(WebAuthnChallenge.DoesNotExist):
WebAuthnChallenge.objects.get(id=expired_challenge.id)
with self.assertRaises(WebAuthnSession.DoesNotExist):
WebAuthnSession.objects.get(id=expired_session.id)

View File

@ -1,8 +1,9 @@
from django.urls import path
from django.urls import path, include
from . import views
urlpatterns = [
path('', views.loadLoginPage, name='adminLogin'),
path('verifyLogin', views.verifyLogin, name='verifyLogin'),
path('logout', views.logout, name='logout'),
path('webauthn/', include('loginSystem.webauthn_urls')),
]

View File

@ -44,10 +44,9 @@ def verifyLogin(request):
username = data.get('username', '')
password = data.get('password', '')
# Debug logging
print(f"Login attempt - Username: {username}, Password length: {len(password) if password else 0}")
print(f"Password contains '$': {'$' in password if password else False}")
print(f"Raw password: {repr(password)}")
# Secure logging (no sensitive data)
from plogical.errorSanitizer import secure_log_error
secure_log_error(Exception(f"Login attempt for user: {username}"), 'verifyLogin')
try:
language_selection = data.get('languageSelection', 'english')
@ -157,8 +156,10 @@ def verifyLogin(request):
response.write(json_data)
return response
except BaseException as msg:
data = {'userID': 0, 'loginStatus': 0, 'error_message': str(msg)}
except Exception as e:
from plogical.errorSanitizer import secure_log_error, secure_error_response
secure_log_error(e, 'verifyLogin')
data = secure_error_response(e, 'Login failed')
json_data = json.dumps(data)
return HttpResponse(json_data)

View File

@ -0,0 +1,453 @@
# -*- coding: utf-8 -*-
import json
import base64
import hashlib
import secrets
import time
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple, Any
from .models import Administrator
from .webauthn_models import WebAuthnCredential, WebAuthnChallenge, WebAuthnSession, WebAuthnSettings
import logging
logger = logging.getLogger(__name__)
class WebAuthnBackend:
"""
WebAuthn backend for handling passkey authentication
"""
def __init__(self):
# Default WebAuthn configuration - can be overridden in Django settings
self.rp_id = 'cyberpanel.local' # Should be your actual domain
self.rp_name = 'CyberPanel'
self.origin = 'https://cyberpanel.local:8090' # Should be your actual origin
self.challenge_timeout = 300 # 5 minutes
def generate_challenge(self) -> str:
"""Generate a random challenge"""
return base64.urlsafe_b64encode(secrets.token_bytes(32)).decode('utf-8').rstrip('=')
def create_registration_challenge(self, user: Administrator, credential_name: str = None) -> Dict[str, Any]:
"""
Create a WebAuthn registration challenge
"""
try:
# Check if user has WebAuthn settings
settings_obj = WebAuthnSettings.get_or_create_settings(user)
if not settings_obj.can_add_credential():
return {
'success': False,
'error': 'Maximum number of credentials reached or multiple credentials not allowed'
}
# Generate challenge
challenge = self.generate_challenge()
# Create challenge record
challenge_obj = WebAuthnChallenge.objects.create(
user=user,
challenge=challenge,
challenge_type='registration',
expires_at=datetime.now() + timedelta(seconds=self.challenge_timeout),
metadata=json.dumps({
'credential_name': credential_name or f"Passkey {datetime.now().strftime('%Y-%m-%d %H:%M')}",
'rp_id': self.rp_id,
'rp_name': self.rp_name,
})
)
# Create WebAuthn challenge object
webauthn_challenge = {
'challenge': challenge,
'rp': {
'id': self.rp_id,
'name': self.rp_name,
},
'user': {
'id': base64.urlsafe_b64encode(str(user.pk).encode()).decode('utf-8').rstrip('='),
'name': user.email or user.userName,
'displayName': f"{user.firstName} {user.lastName}".strip() or user.userName,
},
'pubKeyCredParams': [
{'type': 'public-key', 'alg': -7}, # ES256
{'type': 'public-key', 'alg': -257}, # RS256
],
'timeout': settings_obj.timeout_seconds * 1000,
'attestation': 'none',
'excludeCredentials': self._get_existing_credentials(user),
}
return {
'success': True,
'challenge': webauthn_challenge,
'challenge_id': challenge_obj.id,
}
except Exception as e:
logger.error(f"Error creating registration challenge: {str(e)}")
return {
'success': False,
'error': f'Failed to create registration challenge: {str(e)}'
}
def create_authentication_challenge(self, user: Administrator = None) -> Dict[str, Any]:
"""
Create a WebAuthn authentication challenge
"""
try:
# If user is specified, create user-specific challenge
if user:
settings_obj = WebAuthnSettings.get_or_create_settings(user)
if not settings_obj.enabled:
return {
'success': False,
'error': 'WebAuthn not enabled for this user'
}
credentials = self._get_existing_credentials(user)
if not credentials:
return {
'success': False,
'error': 'No WebAuthn credentials found for this user'
}
else:
# For username-based authentication, we'll need to find the user first
credentials = []
# Generate challenge
challenge = self.generate_challenge()
# Create challenge record
challenge_obj = WebAuthnChallenge.objects.create(
user=user or Administrator.objects.first(), # Fallback for username-based auth
challenge=challenge,
challenge_type='authentication',
expires_at=datetime.now() + timedelta(seconds=self.challenge_timeout),
metadata=json.dumps({
'rp_id': self.rp_id,
'rp_name': self.rp_name,
})
)
# Create WebAuthn challenge object
webauthn_challenge = {
'challenge': challenge,
'timeout': 60000, # 1 minute
'rpId': self.rp_id,
'allowCredentials': credentials,
'userVerification': 'preferred',
}
return {
'success': True,
'challenge': webauthn_challenge,
'challenge_id': challenge_obj.id,
}
except Exception as e:
logger.error(f"Error creating authentication challenge: {str(e)}")
return {
'success': False,
'error': f'Failed to create authentication challenge: {str(e)}'
}
def verify_registration(self, challenge_id: int, credential_data: Dict[str, Any],
client_data_json: str, attestation_object: str) -> Dict[str, Any]:
"""
Verify WebAuthn registration response
"""
try:
# Get challenge
challenge_obj = WebAuthnChallenge.objects.get(
id=challenge_id,
challenge_type='registration',
used=False
)
if challenge_obj.is_expired():
return {
'success': False,
'error': 'Challenge has expired'
}
# Parse client data
client_data = json.loads(base64.urlsafe_b64decode(client_data_json + '=='))
# Verify challenge
if client_data.get('challenge') != challenge_obj.challenge:
return {
'success': False,
'error': 'Challenge mismatch'
}
# Verify origin
if client_data.get('origin') != self.origin:
return {
'success': False,
'error': 'Origin mismatch'
}
# Verify type
if client_data.get('type') != 'webauthn.create':
return {
'success': False,
'error': 'Invalid response type'
}
# For now, we'll do basic validation
# In a production environment, you'd want to use a proper WebAuthn library
# like python-webauthn or webauthn
# Extract credential ID and public key from attestation object
# This is a simplified implementation
credential_id = credential_data.get('id')
public_key = credential_data.get('publicKey')
if not credential_id or not public_key:
return {
'success': False,
'error': 'Invalid credential data'
}
# Get credential name from challenge metadata
metadata = challenge_obj.get_metadata()
credential_name = metadata.get('credential_name', f"Passkey {datetime.now().strftime('%Y-%m-%d %H:%M')}")
# Create credential record
credential = WebAuthnCredential.objects.create(
user=challenge_obj.user,
credential_id=credential_id,
public_key=public_key,
name=credential_name,
counter=0
)
# Mark challenge as used
challenge_obj.mark_used()
# Enable WebAuthn for user if not already enabled
settings_obj = WebAuthnSettings.get_or_create_settings(challenge_obj.user)
if not settings_obj.enabled:
settings_obj.enabled = True
settings_obj.save()
return {
'success': True,
'credential_id': credential.id,
'message': 'Passkey registered successfully'
}
except WebAuthnChallenge.DoesNotExist:
return {
'success': False,
'error': 'Invalid challenge'
}
except Exception as e:
logger.error(f"Error verifying registration: {str(e)}")
return {
'success': False,
'error': f'Registration verification failed: {str(e)}'
}
def verify_authentication(self, challenge_id: int, credential_data: Dict[str, Any],
client_data_json: str, authenticator_data: str) -> Dict[str, Any]:
"""
Verify WebAuthn authentication response
"""
try:
# Get challenge
challenge_obj = WebAuthnChallenge.objects.get(
id=challenge_id,
challenge_type='authentication',
used=False
)
if challenge_obj.is_expired():
return {
'success': False,
'error': 'Challenge has expired'
}
# Parse client data
client_data = json.loads(base64.urlsafe_b64decode(client_data_json + '=='))
# Verify challenge
if client_data.get('challenge') != challenge_obj.challenge:
return {
'success': False,
'error': 'Challenge mismatch'
}
# Verify origin
if client_data.get('origin') != self.origin:
return {
'success': False,
'error': 'Origin mismatch'
}
# Verify type
if client_data.get('type') != 'webauthn.get':
return {
'success': False,
'error': 'Invalid response type'
}
# Get credential
credential_id = credential_data.get('id')
if not credential_id:
return {
'success': False,
'error': 'No credential ID provided'
}
try:
credential = WebAuthnCredential.objects.get(
credential_id=credential_id,
user=challenge_obj.user,
is_active=True
)
except WebAuthnCredential.DoesNotExist:
return {
'success': False,
'error': 'Credential not found'
}
# Verify signature (simplified - in production use proper WebAuthn library)
# For now, we'll just update the counter and mark as successful
# Update credential counter
credential.update_counter(credential.counter + 1)
# Mark challenge as used
challenge_obj.mark_used()
return {
'success': True,
'user_id': challenge_obj.user.pk,
'credential_id': credential.id,
'message': 'Authentication successful'
}
except WebAuthnChallenge.DoesNotExist:
return {
'success': False,
'error': 'Invalid challenge'
}
except Exception as e:
logger.error(f"Error verifying authentication: {str(e)}")
return {
'success': False,
'error': f'Authentication verification failed: {str(e)}'
}
def _get_existing_credentials(self, user: Administrator) -> List[Dict[str, Any]]:
"""Get existing credentials for a user"""
credentials = WebAuthnCredential.objects.filter(
user=user,
is_active=True
)
return [
{
'id': cred.credential_id,
'type': 'public-key',
'transports': ['internal', 'hybrid', 'usb', 'nfc', 'ble']
}
for cred in credentials
]
def get_user_credentials(self, user: Administrator) -> List[Dict[str, Any]]:
"""Get all active credentials for a user"""
credentials = WebAuthnCredential.objects.filter(
user=user,
is_active=True
).order_by('-created_at')
return [
{
'id': cred.id,
'name': cred.name,
'credential_id': cred.credential_id[:16] + '...',
'created_at': cred.created_at.isoformat(),
'last_used': cred.last_used.isoformat() if cred.last_used else None,
}
for cred in credentials
]
def delete_credential(self, user: Administrator, credential_id: int) -> Dict[str, Any]:
"""Delete a WebAuthn credential"""
try:
credential = WebAuthnCredential.objects.get(
id=credential_id,
user=user
)
credential.is_active = False
credential.save()
return {
'success': True,
'message': 'Credential deleted successfully'
}
except WebAuthnCredential.DoesNotExist:
return {
'success': False,
'error': 'Credential not found'
}
except Exception as e:
logger.error(f"Error deleting credential: {str(e)}")
return {
'success': False,
'error': f'Failed to delete credential: {str(e)}'
}
def update_credential_name(self, user: Administrator, credential_id: int, new_name: str) -> Dict[str, Any]:
"""Update credential name"""
try:
credential = WebAuthnCredential.objects.get(
id=credential_id,
user=user
)
credential.name = new_name
credential.save()
return {
'success': True,
'message': 'Credential name updated successfully'
}
except WebAuthnCredential.DoesNotExist:
return {
'success': False,
'error': 'Credential not found'
}
except Exception as e:
logger.error(f"Error updating credential name: {str(e)}")
return {
'success': False,
'error': f'Failed to update credential name: {str(e)}'
}
def cleanup_expired_challenges(self):
"""Clean up expired challenges"""
expired_challenges = WebAuthnChallenge.objects.filter(
expires_at__lt=datetime.now()
)
count = expired_challenges.count()
expired_challenges.delete()
logger.info(f"Cleaned up {count} expired WebAuthn challenges")
def cleanup_expired_sessions(self):
"""Clean up expired sessions"""
expired_sessions = WebAuthnSession.objects.filter(
expires_at__lt=datetime.now()
)
count = expired_sessions.count()
expired_sessions.delete()
logger.info(f"Cleaned up {count} expired WebAuthn sessions")

View File

@ -0,0 +1,204 @@
# -*- coding: utf-8 -*-
from django.db import models
from django.contrib.auth import get_user_model
from .models import Administrator
import json
import base64
from datetime import datetime, timedelta
class WebAuthnCredential(models.Model):
"""
Model to store WebAuthn passkey credentials for users
"""
user = models.ForeignKey(Administrator, on_delete=models.CASCADE, related_name='webauthn_credentials')
credential_id = models.CharField(max_length=255, unique=True, help_text="Base64 encoded credential ID")
public_key = models.TextField(help_text="Base64 encoded public key")
counter = models.BigIntegerField(default=0, help_text="Signature counter for replay protection")
name = models.CharField(max_length=100, help_text="User-friendly name for the passkey")
created_at = models.DateTimeField(auto_now_add=True)
last_used = models.DateTimeField(null=True, blank=True)
is_active = models.BooleanField(default=True, help_text="Whether this credential is active")
class Meta:
db_table = 'webauthn_credentials'
verbose_name = 'WebAuthn Credential'
verbose_name_plural = 'WebAuthn Credentials'
indexes = [
models.Index(fields=['user', 'is_active']),
models.Index(fields=['credential_id']),
]
def __str__(self):
return f"{self.user.userName} - {self.name} ({self.credential_id[:16]}...)"
def get_credential_id_bytes(self):
"""Get credential ID as bytes"""
return base64.urlsafe_b64decode(self.credential_id + '==')
def get_public_key_bytes(self):
"""Get public key as bytes"""
return base64.urlsafe_b64decode(self.public_key + '==')
def update_counter(self, new_counter):
"""Update signature counter"""
if new_counter > self.counter:
self.counter = new_counter
self.last_used = datetime.now()
self.save(update_fields=['counter', 'last_used'])
return True
return False
class WebAuthnChallenge(models.Model):
"""
Model to store WebAuthn challenges for registration and authentication
"""
user = models.ForeignKey(Administrator, on_delete=models.CASCADE, related_name='webauthn_challenges')
challenge = models.CharField(max_length=255, help_text="Base64 encoded challenge")
challenge_type = models.CharField(max_length=20, choices=[
('registration', 'Registration'),
('authentication', 'Authentication'),
])
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
used = models.BooleanField(default=False)
metadata = models.TextField(default='{}', help_text="Additional challenge metadata as JSON")
class Meta:
db_table = 'webauthn_challenges'
verbose_name = 'WebAuthn Challenge'
verbose_name_plural = 'WebAuthn Challenges'
indexes = [
models.Index(fields=['user', 'challenge_type', 'used']),
models.Index(fields=['expires_at']),
]
def __str__(self):
return f"{self.user.userName} - {self.challenge_type} ({self.challenge[:16]}...)"
def is_expired(self):
"""Check if challenge has expired"""
return datetime.now() > self.expires_at
def get_challenge_bytes(self):
"""Get challenge as bytes"""
return base64.urlsafe_b64decode(self.challenge + '==')
def get_metadata(self):
"""Get metadata as dict"""
try:
return json.loads(self.metadata)
except:
return {}
def set_metadata(self, data):
"""Set metadata from dict"""
self.metadata = json.dumps(data)
def mark_used(self):
"""Mark challenge as used"""
self.used = True
self.save(update_fields=['used'])
class WebAuthnSession(models.Model):
"""
Model to store WebAuthn session data for ongoing operations
"""
user = models.ForeignKey(Administrator, on_delete=models.CASCADE, related_name='webauthn_sessions')
session_id = models.CharField(max_length=255, unique=True, help_text="Unique session identifier")
session_type = models.CharField(max_length=20, choices=[
('registration', 'Registration'),
('authentication', 'Authentication'),
])
data = models.TextField(help_text="Session data as JSON")
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
class Meta:
db_table = 'webauthn_sessions'
verbose_name = 'WebAuthn Session'
verbose_name_plural = 'WebAuthn Sessions'
indexes = [
models.Index(fields=['session_id']),
models.Index(fields=['expires_at']),
]
def __str__(self):
return f"{self.user.userName} - {self.session_type} ({self.session_id[:16]}...)"
def is_expired(self):
"""Check if session has expired"""
return datetime.now() > self.expires_at
def get_data(self):
"""Get session data as dict"""
try:
return json.loads(self.data)
except:
return {}
def set_data(self, data):
"""Set session data from dict"""
self.data = json.dumps(data)
@classmethod
def create_session(cls, user, session_type, data, duration_minutes=10):
"""Create a new WebAuthn session"""
import uuid
session_id = str(uuid.uuid4())
expires_at = datetime.now() + timedelta(minutes=duration_minutes)
session = cls.objects.create(
user=user,
session_id=session_id,
session_type=session_type,
data=json.dumps(data),
expires_at=expires_at
)
return session
class WebAuthnSettings(models.Model):
"""
Model to store WebAuthn configuration settings
"""
user = models.OneToOneField(Administrator, on_delete=models.CASCADE, related_name='webauthn_settings')
enabled = models.BooleanField(default=False, help_text="Whether WebAuthn is enabled for this user")
require_passkey = models.BooleanField(default=False, help_text="Require passkey for login (passwordless)")
allow_multiple_credentials = models.BooleanField(default=True, help_text="Allow multiple passkeys per user")
max_credentials = models.IntegerField(default=10, help_text="Maximum number of passkeys allowed")
timeout_seconds = models.IntegerField(default=60, help_text="WebAuthn operation timeout in seconds")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'webauthn_settings'
verbose_name = 'WebAuthn Settings'
verbose_name_plural = 'WebAuthn Settings'
def __str__(self):
return f"WebAuthn Settings for {self.user.userName}"
@classmethod
def get_or_create_settings(cls, user):
"""Get or create WebAuthn settings for a user"""
settings, created = cls.objects.get_or_create(
user=user,
defaults={
'enabled': False,
'require_passkey': False,
'allow_multiple_credentials': True,
'max_credentials': 10,
'timeout_seconds': 60,
}
)
return settings
def can_add_credential(self):
"""Check if user can add another credential"""
if not self.allow_multiple_credentials:
return WebAuthnCredential.objects.filter(user=self.user, is_active=True).count() == 0
return WebAuthnCredential.objects.filter(user=self.user, is_active=True).count() < self.max_credentials

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from django.urls import path
from . import webauthn_views
urlpatterns = [
# WebAuthn Registration
path('registration/start/', webauthn_views.webauthn_registration_start, name='webauthn_registration_start'),
path('registration/complete/', webauthn_views.webauthn_registration_complete, name='webauthn_registration_complete'),
# WebAuthn Authentication
path('authentication/start/', webauthn_views.webauthn_authentication_start, name='webauthn_authentication_start'),
path('authentication/complete/', webauthn_views.webauthn_authentication_complete, name='webauthn_authentication_complete'),
# WebAuthn Credential Management
path('credentials/<str:username>/', webauthn_views.webauthn_credentials_list, name='webauthn_credentials_list'),
path('credential/delete/', webauthn_views.webauthn_credential_delete, name='webauthn_credential_delete'),
path('credential/update/', webauthn_views.webauthn_credential_update, name='webauthn_credential_update'),
# WebAuthn Settings
path('settings/update/', webauthn_views.webauthn_settings_update, name='webauthn_settings_update'),
# WebAuthn Maintenance
path('cleanup/', webauthn_views.webauthn_cleanup, name='webauthn_cleanup'),
]

View File

@ -0,0 +1,463 @@
# -*- coding: utf-8 -*-
import json
import base64
from django.shortcuts import render, HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views import View
from .models import Administrator
from .webauthn_backend import WebAuthnBackend
from .webauthn_models import WebAuthnSettings, WebAuthnCredential
from plogical.acl import ACLManager
import logging
logger = logging.getLogger(__name__)
class WebAuthnAPIView(View):
"""Base class for WebAuthn API views"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.webauthn = WebAuthnBackend()
def json_response(self, data, status=200):
"""Return JSON response"""
return HttpResponse(
json.dumps(data, ensure_ascii=False),
content_type='application/json',
status=status
)
def error_response(self, message, status=400):
"""Return error response"""
return self.json_response({
'success': False,
'error': message
}, status)
@method_decorator(csrf_exempt, name='dispatch')
class WebAuthnRegistrationStart(WebAuthnAPIView):
"""Start WebAuthn registration process"""
def post(self, request):
try:
data = json.loads(request.body)
username = data.get('username')
credential_name = data.get('credential_name', '')
if not username:
return self.error_response('Username is required')
try:
user = Administrator.objects.get(userName=username)
except Administrator.DoesNotExist:
return self.error_response('User not found', 404)
# Check if user has permission to register WebAuthn
if hasattr(request, 'session') and 'userID' in request.session:
current_user_id = request.session['userID']
current_user = Administrator.objects.get(pk=current_user_id)
current_acl = ACLManager.loadedACL(current_user_id)
# Allow if admin, user is modifying themselves, or user is owned by current user
if not (current_acl['admin'] == 1 or
user.pk == current_user.pk or
user.owner == current_user.pk):
return self.error_response('Unauthorized access', 403)
result = self.webauthn.create_registration_challenge(user, credential_name)
return self.json_response(result)
except json.JSONDecodeError:
return self.error_response('Invalid JSON')
except Exception as e:
logger.error(f"Error in registration start: {str(e)}")
return self.error_response(f'Internal server error: {str(e)}', 500)
@method_decorator(csrf_exempt, name='dispatch')
class WebAuthnRegistrationComplete(WebAuthnAPIView):
"""Complete WebAuthn registration process"""
def post(self, request):
try:
data = json.loads(request.body)
challenge_id = data.get('challenge_id')
credential_data = data.get('credential')
client_data_json = data.get('client_data_json')
attestation_object = data.get('attestation_object')
if not all([challenge_id, credential_data, client_data_json, attestation_object]):
return self.error_response('Missing required fields')
result = self.webauthn.verify_registration(
challenge_id=challenge_id,
credential_data=credential_data,
client_data_json=client_data_json,
attestation_object=attestation_object
)
return self.json_response(result)
except json.JSONDecodeError:
return self.error_response('Invalid JSON')
except Exception as e:
logger.error(f"Error in registration complete: {str(e)}")
return self.error_response(f'Internal server error: {str(e)}', 500)
@method_decorator(csrf_exempt, name='dispatch')
class WebAuthnAuthenticationStart(WebAuthnAPIView):
"""Start WebAuthn authentication process"""
def post(self, request):
try:
data = json.loads(request.body)
username = data.get('username')
if not username:
return self.error_response('Username is required')
try:
user = Administrator.objects.get(userName=username)
except Administrator.DoesNotExist:
return self.error_response('User not found', 404)
result = self.webauthn.create_authentication_challenge(user)
return self.json_response(result)
except json.JSONDecodeError:
return self.error_response('Invalid JSON')
except Exception as e:
logger.error(f"Error in authentication start: {str(e)}")
return self.error_response(f'Internal server error: {str(e)}', 500)
@method_decorator(csrf_exempt, name='dispatch')
class WebAuthnAuthenticationComplete(WebAuthnAPIView):
"""Complete WebAuthn authentication process"""
def post(self, request):
try:
data = json.loads(request.body)
challenge_id = data.get('challenge_id')
credential_data = data.get('credential')
client_data_json = data.get('client_data_json')
authenticator_data = data.get('authenticator_data')
if not all([challenge_id, credential_data, client_data_json, authenticator_data]):
return self.error_response('Missing required fields')
result = self.webauthn.verify_authentication(
challenge_id=challenge_id,
credential_data=credential_data,
client_data_json=client_data_json,
authenticator_data=authenticator_data
)
if result['success']:
# Set session for successful authentication
request.session['userID'] = result['user_id']
request.session['webauthn_auth'] = True
request.session.set_expiry(43200) # 12 hours
# Log successful authentication
logger.info(f"WebAuthn authentication successful for user ID: {result['user_id']}")
return self.json_response(result)
except json.JSONDecodeError:
return self.error_response('Invalid JSON')
except Exception as e:
logger.error(f"Error in authentication complete: {str(e)}")
return self.error_response(f'Internal server error: {str(e)}', 500)
@method_decorator(csrf_exempt, name='dispatch')
class WebAuthnCredentialsList(WebAuthnAPIView):
"""List WebAuthn credentials for a user"""
def get(self, request, username=None):
try:
if not username:
return self.error_response('Username is required')
try:
user = Administrator.objects.get(userName=username)
except Administrator.DoesNotExist:
return self.error_response('User not found', 404)
# Check permissions
if hasattr(request, 'session') and 'userID' in request.session:
current_user_id = request.session['userID']
current_user = Administrator.objects.get(pk=current_user_id)
current_acl = ACLManager.loadedACL(current_user_id)
if not (current_acl['admin'] == 1 or
user.pk == current_user.pk or
user.owner == current_user.pk):
return self.error_response('Unauthorized access', 403)
credentials = self.webauthn.get_user_credentials(user)
settings = WebAuthnSettings.get_or_create_settings(user)
return self.json_response({
'success': True,
'credentials': credentials,
'settings': {
'enabled': settings.enabled,
'require_passkey': settings.require_passkey,
'allow_multiple_credentials': settings.allow_multiple_credentials,
'max_credentials': settings.max_credentials,
'can_add_credential': settings.can_add_credential(),
}
})
except Exception as e:
logger.error(f"Error listing credentials: {str(e)}")
return self.error_response(f'Internal server error: {str(e)}', 500)
@method_decorator(csrf_exempt, name='dispatch')
class WebAuthnCredentialDelete(WebAuthnAPIView):
"""Delete a WebAuthn credential"""
def post(self, request):
try:
data = json.loads(request.body)
username = data.get('username')
credential_id = data.get('credential_id')
if not username or not credential_id:
return self.error_response('Username and credential ID are required')
try:
user = Administrator.objects.get(userName=username)
except Administrator.DoesNotExist:
return self.error_response('User not found', 404)
# Check permissions
if hasattr(request, 'session') and 'userID' in request.session:
current_user_id = request.session['userID']
current_user = Administrator.objects.get(pk=current_user_id)
current_acl = ACLManager.loadedACL(current_user_id)
if not (current_acl['admin'] == 1 or
user.pk == current_user.pk or
user.owner == current_user.pk):
return self.error_response('Unauthorized access', 403)
result = self.webauthn.delete_credential(user, credential_id)
return self.json_response(result)
except json.JSONDecodeError:
return self.error_response('Invalid JSON')
except Exception as e:
logger.error(f"Error deleting credential: {str(e)}")
return self.error_response(f'Internal server error: {str(e)}', 500)
@method_decorator(csrf_exempt, name='dispatch')
class WebAuthnCredentialUpdate(WebAuthnAPIView):
"""Update WebAuthn credential name"""
def post(self, request):
try:
data = json.loads(request.body)
username = data.get('username')
credential_id = data.get('credential_id')
new_name = data.get('new_name')
if not all([username, credential_id, new_name]):
return self.error_response('Username, credential ID, and new name are required')
try:
user = Administrator.objects.get(userName=username)
except Administrator.DoesNotExist:
return self.error_response('User not found', 404)
# Check permissions
if hasattr(request, 'session') and 'userID' in request.session:
current_user_id = request.session['userID']
current_user = Administrator.objects.get(pk=current_user_id)
current_acl = ACLManager.loadedACL(current_user_id)
if not (current_acl['admin'] == 1 or
user.pk == current_user.pk or
user.owner == current_user.pk):
return self.error_response('Unauthorized access', 403)
result = self.webauthn.update_credential_name(user, credential_id, new_name)
return self.json_response(result)
except json.JSONDecodeError:
return self.error_response('Invalid JSON')
except Exception as e:
logger.error(f"Error updating credential: {str(e)}")
return self.error_response(f'Internal server error: {str(e)}', 500)
@method_decorator(csrf_exempt, name='dispatch')
class WebAuthnSettingsUpdate(WebAuthnAPIView):
"""Update WebAuthn settings for a user"""
def post(self, request):
try:
data = json.loads(request.body)
username = data.get('username')
enabled = data.get('enabled')
require_passkey = data.get('require_passkey')
allow_multiple_credentials = data.get('allow_multiple_credentials')
max_credentials = data.get('max_credentials')
timeout_seconds = data.get('timeout_seconds')
if not username:
return self.error_response('Username is required')
try:
user = Administrator.objects.get(userName=username)
except Administrator.DoesNotExist:
return self.error_response('User not found', 404)
# Check permissions
if hasattr(request, 'session') and 'userID' in request.session:
current_user_id = request.session['userID']
current_user = Administrator.objects.get(pk=current_user_id)
current_acl = ACLManager.loadedACL(current_user_id)
if not (current_acl['admin'] == 1 or
user.pk == current_user.pk or
user.owner == current_user.pk):
return self.error_response('Unauthorized access', 403)
settings = WebAuthnSettings.get_or_create_settings(user)
if enabled is not None:
settings.enabled = bool(enabled)
if require_passkey is not None:
settings.require_passkey = bool(require_passkey)
if allow_multiple_credentials is not None:
settings.allow_multiple_credentials = bool(allow_multiple_credentials)
if max_credentials is not None:
settings.max_credentials = int(max_credentials)
if timeout_seconds is not None:
settings.timeout_seconds = int(timeout_seconds)
settings.save()
return self.json_response({
'success': True,
'message': 'Settings updated successfully',
'settings': {
'enabled': settings.enabled,
'require_passkey': settings.require_passkey,
'allow_multiple_credentials': settings.allow_multiple_credentials,
'max_credentials': settings.max_credentials,
'timeout_seconds': settings.timeout_seconds,
}
})
except json.JSONDecodeError:
return self.error_response('Invalid JSON')
except Exception as e:
logger.error(f"Error updating settings: {str(e)}")
return self.error_response(f'Internal server error: {str(e)}', 500)
@method_decorator(csrf_exempt, name='dispatch')
class WebAuthnCleanup(WebAuthnAPIView):
"""Cleanup expired WebAuthn data"""
def post(self, request):
try:
# Check if user is admin
if not (hasattr(request, 'session') and 'userID' in request.session):
return self.error_response('Authentication required', 401)
current_user_id = request.session['userID']
current_acl = ACLManager.loadedACL(current_user_id)
if current_acl['admin'] != 1:
return self.error_response('Admin access required', 403)
# Cleanup expired data
self.webauthn.cleanup_expired_challenges()
self.webauthn.cleanup_expired_sessions()
return self.json_response({
'success': True,
'message': 'Cleanup completed successfully'
})
except Exception as e:
logger.error(f"Error during cleanup: {str(e)}")
return self.error_response(f'Internal server error: {str(e)}', 500)
# Traditional function-based views for easier integration
@csrf_exempt
def webauthn_registration_start(request):
"""Start WebAuthn registration - function view"""
view = WebAuthnRegistrationStart()
return view.post(request)
@csrf_exempt
def webauthn_registration_complete(request):
"""Complete WebAuthn registration - function view"""
view = WebAuthnRegistrationComplete()
return view.post(request)
@csrf_exempt
def webauthn_authentication_start(request):
"""Start WebAuthn authentication - function view"""
view = WebAuthnAuthenticationStart()
return view.post(request)
@csrf_exempt
def webauthn_authentication_complete(request):
"""Complete WebAuthn authentication - function view"""
view = WebAuthnAuthenticationComplete()
return view.post(request)
@csrf_exempt
def webauthn_credentials_list(request, username):
"""List WebAuthn credentials - function view"""
view = WebAuthnCredentialsList()
return view.get(request, username)
@csrf_exempt
def webauthn_credential_delete(request):
"""Delete WebAuthn credential - function view"""
view = WebAuthnCredentialDelete()
return view.post(request)
@csrf_exempt
def webauthn_credential_update(request):
"""Update WebAuthn credential - function view"""
view = WebAuthnCredentialUpdate()
return view.post(request)
@csrf_exempt
def webauthn_settings_update(request):
"""Update WebAuthn settings - function view"""
view = WebAuthnSettingsUpdate()
return view.post(request)
@csrf_exempt
def webauthn_cleanup(request):
"""Cleanup WebAuthn data - function view"""
view = WebAuthnCleanup()
return view.post(request)

View File

@ -12,50 +12,145 @@ class PHPManager:
@staticmethod
def findPHPVersions():
# distro = ProcessUtilities.decideDistro()
# if distro == ProcessUtilities.centos:
# return ['PHP 5.3', 'PHP 5.4', 'PHP 5.5', 'PHP 5.6', 'PHP 7.0', 'PHP 7.1', 'PHP 7.2', 'PHP 7.3', 'PHP 7.4', 'PHP 8.0', 'PHP 8.1']
# elif distro == ProcessUtilities.cent8:
# return ['PHP 7.1','PHP 7.2', 'PHP 7.3', 'PHP 7.4', 'PHP 8.0', 'PHP 8.1']
# elif distro == ProcessUtilities.ubuntu20:
# return ['PHP 7.2', 'PHP 7.3', 'PHP 7.4', 'PHP 8.0', 'PHP 8.1']
# else:
# return ['PHP 7.0', 'PHP 7.1', 'PHP 7.2', 'PHP 7.3', 'PHP 7.4', 'PHP 8.0', 'PHP 8.1', 'PHP 8.2', 'PHP 8.3', 'PHP 8.4', 'PHP 8.5']
"""
Comprehensive PHP version detection that checks multiple locations and methods
"""
try:
# Run the shell command and capture the output
result = ProcessUtilities.outputExecutioner('ls -la /usr/local/lsws')
# Get the lines containing 'lsphp' in the output
lsphp_lines = [line for line in result.split('\n') if 'lsphp' in line]
if os.path.exists(ProcessUtilities.debugPath):
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
logging.writeToFile(f'Found PHP lines in findPHPVersions: {lsphp_lines}')
# Extract the version from the lines and format it as 'PHP x.y'
php_versions = ['PHP ' + line.split()[8][5] + '.' + line.split()[8][6:] for line in lsphp_lines]
finalPHPVersions = []
for php in php_versions:
phpString = PHPManager.getPHPString(php)
# Method 1: Check /usr/local/lsws directory (LiteSpeed PHP)
try:
result = ProcessUtilities.outputExecutioner('ls -la /usr/local/lsws')
lsphp_lines = [line for line in result.split('\n') if 'lsphp' in line]
if os.path.exists("/usr/local/lsws/lsphp" + str(phpString) + "/bin/lsphp"):
finalPHPVersions.append(php)
if os.path.exists(ProcessUtilities.debugPath):
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
logging.writeToFile(f'Found PHP lines in findPHPVersions: {lsphp_lines}')
# Parse lsphp directories
for line in lsphp_lines:
try:
parts = line.split()
if len(parts) >= 9:
for part in parts:
if part.startswith('lsphp') and part != 'lsphp':
version_part = part[5:] # Remove 'lsphp' prefix
if len(version_part) >= 2:
major = version_part[0]
minor = version_part[1:]
formatted_version = f'PHP {major}.{minor}'
# Validate the PHP installation
phpString = PHPManager.getPHPString(formatted_version)
php_path = f"/usr/local/lsws/lsphp{phpString}/bin/php"
lsphp_path = f"/usr/local/lsws/lsphp{phpString}/bin/lsphp"
if os.path.exists(php_path) or os.path.exists(lsphp_path):
if formatted_version not in finalPHPVersions:
finalPHPVersions.append(formatted_version)
except (IndexError, ValueError):
continue
except Exception as e:
if os.path.exists(ProcessUtilities.debugPath):
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
logging.writeToFile(f'Error checking /usr/local/lsws: {str(e)}')
# Method 2: Check system-wide PHP installations
try:
# Check for system PHP versions
system_php_versions = ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
for version in system_php_versions:
formatted_version = f'PHP {version}'
if formatted_version not in finalPHPVersions:
# Check if this version exists in system
try:
phpString = PHPManager.getPHPString(formatted_version)
php_path = f"/usr/local/lsws/lsphp{phpString}/bin/php"
lsphp_path = f"/usr/local/lsws/lsphp{phpString}/bin/lsphp"
if os.path.exists(php_path) or os.path.exists(lsphp_path):
finalPHPVersions.append(formatted_version)
except:
continue
except Exception as e:
if os.path.exists(ProcessUtilities.debugPath):
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
logging.writeToFile(f'Error checking system PHP: {str(e)}')
# Method 3: Check package manager for available PHP versions
try:
# Try to detect available PHP packages
if ProcessUtilities.decideDistro() in [ProcessUtilities.centos, ProcessUtilities.cent8]:
# For CentOS/RHEL/AlmaLinux
command = "yum list available | grep lsphp | grep -E 'lsphp[0-9]+' | head -20"
else:
# For Ubuntu/Debian
command = "apt list --installed | grep lsphp | grep -E 'lsphp[0-9]+' | head -20"
result = ProcessUtilities.outputExecutioner(command)
if result and result.strip():
for line in result.split('\n'):
if 'lsphp' in line:
# Extract version from package name
import re
match = re.search(r'lsphp(\d+)(\d+)', line)
if match:
major = match.group(1)
minor = match.group(2)
formatted_version = f'PHP {major}.{minor}'
if formatted_version not in finalPHPVersions:
# Validate installation
try:
phpString = PHPManager.getPHPString(formatted_version)
php_path = f"/usr/local/lsws/lsphp{phpString}/bin/php"
lsphp_path = f"/usr/local/lsws/lsphp{phpString}/bin/lsphp"
if os.path.exists(php_path) or os.path.exists(lsphp_path):
finalPHPVersions.append(formatted_version)
except:
continue
except Exception as e:
if os.path.exists(ProcessUtilities.debugPath):
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
logging.writeToFile(f'Error checking package manager: {str(e)}')
# Method 4: Fallback to checking common PHP versions
if not finalPHPVersions:
fallback_versions = ['PHP 7.4', 'PHP 8.0', 'PHP 8.1', 'PHP 8.2', 'PHP 8.3', 'PHP 8.4', 'PHP 8.5']
for version in fallback_versions:
try:
phpString = PHPManager.getPHPString(version)
php_path = f"/usr/local/lsws/lsphp{phpString}/bin/php"
lsphp_path = f"/usr/local/lsws/lsphp{phpString}/bin/lsphp"
if os.path.exists(php_path) or os.path.exists(lsphp_path):
finalPHPVersions.append(version)
except:
continue
# Sort versions (newest first)
def version_sort_key(version):
try:
# Extract version number for sorting
version_num = version.replace('PHP ', '').split('.')
major = int(version_num[0])
minor = int(version_num[1]) if len(version_num) > 1 else 0
return (major, minor)
except:
return (0, 0)
finalPHPVersions.sort(key=version_sort_key, reverse=True)
if os.path.exists(ProcessUtilities.debugPath):
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
logging.writeToFile(f'Found PHP versions in findPHPVersions: {finalPHPVersions}')
logging.writeToFile(f'Final PHP versions found: {finalPHPVersions}')
return finalPHPVersions if finalPHPVersions else ['PHP 7.4', 'PHP 8.0', 'PHP 8.1', 'PHP 8.2', 'PHP 8.3', 'PHP 8.4', 'PHP 8.5']
# Now php_versions contains the formatted PHP versions
return finalPHPVersions
except BaseException as msg:
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
logging.writeToFile(f'Error while finding php versions on system: {str(msg)}')
return ['PHP 7.0', 'PHP 7.1', 'PHP 7.2', 'PHP 7.3', 'PHP 7.4', 'PHP 8.0', 'PHP 8.1', 'PHP 8.2', 'PHP 8.3', 'PHP 8.4', 'PHP 8.5']
return ['PHP 7.4', 'PHP 8.0', 'PHP 8.1', 'PHP 8.2', 'PHP 8.3', 'PHP 8.4', 'PHP 8.5']
@staticmethod
def findApachePHPVersions():
@ -137,6 +232,127 @@ class PHPManager:
return php
@staticmethod
def validatePHPInstallation(phpVersion):
"""
Validate that a PHP installation is properly configured and accessible
Returns: (is_valid, error_message, php_path)
"""
try:
php = PHPManager.getPHPString(phpVersion)
php_path = f'/usr/local/lsws/lsphp{php}/bin/php'
lsphp_path = f'/usr/local/lsws/lsphp{php}/bin/lsphp'
# Check if PHP binary exists
if os.path.exists(php_path):
return True, None, php_path
elif os.path.exists(lsphp_path):
return True, None, lsphp_path
else:
# Try alternative locations
alternative_paths = [
f'/usr/local/lsws/lsphp{php}/bin/lsphp',
'/usr/bin/php',
'/usr/local/bin/php'
]
for alt_path in alternative_paths:
if os.path.exists(alt_path):
return True, None, alt_path
return False, f'PHP {phpVersion} binary not found. Please install or check PHP installation.', None
except Exception as e:
return False, f'Error validating PHP installation: {str(e)}', None
@staticmethod
def fixPHPConfiguration(phpVersion):
"""
Attempt to fix common PHP configuration issues
"""
try:
php = PHPManager.getPHPString(phpVersion)
php_dir = f'/usr/local/lsws/lsphp{php}'
# Check if PHP directory exists
if not os.path.exists(php_dir):
return False, f'PHP directory {php_dir} does not exist'
# Check for missing php binary and create symlink if needed
php_binary = f'{php_dir}/bin/php'
lsphp_binary = f'{php_dir}/bin/lsphp'
if not os.path.exists(php_binary) and os.path.exists(lsphp_binary):
# Create symlink from lsphp to php
import subprocess
subprocess.run(['ln', '-sf', 'lsphp', 'php'], cwd=f'{php_dir}/bin')
return True, 'PHP binary symlink created successfully'
return True, 'PHP configuration appears to be correct'
except Exception as e:
return False, f'Error fixing PHP configuration: {str(e)}'
@staticmethod
def getLatestPHPVersion():
"""
Get the latest available PHP version from the system
Returns: (latest_version, all_versions)
"""
try:
all_versions = PHPManager.findPHPVersions()
if not all_versions:
return None, []
# Sort versions to get the latest
def version_sort_key(version):
try:
version_num = version.replace('PHP ', '').split('.')
major = int(version_num[0])
minor = int(version_num[1]) if len(version_num) > 1 else 0
return (major, minor)
except:
return (0, 0)
sorted_versions = sorted(all_versions, key=version_sort_key, reverse=True)
latest_version = sorted_versions[0]
return latest_version, all_versions
except Exception as e:
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
logging.writeToFile(f'Error getting latest PHP version: {str(e)}')
return None, []
@staticmethod
def getRecommendedPHPVersion():
"""
Get the recommended PHP version for new installations
Priority: 8.3 (recommended), 8.2, 8.4, 8.5, 8.1, 8.0, 7.4
"""
try:
all_versions = PHPManager.findPHPVersions()
if not all_versions:
return 'PHP 8.3' # Default recommendation
# Priority order for recommendations
recommended_order = ['PHP 8.3', 'PHP 8.2', 'PHP 8.4', 'PHP 8.5', 'PHP 8.1', 'PHP 8.0', 'PHP 7.4']
for recommended in recommended_order:
if recommended in all_versions:
return recommended
# If none of the recommended versions are available, return the latest
latest, _ = PHPManager.getLatestPHPVersion()
return latest if latest else 'PHP 8.3'
except Exception as e:
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
logging.writeToFile(f'Error getting recommended PHP version: {str(e)}')
return 'PHP 8.3'
@staticmethod
def FindPHPFPMPath(phpVersion):
if phpVersion == "PHP 5.3":

View File

@ -1152,6 +1152,12 @@ Automatic backup failed for %s on %s.
#
# command = 'chattr -R -i /home/%s/incbackup/' % (website.domain)
# ProcessUtilities.executioner(command)
#
# command = 'chattr -R -i /home/%s/lscache/' % (website.domain)
# ProcessUtilities.executioner(command)
#
# command = 'chattr -R -i /home/%s/.cagefs/' % (website.domain)
# ProcessUtilities.executioner(command)
# else:
# command = 'chattr -R -i /home/%s/' % (website.domain)
# ProcessUtilities.executioner(command)

View File

@ -139,6 +139,134 @@ class CronUtil:
command = 'chmod 1730 /var/spool/cron/crontabs'
ProcessUtilities.outputExecutioner(command)
@staticmethod
def suspendWebsiteCrons(externalApp):
"""
Suspend all cron jobs for a website by backing up and clearing the cron file.
This prevents cron jobs from running when a website is suspended.
"""
try:
if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
cronPath = "/var/spool/cron/" + externalApp
backupPath = "/var/spool/cron/" + externalApp + ".suspended"
else:
cronPath = "/var/spool/cron/crontabs/" + externalApp
backupPath = "/var/spool/cron/crontabs/" + externalApp + ".suspended"
# Check if cron file exists
if not os.path.exists(cronPath):
print("1,None") # No cron file to suspend
return
# Create backup of current cron jobs
try:
command = f'cp {cronPath} {backupPath}'
ProcessUtilities.executioner(command, 'root')
except Exception as e:
print(f"0,Warning: Could not backup cron file: {str(e)}")
# Clear the cron file to suspend all jobs
try:
CronUtil.CronPrem(1) # Enable permissions
# Create empty cron file or clear existing one
with open(cronPath, 'w') as f:
f.write('') # Empty file to disable all cron jobs
# Set proper ownership
command = f'chown {externalApp}:{externalApp} {cronPath}'
ProcessUtilities.executioner(command, 'root')
CronUtil.CronPrem(0) # Restore permissions
print("1,Cron jobs suspended successfully")
except Exception as e:
CronUtil.CronPrem(0) # Ensure permissions are restored
print(f"0,Failed to suspend cron jobs: {str(e)}")
except Exception as e:
print(f"0,Error suspending cron jobs: {str(e)}")
@staticmethod
def restoreWebsiteCrons(externalApp):
"""
Restore cron jobs for a website by restoring from backup file.
This restores cron jobs when a website is unsuspended.
"""
try:
if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
cronPath = "/var/spool/cron/" + externalApp
backupPath = "/var/spool/cron/" + externalApp + ".suspended"
else:
cronPath = "/var/spool/cron/crontabs/" + externalApp
backupPath = "/var/spool/cron/crontabs/" + externalApp + ".suspended"
# Check if backup file exists
if not os.path.exists(backupPath):
print("1,No suspended cron jobs to restore")
return
try:
CronUtil.CronPrem(1) # Enable permissions
# Restore cron jobs from backup
command = f'cp {backupPath} {cronPath}'
ProcessUtilities.executioner(command, 'root')
# Set proper ownership
command = f'chown {externalApp}:{externalApp} {cronPath}'
ProcessUtilities.executioner(command, 'root')
# Remove backup file
os.remove(backupPath)
CronUtil.CronPrem(0) # Restore permissions
print("1,Cron jobs restored successfully")
except Exception as e:
CronUtil.CronPrem(0) # Ensure permissions are restored
print(f"0,Failed to restore cron jobs: {str(e)}")
except Exception as e:
print(f"0,Error restoring cron jobs: {str(e)}")
@staticmethod
def getCronSuspensionStatus(externalApp):
"""
Check if cron jobs are currently suspended for a website.
Returns 1 if suspended, 0 if active, -1 if error.
"""
try:
if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
cronPath = "/var/spool/cron/" + externalApp
backupPath = "/var/spool/cron/" + externalApp + ".suspended"
else:
cronPath = "/var/spool/cron/crontabs/" + externalApp
backupPath = "/var/spool/cron/crontabs/" + externalApp + ".suspended"
# Check if backup file exists (indicates suspension)
if os.path.exists(backupPath):
print("1,Cron jobs are suspended")
return
elif os.path.exists(cronPath):
# Check if cron file is empty (also indicates suspension)
try:
with open(cronPath, 'r') as f:
content = f.read().strip()
if not content:
print("1,Cron jobs are suspended (empty file)")
else:
print("0,Cron jobs are active")
except Exception as e:
print(f"-1,Error reading cron file: {str(e)}")
else:
print("0,No cron jobs configured")
except Exception as e:
print(f"-1,Error checking cron status: {str(e)}")
def main():
@ -162,6 +290,12 @@ def main():
CronUtil.remCronbyLine(args.externalApp, int(args.line))
elif args.function == "addNewCron":
CronUtil.addNewCron(args.externalApp, args.finalCron)
elif args.function == "suspendWebsiteCrons":
CronUtil.suspendWebsiteCrons(args.externalApp)
elif args.function == "restoreWebsiteCrons":
CronUtil.restoreWebsiteCrons(args.externalApp)
elif args.function == "getCronSuspensionStatus":
CronUtil.getCronSuspensionStatus(args.externalApp)

273
plogical/errorSanitizer.py Normal file
View File

@ -0,0 +1,273 @@
# -*- coding: utf-8 -*-
"""
CyberPanel Error Sanitization Utility
=====================================
This module provides secure error handling and sanitization to prevent
information disclosure vulnerabilities while maintaining useful error
reporting for debugging purposes.
Security Features:
- Sanitizes error messages to prevent information disclosure
- Provides user-friendly error messages
- Maintains detailed logging for administrators
- Prevents sensitive data exposure in API responses
"""
import re
import logging
import traceback
from typing import Optional, Dict, Any
from django.conf import settings
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
class ErrorSanitizer:
"""
Centralized error sanitization and handling utility
"""
# Sensitive patterns that should be masked in error messages
SENSITIVE_PATTERNS = [
# File paths
r'/home/[^/]+/',
r'/usr/local/[^/]+/',
r'/var/[^/]+/',
r'/etc/[^/]+/',
# Database credentials
r'password[=\s]*[^\s]+',
r'passwd[=\s]*[^\s]+',
r'pwd[=\s]*[^\s]+',
# API keys and tokens
r'api[_-]?key[=\s]*[^\s]+',
r'token[=\s]*[^\s]+',
r'secret[=\s]*[^\s]+',
# Connection strings
r'mysql://[^@]+@',
r'postgresql://[^@]+@',
r'mongodb://[^@]+@',
# IP addresses (in some contexts)
r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b',
# Email addresses
r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
]
# Generic error messages for different exception types
GENERIC_ERRORS = {
'DatabaseError': 'Database operation failed. Please try again.',
'ConnectionError': 'Unable to connect to the service. Please check your connection.',
'PermissionError': 'Insufficient permissions to perform this operation.',
'FileNotFoundError': 'Required file not found. Please contact support.',
'OSError': 'System operation failed. Please try again.',
'ValueError': 'Invalid input provided. Please check your data.',
'KeyError': 'Required information is missing. Please try again.',
'TypeError': 'Invalid data type provided. Please check your input.',
'AttributeError': 'System configuration error. Please contact support.',
'ImportError': 'System module error. Please contact support.',
'TimeoutError': 'Operation timed out. Please try again.',
'BaseException': 'An unexpected error occurred. Please try again.',
}
@staticmethod
def sanitize_error_message(error_message: str, exception_type: str = None) -> str:
"""
Sanitize error message by removing sensitive information
Args:
error_message: The original error message
exception_type: The type of exception that occurred
Returns:
Sanitized error message safe for user display
"""
if not error_message:
return "An error occurred. Please try again."
# Convert to string if not already
error_str = str(error_message)
# Apply sensitive pattern masking
for pattern in ErrorSanitizer.SENSITIVE_PATTERNS:
error_str = re.sub(pattern, '[REDACTED]', error_str, flags=re.IGNORECASE)
# Additional sanitization for common sensitive patterns
error_str = re.sub(r'[^\x00-\x7F]+', '[NON-ASCII]', error_str) # Remove non-ASCII chars
error_str = re.sub(r'\s+', ' ', error_str) # Normalize whitespace
# Limit message length
if len(error_str) > 200:
error_str = error_str[:197] + "..."
return error_str.strip()
@staticmethod
def get_user_friendly_message(exception: Exception) -> str:
"""
Get a user-friendly error message based on exception type
Args:
exception: The exception that occurred
Returns:
User-friendly error message
"""
exception_type = type(exception).__name__
# Check for specific exception types first
if exception_type in ErrorSanitizer.GENERIC_ERRORS:
return ErrorSanitizer.GENERIC_ERRORS[exception_type]
# Handle common Django exceptions
if 'DoesNotExist' in exception_type:
return "The requested resource was not found."
elif 'ValidationError' in exception_type:
return "Invalid data provided. Please check your input."
elif 'PermissionDenied' in exception_type:
return "You do not have permission to perform this operation."
# Default generic message
return "An unexpected error occurred. Please try again."
@staticmethod
def create_secure_response(exception: Exception,
user_message: str = None,
include_details: bool = False) -> Dict[str, Any]:
"""
Create a secure error response dictionary
Args:
exception: The exception that occurred
user_message: Custom user message (optional)
include_details: Whether to include sanitized details for debugging
Returns:
Dictionary with secure error information
"""
response = {
'status': 0,
'error_message': user_message or ErrorSanitizer.get_user_friendly_message(exception)
}
# Add sanitized details if requested and in debug mode
if include_details and getattr(settings, 'DEBUG', False):
response['debug_info'] = {
'exception_type': type(exception).__name__,
'sanitized_message': ErrorSanitizer.sanitize_error_message(str(exception))
}
return response
@staticmethod
def log_error_securely(exception: Exception,
context: str = None,
user_id: str = None,
request_info: Dict = None):
"""
Log error securely without exposing sensitive information
Args:
exception: The exception that occurred
context: Context where the error occurred
user_id: ID of the user who encountered the error
request_info: Request information (sanitized)
"""
try:
# Create secure log entry
log_entry = {
'timestamp': logging.get_current_timestamp(),
'exception_type': type(exception).__name__,
'context': context or 'Unknown',
'user_id': user_id or 'Anonymous',
'sanitized_message': ErrorSanitizer.sanitize_error_message(str(exception))
}
# Add request info if provided
if request_info:
log_entry['request_info'] = {
'method': request_info.get('method', 'Unknown'),
'path': request_info.get('path', 'Unknown'),
'ip': request_info.get('ip', 'Unknown')
}
# Log the error
logging.writeToFile(f"SECURE_ERROR_LOG: {log_entry}")
# Also log the full traceback for administrators (in secure location)
if getattr(settings, 'DEBUG', False):
full_traceback = traceback.format_exc()
sanitized_traceback = ErrorSanitizer.sanitize_error_message(full_traceback)
logging.writeToFile(f"FULL_TRACEBACK: {sanitized_traceback}")
except Exception as log_error:
# Fallback logging if the secure logging fails
logging.writeToFile(f"LOGGING_ERROR: Failed to log error - {str(log_error)}")
@staticmethod
def handle_exception(exception: Exception,
context: str = None,
user_id: str = None,
request_info: Dict = None,
return_response: bool = True) -> Optional[Dict[str, Any]]:
"""
Comprehensive exception handling with secure logging and response
Args:
exception: The exception to handle
context: Context where the error occurred
user_id: ID of the user who encountered the error
request_info: Request information
return_response: Whether to return a response dictionary
Returns:
Secure error response dictionary if return_response is True
"""
# Log the error securely
ErrorSanitizer.log_error_securely(exception, context, user_id, request_info)
if return_response:
return ErrorSanitizer.create_secure_response(exception)
return None
class SecureExceptionHandler:
"""
Context manager for secure exception handling
"""
def __init__(self, context: str = None, user_id: str = None, request_info: Dict = None):
self.context = context
self.user_id = user_id
self.request_info = request_info
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
ErrorSanitizer.handle_exception(
exc_val,
self.context,
self.user_id,
self.request_info,
return_response=False
)
# Return True to suppress the exception (we've handled it)
return True
return False
# Convenience functions for common use cases
def secure_error_response(exception: Exception, user_message: str = None) -> Dict[str, Any]:
"""Create a secure error response for API endpoints"""
return ErrorSanitizer.create_secure_response(exception, user_message)
def secure_log_error(exception: Exception, context: str = None, user_id: str = None):
"""Log an error securely without exposing sensitive information"""
ErrorSanitizer.log_error_securely(exception, context, user_id)
def handle_secure_exception(exception: Exception, context: str = None) -> Dict[str, Any]:
"""Handle an exception securely and return a safe response"""
return ErrorSanitizer.handle_exception(exception, context, return_response=True)

View File

@ -89,11 +89,35 @@ class FTPUtilities:
@staticmethod
def ftpFunctions(path,externalApp):
try:
command = 'mkdir %s' % (path)
ProcessUtilities.executioner(command, externalApp)
return 1,'None'
# Enhanced path validation and creation
import os
# Check if path already exists
if os.path.exists(path):
# Path exists, ensure it's a directory
if not os.path.isdir(path):
return 0, "Specified path exists but is not a directory"
# Set proper permissions
command = 'chown -R %s:%s %s' % (externalApp, externalApp, path)
ProcessUtilities.executioner(command, externalApp)
return 1, 'None'
else:
# Create the directory with proper permissions
command = 'mkdir -p %s' % (path)
result = ProcessUtilities.executioner(command, externalApp)
if result == 0:
# Set proper ownership
command = 'chown -R %s:%s %s' % (externalApp, externalApp, path)
ProcessUtilities.executioner(command, externalApp)
# Set proper permissions (755)
command = 'chmod 755 %s' % (path)
ProcessUtilities.executioner(command, externalApp)
return 1, 'None'
else:
return 0, "Failed to create directory: %s" % path
except BaseException as msg:
logging.CyberCPLogFileWriter.writeToFile(
@ -118,30 +142,43 @@ class FTPUtilities:
## gid , uid ends
path = path.lstrip("/")
# Enhanced path validation and handling
if path and path.strip() and path != 'None':
# Clean the path
path = path.strip().lstrip("/")
# Additional security checks
if path.find("..") > -1 or path.find("~") > -1 or path.startswith("/"):
raise BaseException("Invalid path: Path must be relative and not contain '..' or '~' or start with '/'")
# Check for dangerous characters
dangerous_chars = [';', '|', '&', '$', '`', '\'', '"', '<', '>', '*', '?']
if any(char in path for char in dangerous_chars):
raise BaseException("Invalid path: Path contains dangerous characters")
# Construct full path
full_path = "/home/" + domainName + "/" + path
# Additional security: ensure path is within domain directory
domain_home = "/home/" + domainName
if not os.path.abspath(full_path).startswith(os.path.abspath(domain_home)):
raise BaseException("Security violation: Path must be within domain directory")
if path != 'None':
path = "/home/" + domainName + "/" + path
## Security Check
if path.find("..") > -1:
raise BaseException("Specified path must be inside virtual host home!")
result = FTPUtilities.ftpFunctions(path, externalApp)
result = FTPUtilities.ftpFunctions(full_path, externalApp)
if result[0] == 1:
pass
path = full_path
else:
raise BaseException(result[1])
raise BaseException("Path validation failed: " + result[1])
else:
path = "/home/" + domainName
# Enhanced symlink handling
if os.path.islink(path):
print("0, %s file is symlinked." % (path))
return 0
logging.CyberCPLogFileWriter.writeToFile(
"FTP path is symlinked: %s" % path)
raise BaseException("Cannot create FTP account: Path is a symbolic link")
ProcessUtilities.decideDistro()
@ -266,6 +303,9 @@ class FTPUtilities:
ftp.save()
# Apply quota to filesystem if needed
FTPUtilities.applyQuotaToFilesystem(ftp)
return 1, "FTP quota updated successfully"
except Users.DoesNotExist:
@ -274,6 +314,107 @@ class FTPUtilities:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + " [updateFTPQuota]")
return 0, str(msg)
@staticmethod
def applyQuotaToFilesystem(ftp_user):
"""
Apply quota settings to the filesystem level
"""
try:
import subprocess
# Get the user's directory
user_dir = ftp_user.dir
if not user_dir or not os.path.exists(user_dir):
return False, "User directory not found"
# Convert quota from MB to KB for setquota command
quota_kb = ftp_user.quotasize * 1024
# Apply quota using setquota command
# Note: This requires quota tools to be installed
try:
# Set both soft and hard limits to the same value
subprocess.run([
'setquota', '-u', str(ftp_user.uid),
f'{quota_kb}K', f'{quota_kb}K',
'0', '0', # inode limits (unlimited)
user_dir
], check=True, capture_output=True)
logging.CyberCPLogFileWriter.writeToFile(f"Applied quota {quota_kb}KB to user {ftp_user.user} in {user_dir}")
return True, "Quota applied successfully"
except subprocess.CalledProcessError as e:
logging.CyberCPLogFileWriter.writeToFile(f"Failed to apply quota: {e}")
return False, f"Failed to apply quota: {e}"
except FileNotFoundError:
# setquota command not found, quota tools not installed
logging.CyberCPLogFileWriter.writeToFile("setquota command not found - quota tools may not be installed")
return False, "Quota tools not installed"
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error applying quota to filesystem: {str(e)}")
return False, str(e)
@staticmethod
def getFTPQuotaUsage(ftpUsername):
"""
Get current quota usage for an FTP user
"""
try:
ftp = Users.objects.get(user=ftpUsername)
user_dir = ftp.dir
if not user_dir or not os.path.exists(user_dir):
return 0, "User directory not found"
# Get directory size in MB
import subprocess
result = subprocess.run(['du', '-sm', user_dir], capture_output=True, text=True)
if result.returncode == 0:
usage_mb = int(result.stdout.split()[0])
quota_mb = ftp.quotasize
usage_percent = (usage_mb / quota_mb * 100) if quota_mb > 0 else 0
return {
'usage_mb': usage_mb,
'quota_mb': quota_mb,
'usage_percent': round(usage_percent, 2),
'remaining_mb': max(0, quota_mb - usage_mb)
}
else:
return 0, "Failed to get directory size"
except Users.DoesNotExist:
return 0, "FTP user not found"
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error getting quota usage: {str(e)}")
return 0, str(e)
@staticmethod
def migrateExistingFTPUsers():
"""
Migrate existing FTP users to use the new quota system
"""
try:
migrated_count = 0
for ftp_user in Users.objects.all():
# If custom_quota_enabled is not set, set it to False and use package default
if not hasattr(ftp_user, 'custom_quota_enabled') or ftp_user.custom_quota_enabled is None:
ftp_user.custom_quota_enabled = False
ftp_user.custom_quota_size = 0
ftp_user.quotasize = ftp_user.domain.package.diskSpace
ftp_user.save()
migrated_count += 1
return 1, f"Migrated {migrated_count} FTP users to new quota system"
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error migrating FTP users: {str(e)}")
return 0, str(e)
def main():

View File

@ -400,11 +400,12 @@ modsecurity_rules_file /usr/local/lsws/conf/modsec/rules.conf
def setupOWASPRules():
try:
pathTOOWASPFolder = os.path.join(virtualHostUtilities.Server_root, "conf/modsec/owasp")
pathToOWASFolderNew = '%s/modsec/owasp-modsecurity-crs-3.0-master' % (virtualHostUtilities.vhostConfPath)
pathToOWASFolderNew = '%s/modsec/owasp-modsecurity-crs-4.18.0' % (virtualHostUtilities.vhostConfPath)
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: " + str(result) + " [setupOWASPRules]")
return 0
if os.path.exists(pathToOWASFolderNew):
@ -416,22 +417,32 @@ modsecurity_rules_file /usr/local/lsws/conf/modsec/rules.conf
if os.path.exists('owasp.tar.gz'):
os.remove('owasp.tar.gz')
command = "wget https://github.com/coreruleset/coreruleset/archive/v3.3.2/master.zip -O /usr/local/lsws/conf/modsec/owasp.zip"
# Clean up any existing zip file
if os.path.exists('/usr/local/lsws/conf/modsec/owasp.zip'):
os.remove('/usr/local/lsws/conf/modsec/owasp.zip')
command = "wget https://github.com/coreruleset/coreruleset/archive/refs/tags/v4.18.0.zip -O /usr/local/lsws/conf/modsec/owasp.zip"
logging.CyberCPLogFileWriter.writeToFile("Downloading OWASP rules: " + command + " [setupOWASPRules]")
result = subprocess.call(shlex.split(command))
if result != 0:
logging.CyberCPLogFileWriter.writeToFile("Failed to download OWASP rules: " + str(result) + " [setupOWASPRules]")
return 0
command = "unzip -o /usr/local/lsws/conf/modsec/owasp.zip -d /usr/local/lsws/conf/modsec/"
logging.CyberCPLogFileWriter.writeToFile("Extracting OWASP rules: " + command + " [setupOWASPRules]")
result = subprocess.call(shlex.split(command))
if result != 0:
logging.CyberCPLogFileWriter.writeToFile("Failed to extract OWASP rules: " + str(result) + " [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'
command = 'mv /usr/local/lsws/conf/modsec/coreruleset-4.18.0 /usr/local/lsws/conf/modsec/owasp-modsecurity-crs-4.18.0'
logging.CyberCPLogFileWriter.writeToFile("Moving OWASP rules: " + command + " [setupOWASPRules]")
result = subprocess.call(shlex.split(command))
if result != 0:
logging.CyberCPLogFileWriter.writeToFile("Failed to move OWASP rules: " + str(result) + " [setupOWASPRules]")
return 0
command = 'mv %s/crs-setup.conf.example %s/crs-setup.conf' % (pathToOWASFolderNew, pathToOWASFolderNew)
@ -453,32 +464,8 @@ modsecurity_rules_file /usr/local/lsws/conf/modsec/rules.conf
if result != 0:
return 0
content = """include {pathToOWASFolderNew}/crs-setup.conf
include {pathToOWASFolderNew}/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
include {pathToOWASFolderNew}/rules/REQUEST-901-INITIALIZATION.conf
include {pathToOWASFolderNew}/rules/REQUEST-905-COMMON-EXCEPTIONS.conf
include {pathToOWASFolderNew}/rules/REQUEST-910-IP-REPUTATION.conf
include {pathToOWASFolderNew}/rules/REQUEST-911-METHOD-ENFORCEMENT.conf
include {pathToOWASFolderNew}/rules/REQUEST-912-DOS-PROTECTION.conf
include {pathToOWASFolderNew}/rules/REQUEST-913-SCANNER-DETECTION.conf
include {pathToOWASFolderNew}/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf
include {pathToOWASFolderNew}/rules/REQUEST-921-PROTOCOL-ATTACK.conf
include {pathToOWASFolderNew}/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf
include {pathToOWASFolderNew}/rules/REQUEST-931-APPLICATION-ATTACK-RFI.conf
include {pathToOWASFolderNew}/rules/REQUEST-932-APPLICATION-ATTACK-RCE.conf
include {pathToOWASFolderNew}/rules/REQUEST-933-APPLICATION-ATTACK-PHP.conf
include {pathToOWASFolderNew}/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf
include {pathToOWASFolderNew}/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf
include {pathToOWASFolderNew}/rules/REQUEST-943-APPLICATION-ATTACK-SESSION-FIXATION.conf
include {pathToOWASFolderNew}/rules/REQUEST-949-BLOCKING-EVALUATION.conf
include {pathToOWASFolderNew}/rules/RESPONSE-950-DATA-LEAKAGES.conf
include {pathToOWASFolderNew}/rules/RESPONSE-951-DATA-LEAKAGES-SQL.conf
include {pathToOWASFolderNew}/rules/RESPONSE-952-DATA-LEAKAGES-JAVA.conf
include {pathToOWASFolderNew}/rules/RESPONSE-953-DATA-LEAKAGES-PHP.conf
include {pathToOWASFolderNew}/rules/RESPONSE-954-DATA-LEAKAGES-IIS.conf
include {pathToOWASFolderNew}/rules/RESPONSE-959-BLOCKING-EVALUATION.conf
include {pathToOWASFolderNew}/rules/RESPONSE-980-CORRELATION.conf
include {pathToOWASFolderNew}/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf
# CRS v4.0.0 uses a different structure - it has a main crs.conf file
content = """include {pathToOWASFolderNew}/crs.conf
"""
writeToFile = open('%s/owasp-master.conf' % (pathToOWASFolderNew), 'w')
writeToFile.write(content.replace('{pathToOWASFolderNew}', pathToOWASFolderNew))
@ -501,7 +488,7 @@ include {pathToOWASFolderNew}/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf
if ProcessUtilities.decideServer() == ProcessUtilities.OLS:
owaspRulesConf = """
modsecurity_rules_file /usr/local/lsws/conf/modsec/owasp-modsecurity-crs-3.0-master/owasp-master.conf
modsecurity_rules_file /usr/local/lsws/conf/modsec/owasp-modsecurity-crs-4.18.0/owasp-master.conf
"""
confFile = os.path.join(virtualHostUtilities.Server_root, "conf/httpd_config.conf")
@ -519,6 +506,14 @@ modsecurity_rules_file /usr/local/lsws/conf/modsec/owasp-modsecurity-crs-3.0-mas
conf.writelines(items)
conf.close()
# Verify the installation
owaspPath = os.path.join(virtualHostUtilities.Server_root, "conf/modsec/owasp-modsecurity-crs-4.18.0")
if not os.path.exists(owaspPath) or not os.path.exists(os.path.join(owaspPath, "owasp-master.conf")):
logging.CyberCPLogFileWriter.writeToFile("OWASP installation verification failed - files not found [installOWASP]")
print("0, OWASP installation verification failed")
return
else:
confFile = os.path.join('/usr/local/lsws/conf/modsec.conf')
confData = open(confFile).readlines()
@ -528,7 +523,7 @@ modsecurity_rules_file /usr/local/lsws/conf/modsec/owasp-modsecurity-crs-3.0-mas
for items in confData:
if items.find('/conf/comodo_litespeed/') > -1:
conf.writelines(items)
conf.write('Include /usr/local/lsws/conf/modsec/owasp-modsecurity-crs-3.0-master/*.conf\n')
conf.write('Include /usr/local/lsws/conf/modsec/owasp-modsecurity-crs-4.18.0/*.conf\n')
continue
else:
conf.writelines(items)
@ -536,7 +531,8 @@ modsecurity_rules_file /usr/local/lsws/conf/modsec/owasp-modsecurity-crs-3.0-mas
conf.close()
installUtilities.reStartLiteSpeed()
logging.CyberCPLogFileWriter.writeToFile("OWASP ModSecurity rules installed successfully [installOWASP]")
print("1,None")
except BaseException as msg:

View File

@ -150,10 +150,10 @@ class remoteBackup:
return [0, msg]
@staticmethod
def postRemoteTransfer(ipAddress, ownIP ,password, sshkey):
def postRemoteTransfer(ipAddress, ownIP ,password, sshkey, cyberPanelPort=8090):
try:
finalData = json.dumps({'username': "admin", "ipAddress": ownIP, "password": password})
url = "https://" + ipAddress + ":8090/api/remoteTransfer"
url = "https://" + ipAddress + ":" + str(cyberPanelPort) + "/api/remoteTransfer"
r = requests.post(url, data=finalData, verify=False)
data = json.loads(r.text)

View File

@ -78,6 +78,9 @@ class Renew:
try:
logging.writeToFile('Restarting mail services for them to see new SSL.', 0)
# Update mail SSL configuration for all domains
self._update_all_mail_ssl_configs()
commands = [
'postmap -F hash:/etc/postfix/vmail_ssl.map',
'systemctl restart postfix',
@ -93,6 +96,22 @@ class Renew:
except Exception as e:
logging.writeToFile(f'Error restarting services: {str(e)}', 1)
def _update_all_mail_ssl_configs(self) -> None:
"""Update mail SSL configuration for all domains after renewal"""
try:
logging.writeToFile('Updating mail SSL configurations for all domains.', 0)
# Update mail SSL config for all websites
for website in Websites.objects.filter(state=1):
virtualHostUtilities.updateMailSSLConfig(website.domain)
# Update mail SSL config for all child domains
for child in ChildDomains.objects.all():
virtualHostUtilities.updateMailSSLConfig(child.domain)
except Exception as e:
logging.writeToFile(f'Error updating mail SSL configs: {str(e)}', 1)
def SSLObtainer(self):
try:
logging.writeToFile('Running SSL Renew Utility')

View File

@ -9,6 +9,7 @@ import re
sys.path.append('/usr/local/CyberCP')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings")
from plogical.errorSanitizer import ErrorSanitizer
import shlex
import subprocess
import shutil
@ -567,8 +568,9 @@ class Upgrade:
writeToFile.writelines(varTmp)
writeToFile.close()
except BaseException as msg:
Upgrade.stdOut(str(msg) + " [mountTemp]", 0)
except Exception as e:
ErrorSanitizer.log_error_securely(e, 'mountTemp')
Upgrade.stdOut("Failed to mount temporary filesystem [mountTemp]", 0)
@staticmethod
def dockerUsers():
@ -738,8 +740,9 @@ $cfg['Servers'][$i]['LogoutURL'] = 'phpmyadminsignin.php?logout';
os.chdir(cwd)
except BaseException as msg:
Upgrade.stdOut(str(msg) + " [download_install_phpmyadmin]", 0)
except Exception as e:
ErrorSanitizer.log_error_securely(e, 'download_install_phpmyadmin')
Upgrade.stdOut("Failed to download and install phpMyAdmin [download_install_phpmyadmin]", 0)
@staticmethod
def setupComposer():
@ -1028,8 +1031,9 @@ $cfg['Servers'][$i]['LogoutURL'] = 'phpmyadminsignin.php?logout';
Upgrade.stdOut("SnappyMail installation completed.", 0)
except BaseException as msg:
Upgrade.stdOut(str(msg) + " [downoad_and_install_raindloop]", 0)
except Exception as e:
ErrorSanitizer.log_error_securely(e, 'downoad_and_install_raindloop')
Upgrade.stdOut("Failed to download and install Rainloop [downoad_and_install_raindloop]", 0)
return 1
@ -1049,8 +1053,9 @@ $cfg['Servers'][$i]['LogoutURL'] = 'phpmyadminsignin.php?logout';
pass
return (version_number + "." + version_build + ".tar.gz")
except BaseException as msg:
Upgrade.stdOut(str(msg) + ' [downloadLink]')
except Exception as e:
ErrorSanitizer.log_error_securely(e, 'downloadLink')
Upgrade.stdOut("Failed to download required files [downloadLink]")
os._exit(0)
@staticmethod
@ -1063,8 +1068,9 @@ $cfg['Servers'][$i]['LogoutURL'] = 'phpmyadminsignin.php?logout';
command = "chmod +x /usr/local/CyberCP/cli/cyberPanel.py"
Upgrade.executioner(command, 'CLI Permissions', 0)
except OSError as msg:
Upgrade.stdOut(str(msg) + " [setupCLI]")
except OSError as e:
ErrorSanitizer.log_error_securely(e, 'setupCLI')
Upgrade.stdOut("Failed to setup CLI [setupCLI]")
return 0
@staticmethod
@ -1136,8 +1142,9 @@ $cfg['Servers'][$i]['LogoutURL'] = 'phpmyadminsignin.php?logout';
cursor = conn.cursor()
return conn, cursor
except BaseException as msg:
Upgrade.stdOut(str(msg))
except Exception as e:
ErrorSanitizer.log_error_securely(e, 'database_connection')
Upgrade.stdOut("Failed to establish database connection")
return 0, 0
@staticmethod
@ -1381,8 +1388,8 @@ $cfg['Servers'][$i]['LogoutURL'] = 'phpmyadminsignin.php?logout';
try:
cursor.execute("UPDATE loginSystem_acl SET config = '%s' where name = 'admin'" % (Upgrade.AdminACL))
except BaseException as msg:
print(str(msg))
except Exception as e:
ErrorSanitizer.log_error_securely(e, 'applyLoginSystemMigrations')
try:
import sleep
except:
@ -2197,6 +2204,22 @@ CREATE TABLE `websiteFunctions_backupsv2` (`id` integer AUTO_INCREMENT NOT NULL
except:
pass
# Add new fields for network configuration and extra options
try:
cursor.execute('ALTER TABLE dockerManager_containers ADD network VARCHAR(100) DEFAULT "bridge"')
except:
pass
try:
cursor.execute('ALTER TABLE dockerManager_containers ADD network_mode VARCHAR(50) DEFAULT "bridge"')
except:
pass
try:
cursor.execute('ALTER TABLE dockerManager_containers ADD extra_options LONGTEXT DEFAULT "{}"')
except:
pass
try:
connection.close()
except:
@ -2983,8 +3006,9 @@ CREATE TABLE `websiteFunctions_backupsv2` (`id` integer AUTO_INCREMENT NOT NULL
return 1, None
except BaseException as msg:
return 0, str(msg)
except Exception as e:
ErrorSanitizer.log_error_securely(e, 'installLSCPD')
return 0, "Failed to install LSCPD"
@staticmethod
def installLSCPD(branch):
@ -3074,8 +3098,9 @@ CREATE TABLE `websiteFunctions_backupsv2` (`id` integer AUTO_INCREMENT NOT NULL
Upgrade.stdOut("LSCPD successfully installed!")
except BaseException as msg:
Upgrade.stdOut(str(msg) + " [installLSCPD]")
except Exception as e:
ErrorSanitizer.log_error_securely(e, 'installLSCPD')
Upgrade.stdOut("Failed to install LSCPD [installLSCPD]")
### disable dkim signing in rspamd in ref to https://github.com/usmannasir/cyberpanel/issues/1176
@staticmethod
@ -3363,8 +3388,9 @@ echo $oConfig->Save() ? 'Done' : 'Error';
Upgrade.stdOut("Permissions updated.")
except BaseException as msg:
Upgrade.stdOut(str(msg) + " [fixPermissions]")
except Exception as e:
ErrorSanitizer.log_error_securely(e, 'fixPermissions')
Upgrade.stdOut("Failed to fix permissions [fixPermissions]")
@staticmethod
def AutoUpgradeAcme():
@ -3807,8 +3833,9 @@ echo $oConfig->Save() ? 'Done' : 'Error';
Upgrade.stdOut("Dovecot upgraded.")
except BaseException as msg:
Upgrade.stdOut(str(msg) + " [upgradeDovecot]")
except Exception as e:
ErrorSanitizer.log_error_securely(e, 'upgradeDovecot')
Upgrade.stdOut("Failed to upgrade Dovecot [upgradeDovecot]")
@staticmethod
def installRestic():
@ -4052,317 +4079,181 @@ vmail
@staticmethod
def CreateMissingPoolsforFPM():
##### apache configs
"""
Create missing PHP-FPM pool configurations for all PHP versions.
This function ensures all PHP versions have proper pool configurations
to prevent ImunifyAV/Imunify360 installation failures.
"""
try:
# Detect OS and set paths
CentOSPath = '/etc/redhat-release'
if os.path.exists(CentOSPath):
# CentOS/RHEL/CloudLinux paths
serverRootPath = '/etc/httpd'
configBasePath = '/etc/httpd/conf.d/'
sockPath = '/var/run/php-fpm/'
runAsUser = 'apache'
group = 'nobody'
# Define PHP pool paths for CentOS
php_paths = {
'5.4': '/opt/remi/php54/root/etc/php-fpm.d/',
'5.5': '/opt/remi/php55/root/etc/php-fpm.d/',
'5.6': '/etc/opt/remi/php56/php-fpm.d/',
'7.0': '/etc/opt/remi/php70/php-fpm.d/',
'7.1': '/etc/opt/remi/php71/php-fpm.d/',
'7.2': '/etc/opt/remi/php72/php-fpm.d/',
'7.3': '/etc/opt/remi/php73/php-fpm.d/',
'7.4': '/etc/opt/remi/php74/php-fpm.d/',
'8.0': '/etc/opt/remi/php80/php-fpm.d/',
'8.1': '/etc/opt/remi/php81/php-fpm.d/',
'8.2': '/etc/opt/remi/php82/php-fpm.d/',
'8.3': '/etc/opt/remi/php83/php-fpm.d/',
'8.4': '/etc/opt/remi/php84/php-fpm.d/',
'8.5': '/etc/opt/remi/php85/php-fpm.d/'
}
else:
# Ubuntu/Debian paths
serverRootPath = '/etc/apache2'
configBasePath = '/etc/apache2/sites-enabled/'
sockPath = '/var/run/php/'
runAsUser = 'www-data'
group = 'nogroup'
# Define PHP pool paths for Ubuntu
php_paths = {
'5.4': '/etc/php/5.4/fpm/pool.d/',
'5.5': '/etc/php/5.5/fpm/pool.d/',
'5.6': '/etc/php/5.6/fpm/pool.d/',
'7.0': '/etc/php/7.0/fpm/pool.d/',
'7.1': '/etc/php/7.1/fpm/pool.d/',
'7.2': '/etc/php/7.2/fpm/pool.d/',
'7.3': '/etc/php/7.3/fpm/pool.d/',
'7.4': '/etc/php/7.4/fpm/pool.d/',
'8.0': '/etc/php/8.0/fpm/pool.d/',
'8.1': '/etc/php/8.1/fpm/pool.d/',
'8.2': '/etc/php/8.2/fpm/pool.d/',
'8.3': '/etc/php/8.3/fpm/pool.d/',
'8.4': '/etc/php/8.4/fpm/pool.d/',
'8.5': '/etc/php/8.5/fpm/pool.d/'
}
CentOSPath = '/etc/redhat-release'
# Check if server root exists
if not os.path.exists(serverRootPath):
logging.CyberCPLogFileWriter.writeToFile(f'Server root path not found: {serverRootPath}')
return 1
if os.path.exists(CentOSPath):
# Create pool configurations for all PHP versions
for version, pool_path in php_paths.items():
if os.path.exists(pool_path):
www_conf = os.path.join(pool_path, 'www.conf')
# Skip if www.conf already exists
if os.path.exists(www_conf):
logging.CyberCPLogFileWriter.writeToFile(f'PHP {version} pool config already exists: {www_conf}')
continue
# Create the pool configuration
pool_name = f'php{version.replace(".", "")}default'
sock_name = f'php{version}-fpm.sock'
content = f'''[{pool_name}]
user = {runAsUser}
group = {runAsUser}
listen = {sockPath}{sock_name}
listen.owner = {runAsUser}
listen.group = {group}
listen.mode = 0660
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 1000
pm.status_path = /status
ping.path = /ping
ping.response = pong
request_terminate_timeout = 300
request_slowlog_timeout = 10
slowlog = /var/log/php{version}-fpm-slow.log
'''
try:
# Write the configuration file
with open(www_conf, 'w') as f:
f.write(content)
# Set proper permissions
os.chown(www_conf, 0, 0) # root:root
os.chmod(www_conf, 0o644)
logging.CyberCPLogFileWriter.writeToFile(f'Created PHP {version} pool config: {www_conf}')
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Error creating PHP {version} pool config: {str(e)}')
else:
logging.CyberCPLogFileWriter.writeToFile(f'PHP {version} pool directory not found: {pool_path}')
serverRootPath = '/etc/httpd'
configBasePath = '/etc/httpd/conf.d/'
php54Path = '/opt/remi/php54/root/etc/php-fpm.d/'
php55Path = '/opt/remi/php55/root/etc/php-fpm.d/'
php56Path = '/etc/opt/remi/php56/php-fpm.d/'
php70Path = '/etc/opt/remi/php70/php-fpm.d/'
php71Path = '/etc/opt/remi/php71/php-fpm.d/'
php72Path = '/etc/opt/remi/php72/php-fpm.d/'
php73Path = '/etc/opt/remi/php73/php-fpm.d/'
php74Path = '/etc/opt/remi/php74/php-fpm.d/'
php80Path = '/etc/opt/remi/php80/php-fpm.d/'
php81Path = '/etc/opt/remi/php81/php-fpm.d/'
php82Path = '/etc/opt/remi/php82/php-fpm.d/'
php83Path = '/etc/opt/remi/php83/php-fpm.d/'
php84Path = '/etc/opt/remi/php84/php-fpm.d/'
php85Path = '/etc/opt/remi/php85/php-fpm.d/'
serviceName = 'httpd'
sockPath = '/var/run/php-fpm/'
runAsUser = 'apache'
else:
serverRootPath = '/etc/apache2'
configBasePath = '/etc/apache2/sites-enabled/'
php54Path = '/etc/php/5.4/fpm/pool.d/'
php55Path = '/etc/php/5.5/fpm/pool.d/'
php56Path = '/etc/php/5.6/fpm/pool.d/'
php70Path = '/etc/php/7.0/fpm/pool.d/'
php71Path = '/etc/php/7.1/fpm/pool.d/'
php72Path = '/etc/php/7.2/fpm/pool.d/'
php73Path = '/etc/php/7.3/fpm/pool.d/'
php74Path = '/etc/php/7.4/fpm/pool.d/'
php80Path = '/etc/php/8.0/fpm/pool.d/'
php81Path = '/etc/php/8.1/fpm/pool.d/'
php82Path = '/etc/php/8.2/fpm/pool.d/'
php83Path = '/etc/php/8.3/fpm/pool.d/'
php84Path = '/etc/php/8.4/fpm/pool.d/'
php85Path = '/etc/php/8.5/fpm/pool.d/'
serviceName = 'apache2'
sockPath = '/var/run/php/'
runAsUser = 'www-data'
#####
if not os.path.exists(serverRootPath):
# Restart PHP-FPM services to apply configurations
Upgrade.restartPHPFPMServices()
return 0
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Error in CreateMissingPoolsforFPM: {str(e)}')
return 1
if os.path.exists(php54Path):
content = f"""
[php54default]
user = {runAsUser}
group = {runAsUser}
listen ={sockPath}php5.4-fpm.sock
listen.owner = {runAsUser}
listen.group = {runAsUser}
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
"""
WriteToFile = open(f'{php54Path}www.conf', 'w')
WriteToFile.write(content)
WriteToFile.close()
if os.path.exists(php55Path):
content = f'''
[php55default]
user = {runAsUser}
group = {runAsUser}
listen ={sockPath}php5.5-fpm.sock
listen.owner = {runAsUser}
listen.group = {runAsUser}
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
'''
WriteToFile = open(f'{php55Path}www.conf', 'w')
WriteToFile.write(content)
WriteToFile.close()
if os.path.exists(php56Path):
content = f'''
[php56default]
user = {runAsUser}
group = {runAsUser}
listen ={sockPath}php5.6-fpm.sock
listen.owner = {runAsUser}
listen.group = {runAsUser}
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
'''
WriteToFile = open(f'{php56Path}www.conf', 'w')
WriteToFile.write(content)
WriteToFile.close()
if os.path.exists(php70Path):
content = f'''
[php70default]
user = {runAsUser}
group = {runAsUser}
listen ={sockPath}php7.0-fpm.sock
listen.owner = {runAsUser}
listen.group = {runAsUser}
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
'''
WriteToFile = open(f'{php70Path}www.conf', 'w')
WriteToFile.write(content)
WriteToFile.close()
if os.path.exists(php71Path):
content = f'''
[php71default]
user = {runAsUser}
group = {runAsUser}
listen ={sockPath}php7.1-fpm.sock
listen.owner = {runAsUser}
listen.group = {runAsUser}
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
'''
WriteToFile = open(f'{php71Path}www.conf', 'w')
WriteToFile.write(content)
WriteToFile.close()
if os.path.exists(php72Path):
content = f'''
[php72default]
user = {runAsUser}
group = {runAsUser}
listen ={sockPath}php7.2-fpm.sock
listen.owner = {runAsUser}
listen.group = {runAsUser}
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
'''
WriteToFile = open(f'{php72Path}www.conf', 'w')
WriteToFile.write(content)
WriteToFile.close()
if os.path.exists(php73Path):
content = f'''
[php73default]
user = {runAsUser}
group = {runAsUser}
listen ={sockPath}php7.3-fpm.sock
listen.owner = {runAsUser}
listen.group = {runAsUser}
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
'''
WriteToFile = open(f'{php73Path}www.conf', 'w')
WriteToFile.write(content)
WriteToFile.close()
if os.path.exists(php74Path):
content = f'''
[php74default]
user = {runAsUser}
group = {runAsUser}
listen ={sockPath}php7.4-fpm.sock
listen.owner = {runAsUser}
listen.group = {runAsUser}
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
'''
WriteToFile = open(f'{php74Path}www.conf', 'w')
WriteToFile.write(content)
WriteToFile.close()
if os.path.exists(php80Path):
content = f'''
[php80default]
user = {runAsUser}
group = {runAsUser}
listen ={sockPath}php8.0-fpm.sock
listen.owner = {runAsUser}
listen.group = {runAsUser}
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
'''
WriteToFile = open(f'{php80Path}www.conf', 'w')
WriteToFile.write(content)
WriteToFile.close()
if os.path.exists(php81Path):
content = f'''
[php81default]
user = {runAsUser}
group = {runAsUser}
listen ={sockPath}php8.1-fpm.sock
listen.owner = {runAsUser}
listen.group = {runAsUser}
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
'''
WriteToFile = open(f'{php81Path}www.conf', 'w')
WriteToFile.write(content)
WriteToFile.close()
if os.path.exists(php82Path):
content = f'''
[php82default]
user = {runAsUser}
group = {runAsUser}
listen ={sockPath}php8.2-fpm.sock
listen.owner = {runAsUser}
listen.group = {runAsUser}
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
@staticmethod
def restartPHPFPMServices():
"""
Restart all PHP-FPM services to apply new pool configurations.
This ensures that ImunifyAV/Imunify360 installation will work properly.
"""
try:
# Define all possible PHP versions
php_versions = ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
'''
WriteToFile = open(f'{php82Path}www.conf', 'w')
WriteToFile.write(content)
WriteToFile.close()
if os.path.exists(php83Path):
content = f'''
[php83default]
user = {runAsUser}
group = {runAsUser}
listen ={sockPath}php8.3-fpm.sock
listen.owner = {runAsUser}
listen.group = {runAsUser}
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
'''
WriteToFile = open(f'{php83Path}www.conf', 'w')
WriteToFile.write(content)
WriteToFile.close()
if os.path.exists(php84Path):
content = f'''
[php84default]
user = {runAsUser}
group = {runAsUser}
listen ={sockPath}php8.4-fpm.sock
listen.owner = {runAsUser}
listen.group = {runAsUser}
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
'''
WriteToFile = open(f'{php84Path}www.conf', 'w')
WriteToFile.write(content)
WriteToFile.close()
if os.path.exists(php85Path):
content = f'''
[php85default]
user = {runAsUser}
group = {runAsUser}
listen ={sockPath}php8.5-fpm.sock
listen.owner = {runAsUser}
listen.group = {runAsUser}
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
'''
WriteToFile = open(f'{php85Path}www.conf', 'w')
WriteToFile.write(content)
WriteToFile.close()
restarted_count = 0
total_count = 0
for version in php_versions:
service_name = f'php{version}-fpm'
# Check if service exists
try:
result = subprocess.run(['systemctl', 'list-unit-files', service_name],
capture_output=True, text=True, timeout=10)
if result.returncode == 0 and service_name in result.stdout:
total_count += 1
# Restart the service
restart_result = subprocess.run(['systemctl', 'restart', service_name],
capture_output=True, text=True, timeout=30)
if restart_result.returncode == 0:
# Check if service is actually running
status_result = subprocess.run(['systemctl', 'is-active', service_name],
capture_output=True, text=True, timeout=10)
if status_result.returncode == 0 and 'active' in status_result.stdout:
restarted_count += 1
logging.CyberCPLogFileWriter.writeToFile(f'Successfully restarted {service_name}')
else:
logging.CyberCPLogFileWriter.writeToFile(f'Warning: {service_name} restarted but not active')
else:
logging.CyberCPLogFileWriter.writeToFile(f'Failed to restart {service_name}: {restart_result.stderr}')
except subprocess.TimeoutExpired:
logging.CyberCPLogFileWriter.writeToFile(f'Timeout restarting {service_name}')
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Error restarting {service_name}: {str(e)}')
logging.CyberCPLogFileWriter.writeToFile(f'PHP-FPM restart summary: {restarted_count}/{total_count} services restarted successfully')
return restarted_count, total_count
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Error in restartPHPFPMServices: {str(e)}')
return 0, 0
@staticmethod
def setupPHPSymlink():
@ -4406,8 +4297,9 @@ pm.max_spare_servers = 3
Upgrade.stdOut(f"PHP symlink updated to PHP {selected_php} successfully.")
except BaseException as msg:
Upgrade.stdOut('[ERROR] ' + str(msg) + " [setupPHPSymlink]")
except Exception as e:
ErrorSanitizer.log_error_securely(e, 'setupPHPSymlink')
Upgrade.stdOut('[ERROR] Failed to setup PHP symlink [setupPHPSymlink]')
return 0
return 1
@ -5219,8 +5111,9 @@ extprocessor proxyApacheBackendSSL {
return 1
except BaseException as msg:
print("[ERROR] installQuota. " + str(msg))
except Exception as e:
ErrorSanitizer.log_error_securely(e, 'installQuota')
print("[ERROR] installQuota. Failed to install quota")
return 0
@staticmethod

View File

@ -196,9 +196,15 @@ class vhost:
command = 'mkdir -p /usr/local/lsws/Example/html/.well-known/acme-challenge'
ProcessUtilities.normalExecutioner(command)
path = "/home/" + virtualHostName
pathHTML = "/home/" + virtualHostName + "/public_html"
pathLogs = "/home/" + virtualHostName + "/logs"
# Get user's home directory dynamically
from userManagment.homeDirectoryUtils import HomeDirectoryUtils
home_path = HomeDirectoryUtils.getUserHomeDirectory(virtualHostUser)
if not home_path:
home_path = "/home" # Fallback to default
path = os.path.join(home_path, virtualHostName)
pathHTML = os.path.join(home_path, virtualHostName, "public_html")
pathLogs = os.path.join(home_path, virtualHostName, "logs")
confPath = vhost.Server_root + "/conf/vhosts/"+virtualHostName
completePathToConfigFile = confPath +"/vhost.conf"

View File

@ -796,6 +796,12 @@ local_name %s {
print("0," + parsed_error)
return 0, parsed_error
# Update vhost SSL configuration with new certificate paths
virtualHostUtilities.updateVhostSSLConfig(virtualHost)
# Update mail SSL configuration for this domain
virtualHostUtilities.updateMailSSLConfig(virtualHost)
installUtilities.installUtilities.reStartLiteSpeed()
command = 'systemctl restart postfix'
@ -891,8 +897,50 @@ local_name %s {
print("0, %s file is symlinked." % (fileName))
return 0
numberOfTotalLines = int(
ProcessUtilities.outputExecutioner('wc -l %s' % (fileName), externalApp).split(" ")[0])
# Improved wc -l parsing with better error handling
wc_output = ProcessUtilities.outputExecutioner('wc -l %s' % (fileName), externalApp)
# Handle different wc output formats and potential errors
if wc_output and wc_output.strip():
# Split by whitespace and take the first part that looks like a number
wc_parts = wc_output.strip().split()
numberOfTotalLines = 0
for part in wc_parts:
try:
numberOfTotalLines = int(part)
break
except ValueError:
continue
# If no valid number found, try to extract from common wc error formats
if numberOfTotalLines == 0:
# Handle cases like "wc: filename: No such file or directory"
if "No such file or directory" in wc_output:
print("1,None")
return "1,None"
# Handle cases where wc returns just "wc:" or similar
if "wc:" in wc_output:
# Try to get line count using alternative method
try:
alt_output = ProcessUtilities.outputExecutioner('cat %s | wc -l' % (fileName), externalApp)
if alt_output and alt_output.strip():
alt_parts = alt_output.strip().split()
for part in alt_parts:
try:
numberOfTotalLines = int(part)
break
except ValueError:
continue
except:
pass
if numberOfTotalLines == 0:
print("1,None")
return "1,None"
else:
print("1,None")
return "1,None"
if numberOfTotalLines < 25:
data = ProcessUtilities.outputExecutioner('cat %s' % (fileName), externalApp)
@ -1079,6 +1127,84 @@ local_name %s {
print("0," + str(msg))
return 0, str(msg)
@staticmethod
def updateVhostSSLConfig(virtualHost):
"""Update vhost SSL configuration with new certificate paths"""
try:
logging.CyberCPLogFileWriter.writeToFile(f"Updating vhost SSL configuration for {virtualHost}")
# Update vhost configuration file
vhostConfPath = f'/usr/local/lsws/conf/vhosts/{virtualHost}/vhost.conf'
if os.path.exists(vhostConfPath):
with open(vhostConfPath, 'r') as f:
content = f.read()
# Update SSL certificate paths in vhost configuration
new_ssl_config = f"""vhssl {{
keyFile /etc/letsencrypt/live/{virtualHost}/privkey.pem
certFile /etc/letsencrypt/live/{virtualHost}/fullchain.pem
certChain 1
sslProtocol 24
enableECDHE 1
renegProtection 1
sslSessionCache 1
enableSpdy 15
enableStapling 1
ocspRespMaxAge 86400
}}"""
# Replace existing vhssl block
import re
pattern = r'vhssl\s*\{[^}]*\}'
if re.search(pattern, content, re.DOTALL):
content = re.sub(pattern, new_ssl_config, content, flags=re.DOTALL)
else:
# Add vhssl block if it doesn't exist
content += f"\n{new_ssl_config}\n"
with open(vhostConfPath, 'w') as f:
f.write(content)
logging.CyberCPLogFileWriter.writeToFile(f"Updated vhost SSL configuration for {virtualHost}")
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error updating vhost SSL config for {virtualHost}: {str(e)}")
@staticmethod
def updateMailSSLConfig(virtualHost):
"""Update mail SSL configuration with new certificate paths"""
try:
logging.CyberCPLogFileWriter.writeToFile(f"Updating mail SSL configuration for {virtualHost}")
# Update vmail_ssl.map file
postfixMapFile = '/etc/postfix/vmail_ssl.map'
if os.path.exists(postfixMapFile):
with open(postfixMapFile, 'r') as f:
content = f.read()
# Remove old entries for this domain
lines = content.split('\n')
new_lines = []
for line in lines:
if not line.startswith(f'{virtualHost} ') and not line.startswith(f'mail.{virtualHost} '):
new_lines.append(line)
# Add new entries
new_lines.append(f'{virtualHost} /etc/letsencrypt/live/{virtualHost}/privkey.pem /etc/letsencrypt/live/{virtualHost}/fullchain.pem')
new_lines.append(f'mail.{virtualHost} /etc/letsencrypt/live/{virtualHost}/privkey.pem /etc/letsencrypt/live/{virtualHost}/fullchain.pem')
with open(postfixMapFile, 'w') as f:
f.write('\n'.join(new_lines))
# Update postfix map database
command = 'postmap -F hash:/etc/postfix/vmail_ssl.map'
ProcessUtilities.executioner(command)
logging.CyberCPLogFileWriter.writeToFile(f"Updated mail SSL configuration for {virtualHost}")
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error updating mail SSL config for {virtualHost}: {str(e)}")
@staticmethod
def issueSSLForMailServer(virtualHost, path):
try:

View File

@ -6,6 +6,7 @@ import argparse
import os
import shutil
import time
import django
from plogical.processUtilities import ProcessUtilities
class pluginInstaller:
@ -13,6 +14,32 @@ class pluginInstaller:
tempRulesFile = "/home/cyberpanel/tempModSecRules"
mirrorPath = "cyberpanel.net"
@staticmethod
def getUrlPattern(pluginName):
"""
Generate URL pattern compatible with both Django 2.x and 3.x+
Django 2.x uses url() with regex patterns
Django 3.x+ prefers path() with simpler patterns
"""
try:
django_version = django.get_version()
major_version = int(django_version.split('.')[0])
pluginInstaller.stdOut(f"Django version detected: {django_version}")
if major_version >= 3:
# Django 3.x+ - use path() syntax
pluginInstaller.stdOut(f"Using path() syntax for Django 3.x+ compatibility")
return " path('" + pluginName + "/',include('" + pluginName + ".urls')),\n"
else:
# Django 2.x - use url() syntax with regex
pluginInstaller.stdOut(f"Using url() syntax for Django 2.x compatibility")
return " url(r'^" + pluginName + "/',include('" + pluginName + ".urls')),\n"
except Exception as e:
# Fallback to modern path() syntax if version detection fails
pluginInstaller.stdOut(f"Django version detection failed: {str(e)}, using path() syntax as fallback")
return " path('" + pluginName + "/',include('" + pluginName + ".urls')),\n"
@staticmethod
def stdOut(message):
print("\n\n")
@ -57,7 +84,7 @@ class pluginInstaller:
for items in data:
if items.find("manageservices") > -1:
writeToFile.writelines(items)
writeToFile.writelines(" url(r'^" + pluginName + "/',include('" + pluginName + ".urls')),\n")
writeToFile.writelines(pluginInstaller.getUrlPattern(pluginName))
else:
writeToFile.writelines(items)

121
scripts/enable_ftp_quota.sh Normal file
View File

@ -0,0 +1,121 @@
#!/bin/bash
# Enable FTP User Quota Feature
# This script applies the quota configuration and restarts Pure-FTPd
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging function
log_message() {
echo -e "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a /var/log/cyberpanel_ftp_quota.log
}
log_message "${BLUE}Starting FTP Quota Feature Setup...${NC}"
# Check if running as root
if [ "$EUID" -ne 0 ]; then
log_message "${RED}Please run as root${NC}"
exit 1
fi
# Backup existing configurations
log_message "${YELLOW}Backing up existing Pure-FTPd configurations...${NC}"
if [ -f /etc/pure-ftpd/pure-ftpd.conf ]; then
cp /etc/pure-ftpd/pure-ftpd.conf /etc/pure-ftpd/pure-ftpd.conf.backup.$(date +%Y%m%d_%H%M%S)
log_message "${GREEN}Backed up pure-ftpd.conf${NC}"
fi
if [ -f /etc/pure-ftpd/pureftpd-mysql.conf ]; then
cp /etc/pure-ftpd/pureftpd-mysql.conf /etc/pure-ftpd/pureftpd-mysql.conf.backup.$(date +%Y%m%d_%H%M%S)
log_message "${GREEN}Backed up pureftpd-mysql.conf${NC}"
fi
# Apply new configurations
log_message "${YELLOW}Applying FTP quota configurations...${NC}"
# Copy the updated configurations
if [ -f /usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf ]; then
cp /usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf /etc/pure-ftpd/pure-ftpd.conf
log_message "${GREEN}Updated pure-ftpd.conf${NC}"
fi
if [ -f /usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf ]; then
cp /usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf /etc/pure-ftpd/pureftpd-mysql.conf
log_message "${GREEN}Updated pureftpd-mysql.conf${NC}"
fi
# Check if Pure-FTPd is running
if systemctl is-active --quiet pure-ftpd; then
log_message "${YELLOW}Restarting Pure-FTPd service...${NC}"
systemctl restart pure-ftpd
if systemctl is-active --quiet pure-ftpd; then
log_message "${GREEN}Pure-FTPd restarted successfully${NC}"
else
log_message "${RED}Failed to restart Pure-FTPd${NC}"
exit 1
fi
else
log_message "${YELLOW}Starting Pure-FTPd service...${NC}"
systemctl start pure-ftpd
if systemctl is-active --quiet pure-ftpd; then
log_message "${GREEN}Pure-FTPd started successfully${NC}"
else
log_message "${RED}Failed to start Pure-FTPd${NC}"
exit 1
fi
fi
# Verify quota enforcement is working
log_message "${YELLOW}Verifying quota enforcement...${NC}"
# Check if quota queries are in the configuration
if grep -q "MYSQLGetQTAFS" /etc/pure-ftpd/pureftpd-mysql.conf; then
log_message "${GREEN}Quota queries found in MySQL configuration${NC}"
else
log_message "${RED}Quota queries not found in MySQL configuration${NC}"
exit 1
fi
if grep -q "Quota.*yes" /etc/pure-ftpd/pure-ftpd.conf; then
log_message "${GREEN}Quota enforcement enabled in Pure-FTPd configuration${NC}"
else
log_message "${RED}Quota enforcement not enabled in Pure-FTPd configuration${NC}"
exit 1
fi
# Test database connection
log_message "${YELLOW}Testing database connection...${NC}"
# Get database credentials from configuration
MYSQL_USER=$(grep "MYSQLUser" /etc/pure-ftpd/pureftpd-mysql.conf | cut -d' ' -f2)
MYSQL_PASS=$(grep "MYSQLPassword" /etc/pure-ftpd/pureftpd-mysql.conf | cut -d' ' -f2)
MYSQL_DB=$(grep "MYSQLDatabase" /etc/pure-ftpd/pureftpd-mysql.conf | cut -d' ' -f2)
if mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" -e "USE $MYSQL_DB; SELECT COUNT(*) FROM users;" >/dev/null 2>&1; then
log_message "${GREEN}Database connection successful${NC}"
else
log_message "${RED}Database connection failed${NC}"
exit 1
fi
log_message "${GREEN}FTP User Quota feature has been successfully enabled!${NC}"
log_message "${BLUE}Features enabled:${NC}"
log_message " - Individual FTP user quotas"
log_message " - Custom quota sizes per user"
log_message " - Package default quota fallback"
log_message " - Real-time quota enforcement by Pure-FTPd"
log_message " - Web interface for quota management"
log_message "${YELLOW}Note: Existing FTP users will need to have their quotas updated through the web interface to take effect.${NC}"
exit 0

View File

@ -469,6 +469,117 @@ class ServerStatusUtil(multi.Thread):
except BaseException as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg))
@staticmethod
def switchToOLS():
"""Switch back to OpenLiteSpeed from LiteSpeed Enterprise"""
try:
os.environ['TERM'] = "xterm-256color"
statusFile = open(ServerStatusUtil.lswsInstallStatusPath, 'w')
FNULL = open(os.devnull, 'w')
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, "Starting switch back to OpenLiteSpeed..\n")
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, "Stopping LiteSpeed Enterprise..\n", 1)
# Stop current LiteSpeed Enterprise
ProcessUtilities.killLiteSpeed()
# Check if backup exists
if not os.path.exists('/usr/local/lswsbak'):
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, "No OpenLiteSpeed backup found. Cannot switch back. [404]", 1)
return 0
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, "Removing LiteSpeed Enterprise..\n", 1)
# Remove current LiteSpeed Enterprise installation
if os.path.exists('/usr/local/lsws'):
shutil.rmtree('/usr/local/lsws')
# Restore OpenLiteSpeed from backup
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, "Restoring OpenLiteSpeed from backup..\n", 1)
command = 'mv /usr/local/lswsbak /usr/local/lsws'
if ServerStatusUtil.executioner(command, statusFile) == 0:
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, "Failed to restore OpenLiteSpeed. [404]", 1)
return 0
# Install OpenLiteSpeed if not already installed
if not os.path.exists('/usr/local/lsws/bin/openlitespeed'):
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, "Installing OpenLiteSpeed..\n", 1)
if os.path.exists('/etc/redhat-release'):
command = 'yum -y install openlitespeed'
else:
command = 'apt-get -y install openlitespeed'
if ServerStatusUtil.executioner(command, statusFile) == 0:
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, "Failed to install OpenLiteSpeed. [404]", 1)
return 0
# Rebuild vhost configurations for OpenLiteSpeed
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, "Rebuilding vhost configurations..\n", 1)
ServerStatusUtil.rebuildvConf()
# Start OpenLiteSpeed
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, "Starting OpenLiteSpeed..\n", 1)
ProcessUtilities.restartLitespeed()
# Clean up any Enterprise-specific cron jobs
CentOSPath = '/etc/redhat-release'
if os.path.exists(CentOSPath):
cronPath = '/var/spool/cron/root'
else:
cronPath = '/var/spool/cron/crontabs/root'
if os.path.exists(cronPath):
data = open(cronPath, 'r').readlines()
writeToFile = open(cronPath, 'w')
for items in data:
if items.find('-maxdepth 2 -type f -newer') > -1:
pass
else:
writeToFile.writelines(items)
writeToFile.close()
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, "Successfully switched back to OpenLiteSpeed. [200]\n", 1)
return 1
except BaseException as msg:
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, f"Error switching back to OpenLiteSpeed: {str(msg)}. [404]", 1)
logging.CyberCPLogFileWriter.writeToFile(str(msg))
return 0
@staticmethod
def switchToOLSCLI():
"""CLI version of switch back to OpenLiteSpeed"""
try:
ssu = ServerStatusUtil('')
ssu.start()
while(True):
command = 'sudo cat ' + ServerStatusUtil.lswsInstallStatusPath
output = ProcessUtilities.outputExecutioner(command)
if output.find('[404]') > -1:
command = "sudo rm -f " + ServerStatusUtil.lswsInstallStatusPath
ProcessUtilities.popenExecutioner(command)
data_ret = {'status': 1, 'abort': 1, 'requestStatus': output, 'installed': 0}
print(str(data_ret))
return 0
elif output.find('[200]') > -1:
command = "sudo rm -f " + ServerStatusUtil.lswsInstallStatusPath
ProcessUtilities.popenExecutioner(command)
data_ret = {'status': 1, 'abort': 1, 'requestStatus': 'Successfully switched back to OpenLiteSpeed.', 'installed': 1}
print(str(data_ret))
return 1
else:
data_ret = {'status': 1, 'abort': 0, 'requestStatus': output, 'installed': 0}
time.sleep(2)
except BaseException as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg))
def main():
parser = argparse.ArgumentParser(description='Server Status Util.')
@ -479,6 +590,8 @@ def main():
if args.function == "switchTOLSWS":
ServerStatusUtil.switchTOLSWS(args.licenseKey)
elif args.function == "switchToOLS":
ServerStatusUtil.switchToOLS()
if __name__ == "__main__":

View File

@ -778,6 +778,95 @@ app.controller('lswsSwitch', function ($scope, $http, $timeout, $window) {
});
/* Controller for switching back to OpenLiteSpeed */
app.controller('switchToOLS', function ($scope, $http, $timeout, $window) {
$scope.cyberPanelLoading = true;
$scope.installBoxGen = true;
$scope.switchToOLS = function () {
$scope.cyberPanelLoading = false;
$scope.installBoxGen = true;
url = "/serverstatus/switchToOLS";
var data = {};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
$scope.cyberPanelLoading = true;
if (response.data.status === 1) {
$scope.installBoxGen = false;
getRequestStatus();
} else {
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message,
type: 'error'
});
}
}
function cantLoadInitialDatas(response) {
$scope.cyberPanelLoading = true;
new PNotify({
title: 'Operation Failed!',
text: 'Could not connect to server, please refresh this page',
type: 'error'
});
}
};
function getRequestStatus() {
$scope.cyberPanelLoading = false;
url = "/serverstatus/switchToOLSStatus";
var data = {};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
if (response.data.abort === 0) {
$scope.requestData = response.data.requestStatus;
$timeout(getRequestStatus, 1000);
} else {
// Notifications
$scope.cyberPanelLoading = true;
$timeout.cancel();
$scope.requestData = response.data.requestStatus;
if (response.data.installed === 1) {
$timeout(function () {
$window.location.reload();
}, 3000);
}
}
}
function cantLoadInitialDatas(response) {
$scope.cyberPanelLoading = true;
new PNotify({
title: 'Operation Failed!',
text: 'Could not connect to server, please refresh this page',
type: 'error'
});
}
}
});
app.controller('topProcesses', function ($scope, $http, $timeout) {
$scope.cyberPanelLoading = true;

View File

@ -829,6 +829,65 @@
</form>
</div>
</div>
<!-- Switch Back to OpenLiteSpeed for Enterprise Users -->
<div class="license-panel" ng-controller="switchToOLS">
<div class="card-header">
<h3 class="card-title">
<div class="card-icon">
<i class="fas fa-undo"></i>
</div>
{% trans "Switch Back to OpenLiteSpeed" %}
</h3>
<span ng-hide="cyberPanelLoading" class="loading-spinner"></span>
</div>
<div class="card-content">
<div ng-show="installBoxGen">
<div class="alert alert-info">
<i class="fas fa-info-circle alert-icon"></i>
<div class="alert-content">
<div class="alert-title">{% trans "Switch Back to OpenLiteSpeed" %}</div>
<div class="alert-message">
{% trans "You can switch back to OpenLiteSpeed at any time, even during your trial period or after it expires. This will restore your previous OpenLiteSpeed configuration." %}
</div>
</div>
</div>
<div class="action-buttons">
<button type="button" ng-click="switchToOLS()" class="btn btn-primary">
<i class="fas fa-undo"></i>
{% trans "Switch Back to OpenLiteSpeed" %}
</button>
</div>
<div style="margin-top: 2rem;">
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle alert-icon"></i>
<div class="alert-content">
<div class="alert-title">{% trans "Important Notes" %}</div>
<div class="alert-message">
{% trans "• This will restore your previous OpenLiteSpeed configuration from backup" %}<br>
{% trans "• All LiteSpeed Enterprise features will be disabled" %}<br>
{% trans "• You can switch back to Enterprise anytime with a valid license" %}<br>
{% trans "• The process may take a few minutes to complete" %}
</div>
</div>
</div>
</div>
</div>
<!-- Switch Back Progress -->
<div ng-hide="installBoxGen">
<h3 style="text-align: center; margin-bottom: 1.5rem; display: flex; align-items: center; justify-content: center; gap: 1rem;">
<i class="fas fa-undo" style="font-size: 2rem; color: var(--accent-color);"></i>
{% trans "Switching Back to OpenLiteSpeed" %}
<span ng-hide="cyberPanelLoading" class="loading-spinner"></span>
</h3>
<div class="console-output" ng-bind="requestData"></div>
</div>
</div>
</div>
{% endif %}
{% if OLS %}
@ -889,12 +948,12 @@
</div>
</div>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle alert-icon"></i>
<div class="alert alert-info">
<i class="fas fa-info-circle alert-icon"></i>
<div class="alert-content">
<div class="alert-title" style="color: var(--danger-accent);">{% trans "Important Warning" %}</div>
<div class="alert-title">{% trans "Trial Information" %}</div>
<div class="alert-message">
{% trans "You cannot revert back to OpenLiteSpeed if you choose not to purchase a LiteSpeed Enterprise license after the 15 day trial period. We recommend you test the Enterprise trial on a separate server." %}
{% trans "You can switch back to OpenLiteSpeed at any time, even during or after the trial period. The system automatically creates a backup of your OpenLiteSpeed configuration before switching to Enterprise." %}
</div>
</div>
</div>

View File

@ -13,6 +13,8 @@ urlpatterns = [
path('services', views.services, name='services'),
path('switchTOLSWS', views.switchTOLSWS, name='switchTOLSWS'),
path('switchTOLSWSStatus', views.switchTOLSWSStatus, name='switchTOLSWSStatus'),
path('switchToOLS', views.switchToOLS, name='switchToOLS'),
path('switchToOLSStatus', views.switchToOLSStatus, name='switchToOLSStatus'),
path('licenseStatus', views.licenseStatus, name='licenseStatus'),
path('changeLicense', views.changeLicense, name='changeLicense'),
path('refreshLicense', views.refreshLicense, name='refreshLicense'),

View File

@ -554,6 +554,77 @@ def changeLicense(request):
return HttpResponse(final_json)
def switchToOLS(request):
"""Switch back to OpenLiteSpeed from LiteSpeed Enterprise"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson('status', 0)
# Check if we're currently running LiteSpeed Enterprise
if ProcessUtilities.decideServer() == ProcessUtilities.OLS:
data_ret = {'status': 0, 'error_message': 'Already running OpenLiteSpeed. No need to switch.'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Check if backup exists
if not os.path.exists('/usr/local/lswsbak'):
data_ret = {'status': 0, 'error_message': 'No OpenLiteSpeed backup found. Cannot switch back to OpenLiteSpeed.'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
execPath = "sudo /usr/local/CyberCP/bin/python " + virtualHostUtilities.cyberPanel + "/serverStatus/serverStatusUtil.py"
execPath = execPath + " switchToOLS"
ProcessUtilities.popenExecutioner(execPath)
time.sleep(2)
data_ret = {'status': 1, 'error_message': "None"}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'status': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def switchToOLSStatus(request):
"""Check the status of switching back to OpenLiteSpeed"""
try:
command = 'sudo cat ' + serverStatusUtil.ServerStatusUtil.lswsInstallStatusPath
output = ProcessUtilities.outputExecutioner(command)
if output.find('[404]') > -1:
command = "sudo rm -f " + serverStatusUtil.ServerStatusUtil.lswsInstallStatusPath
ProcessUtilities.popenExecutioner(command)
data_ret = {'status': 1, 'abort': 1, 'requestStatus': output, 'installed': 0}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
elif output.find('[200]') > -1:
command = "sudo rm -f " + serverStatusUtil.ServerStatusUtil.lswsInstallStatusPath
ProcessUtilities.popenExecutioner(command)
data_ret = {'status': 1, 'abort': 1, 'requestStatus': output, 'installed': 1}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
else:
data_ret = {'status': 1, 'abort': 0, 'requestStatus': output, 'installed': 0}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
command = "sudo rm -f " + serverStatusUtil.ServerStatusUtil.lswsInstallStatusPath
ProcessUtilities.popenExecutioner(command)
data_ret = {'status': 0, 'abort': 1, 'requestStatus': str(msg), 'installed': 0}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def topProcesses(request):
proc = httpProc(request, "serverStatus/topProcesses.html", None, 'admin')
return proc.render()

View File

@ -6,6 +6,7 @@ html {
font-size: 16px; /* Base font size for better readability */
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
text-size-adjust: 100%;
}
body {
@ -22,7 +23,7 @@ body {
/* Override any light text that might be hard to read */
.text-muted, .text-secondary, .text-light {
color: #64748b !important; /* Darker gray instead of light gray */
color: #2f3640 !important; /* Dark text for better readability on white backgrounds */
}
/* Fix small font sizes that are hard to read */
@ -294,7 +295,6 @@ p {
/* Make tables horizontally scrollable */
.table-responsive {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table {
@ -502,14 +502,14 @@ p {
}
.text-muted {
color: #64748b !important;
color: #2f3640 !important; /* Dark text for better readability */
}
/* Fix any light text on light backgrounds */
.bg-light .text-muted,
.bg-white .text-muted,
.panel .text-muted {
color: #64748b !important;
color: #2f3640 !important; /* Dark text for better readability */
}
/* Ensure proper spacing for touch targets */
@ -518,6 +518,43 @@ a, button, input, select, textarea {
min-width: 44px;
}
/* Additional text readability improvements */
/* Fix any green text issues */
.ng-binding {
color: #2f3640 !important; /* Normal dark text instead of green */
}
/* Ensure all text elements have proper contrast */
span, div, p, label, td, th {
color: inherit;
}
/* Fix specific text color issues */
.text-success {
color: #059669 !important; /* Darker green for better readability */
}
.text-info {
color: #0284c7 !important; /* Darker blue for better readability */
}
.text-warning {
color: #d97706 !important; /* Darker orange for better readability */
}
/* Override Bootstrap's muted text */
.text-muted {
color: #2f3640 !important; /* Dark text instead of grey */
}
/* Fix any remaining light text on light backgrounds */
.bg-white .text-light,
.bg-light .text-light,
.panel .text-light,
.card .text-light {
color: #2f3640 !important;
}
/* Fix for small clickable elements */
.glyph-icon, .icon {
min-width: 44px;

View File

@ -0,0 +1,265 @@
/* CyberPanel Readability & Design Fixes */
/* This file fixes the core design issues with grey text and color inconsistencies */
/* Override CSS Variables for Better Text Contrast */
:root {
/* Ensure all text uses proper dark colors for readability */
--text-primary: #2f3640;
--text-secondary: #2f3640; /* Changed from grey to dark for better readability */
--text-heading: #1e293b;
}
/* Dark theme also uses proper contrast */
[data-theme="dark"] {
--text-primary: #e4e4e7;
--text-secondary: #e4e4e7; /* Changed from grey to light for better readability */
--text-heading: #f3f4f6;
}
/* Fix Green Text Issues */
/* Override Angular binding colors that might be green */
.ng-binding {
color: var(--text-secondary) !important;
}
/* Specific fix for uptime display */
#sidebar .server-info .info-line span,
#sidebar .server-info .info-line .ng-binding,
.server-info .ng-binding {
color: var(--text-secondary) !important;
}
/* Fix Grey Text on White Background */
/* Override all muted and secondary text classes */
.text-muted,
.text-secondary,
.text-light,
small,
.small,
.text-small {
color: var(--text-secondary) !important;
}
/* Fix specific Bootstrap classes */
.text-muted {
color: #2f3640 !important; /* Dark text for better readability */
}
/* Fix text on white/light backgrounds */
.bg-white .text-muted,
.bg-light .text-muted,
.panel .text-muted,
.card .text-muted,
.content-box .text-muted {
color: #2f3640 !important;
}
/* Fix menu items and navigation */
#sidebar .menu-item,
#sidebar .menu-item span,
#sidebar .menu-item i,
.sidebar .menu-item,
.sidebar .menu-item span,
.sidebar .menu-item i {
color: var(--text-secondary) !important;
}
#sidebar .menu-item:hover,
.sidebar .menu-item:hover {
color: var(--accent-color) !important;
}
#sidebar .menu-item.active,
.sidebar .menu-item.active {
color: white !important;
}
/* Fix server info and details */
.server-info,
.server-info *,
.server-details,
.server-details *,
.info-line,
.info-line span,
.info-line strong,
.tagline,
.brand {
color: inherit !important;
}
/* Fix form elements */
label,
.control-label,
.form-label {
color: var(--text-primary) !important;
font-weight: 600;
}
/* Fix table text */
.table th,
.table td {
color: var(--text-primary) !important;
}
.table th {
font-weight: 600;
}
/* Fix alert text */
.alert {
color: var(--text-primary) !important;
}
.alert-success {
color: #059669 !important; /* Darker green for better readability */
}
.alert-info {
color: #0284c7 !important; /* Darker blue for better readability */
}
.alert-warning {
color: #d97706 !important; /* Darker orange for better readability */
}
.alert-danger {
color: #dc2626 !important; /* Darker red for better readability */
}
/* Fix breadcrumb text */
.breadcrumb-item {
color: var(--text-secondary) !important;
}
.breadcrumb-item.active {
color: var(--text-primary) !important;
}
/* Fix modal text */
.modal-content {
color: var(--text-primary) !important;
}
.modal-title {
color: var(--text-heading) !important;
}
/* Fix button text */
.btn {
color: inherit;
}
/* Fix any remaining light text issues */
.bg-light .text-light,
.bg-white .text-light,
.panel .text-light,
.card .text-light {
color: #2f3640 !important;
}
/* Ensure proper contrast for all text elements */
span, div, p, label, td, th, a, li {
color: inherit;
}
/* Fix specific color classes */
.text-success {
color: #059669 !important; /* Darker green for better readability */
}
.text-info {
color: #0284c7 !important; /* Darker blue for better readability */
}
.text-warning {
color: #d97706 !important; /* Darker orange for better readability */
}
.text-danger {
color: #dc2626 !important; /* Darker red for better readability */
}
/* Fix any Angular-specific styling */
[ng-controller] {
color: inherit;
}
[ng-show],
[ng-hide],
[ng-if] {
color: inherit;
}
/* Ensure all content areas have proper text color */
.content-box,
.panel,
.card,
.main-content,
.page-content {
color: var(--text-primary) !important;
}
/* Fix any remaining Bootstrap classes */
.text-dark {
color: #2f3640 !important;
}
.text-body {
color: var(--text-primary) !important;
}
/* Mobile-specific fixes */
@media (max-width: 768px) {
/* Ensure mobile text is also readable */
body,
.container,
.container-fluid {
color: var(--text-primary) !important;
}
/* Fix mobile menu text */
.mobile-menu .menu-item,
.mobile-menu .menu-item span {
color: var(--text-secondary) !important;
}
}
/* Print styles */
@media print {
body,
.content-box,
.panel,
.card {
color: #000000 !important;
background: #ffffff !important;
}
.text-muted,
.text-secondary {
color: #000000 !important;
}
}
/* High contrast mode support */
@media (prefers-contrast: high) {
:root {
--text-primary: #000000;
--text-secondary: #000000;
--text-heading: #000000;
}
[data-theme="dark"] {
--text-primary: #ffffff;
--text-secondary: #ffffff;
--text-heading: #ffffff;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

View File

@ -1,5 +0,0 @@
<?
phpinfo();
?>

View File

@ -1,64 +0,0 @@
#!/usr/bin/env python3
"""
Test script for the new firewall blocking functionality
This script tests the blockIPAddress API endpoint
"""
import requests
import json
import sys
def test_firewall_blocking():
"""
Test the firewall blocking functionality
Note: This is a basic test script. In a real environment, you would need
proper authentication and a test IP address.
"""
print("Testing Firewall Blocking Functionality")
print("=" * 50)
# Test configuration
base_url = "https://localhost:8090" # Adjust based on your CyberPanel setup
test_ip = "192.168.1.100" # Use a test IP that won't block your access
print(f"Base URL: {base_url}")
print(f"Test IP: {test_ip}")
print()
# Test data
test_data = {
"ip_address": test_ip
}
print("Test Data:")
print(json.dumps(test_data, indent=2))
print()
print("Note: This test requires:")
print("1. Valid CyberPanel session with admin privileges")
print("2. CyberPanel addons enabled")
print("3. Active firewalld service")
print()
print("To test manually:")
print("1. Login to CyberPanel dashboard")
print("2. Go to Dashboard -> SSH Security Analysis")
print("3. Look for 'Brute Force Attack Detected' alerts")
print("4. Click the 'Block IP' button next to malicious IPs")
print()
print("Expected behavior:")
print("- Button shows loading state during blocking")
print("- Success notification appears on successful blocking")
print("- IP is marked as 'Blocked' in the interface")
print("- Security analysis refreshes to update alerts")
print()
print("Firewall Commands:")
print("- firewalld: firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=<ip> drop'")
print("- firewalld reload: firewall-cmd --reload")
print()
if __name__ == "__main__":
test_firewall_blocking()

240
test_ftp_fixes.py Normal file
View File

@ -0,0 +1,240 @@
#!/usr/bin/env python3
"""
CyberPanel FTP Account Creation Test Script
This script tests the FTP account creation functionality with various path scenarios
"""
import os
import sys
import tempfile
import shutil
from unittest.mock import patch, MagicMock
# Add CyberPanel to path
sys.path.append('/usr/local/CyberCP')
def test_ftp_path_validation():
"""Test the FTP path validation functionality"""
print("🔍 Testing FTP Path Validation...")
# Import the FTP utilities
try:
from plogical.ftpUtilities import FTPUtilities
print("✅ Successfully imported FTPUtilities")
except ImportError as e:
print(f"❌ Failed to import FTPUtilities: {e}")
return False
# Test cases for path validation
test_cases = [
# Valid paths
("docs", True, "Valid subdirectory"),
("public_html", True, "Valid public_html directory"),
("uploads/images", True, "Valid nested directory"),
("api/v1", True, "Valid API directory"),
("", True, "Empty path (home directory)"),
("None", True, "None path (home directory)"),
# Invalid paths
("../docs", False, "Path traversal with .."),
("~/docs", False, "Home directory reference"),
("/docs", False, "Absolute path"),
("docs;rm -rf /", False, "Command injection"),
("docs|cat /etc/passwd", False, "Pipe command injection"),
("docs&reboot", False, "Background command"),
("docs`whoami`", False, "Command substitution"),
("docs'rm -rf /'", False, "Single quote injection"),
('docs"rm -rf /"', False, "Double quote injection"),
("docs<malicious", False, "Input redirection"),
("docs>malicious", False, "Output redirection"),
("docs*", False, "Wildcard character"),
("docs?", False, "Wildcard character"),
]
print("\n📋 Running Path Validation Tests:")
print("-" * 60)
passed = 0
failed = 0
for path, should_pass, description in test_cases:
try:
# Mock the external dependencies
with patch('pwd.getpwnam') as mock_pwd, \
patch('grp.getgrnam') as mock_grp, \
patch('os.path.exists') as mock_exists, \
patch('os.path.isdir') as mock_isdir, \
patch('os.path.islink') as mock_islink, \
patch('plogical.processUtilities.ProcessUtilities.executioner') as mock_exec, \
patch('websiteFunctions.models.Websites.objects.get') as mock_website, \
patch('loginSystem.models.Administrator.objects.get') as mock_admin:
# Setup mocks
mock_pwd.return_value.pw_uid = 1000
mock_grp.return_value.gr_gid = 1000
mock_exists.return_value = True
mock_isdir.return_value = True
mock_islink.return_value = False
mock_exec.return_value = 0
# Mock website object
mock_website_obj = MagicMock()
mock_website_obj.externalApp = "testuser"
mock_website_obj.package.diskSpace = 1000
mock_website_obj.package.ftpAccounts = 10
mock_website_obj.users_set.all.return_value.count.return_value = 0
mock_website.return_value = mock_website_obj
# Mock admin object
mock_admin_obj = MagicMock()
mock_admin_obj.userName = "testadmin"
mock_admin.return_value = mock_admin_obj
# Test the path validation
result = FTPUtilities.submitFTPCreation(
"testdomain.com",
"testuser",
"testpass",
path,
"testadmin"
)
if should_pass:
if result[0] == 1:
print(f"✅ PASS: {description} ('{path}')")
passed += 1
else:
print(f"❌ FAIL: {description} ('{path}') - Expected success but got: {result[1]}")
failed += 1
else:
if result[0] == 0:
print(f"✅ PASS: {description} ('{path}') - Correctly rejected")
passed += 1
else:
print(f"❌ FAIL: {description} ('{path}') - Expected rejection but got success")
failed += 1
except Exception as e:
if should_pass:
print(f"❌ ERROR: {description} ('{path}') - Unexpected error: {e}")
failed += 1
else:
print(f"✅ PASS: {description} ('{path}') - Correctly rejected with error: {e}")
passed += 1
print("\n" + "=" * 60)
print(f"📊 Test Results: {passed} passed, {failed} failed")
print("=" * 60)
return failed == 0
def test_directory_creation():
"""Test directory creation functionality"""
print("\n🔍 Testing Directory Creation...")
try:
from plogical.ftpUtilities import FTPUtilities
# Create a temporary directory for testing
with tempfile.TemporaryDirectory() as temp_dir:
test_path = os.path.join(temp_dir, "test_ftp_dir")
print(f"📁 Testing directory creation at: {test_path}")
# Test creating a new directory
result = FTPUtilities.ftpFunctions(test_path, "testuser")
if result[0] == 1:
if os.path.exists(test_path) and os.path.isdir(test_path):
print("✅ Directory creation successful")
return True
else:
print("❌ Directory creation failed - directory not found")
return False
else:
print(f"❌ Directory creation failed: {result[1]}")
return False
except Exception as e:
print(f"❌ Directory creation test failed: {e}")
return False
def test_security_features():
"""Test security features"""
print("\n🔍 Testing Security Features...")
security_tests = [
("Path traversal prevention", ".."),
("Home directory reference prevention", "~"),
("Absolute path prevention", "/etc/passwd"),
("Command injection prevention", ";rm -rf /"),
("Pipe command prevention", "|cat /etc/passwd"),
("Background command prevention", "&reboot"),
("Command substitution prevention", "`whoami`"),
]
print("🛡️ Security Test Results:")
print("-" * 40)
for test_name, malicious_path in security_tests:
try:
# This should be caught by our validation
if any(char in malicious_path for char in ['..', '~', '/', ';', '|', '&', '`']):
print(f"{test_name}: Correctly detected malicious path")
else:
print(f"{test_name}: Failed to detect malicious path")
except Exception as e:
print(f"{test_name}: Correctly rejected with error: {e}")
return True
def main():
"""Main test function"""
print("🚀 CyberPanel FTP Account Creation Test Suite")
print("=" * 60)
# Run all tests
tests = [
("Path Validation", test_ftp_path_validation),
("Directory Creation", test_directory_creation),
("Security Features", test_security_features),
]
results = []
for test_name, test_func in tests:
print(f"\n🧪 Running {test_name} Test...")
try:
result = test_func()
results.append((test_name, result))
if result:
print(f"{test_name} Test: PASSED")
else:
print(f"{test_name} Test: FAILED")
except Exception as e:
print(f"{test_name} Test: ERROR - {e}")
results.append((test_name, False))
# Summary
print("\n" + "=" * 60)
print("📋 TEST SUMMARY")
print("=" * 60)
passed = sum(1 for _, result in results if result)
total = len(results)
for test_name, result in results:
status = "✅ PASSED" if result else "❌ FAILED"
print(f"{test_name}: {status}")
print(f"\n🎯 Overall Result: {passed}/{total} tests passed")
if passed == total:
print("🎉 All tests passed! FTP account creation should work correctly.")
return 0
else:
print("⚠️ Some tests failed. Please review the issues above.")
return 1
if __name__ == "__main__":
exit(main())

View File

@ -1,236 +0,0 @@
#!/usr/local/CyberCP/bin/python
"""
Test script for SSL integration
This script tests the SSL reconciliation functionality
"""
import os
import sys
import django
# Add CyberPanel to Python path
sys.path.append('/usr/local/CyberCP')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings")
django.setup()
from plogical.sslReconcile import SSLReconcile
from plogical.sslUtilities import sslUtilities
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
def test_ssl_reconcile_module():
"""Test the SSL reconciliation module"""
print("Testing SSL Reconciliation Module...")
try:
# Test 1: Check if module can be imported
print("✓ SSLReconcile module imported successfully")
# Test 2: Test utility functions
print("Testing utility functions...")
# Test trim function
test_text = " test text "
trimmed = SSLReconcile.trim(test_text)
assert trimmed == "test text", f"Trim failed: '{trimmed}'"
print("✓ trim() function works correctly")
# Test 3: Test certificate fingerprint function
print("Testing certificate functions...")
# Test with non-existent file
fp = SSLReconcile.sha256fp("/nonexistent/file.pem")
assert fp == "", f"Expected empty string for non-existent file, got: '{fp}'"
print("✓ sha256fp() handles non-existent files correctly")
# Test issuer CN function
issuer = SSLReconcile.issuer_cn("/nonexistent/file.pem")
assert issuer == "", f"Expected empty string for non-existent file, got: '{issuer}'"
print("✓ issuer_cn() handles non-existent files correctly")
print("✓ All utility functions working correctly")
return True
except Exception as e:
print(f"✗ SSL reconciliation module test failed: {str(e)}")
return False
def test_ssl_utilities_integration():
"""Test the enhanced SSL utilities"""
print("\nTesting Enhanced SSL Utilities...")
try:
# Test 1: Check if new methods exist
assert hasattr(sslUtilities, 'reconcile_ssl_all'), "reconcile_ssl_all method not found"
assert hasattr(sslUtilities, 'reconcile_ssl_domain'), "reconcile_ssl_domain method not found"
assert hasattr(sslUtilities, 'fix_acme_challenge_context'), "fix_acme_challenge_context method not found"
print("✓ All new SSL utility methods found")
# Test 2: Test method signatures
import inspect
# Check reconcile_ssl_all signature
sig = inspect.signature(sslUtilities.reconcile_ssl_all)
assert len(sig.parameters) == 0, f"reconcile_ssl_all should have no parameters, got: {sig.parameters}"
print("✓ reconcile_ssl_all signature correct")
# Check reconcile_ssl_domain signature
sig = inspect.signature(sslUtilities.reconcile_ssl_domain)
assert 'domain' in sig.parameters, f"reconcile_ssl_domain should have 'domain' parameter, got: {sig.parameters}"
print("✓ reconcile_ssl_domain signature correct")
# Check fix_acme_challenge_context signature
sig = inspect.signature(sslUtilities.fix_acme_challenge_context)
assert 'virtualHostName' in sig.parameters, f"fix_acme_challenge_context should have 'virtualHostName' parameter, got: {sig.parameters}"
print("✓ fix_acme_challenge_context signature correct")
print("✓ All SSL utility method signatures correct")
return True
except Exception as e:
print(f"✗ SSL utilities integration test failed: {str(e)}")
return False
def test_vhost_configuration_fixes():
"""Test that vhost configuration fixes are applied"""
print("\nTesting VHost Configuration Fixes...")
try:
from plogical.vhostConfs import vhostConfs
# Test 1: Check that ACME challenge contexts use $VH_ROOT
ols_master_conf = vhostConfs.olsMasterConf
assert '$VH_ROOT/public_html/.well-known/acme-challenge' in ols_master_conf, "ACME challenge context not fixed in olsMasterConf"
print("✓ olsMasterConf ACME challenge context fixed")
# Test 2: Check child configuration
ols_child_conf = vhostConfs.olsChildConf
assert '$VH_ROOT/public_html/.well-known/acme-challenge' in ols_child_conf, "ACME challenge context not fixed in olsChildConf"
print("✓ olsChildConf ACME challenge context fixed")
# Test 3: Check Apache configurations
apache_conf = vhostConfs.apacheConf
assert '/home/{virtualHostName}/public_html/.well-known/acme-challenge' in apache_conf, "Apache ACME challenge alias not fixed"
print("✓ Apache ACME challenge alias fixed")
print("✓ All vhost configuration fixes applied correctly")
return True
except Exception as e:
print(f"✗ VHost configuration fixes test failed: {str(e)}")
return False
def test_management_command():
"""Test the Django management command"""
print("\nTesting Django Management Command...")
try:
import subprocess
# Test 1: Check if management command exists
result = subprocess.run([
'python', 'manage.py', 'ssl_reconcile', '--help'
], capture_output=True, text=True, cwd='/usr/local/CyberCP')
if result.returncode == 0:
print("✓ SSL reconcile management command exists and responds to --help")
else:
print(f"✗ SSL reconcile management command failed: {result.stderr}")
return False
# Test 2: Check command options
help_output = result.stdout
assert '--all' in help_output, "--all option not found in help"
assert '--domain' in help_output, "--domain option not found in help"
assert '--fix-acme' in help_output, "--fix-acme option not found in help"
print("✓ All management command options present")
print("✓ Django management command working correctly")
return True
except Exception as e:
print(f"✗ Django management command test failed: {str(e)}")
return False
def test_cron_integration():
"""Test that cron integration is properly configured"""
print("\nTesting Cron Integration...")
try:
# Check if cron file exists and contains SSL reconciliation
cron_paths = [
'/var/spool/cron/crontabs/root',
'/etc/crontab'
]
ssl_reconcile_found = False
for cron_path in cron_paths:
if os.path.exists(cron_path):
with open(cron_path, 'r') as f:
content = f.read()
if 'ssl_reconcile --all' in content:
ssl_reconcile_found = True
print(f"✓ SSL reconciliation cron job found in {cron_path}")
break
if not ssl_reconcile_found:
print("✗ SSL reconciliation cron job not found in any cron file")
return False
print("✓ Cron integration working correctly")
return True
except Exception as e:
print(f"✗ Cron integration test failed: {str(e)}")
return False
def main():
"""Run all tests"""
print("=" * 60)
print("SSL Integration Test Suite")
print("=" * 60)
tests = [
test_ssl_reconcile_module,
test_ssl_utilities_integration,
test_vhost_configuration_fixes,
test_management_command,
test_cron_integration
]
passed = 0
total = len(tests)
for test in tests:
try:
if test():
passed += 1
except Exception as e:
print(f"✗ Test {test.__name__} failed with exception: {str(e)}")
print("\n" + "=" * 60)
print(f"Test Results: {passed}/{total} tests passed")
print("=" * 60)
if passed == total:
print("🎉 All tests passed! SSL integration is working correctly.")
return True
else:
print("❌ Some tests failed. Please check the output above.")
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View File

@ -1,120 +0,0 @@
#!/usr/local/CyberCP/bin/python
"""
Test script for subdomain log fix
This script tests the subdomain log fix functionality
"""
import os
import sys
import django
# Add CyberPanel to Python path
sys.path.append('/usr/local/CyberCP')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings")
django.setup()
from websiteFunctions.models import ChildDomains
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
def test_subdomain_log_configuration():
"""Test if subdomain log configurations are correct"""
print("Testing subdomain log configurations...")
issues_found = 0
child_domains = ChildDomains.objects.all()
if not child_domains:
print("No child domains found.")
return True
for child_domain in child_domains:
domain_name = child_domain.domain
master_domain = child_domain.master.domain
vhost_conf_path = f"/usr/local/lsws/conf/vhosts/{domain_name}/vhost.conf"
if not os.path.exists(vhost_conf_path):
print(f"⚠️ VHost config not found for {domain_name}")
issues_found += 1
continue
try:
with open(vhost_conf_path, 'r') as f:
config_content = f.read()
# Check for incorrect log paths
if f'{master_domain}.error_log' in config_content:
print(f"{domain_name}: Using master domain error log")
issues_found += 1
else:
print(f"{domain_name}: Error log configuration OK")
if f'{master_domain}.access_log' in config_content:
print(f"{domain_name}: Using master domain access log")
issues_found += 1
else:
print(f"{domain_name}: Access log configuration OK")
except Exception as e:
print(f"{domain_name}: Error reading config - {str(e)}")
issues_found += 1
if issues_found == 0:
print("\n🎉 All subdomain log configurations are correct!")
return True
else:
print(f"\n⚠️ Found {issues_found} issues with subdomain log configurations")
return False
def test_management_command():
"""Test the management command"""
print("\nTesting management command...")
try:
from django.core.management import call_command
from io import StringIO
# Test dry run
out = StringIO()
call_command('fix_subdomain_logs', '--dry-run', stdout=out)
print("✅ Management command dry run works")
return True
except Exception as e:
print(f"❌ Management command test failed: {str(e)}")
return False
def main():
"""Main test function"""
print("=" * 60)
print("SUBDOMAIN LOG FIX TEST")
print("=" * 60)
# Test 1: Check current configurations
config_test = test_subdomain_log_configuration()
# Test 2: Test management command
cmd_test = test_management_command()
print("\n" + "=" * 60)
print("TEST SUMMARY")
print("=" * 60)
if config_test and cmd_test:
print("🎉 All tests passed!")
print("\nTo fix any issues found:")
print("1. Via Web Interface: Websites > Fix Subdomain Logs")
print("2. Via CLI: python manage.py fix_subdomain_logs --all")
print("3. For specific domain: python manage.py fix_subdomain_logs --domain example.com")
return True
else:
print("⚠️ Some tests failed. Check the output above.")
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View File

@ -1,70 +0,0 @@
#!/usr/bin/env python3
"""
Test script for the dynamic version fetcher
"""
import sys
import os
# Add the plogical directory to the path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'plogical'))
try:
from versionFetcher import VersionFetcher, get_latest_phpmyadmin_version, get_latest_snappymail_version
print("=== Testing Dynamic Version Fetcher ===")
print()
# Test connectivity
print("1. Testing GitHub API connectivity...")
if VersionFetcher.test_connectivity():
print(" ✅ GitHub API is accessible")
else:
print(" ❌ GitHub API is not accessible")
print()
# Test phpMyAdmin version fetching
print("2. Testing phpMyAdmin version fetching...")
try:
phpmyadmin_version = get_latest_phpmyadmin_version()
print(f" Latest phpMyAdmin version: {phpmyadmin_version}")
if phpmyadmin_version != "5.2.2":
print(" ✅ Newer version found!")
else:
print(" Using fallback version (API may be unavailable)")
except Exception as e:
print(f" ❌ Error: {e}")
print()
# Test SnappyMail version fetching
print("3. Testing SnappyMail version fetching...")
try:
snappymail_version = get_latest_snappymail_version()
print(f" Latest SnappyMail version: {snappymail_version}")
if snappymail_version != "2.38.2":
print(" ✅ Newer version found!")
else:
print(" Using fallback version (API may be unavailable)")
except Exception as e:
print(f" ❌ Error: {e}")
print()
# Test all versions
print("4. Testing all versions...")
try:
all_versions = VersionFetcher.get_latest_versions()
print(" All latest versions:")
for component, version in all_versions.items():
print(f" {component}: {version}")
except Exception as e:
print(f" ❌ Error: {e}")
print()
print("=== Test Complete ===")
except ImportError as e:
print(f"❌ Import error: {e}")
print("Make sure you're running this from the cyberpanel directory")
except Exception as e:
print(f"❌ Unexpected error: {e}")

View File

@ -1,141 +0,0 @@
#!/usr/bin/env python3
"""
Test script for FTP User Quota Feature
This script tests the basic functionality of the new quota management system.
"""
import os
import sys
import django
# Add CyberPanel to Python path
sys.path.append('/usr/local/CyberCP')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings")
django.setup()
from ftp.models import Users
from websiteFunctions.models import Websites
from plogical.ftpUtilities import FTPUtilities
def test_quota_feature():
"""Test the FTP quota feature functionality"""
print("🧪 Testing FTP User Quota Feature")
print("=" * 50)
# Test 1: Check if new fields exist in model
print("\n1. Testing model fields...")
try:
# Check if custom quota fields exist
user_fields = [field.name for field in Users._meta.fields]
required_fields = ['custom_quota_enabled', 'custom_quota_size']
for field in required_fields:
if field in user_fields:
print(f"{field} field exists")
else:
print(f"{field} field missing")
return False
except Exception as e:
print(f" ❌ Error checking model fields: {e}")
return False
# Test 2: Test quota update function
print("\n2. Testing quota update function...")
try:
# Test with valid data
result = FTPUtilities.updateFTPQuota("test_user", 100, True)
if result[0] == 0: # Expected to fail since user doesn't exist
print(" ✅ updateFTPQuota handles non-existent user correctly")
else:
print(" ⚠️ updateFTPQuota should have failed for non-existent user")
# Test with invalid quota size
result = FTPUtilities.updateFTPQuota("test_user", 0, True)
if result[0] == 0: # Expected to fail
print(" ✅ updateFTPQuota validates quota size correctly")
else:
print(" ⚠️ updateFTPQuota should have failed for invalid quota size")
except Exception as e:
print(f" ❌ Error testing quota update: {e}")
return False
# Test 3: Test FTP creation with custom quota
print("\n3. Testing FTP creation with custom quota...")
try:
# This will fail because we don't have a real website, but we can test the function signature
try:
result = FTPUtilities.submitFTPCreation(
"test.com", "testuser", "password", "None", "admin",
api="0", customQuotaSize=50, enableCustomQuota=True
)
print(" ✅ submitFTPCreation accepts custom quota parameters")
except Exception as e:
if "test.com" in str(e) or "admin" in str(e):
print(" ✅ submitFTPCreation accepts custom quota parameters (failed as expected due to missing data)")
else:
print(f" ❌ Unexpected error: {e}")
return False
except Exception as e:
print(f" ❌ Error testing FTP creation: {e}")
return False
# Test 4: Check if we can create a test user with custom quota
print("\n4. Testing database operations...")
try:
# Try to get a website to test with
websites = Websites.objects.all()
if websites.exists():
website = websites.first()
# Create a test FTP user
test_user = Users(
domain=website,
user="test_quota_user",
password="hashed_password",
uid=1000,
gid=1000,
dir="/home/test.com",
quotasize=100,
status="1",
ulbandwidth=500000,
dlbandwidth=500000,
custom_quota_enabled=True,
custom_quota_size=50
)
# Don't actually save to avoid database pollution
print(" ✅ Can create Users object with custom quota fields")
# Test the quota logic
if test_user.custom_quota_enabled:
effective_quota = test_user.custom_quota_size
else:
effective_quota = test_user.quotasize
if effective_quota == 50:
print(" ✅ Quota logic works correctly")
else:
print(f" ❌ Quota logic failed: expected 50, got {effective_quota}")
return False
else:
print(" ⚠️ No websites found for testing, skipping database test")
except Exception as e:
print(f" ❌ Error testing database operations: {e}")
return False
print("\n" + "=" * 50)
print("🎉 All tests passed! FTP User Quota feature is working correctly.")
print("\nNext steps:")
print("1. Apply database migration: python manage.py migrate ftp")
print("2. Restart CyberPanel services")
print("3. Test the feature in the web interface")
return True
if __name__ == "__main__":
success = test_quota_feature()
sys.exit(0 if success else 1)

View File

@ -0,0 +1,230 @@
#!/usr/local/CyberCP/bin/python
import os
import sys
import json
import shutil
import subprocess
import pwd
import grp
from django.db import models
from django.conf import settings
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
from plogical.processUtilities import ProcessUtilities
class HomeDirectoryManager:
"""
Manages multiple home directories for CyberPanel users
Supports /home, /home2, /home3, etc. for storage balance
"""
@staticmethod
def detectHomeDirectories():
"""
Automatically detect all available home directories
Returns list of available home directories
"""
try:
home_dirs = []
# Check for /home (default)
if os.path.exists('/home') and os.path.isdir('/home'):
home_dirs.append({
'path': '/home',
'name': 'home',
'available_space': HomeDirectoryManager.getAvailableSpace('/home'),
'total_space': HomeDirectoryManager.getTotalSpace('/home'),
'user_count': HomeDirectoryManager.getUserCount('/home')
})
# Check for /home2, /home3, etc.
for i in range(2, 10): # Check up to /home9
home_path = f'/home{i}'
if os.path.exists(home_path) and os.path.isdir(home_path):
home_dirs.append({
'path': home_path,
'name': f'home{i}',
'available_space': HomeDirectoryManager.getAvailableSpace(home_path),
'total_space': HomeDirectoryManager.getTotalSpace(home_path),
'user_count': HomeDirectoryManager.getUserCount(home_path)
})
return home_dirs
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error detecting home directories: {str(e)}")
return []
@staticmethod
def getAvailableSpace(path):
"""Get available space in bytes for a given path"""
try:
statvfs = os.statvfs(path)
return statvfs.f_frsize * statvfs.f_bavail
except:
return 0
@staticmethod
def getTotalSpace(path):
"""Get total space in bytes for a given path"""
try:
statvfs = os.statvfs(path)
return statvfs.f_frsize * statvfs.f_blocks
except:
return 0
@staticmethod
def getUserCount(path):
"""Get number of users in a home directory"""
try:
count = 0
for item in os.listdir(path):
item_path = os.path.join(path, item)
if os.path.isdir(item_path) and not item.startswith('.'):
# Check if it's a user directory (has public_html)
if os.path.exists(os.path.join(item_path, 'public_html')):
count += 1
return count
except:
return 0
@staticmethod
def getBestHomeDirectory():
"""
Automatically select the best home directory based on available space
Returns the path with most available space
"""
try:
home_dirs = HomeDirectoryManager.detectHomeDirectories()
if not home_dirs:
return '/home' # Fallback to default
# Sort by available space (descending)
home_dirs.sort(key=lambda x: x['available_space'], reverse=True)
return home_dirs[0]['path']
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error selecting best home directory: {str(e)}")
return '/home'
@staticmethod
def createUserDirectory(username, home_path, owner_uid=5003, owner_gid=5003):
"""
Create user directory in specified home path
"""
try:
user_path = os.path.join(home_path, username)
# Create user directory
if not os.path.exists(user_path):
os.makedirs(user_path, mode=0o755)
# Create public_html directory
public_html_path = os.path.join(user_path, 'public_html')
if not os.path.exists(public_html_path):
os.makedirs(public_html_path, mode=0o755)
# Create logs directory
logs_path = os.path.join(user_path, 'logs')
if not os.path.exists(logs_path):
os.makedirs(logs_path, mode=0o755)
# Set ownership
HomeDirectoryManager.setOwnership(user_path, owner_uid, owner_gid)
return True
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error creating user directory: {str(e)}")
return False
@staticmethod
def setOwnership(path, uid, gid):
"""Set ownership for a path recursively"""
try:
# Set ownership for the directory
os.chown(path, uid, gid)
# Set ownership for all contents recursively
for root, dirs, files in os.walk(path):
for d in dirs:
os.chown(os.path.join(root, d), uid, gid)
for f in files:
os.chown(os.path.join(root, f), uid, gid)
return True
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error setting ownership: {str(e)}")
return False
@staticmethod
def migrateUser(username, from_home, to_home, owner_uid=5003, owner_gid=5003):
"""
Migrate user from one home directory to another
"""
try:
from_path = os.path.join(from_home, username)
to_path = os.path.join(to_home, username)
if not os.path.exists(from_path):
return False, f"User directory {from_path} does not exist"
if os.path.exists(to_path):
return False, f"User directory {to_path} already exists"
# Create target directory structure
if not HomeDirectoryManager.createUserDirectory(username, to_home, owner_uid, owner_gid):
return False, "Failed to create target directory"
# Copy all files and directories
shutil.copytree(from_path, to_path, dirs_exist_ok=True)
# Set proper ownership
HomeDirectoryManager.setOwnership(to_path, owner_uid, owner_gid)
# Remove old directory
shutil.rmtree(from_path)
return True, "User migrated successfully"
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error migrating user: {str(e)}")
return False, str(e)
@staticmethod
def getHomeDirectoryStats():
"""
Get comprehensive statistics for all home directories
"""
try:
home_dirs = HomeDirectoryManager.detectHomeDirectories()
stats = {
'total_directories': len(home_dirs),
'total_users': sum(d['user_count'] for d in home_dirs),
'total_space': sum(d['total_space'] for d in home_dirs),
'total_available': sum(d['available_space'] for d in home_dirs),
'directories': home_dirs
}
# Calculate usage percentages
for directory in stats['directories']:
if directory['total_space'] > 0:
used_space = directory['total_space'] - directory['available_space']
directory['usage_percentage'] = (used_space / directory['total_space']) * 100
else:
directory['usage_percentage'] = 0
return stats
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error getting home directory stats: {str(e)}")
return None
@staticmethod
def formatBytes(bytes_value):
"""Convert bytes to human readable format"""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_value < 1024.0:
return f"{bytes_value:.1f} {unit}"
bytes_value /= 1024.0
return f"{bytes_value:.1f} PB"

View File

@ -0,0 +1,148 @@
#!/usr/local/CyberCP/bin/python
import os
import sys
from django.db import models
from loginSystem.models import Administrator
from .models import HomeDirectory, UserHomeMapping
from .homeDirectoryManager import HomeDirectoryManager
class HomeDirectoryUtils:
"""
Utility functions for getting user home directories
"""
@staticmethod
def getUserHomeDirectory(username):
"""
Get the home directory path for a specific user
Returns the home directory path or None if not found
"""
try:
user = Administrator.objects.get(userName=username)
try:
mapping = UserHomeMapping.objects.get(user=user)
return mapping.home_directory.path
except UserHomeMapping.DoesNotExist:
# Fallback to default home directory
default_home = HomeDirectory.objects.filter(is_default=True).first()
if default_home:
return default_home.path
else:
return HomeDirectoryManager.getBestHomeDirectory()
except Administrator.DoesNotExist:
return None
@staticmethod
def getUserHomeDirectoryObject(username):
"""
Get the home directory object for a specific user
Returns the HomeDirectory object or None if not found
"""
try:
user = Administrator.objects.get(userName=username)
try:
mapping = UserHomeMapping.objects.get(user=user)
return mapping.home_directory
except UserHomeMapping.DoesNotExist:
# Fallback to default home directory
return HomeDirectory.objects.filter(is_default=True).first()
except Administrator.DoesNotExist:
return None
@staticmethod
def getWebsitePath(domain, username=None):
"""
Get the website path for a domain, using the user's home directory
"""
if username:
home_path = HomeDirectoryUtils.getUserHomeDirectory(username)
else:
home_path = HomeDirectoryManager.getBestHomeDirectory()
if not home_path:
home_path = '/home' # Fallback
return os.path.join(home_path, domain)
@staticmethod
def getPublicHtmlPath(domain, username=None):
"""
Get the public_html path for a domain
"""
website_path = HomeDirectoryUtils.getWebsitePath(domain, username)
return os.path.join(website_path, 'public_html')
@staticmethod
def getLogsPath(domain, username=None):
"""
Get the logs path for a domain
"""
website_path = HomeDirectoryUtils.getWebsitePath(domain, username)
return os.path.join(website_path, 'logs')
@staticmethod
def updateUserHomeDirectory(username, new_home_directory_id):
"""
Update a user's home directory
"""
try:
user = Administrator.objects.get(userName=username)
home_dir = HomeDirectory.objects.get(id=new_home_directory_id)
# Update or create mapping
mapping, created = UserHomeMapping.objects.get_or_create(
user=user,
defaults={'home_directory': home_dir}
)
if not created:
mapping.home_directory = home_dir
mapping.save()
return True, "Home directory updated successfully"
except Administrator.DoesNotExist:
return False, "User not found"
except HomeDirectory.DoesNotExist:
return False, "Home directory not found"
except Exception as e:
return False, str(e)
@staticmethod
def getAllUsersInHomeDirectory(home_directory_id):
"""
Get all users assigned to a specific home directory
"""
try:
home_dir = HomeDirectory.objects.get(id=home_directory_id)
mappings = UserHomeMapping.objects.filter(home_directory=home_dir)
return [mapping.user for mapping in mappings]
except HomeDirectory.DoesNotExist:
return []
@staticmethod
def getHomeDirectoryUsageStats():
"""
Get usage statistics for all home directories
"""
home_dirs = HomeDirectory.objects.filter(is_active=True)
stats = []
for home_dir in home_dirs:
user_count = UserHomeMapping.objects.filter(home_directory=home_dir).count()
available_space = home_dir.get_available_space()
total_space = home_dir.get_total_space()
usage_percentage = home_dir.get_usage_percentage()
stats.append({
'id': home_dir.id,
'name': home_dir.name,
'path': home_dir.path,
'user_count': user_count,
'available_space': available_space,
'total_space': total_space,
'usage_percentage': usage_percentage,
'is_default': home_dir.is_default
})
return stats

View File

@ -0,0 +1,254 @@
#!/usr/local/CyberCP/bin/python
import json
import os
import sys
from django.shortcuts import render
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from loginSystem.views import loadLoginPage
from loginSystem.models import Administrator
from plogical.acl import ACLManager
from plogical.httpProc import httpProc
from .homeDirectoryManager import HomeDirectoryManager
from .models import HomeDirectory, UserHomeMapping
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
def loadHomeDirectoryManagement(request):
"""Load home directory management interface"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] != 1:
return ACLManager.loadError()
# Get all home directories
home_directories = HomeDirectory.objects.all().order_by('name')
# Get statistics
stats = HomeDirectoryManager.getHomeDirectoryStats()
proc = httpProc(request, 'userManagment/homeDirectoryManagement.html', {
'home_directories': home_directories,
'stats': stats
}, 'admin')
return proc.render()
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error loading home directory management: {str(e)}")
return ACLManager.loadError()
def detectHomeDirectories(request):
"""Detect and add new home directories"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] != 1:
return JsonResponse({'status': 0, 'error_message': 'Unauthorized access'})
# Detect home directories
detected_dirs = HomeDirectoryManager.detectHomeDirectories()
added_count = 0
for dir_info in detected_dirs:
# Check if directory already exists in database
if not HomeDirectory.objects.filter(path=dir_info['path']).exists():
# Create new home directory entry
home_dir = HomeDirectory(
name=dir_info['name'],
path=dir_info['path'],
is_active=True,
is_default=(dir_info['path'] == '/home')
)
home_dir.save()
added_count += 1
return JsonResponse({
'status': 1,
'message': f'Detected and added {added_count} new home directories',
'added_count': added_count
})
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error detecting home directories: {str(e)}")
return JsonResponse({'status': 0, 'error_message': str(e)})
def updateHomeDirectory(request):
"""Update home directory settings"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] != 1:
return JsonResponse({'status': 0, 'error_message': 'Unauthorized access'})
data = json.loads(request.body)
home_dir_id = data.get('id')
is_active = data.get('is_active', True)
is_default = data.get('is_default', False)
max_users = data.get('max_users', 0)
description = data.get('description', '')
try:
home_dir = HomeDirectory.objects.get(id=home_dir_id)
# If setting as default, unset other defaults
if is_default:
HomeDirectory.objects.filter(is_default=True).update(is_default=False)
home_dir.is_active = is_active
home_dir.is_default = is_default
home_dir.max_users = max_users
home_dir.description = description
home_dir.save()
return JsonResponse({'status': 1, 'message': 'Home directory updated successfully'})
except HomeDirectory.DoesNotExist:
return JsonResponse({'status': 0, 'error_message': 'Home directory not found'})
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error updating home directory: {str(e)}")
return JsonResponse({'status': 0, 'error_message': str(e)})
def deleteHomeDirectory(request):
"""Delete home directory (only if no users assigned)"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] != 1:
return JsonResponse({'status': 0, 'error_message': 'Unauthorized access'})
data = json.loads(request.body)
home_dir_id = data.get('id')
try:
home_dir = HomeDirectory.objects.get(id=home_dir_id)
# Check if any users are assigned to this home directory
user_count = UserHomeMapping.objects.filter(home_directory=home_dir).count()
if user_count > 0:
return JsonResponse({
'status': 0,
'error_message': f'Cannot delete home directory. {user_count} users are assigned to it.'
})
# Don't allow deletion of /home (default)
if home_dir.path == '/home':
return JsonResponse({
'status': 0,
'error_message': 'Cannot delete the default /home directory'
})
home_dir.delete()
return JsonResponse({'status': 1, 'message': 'Home directory deleted successfully'})
except HomeDirectory.DoesNotExist:
return JsonResponse({'status': 0, 'error_message': 'Home directory not found'})
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error deleting home directory: {str(e)}")
return JsonResponse({'status': 0, 'error_message': str(e)})
def getHomeDirectoryStats(request):
"""Get home directory statistics"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] != 1:
return JsonResponse({'status': 0, 'error_message': 'Unauthorized access'})
stats = HomeDirectoryManager.getHomeDirectoryStats()
return JsonResponse({'status': 1, 'stats': stats})
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error getting home directory stats: {str(e)}")
return JsonResponse({'status': 0, 'error_message': str(e)})
def getUserHomeDirectories(request):
"""Get available home directories for user creation"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] != 1 and currentACL['createNewUser'] != 1:
return JsonResponse({'status': 0, 'error_message': 'Unauthorized access'})
# Get active home directories
home_dirs = HomeDirectory.objects.filter(is_active=True).order_by('name')
directories = []
for home_dir in home_dirs:
directories.append({
'id': home_dir.id,
'name': home_dir.name,
'path': home_dir.path,
'available_space': home_dir.get_available_space(),
'user_count': home_dir.get_user_count(),
'is_default': home_dir.is_default,
'description': home_dir.description
})
return JsonResponse({'status': 1, 'directories': directories})
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error getting user home directories: {str(e)}")
return JsonResponse({'status': 0, 'error_message': str(e)})
def migrateUser(request):
"""Migrate user to different home directory"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] != 1:
return JsonResponse({'status': 0, 'error_message': 'Unauthorized access'})
data = json.loads(request.body)
username = data.get('username')
target_home_id = data.get('target_home_id')
try:
user = Administrator.objects.get(userName=username)
target_home = HomeDirectory.objects.get(id=target_home_id)
# Get current home directory
try:
current_mapping = UserHomeMapping.objects.get(user=user)
current_home = current_mapping.home_directory
except UserHomeMapping.DoesNotExist:
current_home = HomeDirectory.objects.filter(is_default=True).first()
if not current_home:
current_home = HomeDirectory.objects.first()
if not current_home:
return JsonResponse({'status': 0, 'error_message': 'No home directory found for user'})
# Perform migration
success, message = HomeDirectoryManager.migrateUser(
username,
current_home.path,
target_home.path
)
if success:
# Update user mapping
UserHomeMapping.objects.update_or_create(
user=user,
defaults={'home_directory': target_home}
)
return JsonResponse({'status': 1, 'message': message})
else:
return JsonResponse({'status': 0, 'error_message': message})
except Administrator.DoesNotExist:
return JsonResponse({'status': 0, 'error_message': 'User not found'})
except HomeDirectory.DoesNotExist:
return JsonResponse({'status': 0, 'error_message': 'Target home directory not found'})
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error migrating user: {str(e)}")
return JsonResponse({'status': 0, 'error_message': str(e)})

View File

@ -0,0 +1 @@
# Management commands package

View File

@ -0,0 +1 @@
# Management commands

View File

@ -0,0 +1,49 @@
#!/usr/local/CyberCP/bin/python
from django.core.management.base import BaseCommand
from userManagment.homeDirectoryManager import HomeDirectoryManager
from userManagment.models import HomeDirectory
import os
class Command(BaseCommand):
help = 'Initialize home directories for CyberPanel'
def handle(self, *args, **options):
self.stdout.write('Initializing home directories...')
# Detect home directories
detected_dirs = HomeDirectoryManager.detectHomeDirectories()
if not detected_dirs:
self.stdout.write(self.style.WARNING('No home directories detected'))
return
# Create default /home if it doesn't exist
if not os.path.exists('/home'):
self.stdout.write('Creating default /home directory...')
os.makedirs('/home', mode=0o755)
detected_dirs.insert(0, {
'path': '/home',
'name': 'home',
'available_space': HomeDirectoryManager.getAvailableSpace('/home'),
'total_space': HomeDirectoryManager.getTotalSpace('/home'),
'user_count': 0
})
# Create database entries
created_count = 0
for dir_info in detected_dirs:
if not HomeDirectory.objects.filter(path=dir_info['path']).exists():
home_dir = HomeDirectory(
name=dir_info['name'],
path=dir_info['path'],
is_active=True,
is_default=(dir_info['path'] == '/home'),
description=f"Auto-detected home directory: {dir_info['path']}"
)
home_dir.save()
created_count += 1
self.stdout.write(f'Created home directory: {dir_info["name"]} ({dir_info["path"]})')
self.stdout.write(
self.style.SUCCESS(f'Successfully initialized {created_count} home directories')
)

View File

@ -0,0 +1,48 @@
# Generated migration for home directories feature
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('loginSystem', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='HomeDirectory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Directory name (e.g., home, home2)', max_length=50, unique=True)),
('path', models.CharField(help_text='Full path to home directory', max_length=255, unique=True)),
('is_active', models.BooleanField(default=True, help_text='Whether this home directory is active')),
('is_default', models.BooleanField(default=False, help_text='Whether this is the default home directory')),
('max_users', models.IntegerField(default=0, help_text='Maximum number of users (0 = unlimited)')),
('description', models.TextField(blank=True, help_text='Description of this home directory')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Home Directory',
'verbose_name_plural': 'Home Directories',
'db_table': 'home_directories',
},
),
migrations.CreateModel(
name='UserHomeMapping',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('home_directory', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='userManagment.homedirectory')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='home_mapping', to='loginSystem.administrator')),
],
options={
'verbose_name': 'User Home Mapping',
'verbose_name_plural': 'User Home Mappings',
'db_table': 'user_home_mappings',
},
),
]

View File

@ -1,6 +1,85 @@
# -*- coding: utf-8 -*-
from django.db import models
from loginSystem.models import Administrator
# Create your models here.
class HomeDirectory(models.Model):
"""
Model to store home directory configurations
"""
name = models.CharField(max_length=50, unique=True, help_text="Directory name (e.g., home, home2)")
path = models.CharField(max_length=255, unique=True, help_text="Full path to home directory")
is_active = models.BooleanField(default=True, help_text="Whether this home directory is active")
is_default = models.BooleanField(default=False, help_text="Whether this is the default home directory")
max_users = models.IntegerField(default=0, help_text="Maximum number of users (0 = unlimited)")
description = models.TextField(blank=True, help_text="Description of this home directory")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'home_directories'
verbose_name = 'Home Directory'
verbose_name_plural = 'Home Directories'
def __str__(self):
return f"{self.name} ({self.path})"
def get_available_space(self):
"""Get available space in bytes"""
try:
import os
statvfs = os.statvfs(self.path)
return statvfs.f_frsize * statvfs.f_bavail
except:
return 0
def get_total_space(self):
"""Get total space in bytes"""
try:
import os
statvfs = os.statvfs(self.path)
return statvfs.f_frsize * statvfs.f_blocks
except:
return 0
def get_user_count(self):
"""Get number of users in this home directory"""
try:
import os
count = 0
if os.path.exists(self.path):
for item in os.listdir(self.path):
item_path = os.path.join(self.path, item)
if os.path.isdir(item_path) and not item.startswith('.'):
# Check if it's a user directory (has public_html)
if os.path.exists(os.path.join(item_path, 'public_html')):
count += 1
return count
except:
return 0
def get_usage_percentage(self):
"""Get usage percentage"""
try:
total = self.get_total_space()
if total > 0:
used = total - self.get_available_space()
return (used / total) * 100
return 0
except:
return 0
class UserHomeMapping(models.Model):
"""
Model to map users to their home directories
"""
user = models.OneToOneField(Administrator, on_delete=models.CASCADE, related_name='home_mapping')
home_directory = models.ForeignKey(HomeDirectory, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'user_home_mappings'
verbose_name = 'User Home Mapping'
verbose_name_plural = 'User Home Mappings'
def __str__(self):
return f"{self.user.userName} -> {self.home_directory.name}"

View File

@ -179,6 +179,121 @@ app.controller('modifyUser', function ($scope, $http) {
document.body.removeChild(tempTextarea);
}
};
// WebAuthn Functions
$scope.loadWebAuthnData = function() {
if (!$scope.accountUsername) return;
var url = '/webauthn/credentials/' + $scope.accountUsername + '/';
$http.get(url).then(function(response) {
if (response.data.success) {
$scope.webauthnCredentials = response.data.credentials;
$scope.webauthnEnabled = response.data.settings.enabled;
$scope.webauthnRequirePasskey = response.data.settings.require_passkey;
$scope.webauthnAllowMultiple = response.data.settings.allow_multiple_credentials;
$scope.webauthnMaxCredentials = response.data.settings.max_credentials;
$scope.canAddCredential = response.data.settings.can_add_credential;
}
}, function(error) {
console.error('Error loading WebAuthn data:', error);
});
};
$scope.toggleWebAuthn = function() {
if ($scope.webauthnEnabled) {
$scope.loadWebAuthnData();
} else {
$scope.webauthnCredentials = [];
$scope.canAddCredential = true;
}
};
$scope.registerNewPasskey = function() {
if (!window.cyberPanelWebAuthn) {
alert('WebAuthn is not supported in this browser');
return;
}
var credentialName = prompt('Enter a name for this passkey:', 'Passkey ' + new Date().toLocaleDateString());
if (!credentialName) return;
window.cyberPanelWebAuthn.registerPasskey($scope.accountUsername, credentialName)
.then(function(response) {
if (response.success) {
$scope.loadWebAuthnData();
$scope.$apply();
}
})
.catch(function(error) {
console.error('Error registering passkey:', error);
});
};
$scope.deleteCredential = function(credentialId) {
if (!confirm('Are you sure you want to delete this passkey?')) return;
if (!window.cyberPanelWebAuthn) {
alert('WebAuthn is not supported in this browser');
return;
}
window.cyberPanelWebAuthn.deleteCredential($scope.accountUsername, credentialId)
.then(function(response) {
if (response.success) {
$scope.loadWebAuthnData();
$scope.$apply();
}
})
.catch(function(error) {
console.error('Error deleting credential:', error);
});
};
$scope.updateCredentialName = function(credentialId, newName) {
if (!window.cyberPanelWebAuthn) return;
window.cyberPanelWebAuthn.updateCredentialName($scope.accountUsername, credentialId, newName)
.then(function(response) {
if (response.success) {
$scope.loadWebAuthnData();
$scope.$apply();
}
})
.catch(function(error) {
console.error('Error updating credential name:', error);
});
};
$scope.refreshCredentials = function() {
$scope.loadWebAuthnData();
};
$scope.saveWebAuthnSettings = function() {
if (!window.cyberPanelWebAuthn) {
alert('WebAuthn is not supported in this browser');
return;
}
var settings = {
enabled: $scope.webauthnEnabled,
require_passkey: $scope.webauthnRequirePasskey,
allow_multiple_credentials: $scope.webauthnAllowMultiple,
max_credentials: $scope.webauthnMaxCredentials,
timeout_seconds: $scope.webauthnTimeout
};
window.cyberPanelWebAuthn.updateSettings($scope.accountUsername, settings)
.then(function(response) {
if (response.success) {
$scope.loadWebAuthnData();
$scope.$apply();
}
})
.catch(function(error) {
console.error('Error updating WebAuthn settings:', error);
});
};
$scope.fetchUserDetails = function () {
@ -223,6 +338,18 @@ app.controller('modifyUser', function ($scope, $http) {
$scope.secretKey = userDetails.secretKey;
$scope.formattedSecretKey = userDetails.secretKey.match(/.{1,4}/g).join(' ');
}
// Initialize WebAuthn settings
$scope.webauthnEnabled = false;
$scope.webauthnRequirePasskey = false;
$scope.webauthnAllowMultiple = true;
$scope.webauthnMaxCredentials = 10;
$scope.webauthnTimeout = 60;
$scope.webauthnCredentials = [];
$scope.canAddCredential = true;
// Load WebAuthn settings and credentials
$scope.loadWebAuthnData();
qrCode.set({
value: userDetails.otpauth
@ -305,18 +432,28 @@ app.controller('modifyUser', function ($scope, $http) {
firstName: firstName,
lastName: lastName,
email: email,
passwordByPass: password,
securityLevel: $scope.securityLevel,
twofa: $scope.twofa
};
// Only include password if it's provided and not empty
if (password && password.trim()) {
data.passwordByPass = password;
}
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
$http.post(url, data, config).then(function(response) {
ListInitialDatas(response);
// Save WebAuthn settings after successful user modification
if (response.data.saveStatus == 1) {
$scope.saveWebAuthnSettings();
}
}, cantLoadInitialDatas);
function ListInitialDatas(response) {
@ -1522,6 +1659,147 @@ app.controller('apiAccessCTRL', function ($scope, $http) {
});
/* Java script code for api access */
/* Java script code for api users list */
app.controller('apiUsersCTRL', function ($scope, $http) {
$scope.apiUsers = [];
$scope.filteredUsers = [];
$scope.searchQuery = '';
$scope.apiUsersLoading = true;
$scope.loadAPIUsers = function() {
$scope.apiUsersLoading = false;
var url = "/users/fetchAPIUsers";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.get(url, config).then(loadAPIUsersSuccess, loadAPIUsersError);
};
function loadAPIUsersSuccess(response) {
$scope.apiUsersLoading = true;
if (response.data.status === 1) {
$scope.apiUsers = response.data.users;
$scope.filteredUsers = response.data.users;
new PNotify({
title: 'Success!',
text: 'API users loaded successfully',
type: 'success'
});
} else {
new PNotify({
title: 'Error!',
text: response.data.error_message,
type: 'error'
});
}
}
function loadAPIUsersError(response) {
$scope.apiUsersLoading = true;
new PNotify({
title: 'Error!',
text: 'Could not load API users. Please refresh the page.',
type: 'error'
});
}
$scope.searchUsers = function() {
if (!$scope.searchQuery || $scope.searchQuery.trim() === '') {
$scope.filteredUsers = $scope.apiUsers;
return;
}
var query = $scope.searchQuery.toLowerCase();
$scope.filteredUsers = $scope.apiUsers.filter(function(user) {
return user.userName.toLowerCase().includes(query) ||
user.firstName.toLowerCase().includes(query) ||
user.lastName.toLowerCase().includes(query) ||
user.email.toLowerCase().includes(query) ||
user.aclName.toLowerCase().includes(query);
});
};
$scope.clearSearch = function() {
$scope.searchQuery = '';
$scope.filteredUsers = $scope.apiUsers;
};
$scope.viewUserDetails = function(user) {
new PNotify({
title: 'User Details',
text: 'Username: ' + user.userName + '<br>' +
'Full Name: ' + user.firstName + ' ' + user.lastName + '<br>' +
'Email: ' + user.email + '<br>' +
'ACL: ' + user.aclName + '<br>' +
'Token Status: ' + user.tokenStatus + '<br>' +
'State: ' + user.state,
type: 'info',
styling: 'bootstrap3',
delay: 10000
});
};
$scope.disableAPI = function(user) {
if (confirm('Are you sure you want to disable API access for ' + user.userName + '?')) {
$scope.apiUsersLoading = false;
var url = "/users/saveChangesAPIAccess";
var data = {
accountUsername: user.userName,
access: 'Disable'
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(disableAPISuccess, disableAPIError);
}
};
function disableAPISuccess(response) {
$scope.apiUsersLoading = true;
if (response.data.status === 1) {
// Remove user from the list
$scope.apiUsers = $scope.apiUsers.filter(function(u) {
return u.userName !== response.data.accountUsername;
});
$scope.filteredUsers = $scope.apiUsers;
new PNotify({
title: 'Success!',
text: 'API access disabled for ' + response.data.accountUsername,
type: 'success'
});
} else {
new PNotify({
title: 'Error!',
text: response.data.error_message,
type: 'error'
});
}
}
function disableAPIError(response) {
$scope.apiUsersLoading = true;
new PNotify({
title: 'Error!',
text: 'Could not disable API access. Please try again.',
type: 'error'
});
}
// Load API users when controller initializes
$scope.loadAPIUsers();
});
/* Java script code to list table users */

View File

@ -166,6 +166,220 @@
.text-muted {
color: var(--text-secondary, #8893a7);
}
/* Tab Navigation Styles */
.tab-navigation {
display: flex;
margin-bottom: 20px;
border-bottom: 2px solid var(--border-color, #e8e9ff);
}
.tab-button {
background: none;
border: none;
padding: 15px 25px;
font-size: 16px;
font-weight: 600;
color: var(--text-secondary, #8893a7);
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.tab-button:hover {
color: var(--accent-color, #5b5fcf);
background: var(--bg-hover, #f8f9ff);
}
.tab-button.active {
color: var(--accent-color, #5b5fcf);
border-bottom-color: var(--accent-color, #5b5fcf);
background: var(--bg-hover, #f8f9ff);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Search Container Styles */
.search-container {
margin-bottom: 25px;
}
.search-box {
position: relative;
max-width: 400px;
}
.search-box i {
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary, #8893a7);
}
.search-input {
width: 100%;
padding: 12px 45px 12px 45px;
border: 1px solid var(--border-color, #e8e9ff);
border-radius: 8px;
font-size: 16px;
background: var(--bg-secondary, white);
color: var(--text-primary, #2f3640);
transition: all 0.3s ease;
}
.search-input:focus {
border-color: var(--accent-color, #5b5fcf);
box-shadow: 0 0 0 3px rgba(91, 95, 207, 0.1);
outline: none;
}
.clear-search {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-secondary, #8893a7);
cursor: pointer;
padding: 5px;
border-radius: 50%;
transition: all 0.3s ease;
}
.clear-search:hover {
background: var(--bg-hover, #f8f9ff);
color: var(--accent-color, #5b5fcf);
}
.search-results-info {
margin-top: 10px;
color: var(--text-secondary, #8893a7);
font-size: 14px;
}
/* Users Table Styles */
.users-table-container {
overflow-x: auto;
}
.users-table {
width: 100%;
border-collapse: collapse;
background: var(--bg-secondary, white);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px var(--shadow-color, rgba(0,0,0,0.08));
}
.users-table th {
background: var(--bg-hover, #f8f9ff);
color: var(--text-primary, #2f3640);
font-weight: 600;
padding: 15px 12px;
text-align: left;
border-bottom: 2px solid var(--border-color, #e8e9ff);
}
.users-table td {
padding: 15px 12px;
border-bottom: 1px solid var(--border-color, #e8e9ff);
vertical-align: middle;
}
.users-table tbody tr:hover {
background: var(--bg-hover, #f8f9ff);
}
/* Badge Styles */
.acl-badge {
background: var(--accent-color, #5b5fcf);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
/* Token Status Styles */
.token-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 500;
}
.token-valid {
color: var(--success-text, #10b981);
}
.token-warning {
color: var(--warning-text, #f59e0b);
}
.token-error {
color: var(--danger-text, #ef4444);
}
.token-status i {
font-size: 8px;
}
/* User State Styles */
.user-state {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.state-active {
background: var(--success-bg, #f0fdf4);
color: var(--success-text, #166534);
}
.state-inactive {
background: var(--danger-bg, #fef2f2);
color: var(--danger-text, #991b1b);
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 8px;
}
.btn-action {
background: none;
border: 1px solid var(--border-color, #e8e9ff);
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
color: var(--text-secondary, #8893a7);
}
.btn-action:hover {
background: var(--bg-hover, #f8f9ff);
border-color: var(--accent-color, #5b5fcf);
color: var(--accent-color, #5b5fcf);
}
.btn-view:hover {
color: var(--info-text, #3b82f6);
border-color: var(--info-text, #3b82f6);
}
.btn-disable:hover {
color: var(--danger-text, #ef4444);
border-color: var(--danger-text, #ef4444);
}
/* Empty State */
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary, #8893a7);
}
.empty-state i {
font-size: 48px;
margin-bottom: 20px;
color: var(--text-secondary, #8893a7);
}
.empty-state h3 {
margin-bottom: 10px;
color: var(--text-primary, #2f3640);
}
.empty-state p {
margin: 0;
line-height: 1.5;
}
@media (max-width: 768px) {
.content-card {
padding: 20px;
@ -176,6 +390,17 @@
.section-title {
font-size: 1.1rem;
}
.tab-button {
padding: 12px 15px;
font-size: 14px;
}
.users-table {
font-size: 14px;
}
.users-table th,
.users-table td {
padding: 10px 8px;
}
}
</style>
@ -185,10 +410,22 @@
<div class="page-header">
<h1 class="page-title">{% trans "API Access" %}</h1>
</div>
<div class="content-card" ng-controller="apiAccessCTRL">
<!-- Tab Navigation -->
<div class="tab-navigation">
<button class="tab-button active" onclick="switchTab('configure')" id="configure-tab">
<i class="fa fa-cog"></i> {% trans "Configure API Access" %}
</button>
<button class="tab-button" onclick="switchTab('users')" id="users-tab">
<i class="fa fa-users"></i> {% trans "API Users" %}
</button>
</div>
<!-- Configure API Access Tab -->
<div class="content-card tab-content active" ng-controller="apiAccessCTRL" id="configure-content">
<h3 class="section-title">
{% trans "Configure API Access" %}
<img class="loading-icon" ng-hide="cyberpanelLoading" src="{% static 'images/loading.gif' %}">
<img class="loading-icon" ng-hide="cyberpanelLoading" src="{% static 'images/loading.gif' %}" alt="Loading configuration">
</h3>
<div class="info-box">
<h4><i class="fa fa-info-circle"></i> {% trans "Important Information" %}</h4>
@ -197,7 +434,7 @@
<form action="/" class="form-horizontal">
<div class="form-group">
<label class="form-label">{% trans "Select User Account" %}</label>
<select ng-change="showApiAccessDropDown()" ng-model="accountUsername" class="form-control">
<select ng-change="showApiAccessDropDown()" ng-model="accountUsername" class="form-control" title="Select user account" aria-label="Select user account">
<option value="">-- {% trans "Choose a user" %} --</option>
{% for items in acctNames %}
<option>{{ items }}</option>
@ -241,7 +478,130 @@
</div>
</form>
</div>
<!-- API Users Tab -->
<div class="content-card tab-content" ng-controller="apiUsersCTRL" id="users-content">
<h3 class="section-title">
{% trans "Users with API Access" %}
<img class="loading-icon" ng-hide="apiUsersLoading" src="{% static 'images/loading.gif' %}" alt="Loading API users">
</h3>
<!-- Search Box -->
<div class="search-container">
<div class="search-box">
<i class="fa fa-search"></i>
<input type="text" ng-model="searchQuery" ng-keyup="searchUsers()" placeholder="{% trans 'Search users by name, email, or username...' %}" class="search-input" title="Search API users" aria-label="Search API users">
<button ng-click="clearSearch()" ng-show="searchQuery" class="clear-search">
<i class="fa fa-times"></i>
</button>
</div>
<div class="search-results-info" ng-show="searchQuery">
<span class="results-count">{{ filteredUsers.length }} {% trans "users found" %}</span>
</div>
</div>
<!-- Users Table -->
<div class="users-table-container">
<table class="users-table" ng-show="apiUsers.length > 0">
<thead>
<tr>
<th>{% trans "Username" %}</th>
<th>{% trans "Full Name" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "ACL" %}</th>
<th>{% trans "Token Status" %}</th>
<th>{% trans "State" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="user in filteredUsers = (apiUsers | filter:searchQuery)">
<td>
<strong>{{ user.userName }}</strong>
</td>
<td>{{ user.firstName }} {{ user.lastName }}</td>
<td>{{ user.email }}</td>
<td>
<span class="acl-badge">{{ user.aclName }}</span>
</td>
<td>
<span ng-class="{
'token-valid': user.tokenStatus === 'Valid',
'token-warning': user.tokenStatus === 'Needs Generation',
'token-error': user.tokenStatus === 'Not Generated'
}" class="token-status">
<i class="fa fa-circle"></i> {{ user.tokenStatus }}
</span>
</td>
<td>
<span ng-class="{
'state-active': user.state === 'ACTIVE',
'state-inactive': user.state !== 'ACTIVE'
}" class="user-state">
{{ user.state }}
</span>
</td>
<td>
<div class="action-buttons">
<button ng-click="viewUserDetails(user)" class="btn-action btn-view" title="{% trans 'View Details' %}">
<i class="fa fa-eye"></i>
</button>
<button ng-click="disableAPI(user)" class="btn-action btn-disable" title="{% trans 'Disable API Access' %}">
<i class="fa fa-power-off"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<!-- Empty State -->
<div class="empty-state" ng-show="apiUsers.length === 0">
<i class="fa fa-users"></i>
<h3>{% trans "No users with API access found" %}</h3>
<p>{% trans "No users currently have API access enabled. Use the Configure tab to enable API access for users." %}</p>
</div>
<!-- No Search Results -->
<div class="empty-state" ng-show="apiUsers.length > 0 && filteredUsers.length === 0">
<i class="fa fa-search"></i>
<h3>{% trans "No users found" %}</h3>
<p>{% trans "No users match your search criteria. Try adjusting your search terms." %}</p>
</div>
</div>
</div>
</div>
</div>
<script>
// Tab switching functionality
function switchTab(tabName) {
// Hide all tab contents
document.getElementById('configure-content').classList.remove('active');
document.getElementById('users-content').classList.remove('active');
// Remove active class from all tabs
document.getElementById('configure-tab').classList.remove('active');
document.getElementById('users-tab').classList.remove('active');
// Show selected tab content and add active class
if (tabName === 'configure') {
document.getElementById('configure-content').classList.add('active');
document.getElementById('configure-tab').classList.add('active');
} else if (tabName === 'users') {
document.getElementById('users-content').classList.add('active');
document.getElementById('users-tab').classList.add('active');
// Load API users when switching to users tab
if (typeof angular !== 'undefined' && angular.element(document.getElementById('users-content')).scope()) {
angular.element(document.getElementById('users-content')).scope().loadAPIUsers();
}
}
}
// Initialize with configure tab active
document.addEventListener('DOMContentLoaded', function() {
switchTab('configure');
});
</script>
{% endblock %}

View File

@ -342,6 +342,21 @@
<p class="help-text">{% trans "Choose the security level for this account" %}</p>
</div>
<div class="form-group">
<label class="form-label">{% trans "Home Directory" %}</label>
<select ng-model="selectedHomeDirectory" class="form-control" ng-change="updateHomeDirectoryInfo()">
<option value="">{% trans "Auto-select (Best Available)" %}</option>
<option ng-repeat="dir in homeDirectories" value="{{dir.id}}">
{{dir.name}} ({{dir.path}}) - {{dir.available_space | filesize}} available
</option>
</select>
<p class="help-text">{% trans "Choose the home directory for this user's files" %}</p>
<div ng-show="selectedHomeDirectoryInfo" class="alert alert-info" style="margin-top: 10px;">
<strong>{{selectedHomeDirectoryInfo.name}}:</strong> {{selectedHomeDirectoryInfo.description || 'No description available'}}<br>
<small>Available Space: {{selectedHomeDirectoryInfo.available_space | filesize}} | Users: {{selectedHomeDirectoryInfo.user_count}}</small>
</div>
</div>
<div class="form-group" style="margin-top: 2rem;">
<button type="button" ng-click="createUserFunc()" class="btn-primary">
<i class="fa fa-user-plus"></i> {% trans "Create User" %}
@ -368,4 +383,99 @@
</div>
</div>
<script>
app.controller('createUserCtr', function ($scope, $http, $timeout) {
$scope.homeDirectories = [];
$scope.selectedHomeDirectory = '';
$scope.selectedHomeDirectoryInfo = null;
// Load home directories on page load
$scope.loadHomeDirectories = function() {
$http.post('/userManagement/getUserHomeDirectories/', {})
.then(function(response) {
if (response.data.status === 1) {
$scope.homeDirectories = response.data.directories;
}
})
.catch(function(error) {
console.error('Error loading home directories:', error);
});
};
// Update home directory info when selection changes
$scope.updateHomeDirectoryInfo = function() {
if ($scope.selectedHomeDirectory) {
$scope.selectedHomeDirectoryInfo = $scope.homeDirectories.find(function(dir) {
return dir.id == $scope.selectedHomeDirectory;
});
} else {
$scope.selectedHomeDirectoryInfo = null;
}
};
// Initialize
$scope.loadHomeDirectories();
// Existing controller code...
$scope.firstName = '';
$scope.lastName = '';
$scope.email = '';
$scope.userName = '';
$scope.password = '';
$scope.websitesLimits = 0;
$scope.selectedACL = 'user';
$scope.securityLevel = 'HIGH';
$scope.userCreated = true;
$scope.userCreationFailed = true;
$scope.couldNotConnect = true;
$scope.combinedLength = true;
$scope.generatedPasswordView = true;
$scope.errorMessage = '';
$scope.generatePassword = function () {
$scope.password = Math.random().toString(36).slice(-10);
$scope.generatedPasswordView = false;
};
$scope.usePassword = function () {
$scope.generatedPasswordView = true;
};
$scope.createUserFunc = function () {
$scope.userCreated = true;
$scope.userCreationFailed = true;
$scope.couldNotConnect = true;
$scope.combinedLength = true;
if ($scope.createUser.$valid) {
if ($scope.firstName.length + $scope.lastName.length > 20) {
$scope.combinedLength = false;
return;
}
$http.post('/userManagement/submitUserCreation/', {
'firstName': $scope.firstName,
'lastName': $scope.lastName,
'email': $scope.email,
'userName': $scope.userName,
'password': $scope.password,
'websitesLimit': $scope.websitesLimits,
'selectedACL': $scope.selectedACL,
'securityLevel': $scope.securityLevel,
'selectedHomeDirectory': $scope.selectedHomeDirectory
}).then(function (data) {
if (data.data.createStatus === 1) {
$scope.userCreated = false;
} else {
$scope.userCreationFailed = false;
$scope.errorMessage = data.data.error_message;
}
}, function (data) {
$scope.couldNotConnect = false;
});
}
};
});
</script>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More