mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-05-07 16:56:32 +02:00
Merge pull request #1672 from master3395/v2.5.5-dev
SSL renewal improvements, Cron Management fixes, and randomized renew…
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user