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

SSL renewal improvements, Cron Management fixes, and randomized renew…
This commit is contained in:
Master3395
2026-02-04 01:22:45 +01:00
committed by GitHub
7 changed files with 194 additions and 17 deletions

View File

@@ -5228,17 +5228,20 @@ user_query = SELECT email as user, password, 'vmail' as uid, 'vmail' as gid, '/h
cronFile = open(cronPath, "w")
# Randomize acme.sh cron schedule to avoid traffic spikes to Let's Encrypt
# Generate random hour (0-23) and minute (0-59) for each installation
# Randomize acme.sh and renew.py cron schedules to avoid traffic spikes to Let's Encrypt
# Each installation gets a random day (0-6 Sun-Sat), hour, and minute to spread load
acme_hour = random.randint(0, 23)
acme_minute = random.randint(0, 59)
renew_weekday = random.randint(0, 6) # 0=Sun, 1=Mon, ..., 6=Sat
renew_hour = random.randint(0, 23)
renew_minute = random.randint(0, 59)
content = """
0 * * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/findBWUsage.py >/dev/null 2>&1
0 * * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/postfixSenderPolicy/client.py hourlyCleanup >/dev/null 2>&1
0 0 1 * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/postfixSenderPolicy/client.py monthlyCleanup >/dev/null 2>&1
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
%d %d * * %d /usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/renew.py >/dev/null 2>&1
%d %d * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null
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
@@ -5252,7 +5255,7 @@ user_query = SELECT email as user, password, 'vmail' as uid, 'vmail' as gid, '/h
0 0 * * 0 /usr/local/CyberCP/bin/python /usr/local/CyberCP/IncBackups/IncScheduler.py '1 Week'
*/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
""" % (acme_minute, acme_hour)
""" % (renew_minute, renew_hour, renew_weekday, acme_minute, acme_hour)
cronFile.write(content)
cronFile.close()

View File

@@ -16,6 +16,51 @@ import OpenSSL
from plogical.virtualHostUtilities import virtualHostUtilities
from plogical.processUtilities import ProcessUtilities
def _update_ssl_renewal_schedule_file():
"""Write SSL renewal schedule to world-readable file for web UI (lscpd can't read root crontab)."""
try:
from datetime import datetime, timedelta
config_path = '/usr/local/CyberCP/ssl_renewal_schedule.conf'
cron_paths = ['/var/spool/cron/root', '/var/spool/cron/crontabs/root']
cron_content = None
for path in cron_paths:
if os.path.exists(path):
with open(path, 'r') as f:
cron_content = f.read()
break
if not cron_content:
return
renew_hour, renew_minute, renew_weekday_cron = 0, 0, 4
for line in cron_content.splitlines():
line = line.strip()
if 'renew.py' in line and not line.startswith('#'):
parts = line.split()
if len(parts) >= 5:
try:
renew_minute = int(parts[0]) if parts[0].isdigit() else 0
renew_hour = int(parts[1]) if parts[1].isdigit() else 0
renew_weekday_cron = int(parts[4]) if parts[4].isdigit() and 0 <= int(parts[4]) <= 7 else 4
except (ValueError, IndexError):
pass
elif len(parts) >= 2:
renew_minute = int(parts[0]) if parts[0].isdigit() else 0
renew_hour = int(parts[1]) if parts[1].isdigit() else 0
break
now = datetime.now()
target_weekday = (renew_weekday_cron - 1) % 7 if renew_weekday_cron else 6
days_until = (target_weekday - now.weekday()) % 7
if days_until == 0 and (now.hour > renew_hour or (now.hour == renew_hour and now.minute >= renew_minute)):
days_until = 7
next_run = now.replace(hour=renew_hour, minute=renew_minute, second=0, microsecond=0)
next_run += timedelta(days=days_until)
schedule_str = next_run.strftime('%B %d, %Y %I:%M %p')
with open(config_path, 'w') as f:
f.write(schedule_str)
os.chmod(config_path, 0o644)
except Exception as e:
logging.writeToFile(f'_update_ssl_renewal_schedule_file: {str(e)}', 1)
class Renew:
def _check_and_renew_ssl(self, domain: str, path: str, admin_email: str, is_child: bool = False) -> None:
"""Helper method to check and renew SSL for a domain."""
@@ -114,6 +159,7 @@ class Renew:
def SSLObtainer(self):
try:
_update_ssl_renewal_schedule_file()
logging.writeToFile('Running SSL Renew Utility')
# Process main domains

View File

@@ -5292,17 +5292,20 @@ vmail
data = open(cronPath, 'r').read()
if data.find('findBWUsage') == -1:
# Randomize acme.sh cron schedule to avoid traffic spikes to Let's Encrypt
# Generate random hour (0-23) and minute (0-59) for each installation
# Randomize acme.sh and renew.py cron schedules to avoid traffic spikes to Let's Encrypt
# Each installation gets a random day (0-6 Sun-Sat), hour, and minute to spread load
acme_hour = random.randint(0, 23)
acme_minute = random.randint(0, 59)
renew_weekday = random.randint(0, 6) # 0=Sun, 1=Mon, ..., 6=Sat
renew_hour = random.randint(0, 23)
renew_minute = random.randint(0, 59)
content = """
0 * * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/findBWUsage.py >/dev/null 2>&1
0 * * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/postfixSenderPolicy/client.py hourlyCleanup >/dev/null 2>&1
0 0 1 * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/postfixSenderPolicy/client.py monthlyCleanup >/dev/null 2>&1
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
%d %d * * %d /usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/renew.py >/dev/null 2>&1
%d %d * * * "/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
@@ -5310,7 +5313,7 @@ vmail
"""
writeToFile = open(cronPath, 'w')
writeToFile.write(content % (acme_minute, acme_hour))
writeToFile.write(content % (renew_minute, renew_hour, renew_weekday, acme_minute, acme_hour))
writeToFile.close()
if data.find('IncScheduler.py') == -1:
@@ -5347,23 +5350,26 @@ vmail
else:
# Randomize acme.sh cron schedule to avoid traffic spikes to Let's Encrypt
# Generate random hour (0-23) and minute (0-59) for each installation
# Randomize acme.sh and renew.py cron schedules to avoid traffic spikes to Let's Encrypt
# Each installation gets a random day (0-6 Sun-Sat), hour, and minute to spread load
acme_hour = random.randint(0, 23)
acme_minute = random.randint(0, 59)
renew_weekday = random.randint(0, 6) # 0=Sun, 1=Mon, ..., 6=Sat
renew_hour = random.randint(0, 23)
renew_minute = random.randint(0, 59)
content = """
0 * * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/findBWUsage.py >/dev/null 2>&1
0 * * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/postfixSenderPolicy/client.py hourlyCleanup >/dev/null 2>&1
0 0 1 * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/postfixSenderPolicy/client.py monthlyCleanup >/dev/null 2>&1
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
%d %d * * %d /usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/renew.py >/dev/null 2>&1
%d %d * * * "/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
""" % (acme_minute, acme_hour)
""" % (renew_minute, renew_hour, renew_weekday, acme_minute, acme_hour)
writeToFile = open(cronPath, 'w')
writeToFile.write(content)
writeToFile.close()

View File

@@ -653,7 +653,7 @@
<i class="fas fa-lock" style="font-size: 20px;"></i>
<div>
<h4 style="margin: 0 0 5px 0; font-size: 16px;">{{ authority }}</h4>
<p style="margin: 0;">{% trans "Your SSL will expire in" %} {{ days }} {% trans "days" %}</p>
<p style="margin: 0;">{% trans "Your SSL will expire in" %} {{ days }} {% trans "days" %}{% if renewal_when %} • {% trans "Renews" %} {{ renewal_when }}{% endif %}</p>
</div>
</div>
{% endif %}

View File

@@ -8,6 +8,64 @@
<!-- Current language: {{ LANGUAGE_CODE }} -->
<style>
/* Text visibility fixes - ensure buttons and text have sufficient contrast */
.cron-management-page .btn-primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
color: #ffffff !important;
border: none !important;
}
.cron-management-page .btn-primary:hover {
color: #ffffff !important;
}
.cron-management-page .btn-border {
background: transparent !important;
color: #6366f1 !important;
border: 2px solid #6366f1 !important;
}
.cron-management-page .btn-border:hover {
background: #6366f1 !important;
color: #ffffff !important;
}
.cron-management-page .btn-link {
color: #ffffff !important;
border: 2px solid rgba(255, 255, 255, 0.8) !important;
}
.cron-management-page .btn-link:hover {
color: #ffffff !important;
background: rgba(255, 255, 255, 0.2) !important;
}
.cron-management-page .btn-warning {
background: linear-gradient(135deg, #f59e0b 0%, #dc2626 100%) !important;
color: #ffffff !important;
}
.cron-management-page .btn-warning:hover {
color: #ffffff !important;
}
.cron-management-page #cronTable thead {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
}
.cron-management-page #cronTable thead th {
color: #ffffff !important;
}
.cron-management-page #cronTable tbody td {
color: #1e293b !important;
}
.cron-management-page #cronTable tbody td code {
color: #1e293b !important;
background: #f1f5f9 !important;
}
[data-theme="dark"] .cron-management-page #cronTable tbody td,
[data-theme="dark"] .cron-management-page #cronTable tbody td code {
color: #e4e4e7 !important;
}
[data-theme="dark"] .cron-management-page #cronTable tbody td code {
background: #252550 !important;
}
/* Page title - ensure gradient for Cron Docs button visibility */
.cron-management-page #page-title {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
}
/* Modern page styles matching new design with dark mode support */
.page-wrapper {
background: transparent;
@@ -652,7 +710,7 @@
}
</style>
<div ng-controller="manageCronController" class="container">
<div ng-controller="manageCronController" class="container cron-management-page">
<div id="page-title">
<h2>

View File

@@ -1569,17 +1569,17 @@
<span style="background: #f0fdf4; color: #10b981; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600;">
<i class="fas fa-lock" style="margin-right: 5px;"></i>{% trans "Secure" %}
</span>
<span style="color: #64748b;">• {% trans "Valid for" %} {{ days }} {% trans "days" %}</span>
<span style="color: #64748b;">• {% trans "Valid for" %} {{ days }} {% trans "days" %}{% if renewal_when %} • {% trans "Renews" %} {{ renewal_when }}{% endif %}</span>
{% elif days|add:0 >= 7 %}
<span style="background: #fef3c7; color: #f59e0b; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600;">
<i class="fas fa-clock" style="margin-right: 5px;"></i>{% trans "Expiring Soon" %}
</span>
<span style="color: #64748b;">• {{ days }} {% trans "days left" %}</span>
<span style="color: #64748b;">• {{ days }} {% trans "days left" %}{% if renewal_when %} • {% trans "Renews" %} {{ renewal_when }}{% endif %}</span>
{% else %}
<span style="background: #fee2e2; color: #ef4444; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600;">
<i class="fas fa-exclamation" style="margin-right: 5px;"></i>{% trans "Critical" %}
</span>
<span style="color: #64748b;">• {{ days }} {% trans "days left" %}</span>
<span style="color: #64748b;">• {{ days }} {% trans "days left" %}{% if renewal_when %} • {% trans "Renews" %} {{ renewal_when }}{% endif %}</span>
{% endif %}
</div>
{% endif %}

View File

@@ -49,6 +49,65 @@ from django.http import JsonResponse
import ipaddress
def _get_ssl_renewal_schedule():
"""Get formatted SSL renewal schedule (e.g. 'Thursday 12:00 AM').
Reads from world-readable config file first (web server can't read root crontab).
Cron day_of_week: 0=Sun, 1=Mon, ..., 6=Sat. Python weekday: Mon=0, ..., Sun=6."""
try:
from datetime import datetime, timedelta
# Config file is world-readable; web server (lscpd) cannot read /var/spool/cron/root
config_path = '/usr/local/CyberCP/ssl_renewal_schedule.conf'
if os.path.exists(config_path):
try:
with open(config_path, 'r') as f:
line = f.read().strip()
if line:
return line
except (IOError, OSError):
pass
cron_paths = ['/var/spool/cron/root', '/var/spool/cron/crontabs/root']
cron_content = None
for path in cron_paths:
if os.path.exists(path):
try:
with open(path, 'r') as f:
cron_content = f.read()
except (IOError, OSError):
continue
break
if not cron_content:
return None
renew_hour, renew_minute, renew_weekday_cron = 0, 0, 4 # default Thursday
for line in cron_content.splitlines():
line = line.strip()
if 'renew.py' in line and not line.startswith('#'):
parts = line.split()
if len(parts) >= 5:
try:
renew_minute = int(parts[0]) if parts[0].isdigit() else 0
renew_hour = int(parts[1]) if parts[1].isdigit() else 0
dow = parts[4]
renew_weekday_cron = int(dow) if dow.isdigit() and 0 <= int(dow) <= 7 else 4
except (ValueError, IndexError):
pass
elif len(parts) >= 2:
renew_minute = int(parts[0]) if parts[0].isdigit() else 0
renew_hour = int(parts[1]) if parts[1].isdigit() else 0
break
now = datetime.now()
# Cron: 0/7=Sun, 1=Mon, ..., 6=Sat -> Python: Mon=0, Tue=1, ..., Sun=6
target_weekday = (renew_weekday_cron - 1) % 7 if renew_weekday_cron else 6
days_until = (target_weekday - now.weekday()) % 7
if days_until == 0 and (now.hour > renew_hour or (now.hour == renew_hour and now.minute >= renew_minute)):
days_until = 7
next_run = now.replace(hour=renew_hour, minute=renew_minute, second=0, microsecond=0)
next_run += timedelta(days=days_until)
return next_run.strftime('%B %d, %Y %I:%M %p')
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile('_get_ssl_renewal_schedule: ' + str(e))
return None
class WebsiteManager:
apache = 1
ols = 2
@@ -3782,6 +3841,8 @@ context /cyberpanel_suspension_page.html {
Data['viewSSL'] = 1
Data['days'] = str(diff.days)
Data['authority'] = x509.get_issuer().get_components()[1][1].decode('utf-8')
renewal_when = _get_ssl_renewal_schedule()
Data['renewal_when'] = renewal_when
if Data['authority'] == 'Denial':
Data['authority'] = '%s has SELF-SIGNED SSL.' % (self.domain)
@@ -3790,6 +3851,7 @@ context /cyberpanel_suspension_page.html {
except BaseException as msg:
Data['viewSSL'] = 0
Data['renewal_when'] = None
logging.CyberCPLogFileWriter.writeToFile(str(msg))
servicePath = '/home/cyberpanel/pureftpd'
@@ -4009,6 +4071,7 @@ context /cyberpanel_suspension_page.html {
Data['viewSSL'] = 1
Data['days'] = str(diff.days)
Data['authority'] = x509.get_issuer().get_components()[1][1].decode('utf-8')
Data['renewal_when'] = _get_ssl_renewal_schedule()
if Data['authority'] == 'Denial':
Data['authority'] = '%s has SELF-SIGNED SSL.' % (self.childDomain)
@@ -4017,6 +4080,7 @@ context /cyberpanel_suspension_page.html {
except BaseException as msg:
Data['viewSSL'] = 0
Data['renewal_when'] = None
logging.CyberCPLogFileWriter.writeToFile(str(msg))
proc = httpProc(request, 'websiteFunctions/launchChild.html', Data)