mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-02-16 19:46:48 +01:00
@@ -33,6 +33,27 @@ VERSION = '2.5.5'
|
||||
BUILD = 'dev'
|
||||
|
||||
|
||||
def _version_compare(a, b):
|
||||
"""Return 1 if a > b, -1 if a < b, 0 if equal."""
|
||||
def parse(v):
|
||||
parts = []
|
||||
for p in str(v).split('.'):
|
||||
try:
|
||||
parts.append(int(p))
|
||||
except ValueError:
|
||||
parts.append(0)
|
||||
return parts
|
||||
pa, pb = parse(a), parse(b)
|
||||
for i in range(max(len(pa), len(pb))):
|
||||
va = pa[i] if i < len(pa) else 0
|
||||
vb = pb[i] if i < len(pb) else 0
|
||||
if va > vb:
|
||||
return 1
|
||||
if va < vb:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def renderBase(request):
|
||||
template = 'baseTemplate/homePage.html'
|
||||
@@ -45,27 +66,41 @@ def renderBase(request):
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def versionManagement(request):
|
||||
currentVersion = VERSION
|
||||
currentBuild = str(BUILD)
|
||||
|
||||
getVersion = requests.get('https://cyberpanel.net/version.txt')
|
||||
latest = getVersion.json()
|
||||
latestVersion = latest['version']
|
||||
latestBuild = latest['build']
|
||||
branch_ref = 'v%s.%s' % (latestVersion, latestBuild)
|
||||
|
||||
currentVersion = VERSION
|
||||
currentBuild = str(BUILD)
|
||||
|
||||
u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=v%s.%s" % (latestVersion, latestBuild)
|
||||
logging.writeToFile(u)
|
||||
r = requests.get(u)
|
||||
latestcomit = r.json()[0]['sha']
|
||||
|
||||
command = "git -C /usr/local/CyberCP/ rev-parse HEAD"
|
||||
output = ProcessUtilities.outputExecutioner(command)
|
||||
|
||||
Currentcomt = output.rstrip("\n")
|
||||
notechk = True
|
||||
Currentcomt = ''
|
||||
latestcomit = ''
|
||||
|
||||
if Currentcomt == latestcomit:
|
||||
if _version_compare(currentVersion, latestVersion) > 0:
|
||||
notechk = False
|
||||
else:
|
||||
remote_cmd = 'git -C /usr/local/CyberCP remote get-url origin 2>/dev/null || true'
|
||||
remote_out = ProcessUtilities.outputExecutioner(remote_cmd)
|
||||
is_usmannasir = 'usmannasir/cyberpanel' in (remote_out or '')
|
||||
|
||||
if is_usmannasir:
|
||||
u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=%s" % branch_ref
|
||||
logging.CyberCPLogFileWriter.writeToFile(u)
|
||||
try:
|
||||
r = requests.get(u, timeout=10)
|
||||
r.raise_for_status()
|
||||
latestcomit = r.json()[0]['sha']
|
||||
except (requests.RequestException, IndexError, KeyError) as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile('[versionManagement] GitHub API failed: %s' % str(e))
|
||||
head_cmd = 'git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || true'
|
||||
Currentcomt = ProcessUtilities.outputExecutioner(head_cmd).rstrip('\n')
|
||||
if latestcomit and Currentcomt == latestcomit:
|
||||
notechk = False
|
||||
else:
|
||||
notechk = False
|
||||
|
||||
template = 'baseTemplate/versionManagment.html'
|
||||
finalData = {'build': currentBuild, 'currentVersion': currentVersion, 'latestVersion': latestVersion,
|
||||
@@ -253,31 +288,41 @@ def getLoadAverage(request):
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def versionManagment(request):
|
||||
## Get latest version
|
||||
currentVersion = VERSION
|
||||
currentBuild = str(BUILD)
|
||||
|
||||
getVersion = requests.get('https://cyberpanel.net/version.txt')
|
||||
latest = getVersion.json()
|
||||
latestVersion = latest['version']
|
||||
latestBuild = latest['build']
|
||||
branch_ref = 'v%s.%s' % (latestVersion, latestBuild)
|
||||
|
||||
## Get local version
|
||||
|
||||
currentVersion = VERSION
|
||||
currentBuild = str(BUILD)
|
||||
|
||||
u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=v%s.%s" % (latestVersion, latestBuild)
|
||||
logging.CyberCPLogFileWriter.writeToFile(u)
|
||||
r = requests.get(u)
|
||||
latestcomit = r.json()[0]['sha']
|
||||
|
||||
command = "git -C /usr/local/CyberCP/ rev-parse HEAD"
|
||||
output = ProcessUtilities.outputExecutioner(command)
|
||||
|
||||
Currentcomt = output.rstrip("\n")
|
||||
notechk = True
|
||||
Currentcomt = ''
|
||||
latestcomit = ''
|
||||
|
||||
if (Currentcomt == latestcomit):
|
||||
if _version_compare(currentVersion, latestVersion) > 0:
|
||||
notechk = False
|
||||
else:
|
||||
remote_cmd = 'git -C /usr/local/CyberCP remote get-url origin 2>/dev/null || true'
|
||||
remote_out = ProcessUtilities.outputExecutioner(remote_cmd)
|
||||
is_usmannasir = 'usmannasir/cyberpanel' in (remote_out or '')
|
||||
|
||||
if is_usmannasir:
|
||||
u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=%s" % branch_ref
|
||||
logging.CyberCPLogFileWriter.writeToFile(u)
|
||||
try:
|
||||
r = requests.get(u, timeout=10)
|
||||
r.raise_for_status()
|
||||
latestcomit = r.json()[0]['sha']
|
||||
except (requests.RequestException, IndexError, KeyError) as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile('[versionManagment] GitHub API failed: %s' % str(e))
|
||||
head_cmd = 'git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || true'
|
||||
Currentcomt = ProcessUtilities.outputExecutioner(head_cmd).rstrip('\n')
|
||||
if latestcomit and Currentcomt == latestcomit:
|
||||
notechk = False
|
||||
else:
|
||||
notechk = False
|
||||
|
||||
template = 'baseTemplate/versionManagment.html'
|
||||
finalData = {'build': currentBuild, 'currentVersion': currentVersion, 'latestVersion': latestVersion,
|
||||
|
||||
@@ -795,13 +795,20 @@ if [ "$Server_OS" = "Ubuntu" ]; then
|
||||
fi
|
||||
else
|
||||
rm -rf /usr/local/CyberPanel
|
||||
if [ -e /usr/bin/pip3 ]; then
|
||||
PIP3="/usr/bin/pip3"
|
||||
# AlmaLinux 9/10, Rocky 9: use python3 -m venv (no virtualenv pkg needed)
|
||||
if [[ "$Server_OS" = "AlmaLinux" ]] || [[ "$Server_OS" = "AlmaLinux9" ]] || [[ "$Server_OS" = "RockyLinux" ]]; then
|
||||
if [[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]; then
|
||||
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] AlmaLinux/Rocky $Server_OS_Version: will use python3 -m venv, skipping virtualenv package" | tee -a /var/log/cyberpanel_upgrade_debug.log
|
||||
else
|
||||
if [ -e /usr/bin/pip3 ]; then PIP3="/usr/bin/pip3"; else PIP3="pip3.6"; fi
|
||||
$PIP3 install --default-timeout=3600 virtualenv
|
||||
Check_Return
|
||||
fi
|
||||
else
|
||||
PIP3="pip3.6"
|
||||
if [ -e /usr/bin/pip3 ]; then PIP3="/usr/bin/pip3"; else PIP3="pip3.6"; fi
|
||||
$PIP3 install --default-timeout=3600 virtualenv
|
||||
Check_Return
|
||||
fi
|
||||
$PIP3 install --default-timeout=3600 virtualenv
|
||||
Check_Return
|
||||
fi
|
||||
|
||||
if [[ -f /usr/local/CyberPanel/bin/python2 ]]; then
|
||||
@@ -809,10 +816,15 @@ if [[ -f /usr/local/CyberPanel/bin/python2 ]]; then
|
||||
rm -rf /usr/local/CyberPanel/bin
|
||||
if [[ "$Server_OS" = "Ubuntu" ]] && ([[ "$Server_OS_Version" = "22" ]] || [[ "$Server_OS_Version" = "24" ]]); then
|
||||
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Ubuntu $Server_OS_Version detected, using python3 -m venv..." | tee -a /var/log/cyberpanel_upgrade_debug.log
|
||||
python3 -m venv /usr/local/CyberPanel
|
||||
elif [[ "$Server_OS" = "CentOS" ]] && ([[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]); then
|
||||
PYTHON_PATH=$(which python3 2>/dev/null || which python3.9 2>/dev/null || echo "/usr/bin/python3")
|
||||
virtualenv -p "$PYTHON_PATH" --system-site-packages /usr/local/CyberPanel
|
||||
python3 -m venv --system-site-packages /usr/local/CyberPanel
|
||||
elif [[ "$Server_OS" = "CentOS" ]] || [[ "$Server_OS" = "AlmaLinux" ]] || [[ "$Server_OS" = "AlmaLinux9" ]] || [[ "$Server_OS" = "RockyLinux" ]]; then
|
||||
if [[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]; then
|
||||
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] AlmaLinux/Rocky $Server_OS_Version detected, using python3 -m venv..." | tee -a /var/log/cyberpanel_upgrade_debug.log
|
||||
python3 -m venv --system-site-packages /usr/local/CyberPanel
|
||||
else
|
||||
PYTHON_PATH=$(which python3 2>/dev/null || which python3.9 2>/dev/null || echo "/usr/bin/python3")
|
||||
virtualenv -p "$PYTHON_PATH" --system-site-packages /usr/local/CyberPanel
|
||||
fi
|
||||
else
|
||||
virtualenv -p /usr/bin/python3 --system-site-packages /usr/local/CyberPanel
|
||||
fi
|
||||
@@ -828,14 +840,19 @@ echo -e "\nNothing found, need fresh setup...\n"
|
||||
if [[ "$Server_OS" = "Ubuntu" ]] && ([[ "$Server_OS_Version" = "22" ]] || [[ "$Server_OS_Version" = "24" ]]); then
|
||||
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Ubuntu $Server_OS_Version detected, using python3 -m venv..." | tee -a /var/log/cyberpanel_upgrade_debug.log
|
||||
python3 -m venv /usr/local/CyberPanel
|
||||
elif [[ "$Server_OS" = "CentOS" ]] && ([[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]); then
|
||||
PYTHON_PATH=$(which python3 2>/dev/null || which python3.9 2>/dev/null || echo "/usr/bin/python3")
|
||||
virtualenv -p "$PYTHON_PATH" --system-site-packages /usr/local/CyberPanel
|
||||
elif [[ "$Server_OS" = "CentOS" ]] || [[ "$Server_OS" = "AlmaLinux" ]] || [[ "$Server_OS" = "AlmaLinux9" ]] || [[ "$Server_OS" = "RockyLinux" ]]; then
|
||||
if [[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]; then
|
||||
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] AlmaLinux/Rocky $Server_OS_Version: using python3 -m venv (no virtualenv pkg needed)..." | tee -a /var/log/cyberpanel_upgrade_debug.log
|
||||
python3 -m venv --system-site-packages /usr/local/CyberPanel
|
||||
else
|
||||
PYTHON_PATH=$(which python3 2>/dev/null || which python3.9 2>/dev/null || echo "/usr/bin/python3")
|
||||
virtualenv -p "$PYTHON_PATH" --system-site-packages /usr/local/CyberPanel
|
||||
fi
|
||||
else
|
||||
virtualenv -p /usr/bin/python3 --system-site-packages /usr/local/CyberPanel
|
||||
fi
|
||||
|
||||
# Check if the virtualenv command failed
|
||||
# Check if the virtualenv/venv command failed
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "virtualenv command failed."
|
||||
|
||||
@@ -861,11 +878,15 @@ if [ $? -ne 0 ]; then
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "'packaging' module reinstalled and upgraded successfully."
|
||||
if [[ "$Server_OS" = "Ubuntu" ]] && ([[ "$Server_OS_Version" = "22" ]] || [[ "$Server_OS_Version" = "24" ]]); then
|
||||
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Ubuntu $Server_OS_Version detected, using python3 -m venv..." | tee -a /var/log/cyberpanel_upgrade_debug.log
|
||||
python3 -m venv /usr/local/CyberPanel
|
||||
elif [[ "$Server_OS" = "CentOS" ]] && ([[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]); then
|
||||
PYTHON_PATH=$(which python3 2>/dev/null || which python3.9 2>/dev/null || echo "/usr/bin/python3")
|
||||
virtualenv -p "$PYTHON_PATH" --system-site-packages /usr/local/CyberPanel
|
||||
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Ubuntu: using python3 -m venv..." | tee -a /var/log/cyberpanel_upgrade_debug.log
|
||||
python3 -m venv --system-site-packages /usr/local/CyberPanel
|
||||
elif [[ "$Server_OS" = "CentOS" ]] || [[ "$Server_OS" = "AlmaLinux" ]] || [[ "$Server_OS" = "AlmaLinux9" ]] || [[ "$Server_OS" = "RockyLinux" ]]; then
|
||||
if [[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]; then
|
||||
python3 -m venv --system-site-packages /usr/local/CyberPanel
|
||||
else
|
||||
PYTHON_PATH=$(which python3 2>/dev/null || which python3.9 2>/dev/null || echo "/usr/bin/python3")
|
||||
virtualenv -p "$PYTHON_PATH" --system-site-packages /usr/local/CyberPanel
|
||||
fi
|
||||
else
|
||||
virtualenv -p /usr/bin/python3 --system-site-packages /usr/local/CyberPanel
|
||||
fi
|
||||
@@ -888,7 +909,7 @@ fi
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
. /usr/local/CyberPanel/bin/activate
|
||||
pip install --upgrade setuptools packaging
|
||||
pip install --upgrade pip setuptools packaging
|
||||
|
||||
Download_Requirement
|
||||
|
||||
|
||||
39
fix-pureftpd-quota-once.sh
Normal file
39
fix-pureftpd-quota-once.sh
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
# One-time fix on the server: correct Pure-FTPd Quota line and start the service.
|
||||
# Run as root: sudo bash fix-pureftpd-quota-once.sh
|
||||
# Use when the panel has written invalid "Quota yes" and Pure-FTPd fails to start.
|
||||
|
||||
set -e
|
||||
CONF=/etc/pure-ftpd/pure-ftpd.conf
|
||||
SERVICE=pure-ftpd
|
||||
|
||||
if [ ! -f "$CONF" ]; then
|
||||
echo "Config not found: $CONF"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fix Quota line (Pure-FTPd requires Quota maxfiles:maxsize, not "yes")
|
||||
if grep -q '^Quota' "$CONF"; then
|
||||
sed -i 's/^Quota.*/Quota 100000:100000/' "$CONF"
|
||||
echo "Fixed Quota line in $CONF"
|
||||
else
|
||||
echo 'Quota 100000:100000' >> "$CONF"
|
||||
echo "Appended Quota line to $CONF"
|
||||
fi
|
||||
|
||||
# Optional: disable TLS if cert is missing (common cause of start failure)
|
||||
if grep -q '^TLS[[:space:]]*1' "$CONF" && [ ! -f /etc/ssl/private/pure-ftpd.pem ]; then
|
||||
sed -i 's/^TLS[[:space:]]*1/TLS 0/' "$CONF"
|
||||
echo "Set TLS 0 (certificate missing)"
|
||||
fi
|
||||
|
||||
# Start service
|
||||
systemctl start "$SERVICE"
|
||||
sleep 1
|
||||
if systemctl is-active --quiet "$SERVICE"; then
|
||||
echo "Pure-FTPd is running."
|
||||
exit 0
|
||||
else
|
||||
echo "Pure-FTPd failed to start. Run: systemctl status $SERVICE"
|
||||
exit 1
|
||||
fi
|
||||
BIN
panelAccess.zip
Normal file
BIN
panelAccess.zip
Normal file
Binary file not shown.
2
panelAccess/__init__.py
Normal file
2
panelAccess/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Panel Access app: custom domain / reverse-proxy CSRF trusted origins
|
||||
2
panelAccess/admin.py
Normal file
2
panelAccess/admin.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# No admin models; settings are stored in a config file
|
||||
54
panelAccess/apps.py
Normal file
54
panelAccess/apps.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Panel Access app: merges admin-configured custom panel domains into
|
||||
CSRF_TRUSTED_ORIGINS so POSTs work when the panel is behind a reverse proxy.
|
||||
"""
|
||||
import os
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
def get_panel_csrf_origins_file():
|
||||
"""Path to the file where custom panel origins are stored (one per line)."""
|
||||
return os.environ.get(
|
||||
'PANEL_CSRF_ORIGINS_FILE',
|
||||
'/home/cyberpanel/panel_csrf_origins.conf'
|
||||
)
|
||||
|
||||
|
||||
def read_panel_csrf_origins():
|
||||
"""Read trusted origins from the config file. Returns list of strings."""
|
||||
path = get_panel_csrf_origins_file()
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
try:
|
||||
with open(path, 'r') as f:
|
||||
lines = f.readlines()
|
||||
except (OSError, IOError):
|
||||
return []
|
||||
origins = []
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
origins.append(line)
|
||||
return origins
|
||||
|
||||
|
||||
class PanelaccessConfig(AppConfig):
|
||||
name = 'panelAccess'
|
||||
verbose_name = 'Panel Access (Custom Domain / CSRF)'
|
||||
|
||||
def ready(self):
|
||||
"""Merge admin-configured origins into CSRF_TRUSTED_ORIGINS at startup."""
|
||||
try:
|
||||
from django.conf import settings
|
||||
custom = read_panel_csrf_origins()
|
||||
if not custom:
|
||||
return
|
||||
base = list(getattr(settings, 'CSRF_TRUSTED_ORIGINS', []))
|
||||
for origin in custom:
|
||||
if origin and origin not in base:
|
||||
base.append(origin)
|
||||
settings.CSRF_TRUSTED_ORIGINS = base
|
||||
except Exception:
|
||||
pass
|
||||
11
panelAccess/meta.xml
Normal file
11
panelAccess/meta.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cyberpanelPluginConfig>
|
||||
<name>Panel Access (Custom Domain)</name>
|
||||
<type>Utility</type>
|
||||
<description>Configure custom domain(s) for accessing CyberPanel behind a reverse proxy. Fixes 403 CSRF errors on POST (e.g. Ban IP) and optionally sets up OpenLiteSpeed proxy so the panel is reachable at your domain without manual OLS config. Detects panel port from bind.conf (2087/8090).</description>
|
||||
<version>1.0.1</version>
|
||||
<author>master3395</author>
|
||||
<url>/plugins/panelAccess/</url>
|
||||
<settings_url>/plugins/panelAccess/</settings_url>
|
||||
<paid>false</paid>
|
||||
</cyberpanelPluginConfig>
|
||||
1
panelAccess/migrations/__init__.py
Normal file
1
panelAccess/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
2
panelAccess/models.py
Normal file
2
panelAccess/models.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# No models; origins are stored in a config file (panel_csrf_origins.conf)
|
||||
277
panelAccess/ols_proxy.py
Normal file
277
panelAccess/ols_proxy.py
Normal file
@@ -0,0 +1,277 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
OpenLiteSpeed reverse-proxy setup for Panel Access (custom domain).
|
||||
Creates a proxy-only vhost so the panel is reachable at the custom domain
|
||||
without manual OLS configuration.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
|
||||
# Path used by CyberPanel for panel port (same as ProcessUtilities.portPath); SSH login message uses this
|
||||
BIND_CONF = '/usr/local/lscp/conf/bind.conf'
|
||||
|
||||
|
||||
def get_panel_port():
|
||||
"""Detect panel port from bind.conf (*:PORT). Fallback 8090 (default), then 2087 (common alternate)."""
|
||||
if os.environ.get('PANEL_BACKEND_URL'):
|
||||
# Let caller parse URL if they need port from env
|
||||
pass
|
||||
try:
|
||||
if os.path.isfile(BIND_CONF):
|
||||
with open(BIND_CONF, 'r') as f:
|
||||
line = f.read().strip()
|
||||
if '*' in line and ':' in line:
|
||||
port = line.split(':')[1].strip().split()[0]
|
||||
if port.isdigit():
|
||||
return port
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
return '8090'
|
||||
|
||||
|
||||
def get_panel_backend_url():
|
||||
"""Panel backend URL for proxy. Prefer PANEL_BACKEND_URL env; else detect port from bind.conf."""
|
||||
url = os.environ.get('PANEL_BACKEND_URL', '').strip()
|
||||
if url:
|
||||
return url
|
||||
port = get_panel_port()
|
||||
return 'https://127.0.0.1:{}'.format(port)
|
||||
|
||||
|
||||
# Used when module loads (can be overridden by get_panel_backend_url() at runtime)
|
||||
PANEL_BACKEND_URL = os.environ.get('PANEL_BACKEND_URL') or ('https://127.0.0.1:' + get_panel_port())
|
||||
LSWS_ROOT = '/usr/local/lsws'
|
||||
VHOSTS_DIR = os.path.join(LSWS_ROOT, 'conf', 'vhosts')
|
||||
PANEL_PROXY_VHROOT = '/usr/local/lsws/panel_proxy'
|
||||
HTTPD_CONFIG = '/usr/local/lsws/conf/httpd_config.conf'
|
||||
|
||||
|
||||
def _domain_from_origin(origin):
|
||||
"""Extract host from origin (e.g. https://panel.example.com -> panel.example.com)."""
|
||||
if not origin or not isinstance(origin, str):
|
||||
return None
|
||||
origin = origin.strip().lower()
|
||||
if origin.startswith('http://'):
|
||||
origin = origin[7:]
|
||||
elif origin.startswith('https://'):
|
||||
origin = origin[8:]
|
||||
if '/' in origin:
|
||||
origin = origin.split('/')[0]
|
||||
if ':' in origin:
|
||||
origin = origin.split(':')[0]
|
||||
# Basic hostname validation
|
||||
if origin and re.match(r'^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$', origin):
|
||||
return origin
|
||||
return None
|
||||
|
||||
|
||||
def _vhost_conf_content(domain, backend_url):
|
||||
"""Generate vhost.conf content: proxy entire site to panel backend."""
|
||||
# OLS: extprocessor (proxy) + context / (handler = proxy)
|
||||
return """# Panel Access: reverse proxy to CyberPanel backend (do not edit manually)
|
||||
docRoot {vhroot}/
|
||||
vhDomain {domain}
|
||||
enableGzip 1
|
||||
|
||||
extprocessor panelbackend {{
|
||||
type proxy
|
||||
address {backend}
|
||||
maxConns 100
|
||||
initTimeout 60
|
||||
retryTimeout 0
|
||||
respBuffer 0
|
||||
}}
|
||||
|
||||
context / {{
|
||||
type proxy
|
||||
handler panelbackend
|
||||
addDefaultCharset off
|
||||
}}
|
||||
|
||||
errorlog $VH_ROOT/logs/error.log {{
|
||||
logLevel WARN
|
||||
rollingSize 10M
|
||||
useServer 0
|
||||
}}
|
||||
|
||||
accessLog $VH_ROOT/logs/access.log {{
|
||||
rollingSize 10M
|
||||
keepDays 7
|
||||
useServer 0
|
||||
}}
|
||||
""".format(
|
||||
vhroot=PANEL_PROXY_VHROOT,
|
||||
domain=domain,
|
||||
backend=backend_url,
|
||||
)
|
||||
|
||||
|
||||
def _virtual_host_block(domain):
|
||||
"""VirtualHost block for httpd_config.conf (same shape as olsMasterMainConf but vhRoot fixed)."""
|
||||
return """virtualHost {domain} {{
|
||||
vhRoot {vhroot}
|
||||
configFile $SERVER_ROOT/conf/vhosts/{domain}/vhost.conf
|
||||
allowSymbolLink 1
|
||||
enableScript 1
|
||||
restrained 1
|
||||
}}
|
||||
""".format(domain=domain, vhroot=PANEL_PROXY_VHROOT)
|
||||
|
||||
|
||||
def _domain_already_mapped(domain, lines):
|
||||
"""Return True if domain is already in a listener map."""
|
||||
for line in lines:
|
||||
if 'map' in line and domain in line.split():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _vhost_block_exists(domain, lines):
|
||||
"""Return True if virtualHost {domain} already exists."""
|
||||
marker = 'virtualHost {}'.format(domain)
|
||||
for line in lines:
|
||||
if marker in line and ('virtualHost' in line):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def setup_panel_proxy_vhost(domain_name):
|
||||
"""
|
||||
Create OpenLiteSpeed proxy vhost for the given domain (panel accessible at domain -> backend).
|
||||
Returns (success: bool, message: str).
|
||||
"""
|
||||
try:
|
||||
from plogical.processUtilities import ProcessUtilities
|
||||
from plogical import installUtilities
|
||||
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter
|
||||
except ImportError:
|
||||
return False, 'CyberPanel plumbing not available (run inside CyberPanel).'
|
||||
|
||||
if ProcessUtilities.decideServer() != ProcessUtilities.OLS:
|
||||
return False, 'Only OpenLiteSpeed is supported for automatic proxy setup.'
|
||||
|
||||
domain = _domain_from_origin(domain_name) if _domain_from_origin(domain_name) else domain_name
|
||||
if not domain or not re.match(r'^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$', domain):
|
||||
return False, 'Invalid domain: {}'.format(domain_name)
|
||||
|
||||
vhost_dir = os.path.join(VHOSTS_DIR, domain)
|
||||
vhost_conf = os.path.join(vhost_dir, 'vhost.conf')
|
||||
|
||||
# Create vhRoot and vhost dirs using ProcessUtilities for proper permissions
|
||||
try:
|
||||
# Use ProcessUtilities to create directories with root permissions
|
||||
if not os.path.exists(PANEL_PROXY_VHROOT):
|
||||
command = 'mkdir -p {}'.format(PANEL_PROXY_VHROOT)
|
||||
ProcessUtilities.normalExecutioner(command)
|
||||
command = 'chmod 755 {}'.format(PANEL_PROXY_VHROOT)
|
||||
ProcessUtilities.normalExecutioner(command)
|
||||
|
||||
log_dir = os.path.join(PANEL_PROXY_VHROOT, 'logs')
|
||||
if not os.path.exists(log_dir):
|
||||
command = 'mkdir -p {}'.format(log_dir)
|
||||
ProcessUtilities.normalExecutioner(command)
|
||||
command = 'chmod 755 {}'.format(log_dir)
|
||||
ProcessUtilities.normalExecutioner(command)
|
||||
|
||||
if not os.path.exists(vhost_dir):
|
||||
command = 'mkdir -p {}'.format(vhost_dir)
|
||||
ProcessUtilities.normalExecutioner(command)
|
||||
command = 'chmod 755 {}'.format(vhost_dir)
|
||||
ProcessUtilities.normalExecutioner(command)
|
||||
except Exception as e:
|
||||
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] makedirs: {}'.format(e))
|
||||
return False, 'Could not create directories: {}'.format(e)
|
||||
|
||||
# Write vhost.conf (use detected panel URL so port 2087/8090 is correct)
|
||||
# Write to temp file first, then move with ProcessUtilities for proper permissions
|
||||
backend_url = get_panel_backend_url()
|
||||
import tempfile
|
||||
temp_file = None
|
||||
try:
|
||||
# Create temp file in /tmp
|
||||
temp_fd, temp_file = tempfile.mkstemp(suffix='.conf', prefix='panel_access_', dir='/tmp')
|
||||
with os.fdopen(temp_fd, 'w') as f:
|
||||
f.write(_vhost_conf_content(domain, backend_url))
|
||||
|
||||
# Move temp file to final location using ProcessUtilities
|
||||
command = 'cp {} {}'.format(temp_file, vhost_conf)
|
||||
ProcessUtilities.normalExecutioner(command)
|
||||
command = 'chmod 644 {}'.format(vhost_conf)
|
||||
ProcessUtilities.normalExecutioner(command)
|
||||
|
||||
# Clean up temp file
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
# Clean up temp file on error
|
||||
if temp_file and os.path.exists(temp_file):
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except:
|
||||
pass
|
||||
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] write vhost.conf: {}'.format(e))
|
||||
return False, 'Could not write vhost config: {}'.format(e)
|
||||
|
||||
# Add virtualHost + map to httpd_config.conf (idempotent)
|
||||
# Check if file exists using ProcessUtilities (runs with proper permissions)
|
||||
try:
|
||||
command = 'test -f {} && echo exists || echo notfound'.format(HTTPD_CONFIG)
|
||||
result = ProcessUtilities.outputExecutioner(command).strip()
|
||||
if result == 'notfound':
|
||||
# Try to check with ls command as fallback
|
||||
command2 = 'ls {} 2>&1'.format(HTTPD_CONFIG)
|
||||
result2 = ProcessUtilities.outputExecutioner(command2).strip()
|
||||
if 'No such file' in result2 or 'cannot access' in result2:
|
||||
return False, 'OpenLiteSpeed config not found: {}'.format(HTTPD_CONFIG)
|
||||
# File might exist but have permission issues - log and continue
|
||||
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] Warning: Config file check ambiguous, proceeding: {}'.format(result2))
|
||||
except Exception as e:
|
||||
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] Error checking config file: {}'.format(e))
|
||||
# Don't fail here - let safeModifyHttpdConfig handle it
|
||||
|
||||
def modifier(current_lines):
|
||||
out = list(current_lines)
|
||||
if not _domain_already_mapped(domain, out):
|
||||
for i, line in enumerate(out):
|
||||
if 'listener' in line and 'Default' in line:
|
||||
out.insert(i + 1, ' map {} {}\n'.format(domain, domain))
|
||||
break
|
||||
else:
|
||||
raise ValueError('Default listener not found in httpd_config.conf')
|
||||
if not _vhost_block_exists(domain, out):
|
||||
out.append(_virtual_host_block(domain))
|
||||
return out
|
||||
|
||||
try:
|
||||
success, error = installUtilities.installUtilities.safeModifyHttpdConfig(
|
||||
modifier,
|
||||
'Panel Access: add proxy vhost for {}'.format(domain),
|
||||
skip_validation=True, # Skip validation to avoid pre-existing config errors
|
||||
)
|
||||
if not success:
|
||||
error_msg = error or 'Failed to update httpd_config.conf.'
|
||||
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] safeModifyHttpdConfig failed: {}'.format(error_msg))
|
||||
return False, error_msg
|
||||
except Exception as e:
|
||||
error_msg = 'Error calling safeModifyHttpdConfig: {}'.format(str(e))
|
||||
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] {}'.format(error_msg))
|
||||
return False, error_msg
|
||||
|
||||
# Reload OpenLiteSpeed
|
||||
try:
|
||||
cmd = '/usr/local/lsws/bin/lswsctrl reload'
|
||||
subprocess = __import__('subprocess')
|
||||
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=15)
|
||||
if r.returncode != 0:
|
||||
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] lswsctrl reload: {}'.format(r.stderr or r.stdout))
|
||||
except Exception as e:
|
||||
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] reload: {}'.format(e))
|
||||
|
||||
return True, 'Proxy for {} added. Reload OpenLiteSpeed if needed.'.format(domain)
|
||||
|
||||
|
||||
def domain_from_origin(origin):
|
||||
"""Public helper: extract hostname from origin URL."""
|
||||
return _domain_from_origin(origin)
|
||||
307
panelAccess/templates/panelAccess/settings.html
Normal file
307
panelAccess/templates/panelAccess/settings.html
Normal file
@@ -0,0 +1,307 @@
|
||||
{% extends "baseTemplate/index.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Panel Access (Custom Domain) - CyberPanel" %}{% endblock %}
|
||||
{% block content %}
|
||||
{% load static %}
|
||||
<style>
|
||||
body { background-color: var(--bg-primary, #f0f0ff); }
|
||||
.page-wrapper { background: transparent; padding: 20px; }
|
||||
.page-container { max-width: 800px; margin: 0 auto; }
|
||||
.page-header { margin-bottom: 30px; }
|
||||
.page-title { font-size: 28px; font-weight: 700; color: var(--text-heading, #2f3640); margin-bottom: 8px; }
|
||||
.page-subtitle { font-size: 14px; color: var(--text-secondary, #8893a7); }
|
||||
.content-card {
|
||||
background: var(--bg-secondary, white);
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 2px 8px var(--shadow-color, rgba(0,0,0,0.08));
|
||||
border: 1px solid var(--border-color, #e8e9ff);
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.card-title { font-size: 18px; font-weight: 700; color: var(--text-primary, #2f3640); margin-bottom: 20px; display: flex; align-items: center; gap: 10px; }
|
||||
.card-title::before { content: ''; width: 4px; height: 24px; background: var(--accent-color, #5b5fcf); border-radius: 2px; }
|
||||
.form-label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary, #64748b); margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.btn-primary { background: var(--accent-color, #5b5fcf); color: white; padding: 10px 20px; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; }
|
||||
.btn-primary:hover { background: var(--accent-hover, #4a4fc4); }
|
||||
.alert { padding: 16px 20px; border-radius: 10px; margin-bottom: 20px; }
|
||||
.alert-success { background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534; }
|
||||
.alert-danger { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; }
|
||||
.info-box { background: var(--bg-hover, #f8f9ff); border: 1px solid var(--border-color, #e8e9ff); border-radius: 8px; padding: 15px; font-size: 14px; color: var(--text-secondary, #64748b); margin-top: 16px; }
|
||||
#save-status { margin-top: 12px; }
|
||||
|
||||
/* Searchable Select Styles */
|
||||
.domain-select-wrapper { position: relative; width: 100%; }
|
||||
.domain-search-input {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--border-color, #e8e9ff);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.domain-select-container {
|
||||
border: 1px solid var(--border-color, #e8e9ff);
|
||||
border-radius: 8px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: white;
|
||||
display: none;
|
||||
}
|
||||
.domain-select-container.show { display: block; }
|
||||
.domain-option {
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--border-color, #f0f0f0);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.domain-option:hover { background: var(--bg-hover, #f8f9ff); }
|
||||
.domain-option:last-child { border-bottom: none; }
|
||||
.domain-option.selected { background: var(--accent-color, #5b5fcf); color: white; }
|
||||
.selected-domains {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.selected-domain-tag {
|
||||
background: var(--accent-color, #5b5fcf);
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.selected-domain-tag .remove {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.selected-domain-tag .remove:hover { opacity: 1; }
|
||||
</style>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">{% trans "Panel Access (Custom Domain)" %}</h1>
|
||||
<p class="page-subtitle">{% trans "Select domain(s) from your CyberPanel websites to use for accessing the panel behind a reverse proxy. This fixes 403 errors on POST requests (e.g. Ban IP) and CSRF verification." %}</p>
|
||||
</div>
|
||||
<div class="content-card">
|
||||
<h2 class="card-title">{% trans "Trusted origins" %}</h2>
|
||||
<p class="text-muted small">{% trans "Search and select domain(s) from your existing websites. Both HTTPS and HTTP versions will be added automatically." %}</p>
|
||||
<form id="panel-access-form" method="post" action="/plugins/panelAccess/save">
|
||||
{% csrf_token %}
|
||||
<label class="form-label" for="domain-search">{% trans "Select domain(s)" %}</label>
|
||||
<div class="domain-select-wrapper">
|
||||
<input type="text" id="domain-search" class="domain-search-input" placeholder="{% trans 'Search domains...' %}" autocomplete="off">
|
||||
<div id="domain-select-container" class="domain-select-container"></div>
|
||||
</div>
|
||||
<div id="selected-domains" class="selected-domains"></div>
|
||||
<input type="hidden" name="setup_ols_proxy" value="0">
|
||||
<div class="form-group" style="margin-top: 16px;">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" name="setup_ols_proxy" value="1" checked id="id_setup_ols_proxy">
|
||||
{% trans "Also add domain in OpenLiteSpeed (reverse proxy to panel)" %}
|
||||
</label>
|
||||
<p class="text-muted small" style="margin-top: 4px;">{% trans "Creates a proxy vhost so the panel is reachable at the custom domain without manual OLS configuration. HTTP (port 80) only; for HTTPS use Manage SSL or your own certificate." %}</p>
|
||||
</div>
|
||||
<div id="save-status"></div>
|
||||
<button type="submit" class="btn-primary" style="margin-top: 16px;">{% trans "Save" %}</button>
|
||||
</form>
|
||||
<div class="info-box">
|
||||
<p><strong>{% trans "Note:" %}</strong> {% trans "Save will restart the CyberPanel backend (lscpd) automatically so CSRF changes take effect. If restart fails (e.g. permissions), run systemctl restart lscpd manually. Config file: " %}<code>{{ config_path }}</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
// Parse domains from JSON string
|
||||
var allDomains = [];
|
||||
try {
|
||||
var domainsJson = {% if all_domains %}{{ all_domains|safe }}{% else %}'[]'{% endif %};
|
||||
if (typeof domainsJson === 'string') {
|
||||
allDomains = JSON.parse(domainsJson);
|
||||
} else {
|
||||
allDomains = domainsJson || [];
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('Error parsing domains:', e);
|
||||
allDomains = [];
|
||||
}
|
||||
|
||||
var selectedDomains = [];
|
||||
var currentOrigins = [];
|
||||
try {
|
||||
var originsJson = {% if origins_json %}{{ origins_json|safe }}{% else %}'[]'{% endif %};
|
||||
if (typeof originsJson === 'string') {
|
||||
currentOrigins = JSON.parse(originsJson);
|
||||
} else {
|
||||
currentOrigins = originsJson || [];
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('Error parsing origins:', e);
|
||||
currentOrigins = [];
|
||||
}
|
||||
|
||||
// Extract domains from current origins (remove https:// and http://)
|
||||
if (currentOrigins && currentOrigins.length) {
|
||||
currentOrigins.forEach(function(origin) {
|
||||
var domain = origin.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||
if (domain && selectedDomains.indexOf(domain) === -1) {
|
||||
selectedDomains.push(domain);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var searchInput = document.getElementById('domain-search');
|
||||
var selectContainer = document.getElementById('domain-select-container');
|
||||
var selectedContainer = document.getElementById('selected-domains');
|
||||
|
||||
function renderSelectedDomains() {
|
||||
selectedContainer.innerHTML = '';
|
||||
selectedDomains.forEach(function(domain) {
|
||||
var tag = document.createElement('div');
|
||||
tag.className = 'selected-domain-tag';
|
||||
tag.innerHTML = domain + ' <span class="remove" onclick="removeDomain(\'' + domain + '\')">×</span>';
|
||||
selectedContainer.appendChild(tag);
|
||||
});
|
||||
}
|
||||
|
||||
function filterDomains(query) {
|
||||
query = (query || '').toLowerCase();
|
||||
var filtered = allDomains.filter(function(domain) {
|
||||
return domain.toLowerCase().indexOf(query) !== -1 && selectedDomains.indexOf(domain) === -1;
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function renderDomainOptions(query) {
|
||||
var filtered = filterDomains(query);
|
||||
selectContainer.innerHTML = '';
|
||||
if (filtered.length === 0) {
|
||||
selectContainer.innerHTML = '<div class="domain-option" style="color: #999; cursor: default;">No domains found</div>';
|
||||
} else {
|
||||
filtered.forEach(function(domain) {
|
||||
var option = document.createElement('div');
|
||||
option.className = 'domain-option';
|
||||
option.textContent = domain;
|
||||
option.onclick = function() {
|
||||
addDomain(domain);
|
||||
};
|
||||
selectContainer.appendChild(option);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addDomain(domain) {
|
||||
if (selectedDomains.indexOf(domain) === -1) {
|
||||
selectedDomains.push(domain);
|
||||
renderSelectedDomains();
|
||||
searchInput.value = '';
|
||||
selectContainer.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
window.removeDomain = function(domain) {
|
||||
var index = selectedDomains.indexOf(domain);
|
||||
if (index !== -1) {
|
||||
selectedDomains.splice(index, 1);
|
||||
renderSelectedDomains();
|
||||
}
|
||||
};
|
||||
|
||||
searchInput.addEventListener('focus', function() {
|
||||
renderDomainOptions(this.value);
|
||||
selectContainer.classList.add('show');
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
renderDomainOptions(this.value);
|
||||
selectContainer.classList.add('show');
|
||||
});
|
||||
|
||||
searchInput.addEventListener('blur', function() {
|
||||
setTimeout(function() {
|
||||
selectContainer.classList.remove('show');
|
||||
}, 200);
|
||||
});
|
||||
|
||||
// Load domains from API if not provided or empty
|
||||
if (!allDomains || allDomains.length === 0) {
|
||||
var domainsUrl = '/plugins/panelAccess/domains';
|
||||
fetch(domainsUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(function(r) {
|
||||
if (!r.ok) {
|
||||
throw new Error('HTTP ' + r.status);
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.domains && Array.isArray(data.domains)) {
|
||||
allDomains = data.domains;
|
||||
console.log('Loaded ' + allDomains.length + ' domains from API');
|
||||
renderDomainOptions('');
|
||||
} else {
|
||||
console.error('Invalid domains data:', data);
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('Failed to load domains from API:', err);
|
||||
// Show error to user
|
||||
var status = document.getElementById('save-status');
|
||||
if (status) {
|
||||
status.innerHTML = '<div class="alert alert-danger">Failed to load domains. Please refresh the page.</div>';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('Using ' + allDomains.length + ' domains from template');
|
||||
renderDomainOptions('');
|
||||
}
|
||||
|
||||
// Form submission
|
||||
document.getElementById('panel-access-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
var form = this;
|
||||
var status = document.getElementById('save-status');
|
||||
status.innerHTML = '<span class="text-muted">Saving…</span>';
|
||||
|
||||
var fd = new FormData(form);
|
||||
// Add selected domains as array (without brackets to avoid security middleware blocking)
|
||||
selectedDomains.forEach(function(domain) {
|
||||
fd.append('origins_list', domain);
|
||||
});
|
||||
// CSRF token is already in the form from {% csrf_token %}, don't add it again
|
||||
|
||||
fetch(form.action, { method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); })
|
||||
.then(function(o) {
|
||||
if (o.ok && o.data.save === 1) {
|
||||
var msg = o.data.message || 'Saved.';
|
||||
if (o.data.proxy_results && o.data.proxy_results.length) {
|
||||
msg += '<br><br><strong>OpenLiteSpeed:</strong><br>';
|
||||
o.data.proxy_results.forEach(function(r) {
|
||||
msg += (r.success ? '✔ ' : '✘ ') + r.domain + ': ' + r.message + '<br>';
|
||||
});
|
||||
}
|
||||
status.innerHTML = '<div class="alert alert-success">' + msg + '</div>';
|
||||
} else {
|
||||
status.innerHTML = '<div class="alert alert-danger">' + (o.data.error_message || 'Save failed.') + '</div>';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
status.innerHTML = '<div class="alert alert-danger">Request failed.</div>';
|
||||
});
|
||||
});
|
||||
|
||||
// Initial render
|
||||
renderSelectedDomains();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
37
panelAccess/tests.py
Normal file
37
panelAccess/tests.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.test import TestCase
|
||||
from panelAccess.apps import read_panel_csrf_origins, get_panel_csrf_origins_file
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
|
||||
class PanelAccessOriginsTest(TestCase):
|
||||
def test_read_empty_missing_file(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, 'nonexistent.conf')
|
||||
orig = os.environ.get('PANEL_CSRF_ORIGINS_FILE')
|
||||
try:
|
||||
os.environ['PANEL_CSRF_ORIGINS_FILE'] = path
|
||||
self.assertEqual(read_panel_csrf_origins(), [])
|
||||
finally:
|
||||
if orig is not None:
|
||||
os.environ['PANEL_CSRF_ORIGINS_FILE'] = orig
|
||||
else:
|
||||
os.environ.pop('PANEL_CSRF_ORIGINS_FILE', None)
|
||||
|
||||
def test_read_origins_skips_comments_and_blanks(self):
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.conf', delete=False) as f:
|
||||
f.write('# comment\n\nhttps://a.com\n \nhttp://b.com\n')
|
||||
path = f.name
|
||||
try:
|
||||
orig = os.environ.get('PANEL_CSRF_ORIGINS_FILE')
|
||||
try:
|
||||
os.environ['PANEL_CSRF_ORIGINS_FILE'] = path
|
||||
self.assertEqual(read_panel_csrf_origins(), ['https://a.com', 'http://b.com'])
|
||||
finally:
|
||||
if orig is not None:
|
||||
os.environ['PANEL_CSRF_ORIGINS_FILE'] = orig
|
||||
else:
|
||||
os.environ.pop('PANEL_CSRF_ORIGINS_FILE', None)
|
||||
finally:
|
||||
os.unlink(path)
|
||||
12
panelAccess/urls.py
Normal file
12
panelAccess/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
# Note: app_name removed to avoid namespace issues with dynamic plugin inclusion
|
||||
# URLs are accessed directly via /plugins/panelAccess/ paths
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.settings_page, name='panel_access_settings'),
|
||||
path('save', views.save_origins, name='panel_access_save'),
|
||||
path('domains', views.get_domains_api, name='panel_access_domains'),
|
||||
]
|
||||
230
panelAccess/views.py
Normal file
230
panelAccess/views.py
Normal file
@@ -0,0 +1,230 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Panel Access settings: configure custom panel domain(s) for CSRF when
|
||||
the panel is behind a reverse proxy (e.g. https://panel.example.com -> IP:2087).
|
||||
"""
|
||||
from django.shortcuts import redirect
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.utils.translation import gettext as _
|
||||
from loginSystem.views import loadLoginPage
|
||||
from plogical.httpProc import httpProc
|
||||
from plogical.mailUtilities import mailUtilities
|
||||
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter
|
||||
from plogical.processUtilities import ProcessUtilities
|
||||
from .apps import get_panel_csrf_origins_file, read_panel_csrf_origins
|
||||
from websiteFunctions.models import Websites, ChildDomains
|
||||
import os
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
|
||||
def _ensure_origins_dir():
|
||||
"""Ensure directory for panel_csrf_origins.conf exists."""
|
||||
path = get_panel_csrf_origins_file()
|
||||
d = os.path.dirname(path)
|
||||
if d and not os.path.isdir(d):
|
||||
try:
|
||||
os.makedirs(d, mode=0o755)
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
|
||||
|
||||
def _get_all_domains():
|
||||
"""Get all domains and subdomains from CyberPanel database."""
|
||||
domains = []
|
||||
try:
|
||||
# Get all main websites
|
||||
websites = Websites.objects.filter(state=1).values_list('domain', flat=True)
|
||||
domains.extend([domain for domain in websites])
|
||||
|
||||
# Get all child domains (subdomains)
|
||||
child_domains = ChildDomains.objects.all().values_list('domain', flat=True)
|
||||
domains.extend([domain for domain in child_domains])
|
||||
|
||||
# Remove duplicates and sort
|
||||
domains = sorted(list(set(domains)))
|
||||
except Exception:
|
||||
pass
|
||||
return domains
|
||||
|
||||
|
||||
@require_http_methods(['GET'])
|
||||
def settings_page(request):
|
||||
"""Show Panel Access settings page with current custom domains."""
|
||||
try:
|
||||
request.session['userID']
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
mailUtilities.checkHome()
|
||||
origins = read_panel_csrf_origins()
|
||||
all_domains = _get_all_domains()
|
||||
data = {
|
||||
'origins': origins,
|
||||
'origins_text': '\n'.join(origins),
|
||||
'config_path': get_panel_csrf_origins_file(),
|
||||
'all_domains': json.dumps(all_domains),
|
||||
'origins_json': json.dumps(origins),
|
||||
}
|
||||
proc = httpProc(request, 'panelAccess/settings.html', data, 'admin')
|
||||
return proc.render()
|
||||
|
||||
|
||||
@require_http_methods(['POST'])
|
||||
def save_origins(request):
|
||||
"""Save custom panel origins (one per line). Admin only. Returns JSON."""
|
||||
try:
|
||||
# Check session
|
||||
try:
|
||||
request.session['userID']
|
||||
except KeyError:
|
||||
return JsonResponse({
|
||||
'save': 0,
|
||||
'error_message': _('Session expired. Please refresh the page and log in again.'),
|
||||
}, status=401)
|
||||
|
||||
# Check admin permissions
|
||||
try:
|
||||
from loginSystem.models import Administrator
|
||||
from plogical.acl import ACLManager
|
||||
user_id = request.session['userID']
|
||||
current_acl = ACLManager.loadedACL(user_id)
|
||||
if not current_acl.get('admin'):
|
||||
return JsonResponse({
|
||||
'save': 0,
|
||||
'error_message': _('Only administrators can change Panel Access settings.'),
|
||||
}, status=403)
|
||||
except Exception as e:
|
||||
CyberCPLogFileWriter.writeToFile(f"Panel Access: Authorization check error: {str(e)}")
|
||||
return JsonResponse({
|
||||
'save': 0,
|
||||
'error_message': _('Authorization check failed.'),
|
||||
}, status=500)
|
||||
|
||||
# Handle both old textarea format and new select format
|
||||
origins_raw = request.POST.get('origins', '').strip()
|
||||
# Try both with and without brackets (security middleware blocks brackets in param names)
|
||||
origins_list = request.POST.getlist('origins_list') or request.POST.getlist('origins_list[]') # New format: array of selected domains
|
||||
|
||||
# If new format is used, convert to origin format (add https:// and http://)
|
||||
lines = []
|
||||
if origins_list:
|
||||
for domain in origins_list:
|
||||
domain = domain.strip()
|
||||
if domain:
|
||||
# Add both https and http versions
|
||||
lines.append(f'https://{domain}')
|
||||
lines.append(f'http://{domain}')
|
||||
elif origins_raw:
|
||||
# Fallback to old textarea format
|
||||
lines = [ln.strip() for ln in origins_raw.splitlines() if ln.strip() and not ln.strip().startswith('#')]
|
||||
path = get_panel_csrf_origins_file()
|
||||
_ensure_origins_dir()
|
||||
try:
|
||||
with open(path, 'w') as f:
|
||||
f.write('# Custom panel domain(s) for CSRF (one origin per line)\n')
|
||||
for line in lines:
|
||||
f.write(line + '\n')
|
||||
try:
|
||||
os.chmod(path, 0o600)
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
except (OSError, IOError) as e:
|
||||
return JsonResponse({
|
||||
'save': 0,
|
||||
'error_message': _('Could not write config file: %s') % str(e),
|
||||
})
|
||||
|
||||
message = _('Custom domains saved. Restart the CyberPanel backend (e.g. systemctl restart lscpd) for CSRF to take effect.')
|
||||
proxy_results = []
|
||||
|
||||
setup_ols = request.POST.get('setup_ols_proxy', '').strip().lower() in ('1', 'true', 'yes', 'on')
|
||||
if setup_ols and lines:
|
||||
try:
|
||||
from .ols_proxy import setup_panel_proxy_vhost, domain_from_origin
|
||||
except ImportError as e:
|
||||
CyberCPLogFileWriter.writeToFile(f"Panel Access: Failed to import ols_proxy: {str(e)}")
|
||||
except Exception as e:
|
||||
CyberCPLogFileWriter.writeToFile(f"Panel Access: Error importing ols_proxy: {str(e)}")
|
||||
else:
|
||||
try:
|
||||
seen = set()
|
||||
for origin in lines:
|
||||
try:
|
||||
domain = domain_from_origin(origin)
|
||||
if not domain or domain in seen:
|
||||
continue
|
||||
seen.add(domain)
|
||||
ok, msg = setup_panel_proxy_vhost(domain)
|
||||
proxy_results.append({'domain': domain, 'success': ok, 'message': msg})
|
||||
except Exception as e:
|
||||
CyberCPLogFileWriter.writeToFile(f"Panel Access: Error processing origin {origin}: {str(e)}")
|
||||
proxy_results.append({'domain': origin, 'success': False, 'message': f'Error: {str(e)}'})
|
||||
except Exception as e:
|
||||
CyberCPLogFileWriter.writeToFile(f"Panel Access: Error in OLS proxy setup: {str(e)}")
|
||||
# Don't fail the entire save if OLS setup fails
|
||||
if proxy_results:
|
||||
parts = [message]
|
||||
for r in proxy_results:
|
||||
parts.append('{}: {}'.format(r['domain'], r['message']))
|
||||
message = ' '.join(parts)
|
||||
|
||||
# Restart lscpd so Django loads the new CSRF origins
|
||||
restart_ok = False
|
||||
restart_error = None
|
||||
try:
|
||||
# Use ProcessUtilities like RestartCyberPanel does
|
||||
command = 'systemctl restart lscpd'
|
||||
ProcessUtilities.popenExecutioner(command)
|
||||
restart_ok = True
|
||||
except Exception as e:
|
||||
restart_error = str(e)
|
||||
CyberCPLogFileWriter.writeToFile(f"Panel Access: Failed to restart lscpd: {str(e)}")
|
||||
|
||||
if restart_ok:
|
||||
message = _('Custom domains saved. CyberPanel backend (lscpd) restarted; CSRF changes are active.')
|
||||
if proxy_results:
|
||||
message = message + ' ' + ' '.join('{}: {}.'.format(r['domain'], r['message']) for r in proxy_results)
|
||||
else:
|
||||
message = _('Custom domains saved. Restart the CyberPanel backend manually (systemctl restart lscpd) for CSRF to take effect.')
|
||||
if restart_error:
|
||||
message = message + ' ' + _('Restart failed: %s') % restart_error
|
||||
if proxy_results:
|
||||
message = message + ' ' + ' '.join('{}: {}.'.format(r['domain'], r['message']) for r in proxy_results)
|
||||
|
||||
return JsonResponse({
|
||||
'save': 1,
|
||||
'message': message,
|
||||
'proxy_results': proxy_results,
|
||||
'lscpd_restarted': restart_ok,
|
||||
})
|
||||
except Exception as e:
|
||||
CyberCPLogFileWriter.writeToFile(f"Panel Access: Save error: {str(e)}")
|
||||
import traceback
|
||||
CyberCPLogFileWriter.writeToFile(f"Panel Access: Traceback: {traceback.format_exc()}")
|
||||
return JsonResponse({
|
||||
'save': 0,
|
||||
'error_message': _('An error occurred while saving: %s') % str(e),
|
||||
}, status=500)
|
||||
|
||||
|
||||
@require_http_methods(['GET'])
|
||||
def get_domains_api(request):
|
||||
"""API endpoint to get all domains and subdomains for the selector."""
|
||||
try:
|
||||
request.session['userID']
|
||||
except KeyError:
|
||||
return JsonResponse({'error': 'Not authenticated'}, status=401)
|
||||
|
||||
try:
|
||||
from loginSystem.models import Administrator
|
||||
from plogical.acl import ACLManager
|
||||
user_id = request.session['userID']
|
||||
current_acl = ACLManager.loadedACL(user_id)
|
||||
if not current_acl.get('admin'):
|
||||
return JsonResponse({'error': 'Admin access required'}, status=403)
|
||||
except Exception:
|
||||
return JsonResponse({'error': 'Authorization check failed'}, status=500)
|
||||
|
||||
domains = _get_all_domains()
|
||||
return JsonResponse({'domains': domains})
|
||||
@@ -1,120 +0,0 @@
|
||||
# AWS EC2 + Cursor Remote-SSH – Full Setup Guide
|
||||
|
||||
Use this guide to get **aws-server** (3.144.171.128) working with Cursor Remote-SSH.
|
||||
Do the steps in order. Everything is copy-paste ready.
|
||||
|
||||
---
|
||||
|
||||
## 1. Windows SSH config
|
||||
|
||||
**File:** `C:\Users\kimsk\.ssh\config`
|
||||
|
||||
- Open the file in Notepad or Cursor.
|
||||
- Find the `Host aws-server` block and replace it entirely with the block below (or add it if missing).
|
||||
- Use **straight double quotes** `"`, not curly quotes. Path uses forward slashes to avoid issues.
|
||||
|
||||
**Exact block to use (port 22 – default):**
|
||||
|
||||
```
|
||||
Host aws-server
|
||||
HostName 3.144.171.128
|
||||
User ec2-user
|
||||
Port 22
|
||||
IdentityFile "D:/OneDrive - v-man/Priv/VPS/Cyberpanel.pem"
|
||||
```
|
||||
|
||||
- Save and close.
|
||||
- If you later confirm SSH on the instance is on port 2222, change `Port 22` to `Port 2222` and add an inbound rule for 2222 in the Security Group (see step 3).
|
||||
|
||||
---
|
||||
|
||||
## 2. AWS Security Group – allow SSH (port 22)
|
||||
|
||||
1. **AWS Console** → **EC2** → **Instances**.
|
||||
2. Select the instance whose **Public IPv4** is **3.144.171.128**.
|
||||
3. Open the **Security** tab → click the **Security group** name (e.g. `sg-xxxxx`).
|
||||
4. **Edit inbound rules** → **Add rule**:
|
||||
- **Type:** SSH
|
||||
- **Port:** 22
|
||||
- **Source:** **My IP** (recommended) or **Anywhere-IPv4** (`0.0.0.0/0`) for testing only.
|
||||
5. **Save rules**.
|
||||
|
||||
If you use port 2222 on the instance, add another rule: **Custom TCP**, port **2222**, source **My IP** (or **Anywhere-IPv4** for testing).
|
||||
|
||||
---
|
||||
|
||||
## 3. Start SSH on the instance (fix “Connection refused”)
|
||||
|
||||
You must run commands on the instance without using SSH from your PC. Use one of these.
|
||||
|
||||
### Option A: EC2 Instance Connect (simplest)
|
||||
|
||||
1. **EC2** → **Instances** → select the instance (3.144.171.128).
|
||||
2. Click **Connect**.
|
||||
3. Open the **EC2 Instance Connect** tab → **Connect** (browser shell).
|
||||
|
||||
In the browser terminal, run:
|
||||
|
||||
```bash
|
||||
sudo systemctl status sshd
|
||||
sudo systemctl start sshd
|
||||
sudo systemctl enable sshd
|
||||
sudo ss -tlnp | grep 22
|
||||
```
|
||||
|
||||
You should see `sshd` listening on port 22. Then close the browser and try Cursor.
|
||||
|
||||
### Option B: Session Manager
|
||||
|
||||
1. **EC2** → **Instances** → select the instance → **Connect**.
|
||||
2. Choose **Session Manager** → **Connect**.
|
||||
3. Run the same commands as in Option A.
|
||||
|
||||
### Option C: SSH is on port 2222
|
||||
|
||||
If you know SSH was moved to 2222 on this instance:
|
||||
|
||||
1. In the Security Group, add an **inbound rule**: **Custom TCP**, port **2222**, source **My IP** (or **Anywhere-IPv4** for testing).
|
||||
2. In your SSH config, set `Port 2222` for `aws-server` (see step 1).
|
||||
3. Test (see step 4).
|
||||
|
||||
---
|
||||
|
||||
## 4. Test from Windows
|
||||
|
||||
Open **PowerShell** and run:
|
||||
|
||||
```powershell
|
||||
ssh -i "D:/OneDrive - v-man/Priv/VPS/Cyberpanel.pem" -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new ec2-user@3.144.171.128
|
||||
```
|
||||
|
||||
- If it asks for a host key, type `yes`.
|
||||
- If you get a shell prompt, SSH works. Type `exit` to close.
|
||||
- If you get **Connection refused**: SSH is not listening on 22 (or 2222); repeat step 3 (Instance Connect / Session Manager) and ensure `sshd` is running and listening on the port you use.
|
||||
- If you get **Connection timed out**: Security Group is still blocking the port; recheck step 2 and that you edited the security group attached to this instance.
|
||||
|
||||
---
|
||||
|
||||
## 5. Connect from Cursor
|
||||
|
||||
1. In Cursor: **Ctrl+Shift+P** (or **Cmd+Shift+P** on Mac) → **Remote-SSH: Connect to Host**.
|
||||
2. Choose **aws-server** (or type `aws-server`).
|
||||
3. Wait for the remote window to open. Cursor AI (Chat, Composer) works in that window as usual.
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] SSH config has the `aws-server` block with correct `IdentityFile` and `Port` (22 or 2222).
|
||||
- [ ] Security Group has an inbound rule for the SSH port (22 or 2222) from My IP (or 0.0.0.0/0 for testing).
|
||||
- [ ] `sshd` is running on the instance (started via Instance Connect or Session Manager).
|
||||
- [ ] `ssh ... ec2-user@3.144.171.128` works in PowerShell.
|
||||
- [ ] Cursor **Connect to Host** → **aws-server** succeeds.
|
||||
|
||||
---
|
||||
|
||||
## If it still fails
|
||||
|
||||
- **Connection refused** → Instance side: start/enable `sshd` and confirm it listens on the port you use (step 3).
|
||||
- **Connection timed out** → Network: open that port in the instance’s Security Group (step 2).
|
||||
- **Permission denied (publickey)** → Wrong key or user: confirm the .pem is the one for this instance and the user is `ec2-user` (Amazon Linux) or `ubuntu` (Ubuntu AMI).
|
||||
@@ -1,57 +0,0 @@
|
||||
# Email Limits Fix – Deploy Checklist
|
||||
|
||||
Use this after pulling the Email Limits fixes in this repo so that https://your-panel/email/EmailLimits works (controller registers, email list loads, configure section works).
|
||||
|
||||
## Files that are part of the fix
|
||||
|
||||
| File | Purpose |
|
||||
|------|--------|
|
||||
| `mailServer/mailserverManager.py` | Passes controller JS to template; allows getEmailsForDomain for emailForwarding |
|
||||
| `mailServer/templates/mailServer/EmailLimits.html` | Inline controller in footer_scripts (no static file dependency) |
|
||||
| `mailServer/static/mailServer/mailServer.js` | EmailLimitsNew controller + guard for `$scope.emails` |
|
||||
| `mailServer/static/mailServer/emailLimitsController.js` | Standalone controller + PNotify check fix |
|
||||
|
||||
## Option A: Deploy script (recommended)
|
||||
|
||||
**Run from anywhere** (use the full path to the script so the shell can find it):
|
||||
|
||||
```bash
|
||||
sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh
|
||||
```
|
||||
|
||||
Or from repo root:
|
||||
|
||||
```bash
|
||||
cd /home/cyberpanel-repo && sudo bash deploy-email-limits-fix.sh
|
||||
```
|
||||
|
||||
- Script auto-detects repo at `/home/cyberpanel-repo` if run from another directory.
|
||||
- Default CyberPanel path: `/usr/local/CyberCP`.
|
||||
- Override: `sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh /path/to/repo /usr/local/CyberCP`.
|
||||
- Skip restart: `sudo RESTART_LSCPD=0 bash /home/cyberpanel-repo/deploy-email-limits-fix.sh`.
|
||||
|
||||
## Option B: Manual copy + restart
|
||||
|
||||
On the server, from the repo root (e.g. `/home/cyberpanel-repo`):
|
||||
|
||||
```bash
|
||||
CP_DIR=/usr/local/CyberCP
|
||||
|
||||
cp -f mailServer/mailserverManager.py "$CP_DIR/mailServer/"
|
||||
cp -f mailServer/templates/mailServer/EmailLimits.html "$CP_DIR/mailServer/templates/mailServer/"
|
||||
cp -f mailServer/static/mailServer/mailServer.js "$CP_DIR/mailServer/static/mailServer/"
|
||||
cp -f mailServer/static/mailServer/emailLimitsController.js "$CP_DIR/mailServer/static/mailServer/"
|
||||
|
||||
sudo systemctl restart lscpd
|
||||
```
|
||||
|
||||
## After deploy
|
||||
|
||||
1. Hard refresh the Email Limits page: **Ctrl+Shift+R** (or Cmd+Shift+R).
|
||||
2. Open **Email Limits**, choose a **website**, then check that **email account** dropdown fills and **Configure Email Limits** appears and works.
|
||||
|
||||
## If it still fails
|
||||
|
||||
- Confirm the four files above are present under `$CP_DIR` and were updated (check timestamps).
|
||||
- Check panel/Python logs and browser console for `[$controller:ctrlreg]` or JS errors.
|
||||
- Ensure `lscpd` (or the process serving the panel) was restarted after copying.
|
||||
@@ -1,108 +0,0 @@
|
||||
# Email Limits – Live Server Checklist (vs upstream v2.4.4)
|
||||
|
||||
## Upstream v2.4.4 behaviour
|
||||
|
||||
In [usmannasir/cyberpanel at v2.4.4](https://github.com/usmannasir/cyberpanel/tree/v2.4.4):
|
||||
|
||||
- **Template**: `mailServer/templates/mailServer/EmailLimits.html` exists and uses `ng-controller="EmailLimitsNew"` and `{$ … $}` bindings.
|
||||
- **Routes**: `mailServer/urls.py` has `EmailLimits` and `SaveEmailLimitsNew`.
|
||||
- **Controller**: The **`EmailLimitsNew` controller is not present** in `static/mailServer/mailServer.js`. Upstream `mailServer.js` ends at “List Emails” and has no `EmailLimitsNew` block.
|
||||
|
||||
So on a stock v2.4.4 install, the Email Limits page will show raw `{$ selectedEmail $}` and “Could not connect to server” because the Angular controller is never registered.
|
||||
|
||||
---
|
||||
|
||||
## How it is loaded in v2.4.4
|
||||
|
||||
1. **Base template** (`baseTemplate/templates/baseTemplate/index.html`) loads one script bundle:
|
||||
- `{% static 'mailServer/mailServer.js' %}?v={{ CP_VERSION }}`
|
||||
(in the “Additional Scripts” block at the bottom of the body.)
|
||||
|
||||
2. **Email Limits template** only provides content; it does **not** load any extra script in upstream. It expects `EmailLimitsNew` to come from `mailServer.js`, but that controller is missing in v2.4.4.
|
||||
|
||||
3. **Backend**: `mailServer/views.py` → `EmailLimits`, `SaveEmailLimitsNew`; `mailServer/mailserverManager.py` → `EmailLimits()`, `SaveEmailLimitsNew()`.
|
||||
|
||||
---
|
||||
|
||||
## Files that must be on the live server
|
||||
|
||||
Use the paths below relative to the CyberPanel app root (e.g. `/usr/local/CyberCP/` or your repo root). Django static files may be served from `STATIC_ROOT` after `collectstatic`; templates and Python files must be in the app directories.
|
||||
|
||||
### 1. Python / URLs / views (same as upstream + your tweaks)
|
||||
|
||||
| Path | Purpose |
|
||||
|------|--------|
|
||||
| `mailServer/urls.py` | Must include `EmailLimits` and `SaveEmailLimitsNew` routes. |
|
||||
| `mailServer/views.py` | Must define `EmailLimits` and `SaveEmailLimitsNew` and call manager. |
|
||||
| `mailServer/mailserverManager.py` | Must implement `EmailLimits()` and `SaveEmailLimitsNew()` and render `mailServer/EmailLimits.html` with `websiteList` and `status`. |
|
||||
|
||||
### 2. Template (must load the controller script)
|
||||
|
||||
| Path | Purpose |
|
||||
|------|--------|
|
||||
| `mailServer/templates/mailServer/EmailLimits.html` | Must extend `baseTemplate/index.html`, contain `ng-controller="EmailLimitsNew"`, and **include the script tag** that loads `emailLimitsController.js` at the top of `{% block content %}`. |
|
||||
|
||||
### 3. Base template (unchanged from upstream for Email Limits)
|
||||
|
||||
| Path | Purpose |
|
||||
|------|--------|
|
||||
| `baseTemplate/templates/baseTemplate/index.html` | Must load `{% static 'mailServer/mailServer.js' %}` in the same script block as other app JS (no `load_email_limits_controller` needed). |
|
||||
|
||||
### 4. Static files (at least one of the two options)
|
||||
|
||||
**Option A – Use main bundle (repo’s `mailServer.js` with controller)**
|
||||
|
||||
| Path | Purpose |
|
||||
|------|--------|
|
||||
| `static/mailServer/mailServer.js` | Must define `app` (e.g. `window.app` or `angular.module('CyberCP')`) at the top and register `app.controller('EmailLimitsNew', ...)`. |
|
||||
| `mailServer/static/mailServer/mailServer.js` | Same as above if you use app static dirs. |
|
||||
|
||||
**Option B – Use standalone controller (recommended so it works even if `mailServer.js` is old)**
|
||||
|
||||
| Path | Purpose |
|
||||
|------|--------|
|
||||
| `static/mailServer/emailLimitsController.js` | Standalone script that registers `EmailLimitsNew` on the CyberCP module. |
|
||||
| `mailServer/static/mailServer/emailLimitsController.js` | Same file under the app’s `static` dir. |
|
||||
|
||||
The Email Limits template in this repo loads `emailLimitsController.js` at the top of the content block, so the controller is registered on the Email Limits page even if the live server still has an older `mailServer.js` without `EmailLimitsNew`.
|
||||
|
||||
---
|
||||
|
||||
## Quick verification on the live server
|
||||
|
||||
Run from the CyberPanel app root (e.g. `/usr/local/CyberCP/`):
|
||||
|
||||
```bash
|
||||
# 1. Template must contain the controller script and ng-controller
|
||||
grep -l "emailLimitsController.js" mailServer/templates/mailServer/EmailLimits.html && \
|
||||
grep -l "EmailLimitsNew" mailServer/templates/mailServer/EmailLimits.html && \
|
||||
echo "Template OK" || echo "Template MISSING or WRONG"
|
||||
|
||||
# 2. Standalone controller script must exist (at least one location)
|
||||
([ -f static/mailServer/emailLimitsController.js ] || [ -f mailServer/static/mailServer/emailLimitsController.js ]) && \
|
||||
echo "emailLimitsController.js OK" || echo "emailLimitsController.js MISSING"
|
||||
|
||||
# 3. mailServer.js (if you rely on it for Email Limits) must define EmailLimitsNew
|
||||
grep -q "EmailLimitsNew" static/mailServer/mailServer.js 2>/dev/null || grep -q "EmailLimitsNew" mailServer/static/mailServer/mailServer.js 2>/dev/null && \
|
||||
echo "mailServer.js has EmailLimitsNew" || echo "mailServer.js has NO EmailLimitsNew (use emailLimitsController.js)"
|
||||
|
||||
# 4. Routes
|
||||
grep -q "EmailLimits" mailServer/urls.py && echo "URLs OK" || echo "URLs MISSING"
|
||||
```
|
||||
|
||||
After deploying, run:
|
||||
|
||||
```bash
|
||||
python3 manage.py collectstatic --noinput
|
||||
# Restart your app server (e.g. LiteSpeed / Gunicorn)
|
||||
```
|
||||
|
||||
Then hard-refresh the Email Limits page (Ctrl+Shift+R).
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
- **Upstream v2.4.4**: Email Limits template and routes exist; **controller is missing** from `mailServer.js`, so the page is broken by default.
|
||||
- **This repo**: Adds `EmailLimitsNew` in `mailServer.js` and a standalone `emailLimitsController.js`, and the Email Limits template loads `emailLimitsController.js` so the page works even with an old `mailServer.js`.
|
||||
- **Live server**: Ensure the template, URLs, views, manager, base template, and either the updated `mailServer.js` or `emailLimitsController.js` (or both) are present as in this checklist, then run `collectstatic` and restart the app.
|
||||
@@ -1,21 +0,0 @@
|
||||
# FTP Quota Management – Browser Test Checklist
|
||||
|
||||
Use after deploying latest code. Open: `/ftp/quotaManagement`
|
||||
|
||||
## 1. Page load – status
|
||||
- **Pure-FTPd stopped:** Yellow warning "Pure-FTPd is not running. Please enable Pure-FTPd first (Server Status → Services)..." and Enable button disabled/hidden.
|
||||
- **Pure-FTPd running, quota on:** Green "FTP Quota system is already enabled"; button disabled.
|
||||
- **Pure-FTPd running, quota off:** Blue info and enabled "Enable FTP Quota System" button.
|
||||
|
||||
## 2. Click Enable
|
||||
- If FTP was running: success message and UI switches to "already enabled". No "Pure-FTPd did not start" error.
|
||||
- If FTP was stopped: API returns "Pure-FTPd is not running. Please enable Pure-FTPd first...".
|
||||
|
||||
## 3. Table
|
||||
- Quotas table loads; Refresh works.
|
||||
|
||||
## 4. One-time fix on server (if needed)
|
||||
```bash
|
||||
sudo sed -i 's/^Quota.*/Quota 100000:100000/' /etc/pure-ftpd/pure-ftpd.conf
|
||||
sudo systemctl start pure-ftpd
|
||||
```
|
||||
@@ -1,25 +0,0 @@
|
||||
# FTP Quotas Table Fix
|
||||
|
||||
## Problem
|
||||
- **URL:** https://207.180.193.210:2087/ftp/quotaManagement
|
||||
- **Error:** `(1146, "Table 'cyberpanel.ftp_quotas' doesn't exist")`
|
||||
|
||||
The `FTPQuota` model in `websiteFunctions/models.py` uses `db_table = 'ftp_quotas'`, but the table had never been created in the database.
|
||||
|
||||
## Solution
|
||||
1. **SQL:** `sql/create_ftp_quotas.sql` – `CREATE TABLE IF NOT EXISTS ftp_quotas` with columns and FKs to `loginSystem_administrator` and `websiteFunctions_websites`.
|
||||
2. **Deploy script:** `deploy-ftp-quotas-table.sh` – Copies the SQL to `/usr/local/CyberCP/sql/` and runs it using Django’s DB connection (no password on command line).
|
||||
|
||||
## Deploy (already run)
|
||||
```bash
|
||||
sudo bash /home/cyberpanel-repo/deploy-ftp-quotas-table.sh
|
||||
```
|
||||
|
||||
## Manual run (if needed)
|
||||
From repo root:
|
||||
```bash
|
||||
sudo bash deploy-ftp-quotas-table.sh [REPO_DIR] [CP_DIR]
|
||||
```
|
||||
Default `CP_DIR` is `/usr/local/CyberCP`.
|
||||
|
||||
After deployment, reload `/ftp/quotaManagement` in the browser.
|
||||
@@ -1,201 +0,0 @@
|
||||
# CyberPanel Install, Upgrade, and Downgrade Commands
|
||||
|
||||
Reference for all standard and branch-specific install/upgrade/downgrade commands (master3395 fork and upstream).
|
||||
|
||||
---
|
||||
|
||||
## Installation logs (v2.4.4 / v2.5.5-dev)
|
||||
|
||||
When you run the installer (cyberpanel.sh or install.py), logs are written to:
|
||||
|
||||
| Log | Location | Description |
|
||||
|-----|----------|--------------|
|
||||
| Installer script | `/var/log/CyberPanel/install.log` | Messages from cyberpanel.sh (print_status) |
|
||||
| Installer output | `/var/log/CyberPanel/install_output.log` | Full stdout/stderr of the Python installer (tee) |
|
||||
| Python installer | `/var/log/installLogs.txt` | Detailed log from install.py (installLog module) |
|
||||
|
||||
To inspect after a failed install:
|
||||
|
||||
```bash
|
||||
tail -100 /var/log/CyberPanel/install_output.log
|
||||
tail -100 /var/log/installLogs.txt
|
||||
```
|
||||
|
||||
**If you see ERR_CONNECTION_TIMED_OUT** when opening the panel URL: the install may have failed before LiteSpeed was set up, or ports are blocked. Ensure ports **8090** (panel) and **7080** (LSWS admin) are open in the server firewall and in your cloud security group (e.g. AWS). Re-run the installer after pulling the latest fixes so the install can complete.
|
||||
|
||||
---
|
||||
|
||||
## Fresh install
|
||||
|
||||
### One-liner (official / upstream)
|
||||
|
||||
```bash
|
||||
sh <(curl https://cyberpanel.net/install.sh)
|
||||
```
|
||||
|
||||
### One-liner with sudo (if not root)
|
||||
|
||||
```bash
|
||||
curl -sO https://cyberpanel.net/install.sh && sudo bash install.sh
|
||||
# or
|
||||
curl -sL https://cyberpanel.net/install.sh | sudo bash -s --
|
||||
```
|
||||
|
||||
### Install from master3395 fork (this repo)
|
||||
|
||||
**Stable:**
|
||||
|
||||
```bash
|
||||
curl -sL https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel.sh | sudo bash -s --
|
||||
```
|
||||
|
||||
**Development (v2.5.5-dev):**
|
||||
|
||||
```bash
|
||||
curl -sL https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/cyberpanel.sh | sudo bash -s -- -b v2.5.5-dev
|
||||
```
|
||||
|
||||
### Install with branch/version options
|
||||
|
||||
```bash
|
||||
# Download script first (recommended so -b/-v work reliably)
|
||||
curl -sL -o cyberpanel.sh https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/cyberpanel.sh
|
||||
chmod +x cyberpanel.sh
|
||||
sudo bash cyberpanel.sh [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Example | Description |
|
||||
|--------|---------|-------------|
|
||||
| `-b BRANCH` / `--branch BRANCH` | `-b v2.5.5-dev` | Install from branch or tag |
|
||||
| `-v VER` / `--version VER` | `-v 2.5.5-dev` | Version (script adds `v` prefix as needed) |
|
||||
| `--mariadb-version VER` | `--mariadb-version 10.11` | MariaDB: `10.11`, `11.8`, or `12.1` |
|
||||
| `--auto` | `--auto` | Non-interactive (still asks MariaDB unless `--mariadb-version` is set) |
|
||||
| `--debug` | `--debug` | Debug mode |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
sudo bash cyberpanel.sh # Interactive
|
||||
sudo bash cyberpanel.sh -b v2.5.5-dev # Development branch
|
||||
sudo bash cyberpanel.sh -v 2.5.5-dev # Same as above (v prefix added)
|
||||
sudo bash cyberpanel.sh -v 2.4.4 # Install 2.4.4
|
||||
sudo bash cyberpanel.sh -b main # From main branch
|
||||
sudo bash cyberpanel.sh -b a1b2c3d4 # From specific commit hash
|
||||
sudo bash cyberpanel.sh --mariadb-version 10.11 # MariaDB 10.11
|
||||
sudo bash cyberpanel.sh --mariadb-version 12.1 # MariaDB 12.1
|
||||
sudo bash cyberpanel.sh --auto --mariadb-version 11.8 # Fully non-interactive, MariaDB 11.8
|
||||
sudo bash cyberpanel.sh --debug # Debug
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Upgrade (existing CyberPanel)
|
||||
|
||||
### One-liner upgrade to latest stable
|
||||
|
||||
```bash
|
||||
bash <(curl -sL https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/cyberpanel_upgrade.sh)
|
||||
```
|
||||
|
||||
### Upgrade to a specific branch/version (upstream)
|
||||
|
||||
```bash
|
||||
bash <(curl -sL https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/cyberpanel_upgrade.sh) -b v2.5.5-dev
|
||||
bash <(curl -sL https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/cyberpanel_upgrade.sh) -b 2.4.4
|
||||
```
|
||||
|
||||
### Upgrade using master3395 fork
|
||||
|
||||
```bash
|
||||
sudo bash <(curl -sL https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel_upgrade.sh) -b v2.5.5-dev
|
||||
```
|
||||
|
||||
Or download then run:
|
||||
|
||||
```bash
|
||||
curl -sL -o cyberpanel_upgrade.sh https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/cyberpanel_upgrade.sh
|
||||
chmod +x cyberpanel_upgrade.sh
|
||||
sudo bash cyberpanel_upgrade.sh -b v2.5.5-dev
|
||||
```
|
||||
|
||||
**Upgrade options:**
|
||||
|
||||
| Option | Example | Description |
|
||||
|--------|---------|-------------|
|
||||
| `-b BRANCH` / `--branch BRANCH` | `-b v2.5.5-dev` | Upgrade to this branch/tag |
|
||||
| `--no-system-update` | (optional) | Skip full `yum/dnf update -y` (faster if system is already updated) |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
sudo bash cyberpanel_upgrade.sh -b v2.5.5-dev
|
||||
sudo bash cyberpanel_upgrade.sh -b 2.4.4
|
||||
sudo bash cyberpanel_upgrade.sh -b stable
|
||||
sudo bash cyberpanel_upgrade.sh -b v2.5.5-dev --no-system-update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Downgrade
|
||||
|
||||
Downgrade is done by running the **upgrade** script with the **older** branch/version.
|
||||
|
||||
### Downgrade to 2.4.4 (or another older version)
|
||||
|
||||
```bash
|
||||
sudo bash <(curl -sL https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/cyberpanel_upgrade.sh) -b 2.4.4
|
||||
```
|
||||
|
||||
Or with master3395 fork:
|
||||
|
||||
```bash
|
||||
curl -sL -o cyberpanel_upgrade.sh https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel_upgrade.sh
|
||||
chmod +x cyberpanel_upgrade.sh
|
||||
sudo bash cyberpanel_upgrade.sh -b 2.4.4
|
||||
```
|
||||
|
||||
### Downgrade from v2.5.5-dev to stable
|
||||
|
||||
```bash
|
||||
sudo bash cyberpanel_upgrade.sh -b stable
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pre-upgrade (download upgrade script only)
|
||||
|
||||
From the interactive menu: **Option 5 – Pre-Upgrade**.
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
# Download latest upgrade script to /usr/local/
|
||||
curl -sL -o /usr/local/cyberpanel_upgrade.sh https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/cyberpanel_upgrade.sh
|
||||
chmod 700 /usr/local/cyberpanel_upgrade.sh
|
||||
|
||||
# Run when ready
|
||||
sudo /usr/local/cyberpanel_upgrade.sh -b v2.5.5-dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Action | Command |
|
||||
|--------|---------|
|
||||
| **Install (official)** | `sh <(curl https://cyberpanel.net/install.sh)` |
|
||||
| **Install stable (master3395)** | `curl -sL https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel.sh \| sudo bash -s --` |
|
||||
| **Install v2.5.5-dev** | `curl -sL https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/cyberpanel.sh \| sudo bash -s -- -b v2.5.5-dev` |
|
||||
| **Upgrade to v2.5.5-dev** | `sudo bash <(curl -sL https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel_upgrade.sh) -b v2.5.5-dev` |
|
||||
| **Upgrade to 2.4.4** | `sudo bash <(curl -sL .../cyberpanel_upgrade.sh) -b 2.4.4` |
|
||||
| **Downgrade to 2.4.4** | Same as upgrade: `... cyberpanel_upgrade.sh -b 2.4.4` |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Run as **root** or with **sudo**; if using `curl | sudo bash`, use `bash -s --` and put branch/options after `--` so they are passed to the script.
|
||||
- MariaDB version can be set at install with `--mariadb-version 10.11`, `11.8`, or `12.1`.
|
||||
- Upgrade script branch: `-b v2.5.5-dev`, `-b 2.4.4`, `-b stable`, or `-b <commit-hash>`.
|
||||
@@ -1,34 +0,0 @@
|
||||
# MariaDB 11.8 LTS and 12.1
|
||||
|
||||
## Summary
|
||||
|
||||
CyberPanel install and upgrade support **MariaDB 11.8 LTS** (default) or **12.1**. User can choose at install/upgrade time; **downgrade is supported** (e.g. 12.1 → 11.8 by re-running upgrader with `--mariadb-version 11.8`).
|
||||
|
||||
- **New installs:** `--mariadb-version 11.8|12.1` (default 11.8); `install.py` and `venvsetup.sh` pass it through.
|
||||
- **Upgrades:** `cyberpanel_upgrade.sh --mariadb-version 11.8|12.1` or interactive prompt; writes `/etc/cyberpanel/mariadb_version` for `upgrade.py`.
|
||||
- **Downgrade:** Run upgrader again with the desired version (e.g. `--mariadb-version 11.8` to switch from 12.1 to 11.8). Repo and packages will target the chosen version.
|
||||
- **cyberpanel_upgrade.sh:** Uses `MARIADB_VER` (default 11.8) in `MariaDB.repo` baseurl and writes `/etc/cyberpanel/mariadb_version`.
|
||||
- **plogical/upgrade.py:** `fix_almalinux9_mariadb()` reads `/etc/cyberpanel/mariadb_version` (default 11.8) and runs `mariadb_repo_setup` with that version.
|
||||
- **UI (Database upgrade):** `databases/databaseManager.py` offers 10.6, 10.11, **11.8** for manual upgrade.
|
||||
- **mysqlUtilities.UpgradeMariaDB:** Repo baseurl uses `versionToInstall` (e.g. 11.8).
|
||||
|
||||
## Testing
|
||||
|
||||
From repo root:
|
||||
|
||||
- Shell (upgrader argument parsing and repo URL logic):
|
||||
`./test/upgrader_mariadb_version_test.sh`
|
||||
- Python (mariadb_version file read and downgrade):
|
||||
`python3 test/test_upgrade_mariadb_version.py`
|
||||
|
||||
Both 11.8 and 12.1 paths are tested; downgrade (12.1 → 11.8) is explicitly verified.
|
||||
|
||||
## References
|
||||
|
||||
- `cyberpanel_upgrade.sh`: MARIADB_VER, --mariadb-version, /etc/cyberpanel/mariadb_version
|
||||
- `plogical/upgrade.py`: fix_almalinux9_mariadb() reads mariadb_version file
|
||||
- `install/install.py`: --mariadb-version, preFlightsChecks.mariadb_version
|
||||
- `install/venvsetup.sh`: MARIADB_VER prompt, --mariadb-version to install.py
|
||||
- `install/universal_os_fixes.py`: setup_mariadb_repository() 11.8
|
||||
- `databases/databaseManager.py`: mysqlversions 10.6, 10.11, 11.8
|
||||
- `plogical/mysqlUtilities.py`: UpgradeMariaDB() baseurl for RHEL
|
||||
@@ -1,88 +0,0 @@
|
||||
# MariaDB Installation Fixes
|
||||
|
||||
## Issues Fixed
|
||||
|
||||
### 1. MariaDB-server-compat Package Conflict
|
||||
**Problem**: `MariaDB-server-compat-12.1.2-1.el9.noarch` was conflicting with MariaDB 10.11 installation, causing transaction test errors.
|
||||
|
||||
**Solution**:
|
||||
- Enhanced compat package removal with multiple aggressive removal attempts
|
||||
- Added `--allowerasing` flag to dnf remove commands
|
||||
- Added dnf exclude configuration to prevent compat package reinstallation
|
||||
- Verification step to ensure all compat packages are removed before installation
|
||||
|
||||
**Files Modified**:
|
||||
- `cyberpanel-repo/plogical/upgrade.py` - `fix_almalinux9_mariadb()` function
|
||||
- `cyberpanel-repo/install/install.py` - `installMySQL()` function
|
||||
|
||||
### 2. MySQL Command Not Found Error
|
||||
**Problem**: After MariaDB installation failed, the `changeMYSQLRootPassword()` function tried to use the `mysql` command which didn't exist, causing `FileNotFoundError`.
|
||||
|
||||
**Solution**:
|
||||
- Added verification that MariaDB binaries exist before attempting password change
|
||||
- Added check for mysql/mariadb command availability
|
||||
- Added MariaDB service status verification before password change
|
||||
- Added wait time for MariaDB to be ready after service start
|
||||
|
||||
**Files Modified**:
|
||||
- `cyberpanel-repo/install/install.py` - `changeMYSQLRootPassword()` function
|
||||
- `cyberpanel-repo/install/install.py` - `installMySQL()` function
|
||||
|
||||
### 3. MariaDB Installation Verification
|
||||
**Problem**: Installation was proceeding even when MariaDB wasn't actually installed successfully.
|
||||
|
||||
**Solution**:
|
||||
- Added binary existence check after installation
|
||||
- Added service status verification
|
||||
- Added proper error handling and return values
|
||||
- Installation now fails gracefully if MariaDB wasn't installed
|
||||
|
||||
**Files Modified**:
|
||||
- `cyberpanel-repo/plogical/upgrade.py` - `fix_almalinux9_mariadb()` function
|
||||
- `cyberpanel-repo/install/install.py` - `installMySQL()` function
|
||||
|
||||
## Changes Made
|
||||
|
||||
### upgrade.py
|
||||
1. **Enhanced compat package removal**:
|
||||
- Multiple removal attempts (dnf remove, rpm -e, individual package removal)
|
||||
- Added `--allowerasing` flag
|
||||
- Added dnf exclude configuration
|
||||
- Verification step
|
||||
|
||||
2. **Improved MariaDB installation**:
|
||||
- Added `--exclude='MariaDB-server-compat*'` to dnf install command
|
||||
- Added fallback with `--allowerasing` if conflicts occur
|
||||
- Added binary existence verification after installation
|
||||
- Proper error handling and return values
|
||||
|
||||
### install.py
|
||||
1. **Enhanced compat package removal** (same as upgrade.py)
|
||||
|
||||
2. **Improved installation verification**:
|
||||
- Check for MariaDB binaries after installation
|
||||
- Verify service is running before password change
|
||||
- Added wait time for service to be ready
|
||||
- Proper error handling
|
||||
|
||||
3. **Improved password change function**:
|
||||
- Verify mysql/mariadb command exists before attempting password change
|
||||
- Better error messages
|
||||
- Graceful failure handling
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
1. Test on clean AlmaLinux 9 system
|
||||
2. Test with existing MariaDB-server-compat package installed
|
||||
3. Test with MariaDB 10.x already installed
|
||||
4. Test with MariaDB 12.x already installed
|
||||
5. Verify MariaDB service starts correctly
|
||||
6. Verify mysql/mariadb commands are available
|
||||
7. Verify password change succeeds
|
||||
|
||||
## Notes
|
||||
|
||||
- The fixes maintain backward compatibility
|
||||
- All changes include proper error handling
|
||||
- Installation now fails gracefully with clear error messages
|
||||
- Compat package removal is more aggressive to handle edge cases
|
||||
@@ -1,132 +0,0 @@
|
||||
# What Was in the Old cyberpanel-fix Repo – Pre-Removal Checklist
|
||||
|
||||
Before removing `/home/cyberpanel-fix-backup-20260202`, verify the merged repo has everything you need.
|
||||
|
||||
---
|
||||
|
||||
## 1. Files ONLY in cyberpanel-repo (not in old fix) ✅
|
||||
|
||||
These are in the merged repo and were not in the old fix:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `commit_and_push.sh`, `commit_changes.py`, `push_fix.py`, `push_fix.sh` | Dev/utility scripts |
|
||||
| `fix_todo_git.py`, `remove_todo.py`, `remove_todo_from_git.sh` | Git helpers |
|
||||
| `olves issue -1654: Hostname SSL setup...` | Patch file (typo in filename) |
|
||||
| `pluginHolder/patreon_verifier.py.bak`, `plugin_access.py.bak` | Backups |
|
||||
| `pluginHolder/templates/pluginHolder/plugins.html.backup` | Template backup |
|
||||
| `static/userManagment/modifyUser.html` | UI change |
|
||||
| `to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md` | Notes |
|
||||
| `to-do/REPO-MERGE-2026-02-02.md` | Merge notes |
|
||||
|
||||
**Action:** None. These are already in the merged repo.
|
||||
|
||||
---
|
||||
|
||||
## 2. Files COPIED from old fix into repo ✅
|
||||
|
||||
These were only in the old fix and were copied into repo during the merge:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `cyberpanel_clean.sh` | Clean install script |
|
||||
| `cyberpanel_complete.sh` | Complete install script |
|
||||
| `cyberpanel_simple.sh` | Simple install script |
|
||||
| `cyberpanel_standalone.sh` | Standalone install script |
|
||||
| `fix_installation_issues.sh` | Installation fixes |
|
||||
| `install_phpmyadmin.sh` | phpMyAdmin installer |
|
||||
| ~~`simple_install.sh`~~ | Removed – use official install.sh one-liner |
|
||||
| `INSTALLER_SUMMARY.md` | Installer docs |
|
||||
| `UNIVERSAL_OS_COMPATIBILITY.md` | OS compatibility docs |
|
||||
| `to-do/MARIADB_INSTALLATION_FIXES.md` | MariaDB fixes |
|
||||
|
||||
**Action:** Confirm these exist in `/home/cyberpanel-repo/`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Files that DIFFER – repo is the intended version
|
||||
|
||||
The merged repo keeps the **cyberpanel-repo** versions. Old fix had older or different logic.
|
||||
|
||||
### CyberCP/settings.py
|
||||
- **Repo:** `emailMarketing` is commented out (install via Plugin Store)
|
||||
- **Old fix:** `emailMarketing` was in `INSTALLED_APPS`
|
||||
|
||||
**Check:** Plugin Store for emailMarketing works; no need for it in core install.
|
||||
|
||||
### CyberCP/urls.py
|
||||
- **Repo:** `path('emailMarketing/', ...)` is commented out
|
||||
- **Old fix:** `path('emailMarketing/', ...)` was active
|
||||
|
||||
**Check:** Same as above; emailMarketing via Plugin Store.
|
||||
|
||||
### plogical/mailUtilities.py
|
||||
- **Repo:** DNS fallback logic – falls back to **local DNS** when external API fails
|
||||
- **Old fix:** Returns empty `[]` when external API fails; no local fallback
|
||||
|
||||
**Check:** Hostname SSL / rDNS works when cyberpanel.net API is down or unreachable.
|
||||
|
||||
### emailMarketing/meta.xml
|
||||
- **Repo:** version `1.0.1`, category `Email`
|
||||
- **Old fix:** version `1.0.0`
|
||||
|
||||
### examplePlugin/meta.xml
|
||||
- **Repo:** version `1.0.1`, category `Utility`
|
||||
- **Old fix:** version `1.0.0`
|
||||
|
||||
**Check:** Plugin Store shows correct versions and categories.
|
||||
|
||||
---
|
||||
|
||||
## 4. PluginHolder / Plugin Store (in repo)
|
||||
|
||||
The merged repo has:
|
||||
|
||||
- Collapsible help sections
|
||||
- Freshness badges (NEW/Stable/Unstable/STALE)
|
||||
- Activate All / Deactivate All
|
||||
- Updated categories and premium docs
|
||||
- Version 2.1.0 in the help footer
|
||||
|
||||
**Check:** `/plugins/help/` and `/plugins/installed` behave as expected.
|
||||
|
||||
---
|
||||
|
||||
## 5. Quick verification commands
|
||||
|
||||
```bash
|
||||
# Copied files exist
|
||||
ls -la /home/cyberpanel-repo/cyberpanel_clean.sh \
|
||||
/home/cyberpanel-repo/fix_installation_issues.sh \
|
||||
/home/cyberpanel-repo/install_phpmyadmin.sh
|
||||
|
||||
# Symlink works
|
||||
ls -la /home/cyberpanel-fix
|
||||
# Should show: cyberpanel-fix -> cyberpanel-repo
|
||||
|
||||
# Live deployment
|
||||
ls -la /usr/local/CyberCP/pluginHolder/templates/pluginHolder/help.html
|
||||
# Should have collapsible sections and version 2.1.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Safe to remove when
|
||||
|
||||
- [ ] Plugin Store loads and filters work
|
||||
- [ ] Plugin Development Guide (help) shows collapsible sections and 2.1.0
|
||||
- [ ] Hostname SSL / rDNS works (or you accept no local DNS fallback)
|
||||
- [ ] emailMarketing is installed via Plugin Store, not core (if used)
|
||||
- [ ] Install scripts (`cyberpanel_clean.sh`, etc.) are present and used as needed
|
||||
|
||||
---
|
||||
|
||||
## Remove backup
|
||||
|
||||
```bash
|
||||
rm -rf /home/cyberpanel-fix-backup-20260202
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Created:** 2026-02-02
|
||||
@@ -1,62 +0,0 @@
|
||||
# OpenLiteSpeed and LSWS Version Used by CyberPanel (Install & Upgrade)
|
||||
|
||||
**Updated:** OLS target 1.8.5+ (LiteSpeed repo); LSWS fallback 6.3.4.
|
||||
|
||||
## Summary
|
||||
|
||||
- **Install:** OpenLiteSpeed is installed via the **OS package manager** (no fixed version in code), then optionally replaced by CyberPanel’s **custom static binary** (based on **OpenLiteSpeed 1.8.5 – v2.0.5**).
|
||||
- **Upgrade:** The CyberPanel **upgrade script does not change the OpenLiteSpeed version**. It only updates **LiteSpeed Enterprise** version references. During upgrade, **custom OLS binaries** (same 1.8.5-based build) are (re)installed if OLS is present.
|
||||
|
||||
---
|
||||
|
||||
## Install
|
||||
|
||||
1. **Package install**
|
||||
- `install/install.py` → `installLiteSpeed(ent=0)` → `install_package('openlitespeed')`.
|
||||
- So the **base** install is whatever **openlitespeed** version the distro provides (yum/dnf or apt). There is **no fixed OLS version** in the installer for this step.
|
||||
|
||||
2. **Custom binary (optional)**
|
||||
- Right after that, `installCustomOLSBinaries()` runs (in both `install/install.py` and `plogical/upgrade.py`).
|
||||
- It downloads a **static binary** from `https://cyberpanel.net/` (e.g. `openlitespeed-phpconfig-x86_64-rhel8-static`) and replaces `/usr/local/lsws/bin/openlitespeed`.
|
||||
- Comments in code state this is **OpenLiteSpeed 1.8.5** (upgrade.py) or **1.8.5 – v2.0.5** (install.py). The download URLs do not include a version; the binary is a fixed build hosted by CyberPanel.
|
||||
|
||||
So on **install**, you get either:
|
||||
- **Distro OLS** (version = whatever the OS repo has), or
|
||||
- **CyberPanel custom OLS** (based on **1.8.5 / v2.0.5** static build) if the custom binary install succeeds.
|
||||
|
||||
---
|
||||
|
||||
## Upgrade
|
||||
|
||||
1. **cyberpanel_upgrade.sh**
|
||||
- Fetches **LiteSpeed Enterprise** latest version from:
|
||||
- `LSWS_Latest_URL="https://cyberpanel.sh/update.litespeedtech.com/ws/latest.php"`
|
||||
- Parses `LSWS_Stable_Version` from the `LSWS_STABLE` line.
|
||||
- Uses `LSWS_Stable_Version` only to **sed**-replace hardcoded Enterprise version strings (e.g. `lsws-5.3.8`, `lsws-5.4.2`, `lsws-5.3.5`) in `/usr/local/CyberCP/serverStatus/serverStatusUtil.py`.
|
||||
- So the **upgrade script does not install or upgrade OpenLiteSpeed**; it only updates **Enterprise** version references.
|
||||
|
||||
2. **plogical/upgrade.py**
|
||||
- During upgrade, if OpenLiteSpeed is present (`/usr/local/lsws/bin/openlitespeed` exists), it runs:
|
||||
- `Upgrade.installCustomOLSBinaries()`
|
||||
- That (re)installs the **same custom static OLS binary** (1.8.5-based, from cyberpanel.net). So **upgrade** does not pull a “new” OLS version from upstream; it only refreshes CyberPanel’s custom binary if OLS is in use.
|
||||
|
||||
---
|
||||
|
||||
## References (in repo)
|
||||
|
||||
| What | Where |
|
||||
|------|--------|
|
||||
| OLS package install (no version) | `install/install.py` → `install_package('openlitespeed')` in `installLiteSpeed()` |
|
||||
| Custom OLS binary (1.8.5 / 1.8.5–v2.0.5) | `install/install.py` and `plogical/upgrade.py` → `installCustomOLSBinaries()` and `BINARY_CONFIGS` comments |
|
||||
| LSWS version used in upgrade (Enterprise only) | `cyberpanel_upgrade.sh` → `LSWS_Latest_URL`, `LSWS_Stable_Version`, and sed to `serverStatusUtil.py` |
|
||||
| Custom OLS on upgrade | `plogical/upgrade.py` → `if os.path.exists('/usr/local/lsws/bin/openlitespeed'): Upgrade.installCustomOLSBinaries()` |
|
||||
|
||||
---
|
||||
|
||||
## Short answers
|
||||
|
||||
- **What OpenLiteSpeed version does install use?**
|
||||
Package: **distro default**. If custom binary is used: **OpenLiteSpeed 1.8.5 (or 1.8.5–v2.0.5)** static build from cyberpanel.net.
|
||||
|
||||
- **What OpenLiteSpeed version does upgrade use?**
|
||||
Upgrade does **not** change OLS version from upstream. It only (re)installs the **same custom 1.8.5-based** binary when OLS is present. **LiteSpeed Enterprise** version is the one fetched from `cyberpanel.sh/update.litespeedtech.com/ws/latest.php` and written into `serverStatusUtil.py`.
|
||||
@@ -1,13 +0,0 @@
|
||||
# Plugin Default Removal - 2026-02-01
|
||||
|
||||
## Summary
|
||||
CyberPanel repository no longer requires any plugins by default. Plugins are installed by users from the [Plugin Store](https://github.com/master3395/cyberpanel-plugins) via the CyberPanel Plugin Manager.
|
||||
|
||||
## Changes
|
||||
- **settings.py**: Removed `emailMarketing` from `INSTALLED_APPS`
|
||||
- **urls.py**: Commented out `emailMarketing` route (plugin installer adds it when plugin is installed)
|
||||
|
||||
## Plugin Installation
|
||||
Users install plugins from: https://github.com/master3395/cyberpanel-plugins
|
||||
|
||||
The plugin installer adds apps to `INSTALLED_APPS` and URL routes when plugins are installed via the Plugin Store UI.
|
||||
@@ -1,45 +0,0 @@
|
||||
# Pure-FTPd Quota Syntax Fix (2026-02-04)
|
||||
|
||||
## Problem
|
||||
Pure-FTPd failed to start with:
|
||||
```
|
||||
/etc/pure-ftpd/pure-ftpd.conf:35:1: syntax error line 35: [Quota ...].
|
||||
```
|
||||
|
||||
## Cause
|
||||
The config used `Quota yes`, but Pure-FTPd expects **`Quota maxfiles:maxsize`** (e.g. `Quota 1000:10` for 1000 files and 10 MB). The value is not a boolean.
|
||||
|
||||
## Fix applied
|
||||
|
||||
### On the server
|
||||
- `/etc/pure-ftpd/pure-ftpd.conf`: line 35 set to `Quota 100000:100000` (high default so MySQL per-user quotas apply).
|
||||
- Service started successfully: `systemctl start pure-ftpd`.
|
||||
|
||||
### In the repo
|
||||
- **install/pure-ftpd/pure-ftpd.conf** and **install/pure-ftpd-one/pure-ftpd.conf**: `Quota yes` → `Quota 100000:100000`.
|
||||
- **websiteFunctions/website.py** (`enableFTPQuota`): sed/echo now write `Quota 100000:100000` instead of `Quota yes` (or tabs).
|
||||
|
||||
## One-time fix on server (if "Enable" still breaks it)
|
||||
Run on the server as root (copy script from repo or run inline):
|
||||
|
||||
**Option A – script (repo root: `fix-pureftpd-quota-once.sh`):**
|
||||
```bash
|
||||
sudo bash /path/to/fix-pureftpd-quota-once.sh
|
||||
```
|
||||
|
||||
**Option B – inline:**
|
||||
```bash
|
||||
sudo sed -i 's/^Quota.*/Quota 100000:100000/' /etc/pure-ftpd/pure-ftpd.conf
|
||||
# If TLS 1 is set but cert missing, disable TLS:
|
||||
sudo sed -i 's/^TLS[[:space:]]*1/TLS 0/' /etc/pure-ftpd/pure-ftpd.conf
|
||||
sudo systemctl start pure-ftpd
|
||||
```
|
||||
Then deploy the latest panel code so "Enable" uses the correct Quota syntax.
|
||||
|
||||
## Code safeguards (enableFTPQuota)
|
||||
- **Backup before modify**: Timestamped backup of `pure-ftpd.conf` and `pureftpd-mysql.conf` before any change.
|
||||
- **Safety net before restart**: If the Quota line is not valid (`Quota maxfiles:maxsize`), it is corrected to `Quota 100000:100000` so Pure-FTPd never gets an invalid line on restart.
|
||||
|
||||
## Reference
|
||||
- Upstream: https://github.com/jedisct1/pure-ftpd/blob/master/pure-ftpd.conf.in (comment: "Quota 1000:10").
|
||||
- `pure-ftpd --help`: `-n --quota <opt>`.
|
||||
@@ -1,38 +0,0 @@
|
||||
# CyberPanel Repo Merge – 2026-02-02
|
||||
|
||||
## Summary
|
||||
|
||||
`cyberpanel-repo` and `cyberpanel-fix` have been merged into a single working directory.
|
||||
|
||||
## What Was Done
|
||||
|
||||
1. **Unique files copied from cyberpanel-fix into cyberpanel-repo:**
|
||||
- `cyberpanel_clean.sh`
|
||||
- `cyberpanel_complete.sh`
|
||||
- `cyberpanel_simple.sh`
|
||||
- `cyberpanel_standalone.sh`
|
||||
- `fix_installation_issues.sh`
|
||||
- `install_phpmyadmin.sh`
|
||||
- ~~`simple_install.sh`~~ (removed; use official install.sh)
|
||||
- `INSTALLER_SUMMARY.md`
|
||||
- `UNIVERSAL_OS_COMPATIBILITY.md`
|
||||
- `to-do/MARIADB_INSTALLATION_FIXES.md`
|
||||
|
||||
2. **cyberpanel-fix backup:** Renamed to `cyberpanel-fix-backup-20260202`
|
||||
|
||||
3. **Symlink created:** `cyberpanel-fix` → `cyberpanel-repo`
|
||||
- Paths like `/home/cyberpanel-fix/` now resolve to `/home/cyberpanel-repo/`
|
||||
|
||||
## Single Source of Truth
|
||||
|
||||
Use **`/home/cyberpanel-repo`** (or `/home/cyberpanel-fix` via symlink) for all CyberPanel development and deployment.
|
||||
|
||||
## Backup Location
|
||||
|
||||
The previous cyberpanel-fix tree is preserved at:
|
||||
`/home/cyberpanel-fix-backup-20260202`
|
||||
|
||||
You can remove it after confirming everything works:
|
||||
```bash
|
||||
rm -rf /home/cyberpanel-fix-backup-20260202
|
||||
```
|
||||
@@ -1,83 +0,0 @@
|
||||
# Runtime vs Repo: What Belongs in cyberpanel-repo for 2.5.5-dev
|
||||
|
||||
## Goal
|
||||
|
||||
When users upgrade to **our** (master3395) 2.5.5-dev, the panel should look and behave the same. That means **default** look-and-feel and behavior must be defined in the repo, not only “generated” on the server.
|
||||
|
||||
---
|
||||
|
||||
## What is “runtime generated”?
|
||||
|
||||
On the live server, after install/upgrade you have:
|
||||
|
||||
1. **From the repo (clone/copy)**
|
||||
All app code, templates, static sources, migrations, `version.txt`, default `settings.py`, etc.
|
||||
→ This **should** be in the repo (and already is).
|
||||
|
||||
2. **Generated at install/upgrade**
|
||||
- Python venv under `/usr/local/CyberCP/bin`, `lib`, `lib64`
|
||||
- `collectstatic` output under `/usr/local/CyberCP/public/static`
|
||||
- `version` table and `baseTemplate_cyberpanelcosmetic` row (if created by code/migrations)
|
||||
- `lscpd` binary copy, symlinks, etc.
|
||||
→ The **sources** that produce these (e.g. static sources, migrations) **should** be in the repo.
|
||||
|
||||
3. **Per-server / preserved**
|
||||
- `CyberCP/settings.py` — upgrade **merges** only the `DATABASES` section from the old server; the rest (e.g. `INSTALLED_APPS`) comes from the **new** clone.
|
||||
- `baseTemplate/static/baseTemplate/custom/` (custom CSS files)
|
||||
- DB row `baseTemplate_cyberpanelcosmetic.MainDashboardCSS` (custom dashboard CSS)
|
||||
- `.git/`, phpMyAdmin config, SnappyMail data, etc.
|
||||
→ **Defaults** that define “how 2.5.5-dev looks” should be in the repo; **per-server overrides** stay on the server.
|
||||
|
||||
---
|
||||
|
||||
## What we need in the repo so 2.5.5-dev “looks the same”
|
||||
|
||||
- **Templates, static sources, JS/CSS**
|
||||
Already in repo (e.g. `baseTemplate/`, `static/`). No change needed for “same look” unless you change the design.
|
||||
|
||||
- **Default `settings.py`**
|
||||
Already in repo. Upgrade keeps DB credentials from the server and uses repo for everything else (e.g. `INSTALLED_APPS`).
|
||||
So 2.5.5-dev behavior is driven by the repo’s `settings.py`.
|
||||
|
||||
- **Version**
|
||||
`baseTemplate/views.py` has `VERSION = '2.5.5'`, `BUILD = 'dev'`. Repo’s `version.txt` is `{"version":"2.5.5","build":"dev"}`.
|
||||
Upgrade also writes version into the DB. So version “same as 2.5.5-dev” is already defined in the repo.
|
||||
|
||||
- **Default “look” (cosmetic)**
|
||||
- Code already creates a default `CyberPanelCosmetic` row with **empty** `MainDashboardCSS` if none exists (`baseTemplate/context_processors.py`, `plogical/httpProc.py`, `loginSystem/views.py`).
|
||||
- If **your live server** has custom dashboard CSS (in DB or in `baseTemplate/static/baseTemplate/custom/`), that is **your** customization.
|
||||
- To make “our 2.5.5-dev” ship with that same look as default, you have two options:
|
||||
|
||||
1. **Data migration**
|
||||
Add a baseTemplate data migration that does:
|
||||
- `CyberPanelCosmetic.objects.get_or_create(pk=1, defaults={'MainDashboardCSS': '<your default CSS>'})`
|
||||
so every new/upgraded install gets that default look.
|
||||
|
||||
2. **Static default**
|
||||
Put the CSS in a static file under `baseTemplate/static/` and include it in the base template so the default theme matches your live server.
|
||||
|
||||
- **Migrations**
|
||||
All schema (and optional data) migrations must be in the repo so every 2.5.5-dev install/upgrade runs the same schema and, if you add it, the same default cosmetic data.
|
||||
|
||||
---
|
||||
|
||||
## What should **not** be in the repo
|
||||
|
||||
- **Secrets**: DB password, `SECRET_KEY`, API keys.
|
||||
Keep in `settings.py` only placeholders or env reads; real values stay on the server (or in config.php / env per your rules).
|
||||
|
||||
- **User data**: sites, users, mail, backups.
|
||||
These are per-server.
|
||||
|
||||
- **Generated artifacts**: venv, `collectstatic` output, compiled binaries.
|
||||
Repo holds the **source**; install/upgrade generates these on the server.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
- **Yes:** “Runtime generated” **defaults** that define how 2.5.5-dev looks and behaves **should** be reflected in the repo (templates, static sources, migrations, default cosmetic logic or data).
|
||||
- **Already in repo:** App code, default settings structure, version, static sources, migrations. So 2.5.5-dev upgrades already get the same **code** and **default look** (empty custom CSS).
|
||||
- **Optional:** If your live server has a **specific** custom look (e.g. custom dashboard CSS), and you want that to be the **default** for everyone on 2.5.5-dev, add it to the repo via a data migration or default static CSS as above.
|
||||
|
||||
No change is **required** for “same look” unless you want to ship a non-empty default cosmetic (e.g. your current dashboard CSS) as part of 2.5.5-dev.
|
||||
@@ -1,76 +0,0 @@
|
||||
# v2.5.5-dev Branch Compatibility Check
|
||||
|
||||
**Date:** 2026-02-04
|
||||
**Branches compared:** [v2.5.5-dev](https://github.com/master3395/cyberpanel/tree/v2.5.5-dev) vs [v2.4.4](https://github.com/master3395/cyberpanel/tree/v2.4.4) vs [stable](https://github.com/master3395/cyberpanel/tree/stable)
|
||||
|
||||
---
|
||||
|
||||
## 1. Will your v2.5.5-dev changes work?
|
||||
|
||||
**Yes.** The Ban IP / Firewall Banned IPs changes on v2.5.5-dev are self-contained and consistent:
|
||||
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| **plogical/firewallUtilities.py** | `blockIP` / `unblockIP` use `result == 1` for success; log file path is `/usr/local/CyberCP/data/blocked_ips.log` (writable by `cyberpanel`). |
|
||||
| **plogical/processUtilities.py** | When running as root, `executioner` uses `normalExecutioner` return value (1 = success, 0 = fail). |
|
||||
| **firewall/firewallManager.py** | `addBannedIP` uses `FirewallUtilities.blockIP`; ACL and errors return JSON with `error_message` and `error`; rollback on block failure. |
|
||||
| **firewall/views.py** | `addBannedIP` parses JSON body and calls `fm.addBannedIP(userID, request_data)`. |
|
||||
| **firewall/urls.py** | Routes `getBannedIPs`, `addBannedIP`, `modifyBannedIP`, `removeBannedIP`, `deleteBannedIP`, `exportBannedIPs`, `importBannedIPs` are present. |
|
||||
| **baseTemplate/views.py** | `blockIPAddress` uses `FirewallUtilities.blockIP` (no subprocess). |
|
||||
| **baseTemplate (homePage + system-status.js)** | Ban IP calls `/firewall/addBannedIP` with `ip`, `reason`, `duration`; shows server `error_message` in notifications. |
|
||||
|
||||
**Deployment requirements (already applied on your server):**
|
||||
|
||||
- `/usr/local/CyberCP/data` owned by `cyberpanel:cyberpanel` (for `banned_ips.json` and `blocked_ips.log`).
|
||||
- Deploy updated files from v2.5.5-dev into `/usr/local/CyberCP/` and restart `lscpd`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Does v2.5.5-dev have all functions from v2.4.4 and stable?
|
||||
|
||||
**Summary:**
|
||||
|
||||
- **v2.5.5-dev has more than v2.4.4 and stable** in terms of features (Banned IPs, FTP quotas, email limits, user management, bandwidth management, etc.). It is a development branch built on top of the same base.
|
||||
- **v2.5.5-dev is missing a few items that exist only on stable** (backports or stable-only fixes). Nothing critical for the Ban IP feature; mainly scripts and tests.
|
||||
|
||||
### v2.5.5-dev has everything from v2.4.4 that matters
|
||||
|
||||
- v2.4.4 is older (fewer commits). v2.5.5-dev contains the same core apps (firewall, baseTemplate, loginSystem, backup, etc.) plus many additions.
|
||||
- **Firewall:** v2.4.4 has no Banned IPs routes; v2.5.5-dev adds the full Banned IPs feature (getBannedIPs, addBannedIP, modify, remove, delete, export, import).
|
||||
|
||||
### v2.5.5-dev vs stable
|
||||
|
||||
- **Stable has ~86 files that differ from v2.5.5-dev**, including:
|
||||
- **run_migration.py** – present on stable, **not** on v2.5.5-dev (migration helper).
|
||||
- **test_firewall_blocking.py** – test script on stable.
|
||||
- **rollback_phpmyadmin_redirect.sh** – rollback script on stable.
|
||||
- **install/**, **plogical/** (e.g. mysqlUtilities, upgrade, backup, sslUtilities), **pluginInstaller** – some fixes/improvements on stable that may not be in v2.5.5-dev.
|
||||
|
||||
- **v2.5.5-dev has 3652+ files changed vs stable** – it has many more features (user management, website functions, bandwidth, FTP quotas, email limits, Banned IPs, etc.).
|
||||
|
||||
So:
|
||||
|
||||
- **Feature parity:** v2.5.5-dev has **all the main functions** from v2.4.4 and **adds** Banned IPs and other features. It does **not** lack core features that v2.4.4 or stable have.
|
||||
- **Stable-only extras:** Stable has a few **extra** scripts/fixes (e.g. `run_migration.py`, `rollback_phpmyadmin_redirect.sh`, some plogical/install changes). If you need those, you can cherry-pick or merge from stable into v2.5.5-dev.
|
||||
|
||||
---
|
||||
|
||||
## 3. Directory layout comparison
|
||||
|
||||
| In stable, not in v2.5.5-dev (by name) | In v2.5.5-dev, not in stable |
|
||||
|----------------------------------------|------------------------------|
|
||||
| emailMarketing (or different layout) | bin, docs, modules, public, sql, test, to-do |
|
||||
| examplePlugin | (v2.5.5-dev has more structure) |
|
||||
| guides | |
|
||||
| scripts | |
|
||||
| testPlugin | test (different name) |
|
||||
|
||||
Your current repo (v2.5.5-dev) includes `emailMarketing`, `websiteFunctions`, `firewall`, `baseTemplate`, etc. The diff is mostly naming (e.g. test vs testPlugin) and stable having a few extra scripts/docs.
|
||||
|
||||
---
|
||||
|
||||
## 4. Recommendation
|
||||
|
||||
1. **Use v2.5.5-dev as-is for Ban IP and current features** – the changes are consistent and will work with the deployment steps above.
|
||||
2. **Periodically merge or cherry-pick from stable** into v2.5.5-dev if you want stable’s migration script, phpMyAdmin rollback script, and any plogical/install fixes.
|
||||
3. **You do have all the functions from v2.4.4 and stable** in the sense of core product behavior; v2.5.5-dev adds more (Banned IPs, etc.) and is only missing some optional stable-only scripts/fixes.
|
||||
@@ -1,89 +0,0 @@
|
||||
# v2.5.5-dev: Fixes and Deploy Guide
|
||||
|
||||
This document lists fixes included in **v2.5.5-dev** and how to deploy them on a CyberPanel server.
|
||||
|
||||
---
|
||||
|
||||
## Version and cache busting
|
||||
|
||||
- **baseTemplate:** `CP_VERSION` in `baseTemplate/templates/baseTemplate/index.html` now uses `CYBERPANEL_FULL_VERSION` from context (from `baseTemplate/views.py`: `VERSION = '2.5.5'`, `BUILD = 'dev'`), so static URLs use `?v=2.5.5.dev` and cache busting matches the branch.
|
||||
|
||||
---
|
||||
|
||||
## 1. Email Limits page
|
||||
|
||||
- **Issue:** Raw Angular bindings (`{$ selectedEmail $}`) and `EmailLimitsNew` controller not registered.
|
||||
- **Files:** `mailServer/mailserverManager.py`, `mailServer/templates/mailServer/EmailLimits.html`, `mailServer/static/mailServer/mailServer.js`, `mailServer/static/mailServer/emailLimitsController.js`, and mirrored under `static/mailServer/`.
|
||||
- **Deploy:**
|
||||
```bash
|
||||
sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh
|
||||
```
|
||||
- **Details:** See `to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md`.
|
||||
|
||||
---
|
||||
|
||||
## 2. FTP Create Account page
|
||||
|
||||
- **Issue:** After selecting a website, the “FTP Account Details” form (username, password, path, quota) did not appear.
|
||||
- **Files:** `ftp/templates/ftp/createFTPAccount.html` (inline script + polling + Angular scope sync), `ftp/static/ftp/ftp.js`, `static/ftp/ftp.js`, `public/static/ftp/ftp.js`.
|
||||
- **Deploy:**
|
||||
```bash
|
||||
sudo bash /home/cyberpanel-repo/deploy-ftp-create-account-fix.sh
|
||||
```
|
||||
- **After deploy:** Hard-refresh `/ftp/createFTPAccount` (Ctrl+Shift+R).
|
||||
|
||||
---
|
||||
|
||||
## 3. FTP Quota Management page
|
||||
|
||||
- **Issue:** `(1146, "Table 'cyberpanel.ftp_quotas' doesn't exist")` on `/ftp/quotaManagement`.
|
||||
- **Files:** `sql/create_ftp_quotas.sql`, `websiteFunctions/models.py` (FTPQuota model already present).
|
||||
- **Deploy:**
|
||||
```bash
|
||||
sudo bash /home/cyberpanel-repo/deploy-ftp-quotas-table.sh
|
||||
```
|
||||
- **Details:** See `to-do/FTP-QUOTAS-TABLE-FIX.md`.
|
||||
|
||||
---
|
||||
|
||||
## 4. mailUtilities indentation fix
|
||||
|
||||
- **File:** `plogical/mailUtilities.py` (indentation fix in DNS query try/except block).
|
||||
- **Deploy:** Copy to `/usr/local/CyberCP/plogical/mailUtilities.py` and restart lscpd if needed.
|
||||
|
||||
---
|
||||
|
||||
## Deploy all fixes (in order)
|
||||
|
||||
Run on the server (e.g. from repo root):
|
||||
|
||||
```bash
|
||||
sudo bash deploy-email-limits-fix.sh
|
||||
sudo bash deploy-ftp-create-account-fix.sh
|
||||
sudo bash deploy-ftp-quotas-table.sh
|
||||
```
|
||||
|
||||
Then hard-refresh the FTP Create Account page in the browser. No need to restart lscpd after the FTP quotas table script (it only runs SQL).
|
||||
|
||||
---
|
||||
|
||||
## Files changed / added in v2.5.5-dev (fixes)
|
||||
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `baseTemplate/templates/baseTemplate/index.html` | CP_VERSION from CYBERPANEL_FULL_VERSION |
|
||||
| `mailServer/` (Email Limits) | Controller, template, getEmailsForDomain permission |
|
||||
| `ftp/templates/ftp/createFTPAccount.html` | Inline fallback + polling for details form |
|
||||
| `ftp/static/ftp/ftp.js`, `static/ftp/ftp.js`, `public/static/ftp/ftp.js` | showFTPDetails, select2/change handlers |
|
||||
| `websiteFunctions/models.py` | FTPQuota model (table created via sql script) |
|
||||
| `sql/create_ftp_quotas.sql` | CREATE TABLE ftp_quotas |
|
||||
| `plogical/mailUtilities.py` | DNS block indentation fix |
|
||||
| `deploy-email-limits-fix.sh` | Deploy Email Limits fix |
|
||||
| `deploy-ftp-create-account-fix.sh` | Deploy FTP Create Account template |
|
||||
| `deploy-ftp-quotas-table.sh` | Create ftp_quotas table |
|
||||
|
||||
---
|
||||
|
||||
## Optional (not committed by default)
|
||||
|
||||
- **CyberCP/urls.py:** If `emailMarketing` is commented out for local runserver, leave it uncommitted or revert before pushing so production keeps the route.
|
||||
Reference in New Issue
Block a user