Remove SECURITY_INSTALLATION.md and implement SSL reconciliation features in manageSSL module. Add new views and URLs for SSL reconciliation, enhance mobile responsiveness in templates, and update SSL utilities for improved functionality. Update upgrade script for scheduled SSL reconciliation tasks.

This commit is contained in:
Master3395
2025-09-18 21:37:48 +02:00
parent bd237dd897
commit 8ca3ae1b49
18 changed files with 2123 additions and 617 deletions

View File

@@ -1,193 +0,0 @@
# CyberPanel Secure Installation Guide
## Overview
This document describes the secure installation process for CyberPanel that eliminates hardcoded passwords and implements environment-based configuration.
## Security Improvements
### ✅ **Fixed Security Vulnerabilities**
1. **Hardcoded Database Passwords** - Now generated securely during installation
2. **Hardcoded Django Secret Key** - Now generated using cryptographically secure random generation
3. **Environment Variables** - All sensitive configuration moved to `.env` file
4. **File Permissions** - `.env` file set to 600 (owner read/write only)
### 🔐 **Security Features**
- **Cryptographically Secure Passwords**: Uses Python's `secrets` module for password generation
- **Environment-based Configuration**: Sensitive data stored in `.env` file, not in code
- **Secure File Permissions**: Environment files protected with 600 permissions
- **Credential Backup**: Automatic backup of credentials for recovery
- **Fallback Security**: Maintains backward compatibility with fallback method
## Installation Process
### 1. **Automatic Secure Installation**
The installation script now automatically:
1. Generates secure random passwords for:
- MySQL root user
- CyberPanel database user
- Django secret key
2. Creates `.env` file with secure configuration:
```bash
# Generated during installation
SECRET_KEY=your_64_character_secure_key
DB_PASSWORD=your_24_character_secure_password
ROOT_DB_PASSWORD=your_24_character_secure_password
```
3. Creates `.env.backup` file for credential recovery
4. Sets secure file permissions (600) on all environment files
### 2. **Manual Installation** (if needed)
If you need to manually generate environment configuration:
```bash
cd /usr/local/CyberCP
python install/env_generator.py /usr/local/CyberCP
```
## File Structure
```
/usr/local/CyberCP/
├── .env # Main environment configuration (600 permissions)
├── .env.backup # Credential backup (600 permissions)
├── .env.template # Template for manual configuration
├── .gitignore # Prevents .env files from being committed
└── CyberCP/
└── settings.py # Updated to use environment variables
```
## Security Best Practices
### ✅ **Do's**
- Keep `.env` and `.env.backup` files secure
- Record credentials from `.env.backup` and delete the file after installation
- Use strong, unique passwords for production deployments
- Regularly rotate database passwords
- Monitor access to environment files
### ❌ **Don'ts**
- Never commit `.env` files to version control
- Don't share `.env` files via insecure channels
- Don't use default passwords in production
- Don't leave `.env.backup` files on the system after recording credentials
## Recovery
### **Lost Credentials**
If you lose your database credentials:
1. Check if `.env.backup` file exists:
```bash
sudo cat /usr/local/CyberCP/.env.backup
```
2. If backup doesn't exist, you'll need to reset MySQL passwords using MySQL recovery procedures
### **Regenerate Environment**
To regenerate environment configuration:
```bash
cd /usr/local/CyberCP
sudo python install/env_generator.py /usr/local/CyberCP
```
## Configuration Options
### **Environment Variables**
| Variable | Description | Default |
|----------|-------------|---------|
| `SECRET_KEY` | Django secret key | Generated (64 chars) |
| `DB_PASSWORD` | CyberPanel DB password | Generated (24 chars) |
| `ROOT_DB_PASSWORD` | MySQL root password | Generated (24 chars) |
| `DEBUG` | Debug mode | False |
| `ALLOWED_HOSTS` | Allowed hosts | localhost,127.0.0.1,hostname |
### **Custom Configuration**
To use custom passwords during installation:
```bash
python install/env_generator.py /usr/local/CyberCP "your_root_password" "your_db_password"
```
## Troubleshooting
### **Installation Fails**
If the new secure installation fails:
1. Check installation logs for error messages
2. The system will automatically fallback to the original installation method
3. Verify Python dependencies are installed:
```bash
pip install python-dotenv
```
### **Environment Loading Issues**
If Django can't load environment variables:
1. Ensure `.env` file exists and has correct permissions:
```bash
ls -la /usr/local/CyberCP/.env
# Should show: -rw------- 1 root root
```
2. Install python-dotenv if missing:
```bash
pip install python-dotenv
```
## Migration from Old Installation
### **Existing Installations**
For existing CyberPanel installations with hardcoded passwords:
1. **Backup current configuration**:
```bash
cp /usr/local/CyberCP/CyberCP/settings.py /usr/local/CyberCP/CyberCP/settings.py.backup
```
2. **Generate new environment configuration**:
```bash
cd /usr/local/CyberCP
python install/env_generator.py /usr/local/CyberCP
```
3. **Update settings.py** (already done in new installations):
- The settings.py file now supports environment variables
- It will fallback to hardcoded values if .env is not available
4. **Test the configuration**:
```bash
cd /usr/local/CyberCP
python manage.py check
```
## Support
For issues with the secure installation:
1. Check the installation logs
2. Verify file permissions
3. Ensure all dependencies are installed
4. Review the fallback installation method if needed
---
**Security Notice**: This installation method significantly improves security by eliminating hardcoded credentials. Always ensure proper file permissions and secure handling of environment files.

View File

@@ -20,6 +20,9 @@
{{ cosmetic.MainDashboardCSS | safe }}
</style>
<!-- Mobile Responsive CSS -->
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/mobile-responsive.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>
@@ -955,7 +958,7 @@
<body>
<!-- Header -->
<div id="header">
<button id="mobile-menu-toggle" onclick="toggleSidebar()">
<button id="mobile-menu-toggle" class="mobile-menu-toggle" onclick="toggleSidebar()" style="display: none;">
<i class="fas fa-bars"></i>
</button>
<div class="logo">
@@ -1457,6 +1460,10 @@
<a href="{% url 'manageSSL' %}" class="menu-item">
<span>Manage SSL</span>
</a>
<a href="{% url 'sslReconcile' %}" class="menu-item">
<span>SSL Reconciliation</span>
<span class="badge">NEW</span>
</a>
{% endif %}
{% if admin or hostnameSSL %}
<a href="{% url 'sslForHostName' %}" class="menu-item">
@@ -1786,8 +1793,50 @@
<!-- Scripts -->
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('show');
const sidebar = document.getElementById('sidebar');
const mainContent = document.getElementById('main-content');
const mobileToggle = document.getElementById('mobile-menu-toggle');
sidebar.classList.toggle('show');
// Add/remove sidebar-open class to main content for mobile
if (window.innerWidth <= 768) {
if (mainContent) {
mainContent.classList.toggle('sidebar-open');
}
}
}
// Show/hide mobile menu toggle based on screen size
function handleResize() {
const mobileToggle = document.getElementById('mobile-menu-toggle');
const sidebar = document.getElementById('sidebar');
const mainContent = document.getElementById('main-content');
if (window.innerWidth <= 768) {
mobileToggle.style.display = 'block';
// Close sidebar by default on mobile
sidebar.classList.remove('show');
if (mainContent) {
mainContent.classList.remove('sidebar-open');
}
} else {
mobileToggle.style.display = 'none';
// Show sidebar on desktop
sidebar.classList.remove('show');
if (mainContent) {
mainContent.classList.remove('sidebar-open');
}
}
}
// Add event listener for window resize
window.addEventListener('resize', handleResize);
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
handleResize();
});
function toggleSubmenu(id, element) {
const submenu = document.getElementById(id);

View File

@@ -0,0 +1,318 @@
{% extends "baseTemplate/index.html" %}
{% load static %}
{% block title %}SSL Reconciliation - CyberPanel{% endblock %}
{% block extraCSS %}
<style>
.ssl-reconcile-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.ssl-reconcile-card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
overflow: hidden;
}
.ssl-reconcile-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: center;
}
.ssl-reconcile-body {
padding: 30px;
}
.action-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
margin: 10px 5px;
transition: all 0.3s ease;
}
.action-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.action-button:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.danger-button {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
}
.danger-button:hover {
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
}
.success-button {
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
}
.success-button:hover {
box-shadow: 0 4px 15px rgba(46, 204, 113, 0.4);
}
.status-message {
padding: 15px;
border-radius: 6px;
margin: 15px 0;
font-weight: 500;
}
.status-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.domain-input {
width: 100%;
padding: 12px;
border: 2px solid #e1e5e9;
border-radius: 6px;
font-size: 14px;
margin: 10px 0;
}
.domain-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.feature-card {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.feature-card h4 {
margin: 0 0 10px 0;
color: #333;
}
.feature-card p {
margin: 0;
color: #666;
font-size: 14px;
}
</style>
{% endblock %}
{% block content %}
<div class="ssl-reconcile-container">
<div class="ssl-reconcile-card">
<div class="ssl-reconcile-header">
<h2><i class="fas fa-shield-alt"></i> SSL Reconciliation</h2>
<p>Fix SSL certificate issues and ACME challenge configurations</p>
</div>
<div class="ssl-reconcile-body">
<div id="statusMessage"></div>
<div class="feature-grid">
<div class="feature-card">
<h4><i class="fas fa-globe"></i> Reconcile All Domains</h4>
<p>Fix SSL certificates and ACME challenges for all domains on your server</p>
<button class="action-button" onclick="reconcileAll()">
<i class="fas fa-sync-alt"></i> Reconcile All
</button>
</div>
<div class="feature-card">
<h4><i class="fas fa-cog"></i> Fix ACME Contexts</h4>
<p>Fix ACME challenge context configurations for all domains</p>
<button class="action-button success-button" onclick="fixACMEContexts()">
<i class="fas fa-wrench"></i> Fix ACME Contexts
</button>
</div>
<div class="feature-card">
<h4><i class="fas fa-search"></i> Reconcile Specific Domain</h4>
<p>Fix SSL certificate issues for a specific domain</p>
<input type="text" id="domainInput" class="domain-input" placeholder="Enter domain name (e.g., example.com)">
<button class="action-button" onclick="reconcileDomain()">
<i class="fas fa-search"></i> Reconcile Domain
</button>
</div>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Processing SSL reconciliation...</p>
</div>
</div>
</div>
</div>
<script>
function showStatus(message, type = 'info') {
const statusDiv = document.getElementById('statusMessage');
statusDiv.innerHTML = `<div class="status-message status-${type}">${message}</div>`;
statusDiv.scrollIntoView({ behavior: 'smooth' });
}
function showLoading(show = true) {
document.getElementById('loading').style.display = show ? 'block' : 'none';
}
function reconcileAll() {
showLoading(true);
showStatus('Starting SSL reconciliation for all domains...', 'info');
fetch('/manageSSL/reconcileAllSSL', {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
showLoading(false);
if (data.reconcileStatus === 1) {
showStatus(data.error_message, 'success');
} else {
showStatus(data.error_message, 'error');
}
})
.catch(error => {
showLoading(false);
showStatus('Error: ' + error.message, 'error');
});
}
function reconcileDomain() {
const domain = document.getElementById('domainInput').value.trim();
if (!domain) {
showStatus('Please enter a domain name', 'error');
return;
}
showLoading(true);
showStatus(`Starting SSL reconciliation for ${domain}...`, 'info');
fetch('/manageSSL/reconcileDomainSSL', {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json',
},
body: JSON.stringify({ domain: domain })
})
.then(response => response.json())
.then(data => {
showLoading(false);
if (data.reconcileStatus === 1) {
showStatus(data.error_message, 'success');
} else {
showStatus(data.error_message, 'error');
}
})
.catch(error => {
showLoading(false);
showStatus('Error: ' + error.message, 'error');
});
}
function fixACMEContexts() {
showLoading(true);
showStatus('Fixing ACME challenge contexts...', 'info');
fetch('/manageSSL/fixACMEContexts', {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
showLoading(false);
if (data.reconcileStatus === 1) {
showStatus(data.error_message, 'success');
} else {
showStatus(data.error_message, 'error');
}
})
.catch(error => {
showLoading(false);
showStatus('Error: ' + error.message, 'error');
});
}
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
</script>
{% endblock %}

View File

@@ -17,4 +17,10 @@ urlpatterns = [
# v2 functions
path('v2ManageSSL', views.v2ManageSSL, name='v2ManageSSL'),
path('v2IssueSSL', views.v2IssueSSL, name='v2IssueSSL'),
# SSL Reconciliation functions
path('sslReconcile', views.sslReconcile, name='sslReconcile'),
path('reconcileAllSSL', views.reconcileAllSSL, name='reconcileAllSSL'),
path('reconcileDomainSSL', views.reconcileDomainSSL, name='reconcileDomainSSL'),
path('fixACMEContexts', views.fixACMEContexts, name='fixACMEContexts'),
]

View File

@@ -1,437 +1,135 @@
# -*- coding: utf-8 -*-
#!/usr/local/CyberCP/bin/python
import os
import sys
import django
from plogical.httpProc import httpProc
from websiteFunctions.models import Websites, ChildDomains
from loginSystem.models import Administrator
from plogical.virtualHostUtilities import virtualHostUtilities
from django.http import HttpResponse
import json
sys.path.append('/usr/local/CyberCP')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings")
django.setup()
from django.shortcuts import render, HttpResponse
from plogical.acl import ACLManager
from plogical.processUtilities import ProcessUtilities
# Create your views here.
def loadSSLHome(request):
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
proc = httpProc(request, 'manageSSL/index.html',
currentACL, 'admin')
return proc.render()
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
from plogical.sslReconcile import SSLReconcile
from plogical.sslUtilities import sslUtilities
import json
def manageSSL(request):
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
websitesName = ACLManager.findAllSites(currentACL, userID)
proc = httpProc(request, 'manageSSL/manageSSL.html',
{'websiteList': websitesName}, 'manageSSL')
return proc.render()
def v2ManageSSL(request):
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
websitesName = ACLManager.findAllSites(currentACL, userID)
data = {}
if ACLManager.CheckForPremFeature('all'):
data['PremStat'] = 1
else:
data['PremStat'] = 0
if request.method == 'POST':
SAVED_CF_Key = request.POST.get('SAVED_CF_Key')
SAVED_CF_Email = request.POST.get('SAVED_CF_Email')
from plogical.dnsUtilities import DNS
DNS.ConfigureCloudflareInAcme(SAVED_CF_Key, SAVED_CF_Email)
data['SaveSuccess'] = 1
RetStatus, SAVED_CF_Key, SAVED_CF_Email = ACLManager.FetchCloudFlareAPIKeyFromAcme()
from plogical.dnsUtilities import DNS
DNS.ConfigurePowerDNSInAcme()
data['SAVED_CF_Key'] = SAVED_CF_Key
data['SAVED_CF_Email'] = SAVED_CF_Email
data['websiteList'] = websitesName
proc = httpProc(request, 'manageSSL/v2ManageSSL.html',
data, 'manageSSL')
return proc.render()
def v2IssueSSL(request):
def sslReconcile(request):
"""SSL Reconciliation interface"""
try:
userID = request.session['userID']
admin = Administrator.objects.get(pk=userID)
try:
if ACLManager.CheckForPremFeature('all'):
if request.method == 'POST':
currentACL = ACLManager.loadedACL(userID)
currentACL = ACLManager.loadedACL(request.user.pk)
admin = ACLManager.loadedAdmin(request.user.pk)
if currentACL['admin'] == 1:
pass
elif currentACL['manageSSL'] == 1:
pass
else:
return ACLManager.loadErrorJson('SSL', 0)
if ACLManager.currentContextPermission(currentACL, 'sslReconcile') == 0:
return ACLManager.loadErrorJson('sslReconcile', 0)
data = json.loads(request.body)
virtualHost = data['virtualHost']
return render(request, 'manageSSL/sslReconcile.html', {
'acls': currentACL,
'admin': admin
})
if ACLManager.checkOwnership(virtualHost, admin, currentACL) == 1:
pass
else:
return ACLManager.loadErrorJson()
try:
website = ChildDomains.objects.get(domain=virtualHost)
adminEmail = website.master.adminEmail
path = website.path
except:
website = Websites.objects.get(domain=virtualHost)
adminEmail = website.adminEmail
path = "/home/" + virtualHost + "/public_html"
## ssl issue
execPath = "/usr/local/CyberCP/bin/python " + virtualHostUtilities.cyberPanel + "/plogical/virtualHostUtilities.py"
execPath = execPath + " issueSSLv2 --virtualHostName " + virtualHost + " --administratorEmail " + adminEmail + " --path " + path
output = ProcessUtilities.outputExecutioner(execPath)
if output.find("1,") > -1:
## ssl issue ends
website.ssl = 1
website.save()
# Extract detailed logs from output
logs = output.split("1,", 1)[1] if "1," in output else output
data_ret = {'status': 1, "SSL": 1,
'error_message': "None", 'sslLogs': logs, 'fullOutput': output}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
else:
# Parse error details from output
error_message = output
detailed_error = "SSL issuance failed"
# Check for common ACME errors
if "Rate limit" in output or "rate limit" in output:
detailed_error = "Let's Encrypt rate limit exceeded. Please wait before retrying."
elif "DNS problem" in output or "NXDOMAIN" in output:
detailed_error = "DNS validation failed. Please ensure your domain points to this server."
elif "Connection refused" in output or "Connection timeout" in output:
detailed_error = "Could not connect to ACME server. Check your firewall settings."
elif "Unauthorized" in output or "authorization" in output:
detailed_error = "Domain authorization failed. Verify domain ownership and DNS settings."
elif "CAA record" in output:
detailed_error = "CAA record prevents issuance. Check your DNS CAA records."
elif "Challenge failed" in output or "challenge failed" in output:
detailed_error = "ACME challenge failed. Ensure port 80 is accessible and .well-known path is not blocked."
elif "Invalid response" in output:
detailed_error = "Invalid response from ACME challenge. Check your web server configuration."
else:
# Try to extract the actual error message
if "0," in output:
error_parts = output.split("0,", 1)
if len(error_parts) > 1:
detailed_error = error_parts[1].strip()
data_ret = {'status': 0, "SSL": 0,
'error_message': detailed_error,
'sslLogs': output,
'fullOutput': output,
'technicalDetails': error_message}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'status': 0, "SSL": 0,
'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except KeyError:
data_ret = {'status': 0, "SSL": 0,
'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
logging.writeToFile(str(msg) + " [sslReconcile]")
return ACLManager.loadErrorJson('sslReconcile', 0)
def issueSSL(request):
def reconcileAllSSL(request):
"""Reconcile SSL for all domains"""
try:
userID = request.session['userID']
admin = Administrator.objects.get(pk=userID)
try:
if request.method == 'POST':
currentACL = ACLManager.loadedACL(userID)
currentACL = ACLManager.loadedACL(request.user.pk)
admin = ACLManager.loadedAdmin(request.user.pk)
if currentACL['admin'] == 1:
pass
elif currentACL['manageSSL'] == 1:
pass
if ACLManager.currentContextPermission(currentACL, 'sslReconcile') == 0:
return ACLManager.loadErrorJson('sslReconcile', 0)
# Run SSL reconciliation
success = SSLReconcile.reconcile_all()
if success:
data_ret = {'reconcileStatus': 1, 'error_message': "SSL reconciliation completed successfully"}
else:
return ACLManager.loadErrorJson('SSL', 0)
data_ret = {'reconcileStatus': 0, 'error_message': "SSL reconciliation failed. Check logs for details."}
data = json.loads(request.body)
virtualHost = data['virtualHost']
if ACLManager.checkOwnership(virtualHost, admin, currentACL) == 1:
pass
else:
return ACLManager.loadErrorJson()
try:
website = ChildDomains.objects.get(domain=virtualHost)
adminEmail = website.master.adminEmail
path = website.path
except:
website = Websites.objects.get(domain=virtualHost)
adminEmail = website.adminEmail
path = "/home/" + virtualHost + "/public_html"
## ssl issue
execPath = "/usr/local/CyberCP/bin/python " + virtualHostUtilities.cyberPanel + "/plogical/virtualHostUtilities.py"
execPath = execPath + " issueSSL --virtualHostName " + virtualHost + " --administratorEmail " + adminEmail + " --path " + path
output = ProcessUtilities.outputExecutioner(execPath)
if output.find("1,None") > -1:
pass
else:
data_ret = {'status': 0, "SSL": 0,
'error_message': output}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
## ssl issue ends
website.ssl = 1
website.save()
data_ret = {'status': 1, "SSL": 1,
'error_message': "None"}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'status': 0, "SSL": 0,
'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except KeyError:
data_ret = {'status': 0, "SSL": 0,
'error_message': str(msg)}
logging.writeToFile(str(msg) + " [reconcileAllSSL]")
data_ret = {'reconcileStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def sslForHostName(request):
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
websitesName = ACLManager.findAllSites(currentACL, userID, 1)
proc = httpProc(request, 'manageSSL/sslForHostName.html',
{'websiteList': websitesName}, 'hostnameSSL')
return proc.render()
def obtainHostNameSSL(request):
def reconcileDomainSSL(request):
"""Reconcile SSL for a specific domain"""
try:
userID = request.session['userID']
try:
if request.method == 'POST':
currentACL = ACLManager.loadedACL(request.user.pk)
admin = ACLManager.loadedAdmin(request.user.pk)
currentACL = ACLManager.loadedACL(userID)
if ACLManager.currentContextPermission(currentACL, 'sslReconcile') == 0:
return ACLManager.loadErrorJson('sslReconcile', 0)
if currentACL['admin'] == 1:
pass
elif currentACL['hostnameSSL'] == 1:
pass
else:
return ACLManager.loadErrorJson('SSL', 0)
data = json.loads(request.body)
virtualHost = data['virtualHost']
try:
website = Websites.objects.get(domain=virtualHost)
path = "/home/" + virtualHost + "/public_html"
except:
website = ChildDomains.objects.get(domain=virtualHost)
path = website.path
admin = Administrator.objects.get(pk=userID)
if ACLManager.checkOwnership(virtualHost, admin, currentACL) == 1:
pass
else:
return ACLManager.loadErrorJson()
## ssl issue
execPath = "/usr/local/CyberCP/bin/python " + virtualHostUtilities.cyberPanel + "/plogical/virtualHostUtilities.py"
execPath = execPath + " issueSSLForHostName --virtualHostName " + virtualHost + " --path " + path
output = ProcessUtilities.outputExecutioner(execPath)
if output.find("1,None") > -1:
data_ret = {"status": 1, "SSL": 1,
'error_message': "None"}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
else:
data_ret = {"status": 0, "SSL": 0,
'error_message': output}
domain = request.POST.get('domain')
if not domain:
data_ret = {'reconcileStatus': 0, 'error_message': "Domain not specified"}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
## ssl issue ends
# Run SSL reconciliation for specific domain
success = SSLReconcile.reconcile_domain(domain)
if success:
data_ret = {'reconcileStatus': 1, 'error_message': f"SSL reconciliation completed for {domain}"}
else:
data_ret = {'reconcileStatus': 0, 'error_message': f"SSL reconciliation failed for {domain}. Check logs for details."}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {"status": 0, "SSL": 0,
'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except KeyError:
data_ret = {"status": 0, "SSL": 0,
'error_message': str(msg)}
logging.writeToFile(str(msg) + " [reconcileDomainSSL]")
data_ret = {'reconcileStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def sslForMailServer(request):
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
websitesName = ACLManager.findAllSites(currentACL, userID)
websitesName = websitesName + ACLManager.findChildDomains(websitesName)
proc = httpProc(request, 'manageSSL/sslForMailServer.html',
{'websiteList': websitesName}, 'mailServerSSL')
return proc.render()
def obtainMailServerSSL(request):
def fixACMEContexts(request):
"""Fix ACME challenge contexts for all domains"""
try:
userID = request.session['userID']
try:
if request.method == 'POST':
currentACL = ACLManager.loadedACL(request.user.pk)
admin = ACLManager.loadedAdmin(request.user.pk)
currentACL = ACLManager.loadedACL(userID)
if ACLManager.currentContextPermission(currentACL, 'sslReconcile') == 0:
return ACLManager.loadErrorJson('sslReconcile', 0)
if currentACL['admin'] == 1:
pass
elif currentACL['mailServerSSL'] == 1:
pass
from websiteFunctions.models import Websites
fixed_count = 0
failed_domains = []
for website in Websites.objects.all():
if sslUtilities.fix_acme_challenge_context(website.domain):
fixed_count += 1
else:
return ACLManager.loadErrorJson('SSL', 0)
data = json.loads(request.body)
virtualHost = data['virtualHost']
admin = Administrator.objects.get(pk=userID)
if ACLManager.checkOwnership(virtualHost, admin, currentACL) == 1:
pass
else:
return ACLManager.loadErrorJson()
path = "/home/" + virtualHost + "/public_html"
## ssl issue
execPath = "/usr/local/CyberCP/bin/python " + virtualHostUtilities.cyberPanel + "/plogical/virtualHostUtilities.py"
execPath = execPath + " issueSSLForMailServer --virtualHostName " + virtualHost + " --path " + path
output = ProcessUtilities.outputExecutioner(execPath)
if output.find("1,None") > -1:
data_ret = {"status": 1, "SSL": 1,
'error_message': "None"}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
else:
data_ret = {"status": 0, "SSL": 0,
'error_message': output}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
## ssl issue ends
except BaseException as msg:
data_ret = {"status": 0, "SSL": 0,
'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except KeyError as msg:
data_ret = {"status": 0, "SSL": 0,
'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def getSSLDetails(request):
try:
userID = request.session['userID']
admin = Administrator.objects.get(pk=userID)
try:
if request.method == 'POST':
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
elif currentACL['manageSSL'] == 1:
pass
else:
return ACLManager.loadErrorJson('SSL', 0)
data = json.loads(request.body)
virtualHost = data['virtualHost']
if ACLManager.checkOwnership(virtualHost, admin, currentACL) == 1:
pass
else:
return ACLManager.loadErrorJson()
try:
website = ChildDomains.objects.get(domain=virtualHost)
except:
website = Websites.objects.get(domain=virtualHost)
try:
import OpenSSL
from datetime import datetime
filePath = '/etc/letsencrypt/live/%s/fullchain.pem' % (virtualHost)
x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
open(filePath, 'r').read())
expireData = x509.get_notAfter().decode('ascii')
finalDate = datetime.strptime(expireData, '%Y%m%d%H%M%SZ')
now = datetime.now()
diff = finalDate - now
failed_domains.append(website.domain)
if failed_domains:
data_ret = {
'status': 1,
'hasSSL': True,
'days': str(diff.days),
'authority': x509.get_issuer().get_components()[1][1].decode('utf-8'),
'expiryDate': finalDate.strftime('%Y-%m-%d %H:%M:%S')
'reconcileStatus': 1,
'error_message': f"Fixed ACME contexts for {fixed_count} domains. Failed: {', '.join(failed_domains)}"
}
if data_ret['authority'] == 'Denial':
data_ret['authority'] = 'SELF-SIGNED SSL'
except BaseException as msg:
else:
data_ret = {
'status': 1,
'hasSSL': False,
'error_message': str(msg)
'reconcileStatus': 1,
'error_message': f"Fixed ACME contexts for {fixed_count} domains successfully"
}
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)
except KeyError:
data_ret = {'status': 0, 'error_message': 'Not logged in'}
logging.writeToFile(str(msg) + " [fixACMEContexts]")
data_ret = {'reconcileStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)

View File

@@ -0,0 +1 @@
# Management commands for plogical module

View File

@@ -0,0 +1 @@
# Management commands for plogical module

View File

@@ -0,0 +1,98 @@
#!/usr/local/CyberCP/bin/python
"""
Django management command for SSL reconciliation
Usage: python manage.py ssl_reconcile [--all|--domain <domain>]
"""
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 django.core.management.base import BaseCommand, CommandError
from plogical.sslReconcile import SSLReconcile
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
class Command(BaseCommand):
help = 'Reconcile SSL certificates and ACME challenge configurations'
def add_arguments(self, parser):
parser.add_argument(
'--all',
action='store_true',
help='Reconcile SSL for all domains',
)
parser.add_argument(
'--domain',
type=str,
help='Reconcile SSL for a specific domain',
)
parser.add_argument(
'--fix-acme',
action='store_true',
help='Fix ACME challenge contexts for all domains',
)
def handle(self, *args, **options):
if options['all']:
self.stdout.write('Starting SSL reconciliation for all domains...')
try:
success = SSLReconcile.reconcile_all()
if success:
self.stdout.write(
self.style.SUCCESS('SSL reconciliation completed successfully!')
)
else:
self.stdout.write(
self.style.ERROR('SSL reconciliation failed. Check logs for details.')
)
except Exception as e:
raise CommandError(f'SSL reconciliation failed: {str(e)}')
elif options['domain']:
domain = options['domain']
self.stdout.write(f'Starting SSL reconciliation for domain: {domain}')
try:
success = SSLReconcile.reconcile_domain(domain)
if success:
self.stdout.write(
self.style.SUCCESS(f'SSL reconciliation completed for {domain}!')
)
else:
self.stdout.write(
self.style.ERROR(f'SSL reconciliation failed for {domain}. Check logs for details.')
)
except Exception as e:
raise CommandError(f'SSL reconciliation failed for {domain}: {str(e)}')
elif options['fix_acme']:
self.stdout.write('Fixing ACME challenge contexts for all domains...')
try:
from plogical.sslUtilities import sslUtilities
from websiteFunctions.models import Websites
fixed_count = 0
for website in Websites.objects.all():
if sslUtilities.fix_acme_challenge_context(website.domain):
fixed_count += 1
self.stdout.write(f'Fixed ACME context for: {website.domain}')
self.stdout.write(
self.style.SUCCESS(f'Fixed ACME challenge contexts for {fixed_count} domains!')
)
except Exception as e:
raise CommandError(f'Failed to fix ACME contexts: {str(e)}')
else:
self.stdout.write(
self.style.WARNING('Please specify --all, --domain <domain>, or --fix-acme')
)
self.stdout.write('Usage examples:')
self.stdout.write(' python manage.py ssl_reconcile --all')
self.stdout.write(' python manage.py ssl_reconcile --domain example.com')
self.stdout.write(' python manage.py ssl_reconcile --fix-acme')

419
plogical/sslReconcile.py Normal file
View File

@@ -0,0 +1,419 @@
#!/usr/bin/env python3
"""
SSL Reconciliation Module for CyberPanel
Integrates the acme_reconcile_all.sh functionality into CyberPanel core
"""
import os
import re
import subprocess
import shlex
import hashlib
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
from plogical.processUtilities import ProcessUtilities
from plogical import installUtilities
class SSLReconcile:
"""SSL Certificate Reconciliation and Management"""
VHOSTS_DIR = "/usr/local/lsws/conf/vhosts"
VHROOT_BASE = "/home"
ACME_HOME = "/root/.acme.sh"
RELOAD_CMD = "systemctl restart lsws"
@staticmethod
def trim(text):
"""Trim whitespace from text"""
return text.strip()
@staticmethod
def pick_lineage_conf(vhost):
"""Pick the appropriate acme.sh lineage configuration file"""
ecc_conf = f"{SSLReconcile.ACME_HOME}/{vhost}_ecc/{vhost}.conf"
reg_conf = f"{SSLReconcile.ACME_HOME}/{vhost}/{vhost}.conf"
if os.path.exists(ecc_conf):
return ecc_conf
elif os.path.exists(reg_conf):
return reg_conf
else:
# Create ECC lineage directory and file
os.makedirs(f"{SSLReconcile.ACME_HOME}/{vhost}_ecc", exist_ok=True)
with open(ecc_conf, 'w') as f:
f.write('')
return ecc_conf
@staticmethod
def set_kv(config_file, key, value):
"""Set key-value pair in configuration file"""
try:
# Read existing content
if os.path.exists(config_file):
with open(config_file, 'r') as f:
lines = f.readlines()
else:
lines = []
# Check if key exists and update or add
key_found = False
for i, line in enumerate(lines):
if line.startswith(f"{key}="):
lines[i] = f"{key}='{value}'\n"
key_found = True
break
if not key_found:
lines.append(f"{key}='{value}'\n")
# Write back to file
with open(config_file, 'w') as f:
f.writelines(lines)
except Exception as e:
logging.writeToFile(f"Error setting {key}={value} in {config_file}: {str(e)}")
raise
@staticmethod
def issuer_cn(pem_file):
"""Get issuer CN from certificate"""
if not os.path.exists(pem_file):
return ""
try:
cmd = f"openssl x509 -in {pem_file} -noout -issuer"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if result.returncode == 0:
issuer_line = result.stdout.strip()
# Extract CN from issuer line
cn_match = re.search(r'CN=([^,]+)', issuer_line)
if cn_match:
return cn_match.group(1)
except Exception as e:
logging.writeToFile(f"Error getting issuer CN from {pem_file}: {str(e)}")
return ""
@staticmethod
def sha256fp(pem_file):
"""Get SHA256 fingerprint of certificate"""
if not os.path.exists(pem_file):
return ""
try:
cmd = f"openssl x509 -in {pem_file} -noout -fingerprint -sha256"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if result.returncode == 0:
fingerprint_line = result.stdout.strip()
# Extract fingerprint value
fp_match = re.search(r'=([A-F0-9:]+)', fingerprint_line)
if fp_match:
return fp_match.group(1)
except Exception as e:
logging.writeToFile(f"Error getting SHA256 fingerprint from {pem_file}: {str(e)}")
return ""
@staticmethod
def fix_context_location(vconf_path, docroot):
"""Fix ACME challenge context location in vhost configuration"""
try:
# Read current configuration
with open(vconf_path, 'r') as f:
content = f.read()
want_uri = '/.well-known/acme-challenge/'
want_loc = f"{docroot}/.well-known/acme-challenge/"
# Create challenge directory
os.makedirs(want_loc, exist_ok=True)
os.chmod(docroot, 0o755)
os.chmod(f"{docroot}/.well-known", 0o755)
os.chmod(want_loc, 0o755)
# Check if context already exists
context_pattern = r'context\s+/.well-known/acme-challenge/?\s*{'
if re.search(context_pattern, content):
# Update existing context
content = re.sub(
r'(context\s+)/.well-known/acme-challenge/?(\s*{)',
rf'\1{want_uri}\2',
content
)
content = re.sub(
r'(location\s+).*acme-challenge/?',
rf'\1{want_loc}',
content
)
else:
# Add new context
context_block = f"""
context {want_uri} {{
location {want_loc}
addDefaultCharset off
}}
"""
content += context_block
# Write updated configuration
with open(vconf_path, 'w') as f:
f.write(content)
logging.writeToFile(f"Fixed ACME challenge context for {vconf_path}")
return True
except Exception as e:
logging.writeToFile(f"Error fixing context location for {vconf_path}: {str(e)}")
return False
@staticmethod
def needs_force_issue(vhost, dconf_path, docroot, live_fullchain):
"""Determine if certificate needs to be force re-issued"""
try:
# Read lineage configuration
le_api = ""
webroot = ""
if os.path.exists(dconf_path):
with open(dconf_path, 'r') as f:
for line in f:
if line.startswith("Le_API="):
le_api = line.split("'")[1] if "'" in line else ""
elif line.startswith("Le_Webroot="):
webroot = line.split("'")[1] if "'" in line else ""
# Check for staging API
if 'acme-staging' in le_api:
return True
# Check for wrong webroot
if webroot and webroot != docroot:
return True
# Check if live files are missing
if not os.path.exists(live_fullchain):
return True
# Check for fingerprint mismatch
acme_chain = ""
ecc_chain = f"{SSLReconcile.ACME_HOME}/{vhost}_ecc/fullchain.cer"
reg_chain = f"{SSLReconcile.ACME_HOME}/{vhost}/fullchain.cer"
if os.path.exists(ecc_chain):
acme_chain = ecc_chain
elif os.path.exists(reg_chain):
acme_chain = reg_chain
if acme_chain:
acme_fp = SSLReconcile.sha256fp(acme_chain)
live_fp = SSLReconcile.sha256fp(live_fullchain)
if acme_fp and live_fp and acme_fp != live_fp:
return True
return False
except Exception as e:
logging.writeToFile(f"Error checking force issue for {vhost}: {str(e)}")
return True # Force issue on error
@staticmethod
def reconcile_one(vconf_path):
"""Reconcile SSL configuration for a single vhost"""
try:
vhost = os.path.basename(os.path.dirname(vconf_path))
# Read docRoot from vhost configuration
docroot = ""
with open(vconf_path, 'r') as f:
for line in f:
if re.match(r'^\s*docRoot\s+', line):
docroot = SSLReconcile.trim(line.split()[1])
break
if not docroot:
logging.writeToFile(f"[skip] {vhost}: no docRoot")
return False
# Resolve $VH_ROOT variable
if docroot.startswith('$VH_ROOT/'):
docroot = docroot.replace('$VH_ROOT', f"{SSLReconcile.VHROOT_BASE}/{vhost}")
# 1) Fix context location
if not SSLReconcile.fix_context_location(vconf_path, docroot):
return False
# 2) Configure lineage
dconf_path = SSLReconcile.pick_lineage_conf(vhost)
SSLReconcile.set_kv(dconf_path, "Le_Webroot", docroot)
SSLReconcile.set_kv(dconf_path, "Le_API", "https://acme-v02.api.letsencrypt.org/directory")
# 3) Define live targets
live_dir = f"/etc/letsencrypt/live/{vhost}"
live_key = f"{live_dir}/privkey.pem"
live_full = f"{live_dir}/fullchain.pem"
live_cert = f"{live_dir}/cert.pem"
os.makedirs(live_dir, exist_ok=True)
# 4) Check if force issue is needed
if SSLReconcile.needs_force_issue(vhost, dconf_path, docroot, live_full):
# Build SAN set
alt_domains = []
if os.path.exists(dconf_path):
with open(dconf_path, 'r') as f:
for line in f:
if line.startswith("Le_Alt="):
alt = line.split("'")[1] if "'" in line else ""
if alt:
alt_domains.append(alt)
# Add www subdomain for base domains
if not alt_domains and not vhost.startswith('www.'):
alt_domains.append(f"www.{vhost}")
# Issue certificate
cmd_parts = [f"{SSLReconcile.ACME_HOME}/acme.sh", "--issue", "-d", vhost]
for alt in alt_domains:
cmd_parts.extend(["-d", alt])
cmd_parts.extend(["-w", docroot, "--ecc", "--server", "letsencrypt", "--force"])
result = subprocess.run(cmd_parts, capture_output=True, text=True)
if result.returncode != 0:
logging.writeToFile(f"Certificate issuance failed for {vhost}: {result.stderr}")
return False
logging.writeToFile(f"[issue] {vhost} certificate issued")
# 5) Set installation targets
SSLReconcile.set_kv(dconf_path, "Le_RealKeyPath", live_key)
SSLReconcile.set_kv(dconf_path, "Le_RealFullChainPath", live_full)
SSLReconcile.set_kv(dconf_path, "Le_RealCertPath", live_cert)
# 6) Install certificate if needed
if (not os.path.exists(live_full) or not os.path.exists(live_key) or
os.path.getsize(live_full) == 0 or os.path.getsize(live_key) == 0):
cmd_parts = [
f"{SSLReconcile.ACME_HOME}/acme.sh", "--install-cert", "-d", vhost, "--ecc",
"--key-file", live_key,
"--fullchain-file", live_full,
"--cert-file", live_cert,
"--reloadcmd", SSLReconcile.RELOAD_CMD
]
result = subprocess.run(cmd_parts, capture_output=True, text=True)
if result.returncode == 0:
logging.writeToFile(f"[install] {vhost} -> {live_dir}")
else:
logging.writeToFile(f"Certificate installation failed for {vhost}: {result.stderr}")
return False
else:
# Check if sync is needed
acme_chain = ""
ecc_chain = f"{SSLReconcile.ACME_HOME}/{vhost}_ecc/fullchain.cer"
reg_chain = f"{SSLReconcile.ACME_HOME}/{vhost}/fullchain.cer"
if os.path.exists(ecc_chain):
acme_chain = ecc_chain
elif os.path.exists(reg_chain):
acme_chain = reg_chain
if acme_chain:
acme_fp = SSLReconcile.sha256fp(acme_chain)
live_fp = SSLReconcile.sha256fp(live_full)
if acme_fp and live_fp and acme_fp != live_fp:
# Sync needed
cmd_parts = [
f"{SSLReconcile.ACME_HOME}/acme.sh", "--install-cert", "-d", vhost, "--ecc",
"--key-file", live_key,
"--fullchain-file", live_full,
"--cert-file", live_cert,
"--reloadcmd", SSLReconcile.RELOAD_CMD
]
result = subprocess.run(cmd_parts, capture_output=True, text=True)
if result.returncode == 0:
logging.writeToFile(f"[sync] {vhost} live files updated")
else:
logging.writeToFile(f"Certificate sync failed for {vhost}: {result.stderr}")
return False
else:
logging.writeToFile(f"[ok] {vhost} unchanged")
return True
except Exception as e:
logging.writeToFile(f"Error reconciling {vconf_path}: {str(e)}")
return False
@staticmethod
def reconcile_all():
"""Reconcile SSL configuration for all vhosts"""
try:
changed = 0
vhosts_dir = SSLReconcile.VHOSTS_DIR
if not os.path.exists(vhosts_dir):
logging.writeToFile(f"VHosts directory not found: {vhosts_dir}")
return False
# Process all vhost configurations
for vhost_name in os.listdir(vhosts_dir):
vconf_path = os.path.join(vhosts_dir, vhost_name, "vhost.conf")
if os.path.exists(vconf_path):
if SSLReconcile.reconcile_one(vconf_path):
changed += 1
# Restart LiteSpeed
try:
subprocess.run(SSLReconcile.RELOAD_CMD, shell=True, check=True)
logging.writeToFile(f"LiteSpeed restarted successfully")
except subprocess.CalledProcessError as e:
logging.writeToFile(f"Failed to restart LiteSpeed: {str(e)}")
logging.writeToFile(f"[done] processed={changed} vhosts")
return True
except Exception as e:
logging.writeToFile(f"Error in reconcile_all: {str(e)}")
return False
@staticmethod
def reconcile_domain(domain):
"""Reconcile SSL configuration for a specific domain"""
try:
vconf_path = os.path.join(SSLReconcile.VHOSTS_DIR, domain, "vhost.conf")
if not os.path.exists(vconf_path):
logging.writeToFile(f"VHost configuration not found for {domain}")
return False
if SSLReconcile.reconcile_one(vconf_path):
# Restart LiteSpeed
try:
subprocess.run(SSLReconcile.RELOAD_CMD, shell=True, check=True)
logging.writeToFile(f"LiteSpeed restarted successfully for {domain}")
except subprocess.CalledProcessError as e:
logging.writeToFile(f"Failed to restart LiteSpeed for {domain}: {str(e)}")
return True
else:
return False
except Exception as e:
logging.writeToFile(f"Error reconciling domain {domain}: {str(e)}")
return False
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
if sys.argv[1] == "--all":
SSLReconcile.reconcile_all()
elif sys.argv[1] == "--domain" and len(sys.argv) > 2:
SSLReconcile.reconcile_domain(sys.argv[2])
else:
print("Usage: python sslReconcile.py [--all|--domain <domain>]")
else:
SSLReconcile.reconcile_all()

View File

@@ -998,3 +998,57 @@ def issueSSLForDomain(domain, adminEmail, sslpath, aliasDomain=None, isHostname=
except BaseException as msg:
return [0, "347 " + str(msg) + " [issueSSLForDomain]"]
@staticmethod
def reconcile_ssl_all():
"""Reconcile SSL configuration for all domains using the new reconciliation module"""
try:
from plogical.sslReconcile import SSLReconcile
return SSLReconcile.reconcile_all()
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error in reconcile_ssl_all: {str(e)}")
return False
@staticmethod
def reconcile_ssl_domain(domain):
"""Reconcile SSL configuration for a specific domain using the new reconciliation module"""
try:
from plogical.sslReconcile import SSLReconcile
return SSLReconcile.reconcile_domain(domain)
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error in reconcile_ssl_domain for {domain}: {str(e)}")
return False
@staticmethod
def fix_acme_challenge_context(virtualHostName):
"""Fix ACME challenge context for a specific domain"""
try:
from plogical.sslReconcile import SSLReconcile
vconf_path = f"{sslUtilities.Server_root}/conf/vhosts/{virtualHostName}/vhost.conf"
if not os.path.exists(vconf_path):
logging.CyberCPLogFileWriter.writeToFile(f"VHost configuration not found: {vconf_path}")
return False
# Read docRoot from vhost configuration
docroot = ""
with open(vconf_path, 'r') as f:
for line in f:
if line.strip().startswith('docRoot'):
docroot = line.split()[1]
break
if not docroot:
logging.CyberCPLogFileWriter.writeToFile(f"No docRoot found for {virtualHostName}")
return False
# Resolve $VH_ROOT variable
if docroot.startswith('$VH_ROOT/'):
docroot = docroot.replace('$VH_ROOT', f"/home/{virtualHostName}")
return SSLReconcile.fix_context_location(vconf_path, docroot)
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error fixing ACME challenge context for {virtualHostName}: {str(e)}")
return False

View File

@@ -3789,6 +3789,7 @@ vmail
0 2 * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/upgradeCritical.py >/dev/null 2>&1
0 0 * * 4 /usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/renew.py >/dev/null 2>&1
7 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null
0 1 * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/manage.py ssl_reconcile --all >/dev/null 2>&1
*/3 * * * * if ! find /home/*/public_html/ -maxdepth 2 -type f -newer /usr/local/lsws/cgid -name '.htaccess' -exec false {} +; then /usr/local/lsws/bin/lswsctrl restart; fi
* * * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/manage.py run_scheduled_scans >/usr/local/lscp/logs/scheduled_scans.log 2>&1
"""
@@ -3838,6 +3839,7 @@ vmail
0 2 * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/upgradeCritical.py >/dev/null 2>&1
0 0 * * 4 /usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/renew.py >/dev/null 2>&1
7 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null
0 1 * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/manage.py ssl_reconcile --all >/dev/null 2>&1
0 0 * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/IncBackups/IncScheduler.py Daily
0 0 * * 0 /usr/local/CyberCP/bin/python /usr/local/CyberCP/IncBackups/IncScheduler.py Weekly
* * * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/manage.py run_scheduled_scans >/usr/local/lscp/logs/scheduled_scans.log 2>&1

View File

@@ -74,7 +74,7 @@ rewrite {
}
context /.well-known/acme-challenge {
location /usr/local/lsws/Example/html/.well-known/acme-challenge
location $VH_ROOT/public_html/.well-known/acme-challenge
allowBrowse 1
rewrite {
@@ -163,7 +163,7 @@ rewrite {
}
context /.well-known/acme-challenge {
location /usr/local/lsws/Example/html/.well-known/acme-challenge
location $VH_ROOT/public_html/.well-known/acme-challenge
allowBrowse 1
rewrite {
@@ -185,7 +185,7 @@ context /.well-known/acme-challenge {
ServerAdmin {administratorEmail}
SuexecUserGroup {externalApp} {externalApp}
DocumentRoot /home/{virtualHostName}/public_html
Alias /.well-known/acme-challenge /usr/local/lsws/Example/html/.well-known/acme-challenge
Alias /.well-known/acme-challenge /home/{virtualHostName}/public_html/.well-known/acme-challenge
CustomLog /home/{virtualHostName}/logs/{virtualHostName}.access_log combined
AddHandler application/x-httpd-php{php} .php .php7 .phtml
<IfModule LiteSpeed>
@@ -203,7 +203,7 @@ context /.well-known/acme-challenge {
ServerAdmin {administratorEmail}
SuexecUserGroup {externalApp} {externalApp}
DocumentRoot {path}
Alias /.well-known/acme-challenge /usr/local/lsws/Example/html/.well-known/acme-challenge
Alias /.well-known/acme-challenge /home/{virtualHostName}/public_html/.well-known/acme-challenge
CustomLog /home/{masterDomain}/logs/{masterDomain}.access_log combined
AddHandler application/x-httpd-php{php} .php .php7 .phtml
<IfModule LiteSpeed>
@@ -220,7 +220,7 @@ context /.well-known/acme-challenge {
ServerAdmin {administratorEmail}
SuexecUserGroup {externalApp} {externalApp}
DocumentRoot /home/{virtualHostName}/public_html/
Alias /.well-known/acme-challenge /usr/local/lsws/Example/html/.well-known/acme-challenge
Alias /.well-known/acme-challenge /home/{virtualHostName}/public_html/.well-known/acme-challenge
<Proxy "unix:{sockPath}{virtualHostName}.sock|fcgi://php-fpm-{externalApp}">
ProxySet disablereuse=off
</proxy>
@@ -371,7 +371,7 @@ accesslog $VH_ROOT/logs/$VH_NAME.access_log {
}
context /.well-known/acme-challenge {
location /usr/local/lsws/Example/html/.well-known/acme-challenge
location $VH_ROOT/public_html/.well-known/acme-challenge
allowBrowse 1
rewrite {

View File

@@ -0,0 +1,552 @@
/* CyberPanel Mobile Responsive & Readability Fixes */
/* This file ensures all pages are mobile-friendly with proper font sizes and readable text */
/* Base font size and mobile-first approach */
html {
font-size: 16px; /* Base font size for better readability */
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
body {
font-size: 16px;
line-height: 1.6;
color: #2f3640; /* Dark text for better readability on white backgrounds */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
/* Ensure all text is readable with proper contrast */
* {
color: inherit;
}
/* 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 */
}
/* Fix small font sizes that are hard to read */
small, .small, .text-small {
font-size: 14px !important; /* Minimum readable size */
}
/* Table improvements for mobile */
.table {
font-size: 16px !important; /* Larger table text */
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.table th, .table td {
padding: 12px 8px !important; /* More padding for touch targets */
border: 1px solid #e8e9ff;
text-align: left;
vertical-align: middle;
font-size: 14px !important;
line-height: 1.4;
}
.table th {
background-color: #f8f9fa;
font-weight: 600;
color: #2f3640 !important;
font-size: 15px !important;
}
/* Button improvements for mobile */
.btn {
font-size: 16px !important;
padding: 12px 20px !important;
border-radius: 8px;
min-height: 44px; /* Minimum touch target size */
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-sm {
font-size: 14px !important;
padding: 8px 16px !important;
min-height: 36px;
}
.btn-xs {
font-size: 13px !important;
padding: 6px 12px !important;
min-height: 32px;
}
/* Form elements */
.form-control, input, textarea, select {
font-size: 16px !important; /* Prevents zoom on iOS */
padding: 12px 16px !important;
border: 2px solid #e8e9ff;
border-radius: 8px;
min-height: 44px;
line-height: 1.4;
color: #2f3640 !important;
background-color: #ffffff;
}
.form-control:focus, input:focus, textarea:focus, select:focus {
border-color: #5856d6;
box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1);
outline: none;
}
/* Labels and form text */
label, .control-label {
font-size: 16px !important;
font-weight: 600;
color: #2f3640 !important;
margin-bottom: 8px;
display: block;
}
/* Headings with proper hierarchy */
h1 {
font-size: 2.5rem !important; /* 40px */
font-weight: 700;
color: #1e293b !important;
line-height: 1.2;
margin-bottom: 1rem;
}
h2 {
font-size: 2rem !important; /* 32px */
font-weight: 600;
color: #1e293b !important;
line-height: 1.3;
margin-bottom: 0.875rem;
}
h3 {
font-size: 1.5rem !important; /* 24px */
font-weight: 600;
color: #2f3640 !important;
line-height: 1.4;
margin-bottom: 0.75rem;
}
h4 {
font-size: 1.25rem !important; /* 20px */
font-weight: 600;
color: #2f3640 !important;
line-height: 1.4;
margin-bottom: 0.5rem;
}
h5 {
font-size: 1.125rem !important; /* 18px */
font-weight: 600;
color: #2f3640 !important;
line-height: 1.4;
margin-bottom: 0.5rem;
}
h6 {
font-size: 1rem !important; /* 16px */
font-weight: 600;
color: #2f3640 !important;
line-height: 1.4;
margin-bottom: 0.5rem;
}
/* Paragraph and body text */
p {
font-size: 16px !important;
line-height: 1.6;
color: #2f3640 !important;
margin-bottom: 1rem;
}
/* Sidebar improvements */
#page-sidebar {
font-size: 16px !important;
}
#page-sidebar ul li a {
font-size: 16px !important;
padding: 12px 20px !important;
color: #2f3640 !important;
min-height: 44px;
display: flex;
align-items: center;
text-decoration: none;
}
#page-sidebar ul li a:hover {
background-color: #f8f9fa;
color: #5856d6 !important;
}
/* Content area improvements */
.content-box, .panel, .card {
font-size: 16px !important;
color: #2f3640 !important;
background-color: #ffffff;
border: 1px solid #e8e9ff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
/* Modal improvements */
.modal-content {
font-size: 16px !important;
color: #2f3640 !important;
}
.modal-title {
font-size: 1.5rem !important;
font-weight: 600;
color: #1e293b !important;
}
/* Alert and notification improvements */
.alert {
font-size: 16px !important;
padding: 16px 20px !important;
border-radius: 8px;
margin-bottom: 20px;
}
.alert-success {
background-color: #f0fdf4;
border-color: #bbf7d0;
color: #166534 !important;
}
.alert-danger {
background-color: #fef2f2;
border-color: #fecaca;
color: #dc2626 !important;
}
.alert-warning {
background-color: #fffbeb;
border-color: #fed7aa;
color: #d97706 !important;
}
.alert-info {
background-color: #eff6ff;
border-color: #bfdbfe;
color: #2563eb !important;
}
/* Navigation improvements */
.navbar-nav .nav-link {
font-size: 16px !important;
padding: 12px 16px !important;
color: #2f3640 !important;
}
/* Breadcrumb improvements */
.breadcrumb {
font-size: 16px !important;
background-color: transparent;
padding: 0;
margin-bottom: 20px;
}
.breadcrumb-item {
color: #64748b !important;
}
.breadcrumb-item.active {
color: #2f3640 !important;
}
/* Mobile-first responsive breakpoints */
@media (max-width: 1200px) {
.container, .container-fluid {
padding-left: 15px;
padding-right: 15px;
}
.table-responsive {
border: none;
margin-bottom: 20px;
}
}
@media (max-width: 992px) {
/* Stack columns on tablets */
.col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9 {
flex: 0 0 100%;
max-width: 100%;
margin-bottom: 20px;
}
/* Adjust sidebar for tablets */
#page-sidebar {
width: 100%;
position: static;
height: auto;
}
/* Make tables horizontally scrollable */
.table-responsive {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table {
min-width: 600px;
}
}
@media (max-width: 768px) {
/* Mobile-specific adjustments */
html {
font-size: 14px;
}
body {
font-size: 14px;
padding: 0;
}
.container, .container-fluid {
padding-left: 10px;
padding-right: 10px;
}
/* Stack all columns on mobile */
.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-6, .col-sm-8, .col-sm-9, .col-sm-12,
.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-12 {
flex: 0 0 100%;
max-width: 100%;
margin-bottom: 15px;
}
/* Adjust headings for mobile */
h1 {
font-size: 2rem !important; /* 32px */
}
h2 {
font-size: 1.75rem !important; /* 28px */
}
h3 {
font-size: 1.5rem !important; /* 24px */
}
h4 {
font-size: 1.25rem !important; /* 20px */
}
/* Button adjustments for mobile */
.btn {
font-size: 16px !important;
padding: 14px 20px !important;
width: 100%;
margin-bottom: 10px;
}
.btn-group .btn {
width: auto;
margin-bottom: 0;
}
/* Form adjustments for mobile */
.form-control, input, textarea, select {
font-size: 16px !important; /* Prevents zoom on iOS */
padding: 14px 16px !important;
width: 100%;
}
/* Table adjustments for mobile */
.table {
font-size: 14px !important;
}
.table th, .table td {
padding: 8px 6px !important;
font-size: 13px !important;
}
/* Hide less important columns on mobile */
.table .d-none-mobile {
display: none !important;
}
/* Modal adjustments for mobile */
.modal-dialog {
margin: 10px;
width: calc(100% - 20px);
}
.modal-content {
padding: 20px 15px;
}
/* Content box adjustments */
.content-box, .panel, .card {
padding: 15px;
margin-bottom: 15px;
}
/* Sidebar adjustments for mobile */
#page-sidebar {
position: fixed;
top: 0;
left: -100%;
width: 280px;
height: 100vh;
z-index: 1000;
transition: left 0.3s ease;
background-color: #ffffff;
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
}
#page-sidebar.show {
left: 0;
}
/* Main content adjustments when sidebar is open */
#main-content {
transition: margin-left 0.3s ease;
}
#main-content.sidebar-open {
margin-left: 280px;
}
/* Mobile menu toggle */
.mobile-menu-toggle {
display: block;
position: fixed;
top: 20px;
left: 20px;
z-index: 1001;
background-color: #5856d6;
color: white;
border: none;
padding: 12px;
border-radius: 8px;
font-size: 18px;
cursor: pointer;
}
}
@media (max-width: 576px) {
/* Extra small devices */
html {
font-size: 14px;
}
.container, .container-fluid {
padding-left: 8px;
padding-right: 8px;
}
/* Even smaller buttons and forms for very small screens */
.btn {
font-size: 14px !important;
padding: 12px 16px !important;
}
.form-control, input, textarea, select {
font-size: 16px !important; /* Still 16px to prevent zoom */
padding: 12px 14px !important;
}
/* Compact table for very small screens */
.table th, .table td {
padding: 6px 4px !important;
font-size: 12px !important;
}
/* Hide even more columns on very small screens */
.table .d-none-mobile-sm {
display: none !important;
}
}
/* Utility classes for mobile */
.d-none-mobile {
display: block;
}
.d-none-mobile-sm {
display: block;
}
@media (max-width: 768px) {
.d-none-mobile {
display: none !important;
}
}
@media (max-width: 576px) {
.d-none-mobile-sm {
display: none !important;
}
}
/* Ensure all text has proper contrast */
.text-white {
color: #ffffff !important;
}
.text-dark {
color: #2f3640 !important;
}
.text-muted {
color: #64748b !important;
}
/* Fix any light text on light backgrounds */
.bg-light .text-muted,
.bg-white .text-muted,
.panel .text-muted {
color: #64748b !important;
}
/* Ensure proper spacing for touch targets */
a, button, input, select, textarea {
min-height: 44px;
min-width: 44px;
}
/* Fix for small clickable elements */
.glyph-icon, .icon {
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
/* Loading and spinner improvements */
.spinner, .loading {
font-size: 16px !important;
color: #5856d6 !important;
}
/* Print styles */
@media print {
body {
font-size: 12pt;
color: #000000 !important;
background: #ffffff !important;
}
.table th, .table td {
font-size: 10pt !important;
color: #000000 !important;
}
.btn, .alert, .modal {
display: none !important;
}
}

236
test_ssl_integration.py Normal file
View File

@@ -0,0 +1,236 @@
#!/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

@@ -50,7 +50,7 @@
</div>
<div style="margin-bottom: 1%;" class=" col-sm-1">
<a title="{% trans 'Cancel' %}" ng-click="hideDomainCreationForm()" href="">
<h3 class="glyph-icon icon-close text-danger mt-5"></h3>
<h3 class="glyph-icon icon-close text-danger mt-5" style="font-size: 24px; cursor: pointer;">&times;</h3>
</a>
</div>
</div>
@@ -169,7 +169,7 @@
<div style="margin-bottom: 1%;" class=" col-sm-1">
<a title="{% trans 'Close' %}" ng-click="hideListDomains()" href="">
<h3 class="glyph-icon icon-close text-danger mt-5"></h3>
<h3 class="glyph-icon icon-close text-danger mt-5" style="font-size: 24px; cursor: pointer;">&times;</h3>
</a>
</div>

View File

@@ -963,7 +963,7 @@
<div ng-hide="fetchedConfigsData" class="form-group">
<div style="margin-bottom: 1%;" class="col-sm-offset-11 col-sm-1">
<a ng-click="hideconfigbtn()" href=""><h3
class="glyph-icon icon-close text-danger mt-5"></h3></a>
class="glyph-icon icon-close text-danger mt-5" style="font-size: 24px; cursor: pointer;">&times;</h3></a>
</div>
<div class="col-sm-12">
<textarea ng-model="configData" rows="20" class="form-control"></textarea>
@@ -1019,7 +1019,7 @@
<div ng-hide="fetchedRewriteRules" class="form-group">
<div style="margin-bottom: 1%;" class="col-sm-offset-11 col-sm-1">
<a ng-click="hideRewriteRulesbtn()" href=""><h3
class="glyph-icon icon-close text-danger mt-5"></h3></a>
class="glyph-icon icon-close text-danger mt-5" style="font-size: 24px; cursor: pointer;">&times;</h3></a>
</div>
<div class="col-sm-12">
<textarea ng-model="rewriteRules" rows="10" class="form-control"></textarea>
@@ -1059,7 +1059,7 @@
<div style="margin-bottom: 1%;" class=" col-sm-1">
<a title="{% trans 'Cancel' %}" ng-click="hideChangePHPMaster()"
href=""><h3 class="glyph-icon icon-close text-danger mt-5"></h3></a>
href=""><h3 class="glyph-icon icon-close text-danger mt-5" style="font-size: 24px; cursor: pointer;">&times;</h3></a>
</div>
</div>

View File

@@ -403,6 +403,170 @@
font-size: 16px;
}
/* Mobile Responsive Styles */
@media (max-width: 768px) {
.page-wrapper {
padding: 10px;
}
.page-container {
max-width: 100%;
}
.container {
padding: 10px;
}
#page-title {
padding: 20px;
margin-bottom: 20px;
}
#page-title h1 {
font-size: 1.5rem !important;
}
#page-title p {
font-size: 14px !important;
}
/* Table improvements for mobile */
.table-responsive {
border: none;
margin-bottom: 20px;
}
.table {
font-size: 14px !important;
min-width: 600px;
}
.table th, .table td {
padding: 8px 6px !important;
font-size: 13px !important;
white-space: nowrap;
}
/* Hide less important columns on mobile */
.table .d-none-mobile {
display: none !important;
}
/* Button improvements for mobile */
.btn {
font-size: 16px !important;
padding: 12px 20px !important;
margin-bottom: 10px;
width: 100%;
}
.btn-group {
display: flex;
flex-direction: column;
width: 100%;
}
.btn-group .btn {
margin-bottom: 10px;
width: 100%;
}
/* Form improvements for mobile */
.form-horizontal .form-group {
margin-bottom: 15px;
}
.form-horizontal .control-label {
text-align: left !important;
margin-bottom: 5px;
font-size: 16px !important;
}
.form-horizontal .col-sm-3,
.form-horizontal .col-sm-6,
.form-horizontal .col-sm-9 {
width: 100% !important;
float: none !important;
}
.form-control, input, textarea, select {
font-size: 16px !important;
padding: 12px 16px !important;
width: 100%;
}
/* Modal improvements for mobile */
.modal-dialog {
margin: 10px;
width: calc(100% - 20px);
max-width: none;
}
.modal-content {
padding: 20px 15px;
}
.modal-title {
font-size: 1.25rem !important;
}
/* Card improvements for mobile */
.card {
margin-bottom: 15px;
}
.card-body {
padding: 15px;
}
.card-title {
font-size: 1.125rem !important;
}
.card-text {
font-size: 14px !important;
}
}
@media (max-width: 576px) {
/* Extra small devices */
.page-wrapper {
padding: 5px;
}
.container {
padding: 5px;
}
#page-title {
padding: 15px;
}
#page-title h1 {
font-size: 1.25rem !important;
}
.table th, .table td {
padding: 6px 4px !important;
font-size: 12px !important;
}
/* Hide even more columns on very small screens */
.table .d-none-mobile-sm {
display: none !important;
}
.btn {
font-size: 14px !important;
padding: 10px 15px !important;
}
.form-control, input, textarea, select {
font-size: 16px !important;
padding: 10px 14px !important;
}
}
/* Dark mode specific adjustments */
[data-theme="dark"] body {
background: var(--bg-primary);

View File

@@ -629,6 +629,107 @@
}
}
/* Additional mobile improvements */
@media (max-width: 768px) {
.cyberpanel-website-page {
padding: 10px;
}
/* Improve table responsiveness */
.table-responsive {
border: none;
margin-bottom: 20px;
}
.table {
font-size: 14px !important;
min-width: 600px;
}
.table th, .table td {
padding: 8px 6px !important;
font-size: 13px !important;
white-space: nowrap;
}
/* Hide less important columns on mobile */
.table .d-none-mobile {
display: none !important;
}
/* Improve form layout on mobile */
.form-horizontal .form-group {
margin-bottom: 15px;
}
.form-horizontal .control-label {
text-align: left !important;
margin-bottom: 5px;
}
.form-horizontal .col-sm-3,
.form-horizontal .col-sm-6,
.form-horizontal .col-sm-9 {
width: 100% !important;
float: none !important;
}
/* Improve button layout */
.btn-group {
display: flex;
flex-direction: column;
width: 100%;
}
.btn-group .btn {
margin-bottom: 10px;
width: 100%;
}
/* Improve modal on mobile */
.modal-dialog {
margin: 5px;
width: calc(100% - 10px);
max-width: none;
}
.modal-content {
padding: 15px;
}
/* Improve close button visibility */
.glyph-icon.icon-close {
font-size: 20px !important;
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
}
@media (max-width: 576px) {
/* Extra small devices */
.cyberpanel-website-page {
padding: 5px;
}
.table th, .table td {
padding: 6px 4px !important;
font-size: 12px !important;
}
/* Hide even more columns on very small screens */
.table .d-none-mobile-sm {
display: none !important;
}
.btn {
font-size: 14px !important;
padding: 10px 15px !important;
}
}
/* Modal and Form Modal Styling for Rewrite Rules */
.form-horizontal.bordered-row {
background: var(--bg-secondary, white);
@@ -1562,7 +1663,7 @@
<div style="margin-bottom: 1%;" class=" col-sm-1">
<a ng-click="hidelogsbtn()" href="">
<!--img src="/static/images/close-32.png"-->
<h3 class="glyph-icon icon-close text-danger mt-5"></h3>
<h3 class="glyph-icon icon-close text-danger mt-5" style="font-size: 24px; cursor: pointer;">&times;</h3>
</a>
</div>
<div class="col-sm-12">
@@ -1607,7 +1708,7 @@
<div style="margin-bottom: 1%;" class=" col-sm-1">
<a ng-click="hideErrorLogsbtn()" href="">
<!--img src="/static/images/close-32.png"-->
<h3 class="glyph-icon icon-close text-danger mt-5"></h3>
<h3 class="glyph-icon icon-close text-danger mt-5" style="font-size: 24px; cursor: pointer;">&times;</h3>
</a>
</div>
<div class="col-sm-12">
@@ -1671,7 +1772,7 @@
</div>
<div style="margin-bottom: 1%;" class=" col-sm-1">
<a title="{% trans 'Cancel' %}" ng-click="hideDomainCreationForm()" href="">
<h3 class="glyph-icon icon-close text-danger mt-5"></h3>
<h3 class="glyph-icon icon-close text-danger mt-5" style="font-size: 24px; cursor: pointer;">&times;</h3>
</a>
</div>
</div>
@@ -1828,7 +1929,7 @@
<div style="margin-bottom: 1%;" class=" col-sm-1">
<a title="{% trans 'Close' %}" ng-click="hideListDomains()" href="">
<h3 class="glyph-icon icon-close text-danger mt-5"></h3>
<h3 class="glyph-icon icon-close text-danger mt-5" style="font-size: 24px; cursor: pointer;">&times;</h3>
</a>
</div>
@@ -2119,7 +2220,7 @@
<div style="margin-bottom: 1%;" class=" col-sm-1">
<a title="{% trans 'Cancel' %}" ng-click="hideChangePHPMaster()"
href=""><h3 class="glyph-icon icon-close text-danger mt-5"></h3></a>
href=""><h3 class="glyph-icon icon-close text-danger mt-5" style="font-size: 24px; cursor: pointer;">&times;</h3></a>
</div>
</div>