mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2025-11-05 12:55:44 +01:00
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:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
318
manageSSL/templates/manageSSL/sslReconcile.html
Normal file
318
manageSSL/templates/manageSSL/sslReconcile.html
Normal 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 %}
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
1
plogical/management/__init__.py
Normal file
1
plogical/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Management commands for plogical module
|
||||
1
plogical/management/commands/__init__.py
Normal file
1
plogical/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Management commands for plogical module
|
||||
98
plogical/management/commands/ssl_reconcile.py
Normal file
98
plogical/management/commands/ssl_reconcile.py
Normal 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
419
plogical/sslReconcile.py
Normal 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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
552
static/baseTemplate/assets/mobile-responsive.css
Normal file
552
static/baseTemplate/assets/mobile-responsive.css
Normal 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
236
test_ssl_integration.py
Normal 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)
|
||||
@@ -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;">×</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;">×</h3>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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;">×</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;">×</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;">×</h3></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;">×</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;">×</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;">×</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;">×</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;">×</h3></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user