diff --git a/README.md b/README.md index 00dce7845..9568e658c 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,22 @@ journalctl -u lscpd -f --- +## Testing + +### OLS Feature Test Suite + +The OpenLiteSpeed feature test suite (128 tests) validates binary integrity, CyberPanel module, Auto-SSL config, SSL listener auto-mapping, .htaccess processing, ReadApacheConf directives, and more. + +```bash +# Run from CyberPanel repo root +./tests/ols_test_setup.sh # One-time setup +./tests/ols_feature_tests.sh +``` + +Requires a live CyberPanel + OLS installation. + +--- + ## Resources * Official site: [https://cyberpanel.net](https://cyberpanel.net) diff --git a/cyberpanel.sh b/cyberpanel.sh index 0748c7212..52713fb5c 100644 --- a/cyberpanel.sh +++ b/cyberpanel.sh @@ -11,6 +11,7 @@ OS_FAMILY="" PACKAGE_MANAGER="" ARCHITECTURE="" BRANCH_NAME="" +MARIADB_VER="" DEBUG_MODE=false AUTO_INSTALL=false INSTALLATION_TYPE="" @@ -579,6 +580,50 @@ cleanup_existing_cyberpanel() { # Function to install CyberPanel directly using the working method install_cyberpanel_direct() { + # Ask web server (OpenLiteSpeed vs LiteSpeed Enterprise) BEFORE MariaDB; default OpenLiteSpeed + if [ -z "$LS_ENT" ]; then + if [ "$AUTO_INSTALL" = true ]; then + LS_ENT="" + echo " Using OpenLiteSpeed (auto mode)." + else + echo "" + echo " Web server: 1) OpenLiteSpeed (default), 2) LiteSpeed Enterprise" + read -r -t 60 -p " Enter 1 or 2 [1]: " LS_CHOICE || true + LS_CHOICE="${LS_CHOICE:-1}" + LS_CHOICE="${LS_CHOICE// /}" + if [ "$LS_CHOICE" = "2" ]; then + echo " LiteSpeed Enterprise selected. Enter serial/key (required):" + read -r -t 120 -p " Serial: " LS_SERIAL || true + LS_SERIAL="${LS_SERIAL:-}" + if [ -z "$LS_SERIAL" ]; then + echo " No serial provided. Defaulting to OpenLiteSpeed." + LS_ENT="" + else + LS_ENT="ent" + echo " Using LiteSpeed Enterprise with provided serial." + fi + else + LS_ENT="" + echo " Using OpenLiteSpeed." + fi + echo "" + fi + fi + + # Ask MariaDB version (after web server choice) if not set via --mariadb-version + if [ -z "$MARIADB_VER" ]; then + echo "" + echo " MariaDB version: 10.11, 11.8 (LTS, default) or 12.1?" + read -r -t 60 -p " Enter 10.11, 11.8 or 12.1 [11.8]: " MARIADB_VER || true + MARIADB_VER="${MARIADB_VER:-11.8}" + MARIADB_VER="${MARIADB_VER// /}" + if [ "$MARIADB_VER" != "10.11" ] && [ "$MARIADB_VER" != "11.8" ] && [ "$MARIADB_VER" != "12.1" ]; then + MARIADB_VER="11.8" + fi + echo " Using MariaDB $MARIADB_VER" + echo "" + fi + echo " πŸ”„ Downloading CyberPanel installation files..." # Check if CyberPanel is already installed @@ -609,24 +654,27 @@ install_cyberpanel_direct() { systemctl enable mariadb 2>/dev/null || true systemctl enable lsws 2>/dev/null || true + # Clear any previous install temp folders so we never use stale extracted files + rm -rf /tmp/cyberpanel_install_* 2>/dev/null || true + # Create temporary directory for installation local temp_dir="/tmp/cyberpanel_install_$$" mkdir -p "$temp_dir" cd "$temp_dir" || return 1 - # CRITICAL: Disable MariaDB 12.1 repository and add dnf exclude if MariaDB 10.x is installed - # This must be done BEFORE Pre_Install_Setup_Repository runs + # Only add dnf exclude when we want to KEEP the current MariaDB (same version as user chose). + # If user chose 11.8 but 10.11 is installed, do NOT exclude β€” allow install.py to upgrade. if command -v rpm >/dev/null 2>&1; then - # Check if MariaDB 10.x is installed if rpm -qa | grep -qiE "^(mariadb-server|mysql-server|MariaDB-server)" 2>/dev/null; then local mariadb_version=$(mysql --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) if [ -n "$mariadb_version" ]; then local major_ver=$(echo "$mariadb_version" | cut -d. -f1) local minor_ver=$(echo "$mariadb_version" | cut -d. -f2) - - # Check if it's MariaDB 10.x (major version < 12) - if [ "$major_ver" -lt 12 ]; then - print_status "MariaDB $mariadb_version detected, adding dnf exclude to prevent upgrade attempts" + local installed_majmin="${major_ver}.${minor_ver}" + local chosen_ver="${MARIADB_VER:-11.8}" + # Only add exclude when installed version matches user's choice (preserve, no upgrade) + if [ "$installed_majmin" = "$chosen_ver" ]; then + print_status "MariaDB $mariadb_version matches chosen $chosen_ver, adding dnf exclude to preserve it" # Add MariaDB-server to dnf excludes (multiple formats for compatibility) local dnf_conf="/etc/dnf/dnf.conf" @@ -763,6 +811,19 @@ except: local monitor_pid=$! echo "$monitor_pid" > /tmp/cyberpanel_repo_monitor.pid print_status "Started background process to monitor and disable MariaDB repositories" + else + # User chose a different version (e.g. 11.8) than installed (e.g. 10.11) β€” allow upgrade + print_status "MariaDB $mariadb_version installed but you chose $chosen_ver; not adding dnf exclude (installer will upgrade)" + # Remove any existing MariaDB exclude from a previous run so install can proceed + for c in /etc/dnf/dnf.conf /etc/yum.conf; do + if [ -f "$c" ] && grep -q "exclude=.*MariaDB-server" "$c" 2>/dev/null; then + sed -i 's/ *MariaDB-server\* *//g; s/exclude= *$/exclude=/; s/exclude=\s*$/exclude=/' "$c" 2>/dev/null + if grep -q "^exclude=\s*$" "$c" 2>/dev/null; then + sed -i '/^exclude=\s*$/d' "$c" 2>/dev/null + fi + print_status "Removed MariaDB-server from excludes in $c to allow upgrade" + fi + done fi fi fi @@ -774,19 +835,23 @@ except: echo "Downloading from: https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/cyberpanel.sh" # First, try to download the repository archive to get the correct installer - local archive_url="https://github.com/master3395/cyberpanel/archive/v2.5.5-dev.tar.gz" + # GitHub: branch archives use refs/heads/BRANCH; GitHub returns 302 redirect to codeload, so we must use -L + local archive_url="" local installer_url="https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/cyberpanel.sh" - - # Test if the development branch archive exists - if curl -s --head "$archive_url" | grep -q "200 OK"; then + if curl -s -L --head "https://github.com/master3395/cyberpanel/archive/refs/heads/v2.5.5-dev.tar.gz" | grep -q "200 OK"; then + archive_url="https://github.com/master3395/cyberpanel/archive/refs/heads/v2.5.5-dev.tar.gz" + echo " Using development branch (v2.5.5-dev) from master3395/cyberpanel" + elif curl -s -L --head "https://github.com/master3395/cyberpanel/archive/v2.5.5-dev.tar.gz" | grep -q "200 OK"; then + archive_url="https://github.com/master3395/cyberpanel/archive/v2.5.5-dev.tar.gz" echo " Using development branch (v2.5.5-dev) from master3395/cyberpanel" else echo " Development branch archive not available, trying installer script directly..." - # Test if the installer script exists - if ! curl -s --head "$installer_url" | grep -q "200 OK"; then + if ! curl -s -L --head "$installer_url" | grep -q "200 OK"; then echo " Development branch not available, falling back to stable" installer_url="https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel.sh" archive_url="https://github.com/master3395/cyberpanel/archive/stable.tar.gz" + else + archive_url="https://github.com/master3395/cyberpanel/archive/refs/heads/v2.5.5-dev.tar.gz" fi fi @@ -796,83 +861,22 @@ except: return 1 fi - # CRITICAL: Patch the installer script to skip MariaDB installation if 10.x is already installed - if [ -n "$MARIADB_VERSION" ] && [ "$major_ver" -lt 12 ] 2>/dev/null; then - print_status "Patching installer script to skip MariaDB installation..." - - # Create a backup - cp cyberpanel_installer.sh cyberpanel_installer.sh.backup - - # Use Python to properly patch the installer script - python3 -c " -import re -import sys - -try: - with open('cyberpanel_installer.sh', 'r') as f: - content = f.read() + # Do NOT patch installer to add --exclude=MariaDB-server*: it blocks initial MariaDB install + # and causes "MariaDB-server requires MariaDB-client but none of the providers can be installed". - original_content = content - - # Pattern: Add --exclude=MariaDB-server* to dnf/yum install commands that install mariadb-server - # Match: (dnf|yum) install [flags] [packages including mariadb-server] - def add_exclude(match): - cmd = match.group(0) - # Check if --exclude is already present - if '--exclude=MariaDB-server' in cmd: - return cmd - # Add --exclude=MariaDB-server* after install and flags, before packages - return re.sub(r'((?:dnf|yum)\s+install\s+(?:-[^\s]+\s+)*)', r'\1--exclude=MariaDB-server* ', cmd, flags=re.IGNORECASE) - - # Find all dnf/yum install commands that mention mariadb-server - content = re.sub( - r'(?:dnf|yum)\s+install[^;]*?mariadb-server[^;]*', - add_exclude, - content, - flags=re.IGNORECASE | re.MULTILINE - ) - - # Also handle MariaDB-server (capitalized) - content = re.sub( - r'(?:dnf|yum)\s+install[^;]*?MariaDB-server[^;]*', - add_exclude, - content, - flags=re.IGNORECASE | re.MULTILINE - ) - - # Only write if content changed - if content != original_content: - with open('cyberpanel_installer.sh', 'w') as f: - f.write(content) - print('Installer script patched successfully') - else: - print('No changes needed in installer script') - -except Exception as e: - print(f'Error patching installer script: {e}') - sys.exit(1) -" 2>/dev/null && print_status "Installer script patched successfully" || { - # Fallback: Simple sed-based patching if Python fails - sed -i 's/\(dnf\|yum\) install\([^;]*\)mariadb-server/\1 install\2--exclude=MariaDB-server* mariadb-server/gi' cyberpanel_installer.sh 2>/dev/null - sed -i 's/\(dnf\|yum\) install\([^;]*\)MariaDB-server/\1 install\2--exclude=MariaDB-server* MariaDB-server/gi' cyberpanel_installer.sh 2>/dev/null - print_status "Installer script patched (fallback method)" - } - - print_status "Installer script patched to exclude MariaDB-server from installation" - fi - - # Make script executable and verify - chmod 755 cyberpanel_installer.sh 2>/dev/null || true + # Make script executable (use full path in case cwd has noexec) + chmod 755 cyberpanel_installer.sh 2>/dev/null || chmod +x cyberpanel_installer.sh 2>/dev/null || true if [ ! -x "cyberpanel_installer.sh" ]; then - print_status "WARNING: Could not make cyberpanel_installer.sh executable, will use bash to execute" + print_status "Note: Script will be run with bash (executable bit not set)" fi - # Download the install directory + # Download the install directory (use archive_url set above; may be branch or stable) echo "Downloading installation files..." - local archive_url="https://github.com/master3395/cyberpanel/archive/v2.5.5-dev.tar.gz" - if [ "$installer_url" = "https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel.sh" ]; then + if [ -z "$archive_url" ] || [ "$installer_url" = "https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel.sh" ]; then archive_url="https://github.com/master3395/cyberpanel/archive/stable.tar.gz" fi + # Append cache-bust so CDNs/proxies don't serve old installer (GitHub ignores query params) + archive_url="${archive_url}?nocache=$(date +%s 2>/dev/null || echo 0)" curl --silent -L -o install_files.tar.gz "$archive_url" 2>/dev/null if [ $? -ne 0 ] || [ ! -s "install_files.tar.gz" ]; then @@ -925,8 +929,13 @@ except Exception as e: echo "This may take several minutes. Please be patient." echo "" - # Create log directory + # Create log directory (same as v2.4.4: installer logs go here) mkdir -p /var/log/CyberPanel + echo " Installation logs:" + echo " β€’ /var/log/CyberPanel/install.log (installer script messages)" + echo " β€’ /var/log/CyberPanel/install_output.log (Python installer stdout/stderr)" + echo " β€’ /var/log/installLogs.txt (install.py detailed log)" + echo "" # Run the installer with live output monitoring echo "Starting CyberPanel installer with live progress monitoring..." @@ -953,67 +962,22 @@ except Exception as e: if [ -f "$installer_py" ]; then print_status "Using install/install.py directly for installation (non-interactive mode)" - # CRITICAL: Patch install.py to exclude MariaDB-server from dnf/yum commands - if [ -n "$MARIADB_VERSION" ] && [ "$major_ver" -lt 12 ] 2>/dev/null; then - print_status "Patching install.py to exclude MariaDB-server from installation commands..." - - # Create backup - cp "$installer_py" "${installer_py}.backup" 2>/dev/null || true - - # Patch install.py to add --exclude=MariaDB-server* to dnf/yum install commands - python3 -c " -import re -import sys - -try: - with open('$installer_py', 'r') as f: - content = f.read() - - original_content = content - - # Pattern: Add --exclude=MariaDB-server* to dnf/yum install commands that install mariadb-server - def add_exclude(match): - cmd = match.group(0) - # Check if --exclude is already present - if '--exclude=MariaDB-server' in cmd: - return cmd - # Add --exclude=MariaDB-server* after install and flags, before packages - return re.sub(r'((?:dnf|yum)\s+install\s+(?:-[^\s]+\s+)*)', r'\1--exclude=MariaDB-server* ', cmd, flags=re.IGNORECASE) - - # Find all dnf/yum install commands that mention mariadb-server - content = re.sub( - r'(?:dnf|yum)\s+install[^;]*?mariadb-server[^;]*', - add_exclude, - content, - flags=re.IGNORECASE | re.MULTILINE - ) - - # Also handle MariaDB-server (capitalized) and in Python strings - content = re.sub( - r'(\"|\')(?:dnf|yum)\s+install[^\"]*?mariadb-server[^\"]*(\"|\')', - lambda m: m.group(1) + re.sub(r'((?:dnf|yum)\s+install\s+(?:-[^\s]+\s+)*)', r'\1--exclude=MariaDB-server* ', m.group(0)[1:-1], flags=re.IGNORECASE) + m.group(2), - content, - flags=re.IGNORECASE | re.MULTILINE - ) - - # Only write if content changed - if content != original_content: - with open('$installer_py', 'w') as f: - f.write(content) - print('install.py patched successfully') - else: - print('No changes needed in install.py') + # NOTE: We do NOT patch install.py to add --exclude=MariaDB-server* to dnf install. + # That would block the initial MariaDB-server install. install.py now clears dnf exclude + # before installing MariaDB and uses official MariaDB-server packages. -except Exception as e: - print(f'Error patching install.py: {e}') - sys.exit(1) -" 2>/dev/null && print_status "install.py patched successfully" || { - # Fallback: Simple sed-based patching if Python fails - sed -i 's/\(dnf\|yum\) install\([^;]*\)mariadb-server/\1 install\2--exclude=MariaDB-server* mariadb-server/gi' "$installer_py" 2>/dev/null - sed -i 's/\(dnf\|yum\) install\([^;]*\)MariaDB-server/\1 install\2--exclude=MariaDB-server* MariaDB-server/gi' "$installer_py" 2>/dev/null - print_status "install.py patched (fallback method)" - } - fi + # Clear MariaDB-server from dnf/yum exclude so the installer can install or reinstall it + # (cyberpanel.sh may have added it earlier when 10.x was detected; partial installs leave exclude in place) + for conf in /etc/dnf/dnf.conf /etc/yum.conf; do + if [ -f "$conf" ] && grep -q "exclude=.*MariaDB-server" "$conf" 2>/dev/null; then + sed -i '/^exclude=/s/MariaDB-server\*\s*//g' "$conf" + sed -i '/^exclude=/s/\s*MariaDB-server\*//g' "$conf" + sed -i '/^exclude=/s/MariaDB-server\s*//g' "$conf" + sed -i '/^exclude=\s*$/d' "$conf" + sed -i '/^exclude=$/d' "$conf" + print_status "Cleared MariaDB-server from exclude in $conf for installation" + fi + done # If MariaDB 10.x is installed, disable repositories right before running installer if [ -n "$MARIADB_VERSION" ] && [ -f /tmp/cyberpanel_repo_monitor.pid ]; then @@ -1161,9 +1125,10 @@ except Exception as e: fi fi - # Verify MySQLdb is available + # Verify MySQLdb is available (mysqlclient; some builds lack __version__) print_status "Verifying MySQLdb module availability..." - if python3 -c "import MySQLdb; print('MySQLdb version:', MySQLdb.__version__)" 2>&1; then + if python3 -c "import MySQLdb; getattr(MySQLdb, '__version__', 'ok'); print('MySQLdb OK')" 2>/dev/null || \ + python3 -c "import MySQLdb; MySQLdb; print('MySQLdb OK')" 2>/dev/null; then print_status "βœ“ MySQLdb module is available and working" else print_status "⚠️ WARNING: MySQLdb module not available" @@ -1178,19 +1143,40 @@ except Exception as e: # install.py requires publicip as first positional argument local install_args=("$server_ip") - # Add optional arguments based on user preferences + # Web server: OpenLiteSpeed (default) or LiteSpeed Enterprise (--ent + --serial) + if [ -n "$LS_ENT" ] && [ -n "$LS_SERIAL" ]; then + install_args+=("--ent" "$LS_ENT" "--serial" "$LS_SERIAL") + fi # Default: OpenLiteSpeed, Full installation (postfix, powerdns, ftp), Local MySQL - # These match what the user selected in the interactive prompts install_args+=("--postfix" "ON") install_args+=("--powerdns" "ON") install_args+=("--ftp" "ON") install_args+=("--remotemysql" "OFF") + # Only pass --mariadb-version if this install.py supports it (avoids "unrecognized arguments" on older archives) + if grep -q "mariadb-version\|mariadb_version" "$installer_py" 2>/dev/null; then + install_args+=("--mariadb-version" "${MARIADB_VER:-11.8}") + fi if [ "$DEBUG_MODE" = true ]; then # Note: install.py doesn't have --debug, but we can set it via environment export DEBUG_MODE=true fi + # CRITICAL: If CyberPanel Python does not exist yet, patch installer to use system Python. + # Fixes FileNotFoundError when archive is cached/old and still references /usr/local/CyberPanel/bin/python. + if [ ! -f /usr/local/CyberPanel/bin/python ]; then + sys_python="/usr/bin/python3" + [ -x "$sys_python" ] || sys_python="/usr/local/bin/python3" + if [ -x "$sys_python" ]; then + for f in install/install_utils.py install/install.py; do + if [ -f "$f" ] && grep -q '/usr/local/CyberPanel/bin/python' "$f" 2>/dev/null; then + sed -i "s|/usr/local/CyberPanel/bin/python|$sys_python|g" "$f" + print_status "Patched $f to use $sys_python (CyberPanel python not yet installed)" + fi + done + fi + fi + # Run the Python installer directly if [ "$DEBUG_MODE" = true ]; then python3 "$installer_py" "${install_args[@]}" 2>&1 | tee /var/log/CyberPanel/install_output.log @@ -1257,6 +1243,11 @@ except Exception as e: echo " INSTALLATION COMPLETED" echo "===============================================================================================================" echo "" + echo " Installation logs (for troubleshooting):" + echo " β€’ /var/log/CyberPanel/install.log (installer script messages)" + echo " β€’ /var/log/CyberPanel/install_output.log (Python installer stdout/stderr)" + echo " β€’ /var/log/installLogs.txt (install.py detailed log)" + echo "" # Check if installation was successful if [ $install_exit_code -ne 0 ]; then @@ -1352,8 +1343,9 @@ apply_fixes() { systemctl start mariadb 2>/dev/null || true systemctl enable mariadb 2>/dev/null || true - # Fix LiteSpeed service - cat > /etc/systemd/system/lsws.service << 'EOF' + # Fix LiteSpeed service only if the web server was actually installed + if [ -x /usr/local/lsws/bin/lswsctrl ] || [ -x /usr/local/lsws/bin/lsctrl ] || [ -f /usr/local/lsws/bin/openlitespeed ]; then + cat > /etc/systemd/system/lsws.service << 'EOF' [Unit] Description=LiteSpeed Web Server After=network.target @@ -1372,9 +1364,16 @@ RestartSec=5 WantedBy=multi-user.target EOF - systemctl daemon-reload - systemctl enable lsws - systemctl start lsws + systemctl daemon-reload + systemctl enable lsws + systemctl start lsws || true + else + echo " β€’ LiteSpeed/OpenLiteSpeed not found at /usr/local/lsws - skipping lsws.service (install may have skipped web server)" + echo " β€’ If the installer failed earlier (e.g. Python error), re-run the installer. Once it completes, open ports 8090 and 7080 in your cloud security group (e.g. AWS EC2 Security Group inbound rules)." + systemctl disable lsws 2>/dev/null || true + rm -f /etc/systemd/system/lsws.service + systemctl daemon-reload + fi # Set OpenLiteSpeed admin password to match CyberPanel echo " β€’ Configuring OpenLiteSpeed admin password..." @@ -1395,11 +1394,62 @@ EOF # Give services a moment to start sleep 3 + # Ensure both 8090 (CyberPanel) and 7080 (LiteSpeed/OLS) are accessible + echo " β€’ Ensuring ports 8090 and 7080 are accessible..." + port_check() { + local port=$1 + command -v ss >/dev/null 2>&1 && ss -tlnp 2>/dev/null | grep -q ":$port " && return 0 + command -v netstat >/dev/null 2>&1 && netstat -tlnp 2>/dev/null | grep -q ":$port " && return 0 + return 1 + } + max_attempts=18 + attempt=0 + while [ $attempt -lt $max_attempts ]; do + need_restart=false + systemctl is-active --quiet mariadb || { systemctl start mariadb 2>/dev/null; need_restart=true; } + systemctl is-active --quiet lsws 2>/dev/null || { [ -x /usr/local/lsws/bin/lswsctrl ] && systemctl start lsws 2>/dev/null; need_restart=true; } + systemctl is-active --quiet lscpd 2>/dev/null || { systemctl start lscpd 2>/dev/null; need_restart=true; } + [ "$need_restart" = true ] && sleep 5 + if port_check 8090 && port_check 7080; then + echo " βœ“ Port 8090 (CyberPanel) and 7080 (OpenLiteSpeed) are listening" + break + fi + attempt=$((attempt + 1)) + [ $attempt -lt $max_attempts ] && sleep 5 + done + if ! port_check 8090 || ! port_check 7080; then + systemctl start lscpd 2>/dev/null + systemctl start lsws 2>/dev/null + sleep 10 + if port_check 8090 && port_check 7080; then + echo " βœ“ Port 8090 and 7080 are now listening" + else + echo " ⚠ One or both ports not yet listening. Run: systemctl start mariadb lsws lscpd" + echo " ⚠ On AWS/cloud: add inbound rules for TCP 8090 and 7080 in the instance security group." + fi + fi + echo " βœ“ Post-installation configurations completed" } +# Helper: check if a port is listening +_port_listening() { + local port=$1 + command -v ss >/dev/null 2>&1 && ss -tlnp 2>/dev/null | grep -q ":$port " && return 0 + command -v netstat >/dev/null 2>&1 && netstat -tlnp 2>/dev/null | grep -q ":$port " && return 0 + return 1 +} + # Function to show status summary show_status_summary() { + # Last-chance: try to start services so 8090 and 7080 are accessible + if ! _port_listening 8090 || ! _port_listening 7080; then + systemctl start mariadb 2>/dev/null || true + systemctl start lsws 2>/dev/null || true + systemctl start lscpd 2>/dev/null || true + sleep 8 + fi + echo "===============================================================================================================" echo " FINAL STATUS CHECK" echo "===============================================================================================================" @@ -1427,6 +1477,22 @@ show_status_summary() { echo " βœ“ CyberPanel Application - Running" else echo " βœ— CyberPanel Application - Not Running (may take a moment to start)" + all_services_running=false + fi + + echo "" + echo "Port Accessibility:" + if _port_listening 8090; then + echo " βœ“ Port 8090 (CyberPanel) - Accessible" + else + echo " βœ— Port 8090 (CyberPanel) - Not listening (run: systemctl start lscpd)" + all_services_running=false + fi + if _port_listening 7080; then + echo " βœ“ Port 7080 (OpenLiteSpeed) - Accessible" + else + echo " βœ— Port 7080 (OpenLiteSpeed) - Not listening (run: systemctl start lsws)" + all_services_running=false fi # Get the actual password that was set @@ -1455,7 +1521,7 @@ show_status_summary() { echo "===============================================================================================================" if [ "$all_services_running" = true ]; then - echo "βœ“ Installation completed successfully!" + echo "βœ“ Installation completed successfully! Ports 8090 and 7080 are accessible." else echo "⚠ Installation completed with warnings. Some services may need attention." fi @@ -1664,7 +1730,7 @@ show_version_selection() { echo "" echo " 1. Latest Stable (Recommended)" echo " 2. v2.5.5-dev (Development)" - echo " 3. v2.5.4 (Previous Stable)" + echo " 3. v2.4.4 (Previous Stable)" echo " 4. Custom Branch Name" echo " 5. Custom Commit Hash" echo "" @@ -1685,7 +1751,7 @@ show_version_selection() { break ;; 3) - BRANCH_NAME="v2.5.4" + BRANCH_NAME="v2.4.4" break ;; 4) @@ -2674,6 +2740,21 @@ parse_arguments() { set -x shift ;; + --mariadb-version) + if [ -n "$2" ] && [ "$2" = "10.11" ]; then + MARIADB_VER="10.11" + shift 2 + elif [ -n "$2" ] && [ "$2" = "11.8" ]; then + MARIADB_VER="11.8" + shift 2 + elif [ -n "$2" ] && [ "$2" = "12.1" ]; then + MARIADB_VER="12.1" + shift 2 + else + echo "ERROR: --mariadb-version requires 10.11, 11.8 or 12.1" + exit 1 + fi + ;; --auto) AUTO_INSTALL=true shift @@ -2683,8 +2764,9 @@ parse_arguments() { echo "Options:" echo " -b, --branch BRANCH Install from specific branch/commit" echo " -v, --version VER Install specific version (auto-adds v prefix)" + echo " --mariadb-version VER MariaDB version: 10.11, 11.8 or 12.1 (asked after web server)" echo " --debug Enable debug mode" - echo " --auto Auto mode without prompts" + echo " --auto Auto mode: OpenLiteSpeed + MariaDB 11.8 unless --mariadb-version set" echo " -h, --help Show this help message" echo "" echo "Examples:" @@ -2696,6 +2778,9 @@ parse_arguments() { echo " $0 -v 2.4.3 # Install version 2.4.3" echo " $0 -b main # Install from main branch" echo " $0 -b a1b2c3d4 # Install from specific commit" + echo " $0 --mariadb-version 10.11 # Use MariaDB 10.11 (same as v2.4.4 style)" + echo " $0 --mariadb-version 12.1 # Use MariaDB 12.1 (no prompt)" + echo " $0 --auto --mariadb-version 11.8 # Fully non-interactive with MariaDB 11.8" echo "" echo "Standard CyberPanel Installation Methods:" echo " sh <(curl https://cyberpanel.net/install.sh)" @@ -2832,7 +2917,10 @@ main() { print_status "SUCCESS: Installation completed successfully!" else - # Run interactive mode + # Run interactive mode - ensure stdin is the terminal for prompts (e.g. when script was piped from curl) + if [ ! -t 0 ]; then + exec 0/dev/null; then + echo "" + echo "This script must be run as root." + echo "Run: sudo bash <(curl -sL https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel_upgrade.sh) -b v2.5.5-dev" + echo "Or: sudo su - then run the same command without sudo" + echo "" + exit 1 +fi + Sudo_Test=$(set) #for SUDO check @@ -95,20 +105,21 @@ echo -e "\n${1}" >> /var/log/upgradeLogs.txt Check_Root() { echo -e "\nChecking root privileges..." + # If we're actually root (uid 0), allow regardless of SUDO in environment (e.g. curl | sudo bash) + if [[ $(id -u) -eq 0 ]] 2>/dev/null; then + echo -e "\nYou are running as root...\n" + return 0 + fi + if echo "$Sudo_Test" | grep SUDO >/dev/null; then echo -e "\nYou are using SUDO, please run as root user...\n" echo -e "\nIf you don't have direct access to root user, please run \e[31msudo su -\e[39m command (do NOT miss the \e[31m-\e[39m at end or it will fail) and then run installation command again." - exit + exit 1 fi - if [[ $(id -u) != 0 ]] >/dev/null; then - echo -e "\nYou must run as root user to install CyberPanel...\n" - echo -e "or run the following command: (do NOT miss the quotes)" - echo -e "\e[31msudo su -c \"sh <(curl https://cyberpanel.sh || wget -O - https://cyberpanel.sh)\"\e[39m" - exit 1 - else - echo -e "\nYou are running as root...\n" - fi + echo -e "\nYou must run as root user to install CyberPanel...\n" + echo -e "Run: \e[31msudo su -\e[39m then run this script again, or: curl -sL | sudo bash -s -- " + exit 1 } Check_Server_IP() { @@ -234,7 +245,13 @@ if [[ "$1" = *.*.* ]]; then echo -e "\nYou must use version number higher than 2.3.4" exit else - Branch_Name="v${1//[[:space:]]/}" + raw="${1//[[:space:]]/}" + # Do not add "v" if user already passed e.g. v2.5.5-dev (avoids vv2.5.5-dev) + if [[ "$raw" = v* ]]; then + Branch_Name="$raw" + else + Branch_Name="v$raw" + fi echo -e "\nSet branch name to $Branch_Name...\n" fi else @@ -349,12 +366,12 @@ if [[ "$*" = *"--no-system-update"* ]]; then Skip_System_Update="yes" echo -e "\nUsing --no-system-update: skipping full system package update.\n" fi -# Parse --mariadb-version 11.8|12.1 (default 11.8) +# Parse --mariadb-version 10.11|11.8|12.1 (default 11.8) if [[ "$*" = *"--mariadb-version "* ]]; then MARIADB_VER=$(echo "$*" | sed -n 's/.*--mariadb-version \([^ ]*\).*/\1/p' | head -1) MARIADB_VER="${MARIADB_VER:-11.8}" fi -if [[ "$MARIADB_VER" != "11.8" ]] && [[ "$MARIADB_VER" != "12.1" ]]; then +if [[ "$MARIADB_VER" != "10.11" ]] && [[ "$MARIADB_VER" != "11.8" ]] && [[ "$MARIADB_VER" != "12.1" ]]; then MARIADB_VER="11.8" fi } @@ -930,10 +947,21 @@ Pre_Upgrade_Branch_Input() { Main_Upgrade() { echo -e "\n[$(date +"%Y-%m-%d %H:%M:%S")] Starting Main_Upgrade function..." | tee -a /var/log/cyberpanel_upgrade_debug.log -echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Running: /usr/local/CyberPanel/bin/python upgrade.py $Branch_Name" | tee -a /var/log/cyberpanel_upgrade_debug.log + +# Resolve Python for upgrade (avoid FileNotFoundError when /usr/local/CyberPanel/bin/python missing) +CP_PYTHON="" +for py in /usr/local/CyberPanel/bin/python /usr/local/CyberCP/bin/python /usr/bin/python3 /usr/local/bin/python3; do + if [[ -x "$py" ]]; then CP_PYTHON="$py"; break; fi +done +if [[ -z "$CP_PYTHON" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] ERROR: No Python found for upgrade (tried CyberPanel, CyberCP, python3)" | tee -a /var/log/cyberpanel_upgrade_debug.log + exit 1 +fi +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Using Python: $CP_PYTHON" | tee -a /var/log/cyberpanel_upgrade_debug.log +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Running: $CP_PYTHON upgrade.py $Branch_Name" | tee -a /var/log/cyberpanel_upgrade_debug.log # Run upgrade.py and capture output -upgrade_output=$(/usr/local/CyberPanel/bin/python upgrade.py "$Branch_Name" 2>&1) +upgrade_output=$("$CP_PYTHON" upgrade.py "$Branch_Name" 2>&1) RETURN_CODE=$? echo "$upgrade_output" | tee -a /var/log/cyberpanel_upgrade_debug.log @@ -1200,7 +1228,9 @@ tar xf wsgi-lsapi-2.1.tgz cd wsgi-lsapi-2.1 || exit echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Configuring WSGI..." | tee -a /var/log/cyberpanel_upgrade_debug.log -/usr/local/CyberPanel/bin/python ./configure.py 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log +PYTHON_CFG="${CP_PYTHON:-/usr/bin/python3}" +[[ -x "$PYTHON_CFG" ]] || PYTHON_CFG="/usr/bin/python3" +"$PYTHON_CFG" ./configure.py 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log # Fix Makefile to use proper optimization flags to avoid _FORTIFY_SOURCE warnings echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Optimizing Makefile for proper compilation..." | tee -a /var/log/cyberpanel_upgrade_debug.log @@ -1671,12 +1701,16 @@ if [[ "$*" != *"--branch "* ]] && [[ "$*" != *"-b "* ]] ; then Pre_Upgrade_Branch_Input fi -# Prompt for MariaDB version if not set via --mariadb-version (default 11.8). Downgrade supported (e.g. re-run with --mariadb-version 11.8). +# Prompt for MariaDB version if not set via --mariadb-version (default 11.8). Options: 10.11, 11.8, 12.1. if [[ "$*" != *"--mariadb-version "* ]]; then - echo -e "\nMariaDB version: \e[31m11.8\e[39m LTS (default) or \e[31m12.1\e[39m. You can switch later by re-running with --mariadb-version 11.8 or 12.1." - echo -e "Press Enter for 11.8 LTS, or type \e[31m12.1\e[39m and Enter for 12.1 (5 sec timeout): " + echo -e "\nMariaDB version: \e[31m10.11\e[39m, \e[31m11.8\e[39m LTS (default) or \e[31m12.1\e[39m. You can switch later by re-running with --mariadb-version 10.11, 11.8 or 12.1." + echo -e "Press Enter for 11.8 LTS, or type \e[31m10.11\e[39m or \e[31m12.1\e[39m (5 sec timeout): " read -r -t 5 Tmp_MariaDB_Ver || true - if [[ "$Tmp_MariaDB_Ver" = "12.1" ]]; then + Tmp_MariaDB_Ver="${Tmp_MariaDB_Ver// /}" + if [[ "$Tmp_MariaDB_Ver" = "10.11" ]]; then + MARIADB_VER="10.11" + echo -e "MariaDB 10.11 selected.\n" + elif [[ "$Tmp_MariaDB_Ver" = "12.1" ]]; then MARIADB_VER="12.1" echo -e "MariaDB 12.1 selected.\n" else diff --git a/install/install.py b/install/install.py index ac0224a1e..745906b30 100644 --- a/install/install.py +++ b/install/install.py @@ -1,9 +1,17 @@ import sys +import os +import re + +# Ensure install dir is on path for ols_binaries_config +_install_dir = os.path.dirname(os.path.abspath(__file__)) +if _install_dir not in sys.path: + sys.path.insert(0, _install_dir) +import ols_binaries_config + import subprocess import shutil import installLog as logging import argparse -import os import errno import shlex from firewallUtilities import FirewallUtilities @@ -405,48 +413,11 @@ class preFlightsChecks: except Exception as e: self.stdOut("Warning: Could not remove compat packages: " + str(e), 0) - # Check if MariaDB is already installed before attempting installation + # Do NOT install MariaDB here with plain dnf (that would install distro default 10.11). + # installMySQL() runs later with the user's chosen version (--mariadb-version: 10.11, 11.8 or 12.1). is_installed, installed_version, major_minor = self.checkExistingMariaDB() - if is_installed: - self.stdOut(f"MariaDB/MySQL is already installed (version: {installed_version}), skipping installation", 1) - mariadb_installed = True - else: - # Install MariaDB with enhanced AlmaLinux 9.6 support - self.stdOut("Installing MariaDB for AlmaLinux 9.6...", 1) - - # Try multiple installation methods for maximum compatibility - mariadb_commands = [ - "dnf install -y mariadb-server mariadb-devel mariadb-client --skip-broken --nobest", - "dnf install -y mariadb-server mariadb-devel mariadb-client --allowerasing", - "dnf install -y mariadb-server mariadb-devel --skip-broken --nobest --allowerasing", - "dnf install -y mariadb-server --skip-broken --nobest --allowerasing" - ] - - mariadb_installed = False - for cmd in mariadb_commands: - try: - result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=300) - if result.returncode == 0: - mariadb_installed = True - self.stdOut(f"MariaDB installed successfully with command: {cmd}", 1) - break - except subprocess.TimeoutExpired: - self.stdOut(f"Timeout installing MariaDB with command: {cmd}", 0) - continue - except Exception as e: - self.stdOut(f"Error installing MariaDB with command: {cmd} - {str(e)}", 0) - continue - - if not mariadb_installed: - self.stdOut("MariaDB installation failed, trying MySQL as fallback...", 0) - try: - command = "dnf install -y mysql-server mysql-devel --skip-broken --nobest --allowerasing" - self.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) - self.stdOut("MySQL installed as fallback for MariaDB", 1) - mariadb_installed = True - except: - self.stdOut("Both MariaDB and MySQL installation failed", 0) + self.stdOut(f"MariaDB/MySQL already installed (version: {installed_version}), skipping", 1) # Install additional required packages self.stdOut("Installing additional required packages...", 1) @@ -1213,30 +1184,7 @@ class preFlightsChecks: platform = self.detectPlatform() self.stdOut(f"Detected platform: {platform}", 1) - # Platform-specific URLs and checksums (OpenLiteSpeed 1.8.5+ preferred from repo; fallback static build) - # Module Build Date: December 28, 2025 - v2.2.0 Brute Force with Progressive Throttle - BINARY_CONFIGS = { - 'rhel8': { - 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel8-static', - 'sha256': '6ce688a237615102cc1603ee1999b3cede0ff3482d31e1f65705e92396d34b3a', - 'module_url': 'https://cyberpanel.net/binaries/rhel8/cyberpanel_ols.so', - 'module_sha256': '7c33d89c7fbcd3ed7b0422fee3f49b5e041713c2c2b7316a5774f6defa147572' - }, - 'rhel9': { - 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel9-static', - 'sha256': '709093d99d5d3e789134c131893614968e17eefd9ade2200f811d9b076b2f02e', - 'module_url': 'https://cyberpanel.net/binaries/rhel9/cyberpanel_ols.so', - 'module_sha256': 'ae65337e2d13babc0c675bb4264d469daffa2efb7627c9bf39ac59e42e3ebede' - }, - 'ubuntu': { - 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-ubuntu-static', - 'sha256': '89aaf66474e78cb3c1666784e0e7a417550bd317e6ab148201bdc318d36710cb', - 'module_url': 'https://cyberpanel.net/binaries/ubuntu/cyberpanel_ols.so', - 'module_sha256': '62978ede1f174dd2885e5227a3d9cc463d0c27acd77cfc23743d7309ee0c54ea' - } - } - - config = BINARY_CONFIGS.get(platform) + config = ols_binaries_config.BINARY_CONFIGS.get(platform) if not config: self.stdOut(f"ERROR: No binaries available for platform {platform}", 1) self.stdOut("Skipping custom binary installation", 1) @@ -1366,6 +1314,24 @@ class preFlightsChecks: self.stdOut("=" * 50, 1) # Configure module after installation self.configureCustomModule() + # Enable Auto-SSL if not already configured + conf_path = '/usr/local/lsws/conf/httpd_config.conf' + try: + if os.path.exists(conf_path): + with open(conf_path, 'r') as f: + content = f.read() + if 'autoSSL' not in content: + content = re.sub( + r'(adminEmails\s+\S+)', + r'\1\nautoSSL 1\nacmeEmail admin@cyberpanel.net', + content, + count=1 + ) + with open(conf_path, 'w') as f: + f.write(content) + self.stdOut("Auto-SSL enabled in httpd_config.conf", 1) + except Exception as e: + self.stdOut(f"WARNING: Could not enable Auto-SSL: {e}", 1) return True self.stdOut("ERROR: Installation verification failed", 1) @@ -1889,12 +1855,12 @@ module cyberpanel_ols { if is_installed: self.stdOut(f"MariaDB/MySQL is already installed (version: {installed_version}), skipping installation", 1) - # Use existing if already on 11.x or 12.x + # Use existing if already on 10.x, 11.x or 12.x if major_minor and major_minor != "unknown": try: major_ver = float(major_minor) - if major_ver >= 11.0: - self.stdOut("Using existing MariaDB installation (11.x/12.x)", 1) + if major_ver >= 10.0: + self.stdOut("Using existing MariaDB installation (10.x/11.x/12.x)", 1) self.startMariaDB() self.changeMYSQLRootPassword() self.fixMariaDB() @@ -1902,12 +1868,45 @@ module cyberpanel_ols { except (ValueError, TypeError): pass - # Set up MariaDB repository only if not already installed (version from --mariadb-version, default 11.8) + # Set up MariaDB repository only if not already installed (version from --mariadb-version: 10.11, 11.8 or 12.1) mariadb_ver = getattr(preFlightsChecks, 'mariadb_version', '11.8') command = f'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version={mariadb_ver}' self.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) - - command = 'dnf install mariadb-server mariadb-devel mariadb-client-utils -y' + # Allow MariaDB-server to be installed: remove from dnf exclude if present (e.g. from previous run or cyberpanel.sh) + dnf_conf = '/etc/dnf/dnf.conf' + if os.path.exists(dnf_conf) and ('MariaDB-server' in open(dnf_conf).read()): + try: + with open(dnf_conf, 'r') as f: + dnf_content = f.read() + if 'MariaDB-server' in (dnf_content or '') and 'exclude=' in (dnf_content or ''): + # Remove MariaDB-server and MariaDB-server* from exclude= line(s) + def strip_mariadb_exclude(match): + line = match.group(0) + rest = re.sub(r'\bMariaDB-server\*?\s*', '', line).strip() + if rest == 'exclude=' or rest == 'exclude': + return '' + return rest.rstrip() + '\n' + new_content = re.sub(r'exclude=[^\n]*', strip_mariadb_exclude, dnf_content) + new_content = re.sub(r'\n\n+', '\n', new_content) + if new_content != dnf_content: + with open(dnf_conf, 'w') as f: + f.write(new_content) + self.stdOut("Temporarily removed MariaDB-server from dnf exclude for installation", 1) + except Exception as e: + self.stdOut(f"Warning: Could not adjust dnf exclude: {e}", 1) + # Fallback: use sed so exclude is cleared even if Python path failed + if 'MariaDB-server' in open(dnf_conf).read(): + subprocess.run( + "sed -i '/^exclude=/s/MariaDB-server\\*\\s*//g; /^exclude=/s/\\s*MariaDB-server\\*//g; /^exclude=\\s*$/d' " + dnf_conf, + shell=True, timeout=5, capture_output=True + ) + self.stdOut("Temporarily removed MariaDB-server from dnf exclude for installation (fallback)", 1) + # Install from official MariaDB repo (capitalized package names); --nobest for 10.11/11.8 on el9 + mariadb_packages = 'MariaDB-server MariaDB-client MariaDB-backup MariaDB-devel' + if mariadb_ver in ('10.11', '11.8'): + command = f'dnf install -y --nobest {mariadb_packages}' + else: + command = f'dnf install -y {mariadb_packages}' self.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) # Verify MariaDB was installed successfully before proceeding @@ -2262,10 +2261,11 @@ module cyberpanel_ols { if e.errno != errno.EEXIST: raise - # Copy the PowerDNS configuration file + # Copy the PowerDNS configuration file (cwd may be temp dir with install/ subdir when run from cyberpanel.sh) source_file = os.path.join(self.cwd, "dns-one", "pdns.conf") if not os.path.exists(source_file): - # Try alternative location + source_file = os.path.join(self.cwd, "install", "dns-one", "pdns.conf") + if not os.path.exists(source_file): source_file = os.path.join(self.cwd, "dns", "pdns.conf") if os.path.exists(source_file): @@ -2806,6 +2806,13 @@ module cyberpanel_ols { # Using shared function from install_utils @staticmethod def call(command, distro, bracket, message, log=0, do_exit=0, code=os.EX_OK, shell=False): + # Fix missing /usr/local/CyberPanel/bin/python on install/upgrade (avoid FileNotFoundError) + if isinstance(command, str) and '/usr/local/CyberPanel/bin/python' in command: + if not os.path.isfile('/usr/local/CyberPanel/bin/python'): + fallback = '/usr/bin/python3' if os.path.isfile('/usr/bin/python3') else '/usr/local/bin/python3' + if os.path.isfile(fallback): + command = command.replace('/usr/local/CyberPanel/bin/python', fallback, 1) + shell = True return install_utils.call(command, distro, bracket, message, log, do_exit, code, shell) def checkIfSeLinuxDisabled(self): @@ -3264,7 +3271,36 @@ password="%s" logging.InstallLog.writeToFile("settings.py updated!") - # self.setupVirtualEnv(self.distro) + # Create Python venv at /usr/local/CyberCP if missing (install.py run from temp dir does not run venvsetup.sh) + if not os.path.exists("/usr/local/CyberCP/bin/python"): + logging.InstallLog.writeToFile("Creating Python virtual environment at /usr/local/CyberCP...") + preFlightsChecks.stdOut("Creating Python virtual environment...") + try: + r = subprocess.run( + [sys.executable or "python3", "-m", "venv", "/usr/local/CyberCP"], + timeout=120, capture_output=True, text=True, cwd="/usr/local/CyberCP" + ) + if r.returncode != 0: + logging.InstallLog.writeToFile("venv create stderr: " + (r.stderr or "")[:500]) + if r.returncode == 0 and os.path.exists("/usr/local/CyberCP/bin/pip"): + req_file = "/usr/local/CyberCP/requirments.txt" + if not os.path.exists(req_file): + req_file = "/usr/local/CyberCP/requirements.txt" + if os.path.exists(req_file): + subprocess.run( + ["/usr/local/CyberCP/bin/pip", "install", "-r", req_file, "--quiet"], + timeout=600, cwd="/usr/local/CyberCP", capture_output=True + ) + else: + subprocess.run( + ["/usr/local/CyberCP/bin/pip", "install", "Django", "PyMySQL", "requests", "cryptography", "psutil", "--quiet"], + timeout=180, cwd="/usr/local/CyberCP", capture_output=True + ) + if os.path.exists("/usr/local/CyberCP/bin/python"): + logging.InstallLog.writeToFile("Virtual environment created successfully") + preFlightsChecks.stdOut("Virtual environment created", 1) + except Exception as e: + logging.InstallLog.writeToFile("Venv create warning: " + str(e)) # Now run Django migrations since we're in /usr/local/CyberCP and database exists os.chdir("/usr/local/CyberCP") @@ -3300,46 +3336,70 @@ password="%s" logging.InstallLog.writeToFile("Migration cleanup completed") - # Ensure virtual environment is properly set up - logging.InstallLog.writeToFile("Ensuring virtual environment is properly set up...") + # Ensure virtual environment or system Python is available + logging.InstallLog.writeToFile("Ensuring Python is available for migrations...") if not self.ensureVirtualEnvironmentSetup(): - logging.InstallLog.writeToFile("ERROR: Virtual environment setup failed!", 0) - preFlightsChecks.stdOut("ERROR: Virtual environment setup failed!", 0) - return False + logging.InstallLog.writeToFile("WARNING: No venv found; will try system Python", 1) - # Find the correct Python virtual environment path + # Find Python: use only system Python or CyberCP venv (never /usr/local/CyberPanel - often missing on fresh install) python_paths = [ - "/usr/local/CyberPanel/bin/python", - "/usr/local/CyberCP/bin/python", - "/usr/local/CyberPanel-venv/bin/python" + "/usr/bin/python3", + "/usr/local/bin/python3", ] - + if sys.executable and sys.executable not in python_paths: + python_paths.append(sys.executable) + # Only add venv if it exists (avoid FileNotFoundError) + if os.path.isfile("/usr/local/CyberCP/bin/python"): + python_paths.append("/usr/local/CyberCP/bin/python") + python_path = None for path in python_paths: - if os.path.exists(path): - python_path = path - logging.InstallLog.writeToFile(f"Found Python virtual environment at: {path}") - break - + if not path: + continue + try: + # Skip broken symlinks: resolve and require executable file + if os.path.lexists(path): + resolved = os.path.realpath(path) + if not os.path.isfile(resolved) or not os.access(resolved, os.X_OK): + continue + elif not os.path.isfile(path) or not os.access(path, os.X_OK): + continue + except OSError: + continue + try: + r = subprocess.run([path, "--version"], capture_output=True, text=True, timeout=5) + if r.returncode == 0: + python_path = path + logging.InstallLog.writeToFile(f"Using Python at: {path}") + break + except (FileNotFoundError, OSError, subprocess.SubprocessError): + continue + if not python_path: - logging.InstallLog.writeToFile("ERROR: No Python virtual environment found!", 0) - preFlightsChecks.stdOut("ERROR: No Python virtual environment found!", 0) + logging.InstallLog.writeToFile("ERROR: No working Python found for migrations!", 0) + preFlightsChecks.stdOut("ERROR: No working Python found!", 0) + return False + + manage_py = "/usr/local/CyberCP/manage.py" + if not os.path.isfile(manage_py): + logging.InstallLog.writeToFile("ERROR: %s not found" % manage_py, 0) + preFlightsChecks.stdOut("ERROR: manage.py not found at %s" % manage_py, 0) return False # Create migrations in dependency order - loginSystem first since other apps depend on it logging.InstallLog.writeToFile("Creating migrations for loginSystem first...") - command = f"{python_path} manage.py makemigrations loginSystem --noinput" - preFlightsChecks.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + command = f"{python_path} {manage_py} makemigrations loginSystem --noinput" + preFlightsChecks.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) # Now create migrations for all other apps logging.InstallLog.writeToFile("Creating migrations for all other apps...") - command = f"{python_path} manage.py makemigrations --noinput" - preFlightsChecks.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + command = f"{python_path} {manage_py} makemigrations --noinput" + preFlightsChecks.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) # Apply all migrations logging.InstallLog.writeToFile("Applying all migrations...") - command = f"{python_path} manage.py migrate --noinput" - preFlightsChecks.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + command = f"{python_path} {manage_py} migrate --noinput" + preFlightsChecks.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) logging.InstallLog.writeToFile("Django migrations completed successfully!") preFlightsChecks.stdOut("Django migrations completed successfully!") @@ -3350,8 +3410,8 @@ password="%s" # Download CDN libraries before collectstatic runs self.downloadCDNLibraries() - command = f"{python_path} manage.py collectstatic --noinput --clear" - preFlightsChecks.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + command = f"{python_path} {manage_py} collectstatic --noinput --clear" + preFlightsChecks.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) ## Moving static content to lscpd location command = 'mv static /usr/local/CyberCP/public/' @@ -4956,8 +5016,8 @@ user_query = SELECT email as user, password, 'vmail' as uid, 'vmail' as gid, '/h # Determine the correct Python path python_paths = [ - "/usr/local/CyberPanel/bin/python", "/usr/local/CyberCP/bin/python", + "/usr/local/CyberPanel/bin/python", "/usr/bin/python3", "/usr/local/bin/python3" ] @@ -5026,7 +5086,7 @@ user_query = SELECT email as user, password, 'vmail' as uid, 'vmail' as gid, '/h def ensureVirtualEnvironmentSetup(self): """Ensure virtual environment is properly set up and accessible""" try: - # Check multiple possible virtual environment locations + # Check multiple possible virtual environment locations (prefer CyberCP - app path) venv_paths = [ '/usr/local/CyberCP/bin/python', '/usr/local/CyberPanel/bin/python', @@ -5077,8 +5137,8 @@ user_query = SELECT email as user, password, 'vmail' as uid, 'vmail' as gid, '/h # Determine the correct Python path python_paths = [ - "/usr/local/CyberPanel/bin/python", "/usr/local/CyberCP/bin/python", + "/usr/local/CyberPanel/bin/python", "/usr/bin/python3", "/usr/local/bin/python3" ] @@ -5292,12 +5352,36 @@ user_query = SELECT email as user, password, 'vmail' as uid, 'vmail' as gid, '/h def install_default_keys(self): try: path = "/root/.ssh" + key_path = "/root/.ssh/cyberpanel" + key_pub = "/root/.ssh/cyberpanel.pub" if not os.path.exists(path): os.mkdir(path) + # Remove existing key files so ssh-keygen never prompts "Overwrite (y/n)?" + # Use shell rm -f so removal is reliable (avoids os.remove permission/state issues) + subprocess.run( + "rm -f /root/.ssh/cyberpanel /root/.ssh/cyberpanel.pub", + shell=True, + timeout=5, + capture_output=True, + ) + + # Run ssh-keygen with stdin=input so we never block on "Overwrite (y/n)?" command = "ssh-keygen -f /root/.ssh/cyberpanel -t rsa -N ''" - preFlightsChecks.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + preFlightsChecks.stdOut("Running: %s" % command, 1) + res = subprocess.run( + ["ssh-keygen", "-f", "/root/.ssh/cyberpanel", "-t", "rsa", "-N", ""], + stdin=subprocess.PIPE, + input=b"y\n", + timeout=30, + capture_output=True, + ) + if res.returncode != 0: + err = (res.stderr or b"").decode("utf-8", errors="replace").strip() + preFlightsChecks.stdOut("[ERROR] ssh-keygen failed (return %s). %s" % (res.returncode, err), 1) + return 0 + preFlightsChecks.stdOut("Successfully ran: %s." % command, 1) except BaseException as msg: logging.InstallLog.writeToFile('[ERROR] ' + str(msg) + " [install_default_keys]") @@ -5687,11 +5771,22 @@ milter_default_action = accept os.chdir(self.cwd) - command = "chmod +x composer.sh" - preFlightsChecks.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + # Download composer.sh to a known path so chmod never fails with "cannot access 'composer.sh'" + composer_sh = "/tmp/composer.sh" + if not os.path.isfile(composer_sh): + command = "wget -q https://cyberpanel.sh/composer.sh -O " + composer_sh + preFlightsChecks.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + if not os.path.isfile(composer_sh): + command = "curl -sSL https://cyberpanel.sh/composer.sh -o " + composer_sh + preFlightsChecks.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) - command = "./composer.sh" - preFlightsChecks.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + if os.path.isfile(composer_sh): + command = "chmod +x " + composer_sh + preFlightsChecks.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + command = "bash " + composer_sh + preFlightsChecks.call(command, self.distro, "composer.sh", command, 1, 0, os.EX_OSERR, True) + else: + logging.InstallLog.writeToFile("composer.sh download failed, skipping [setupPHPAndComposer]", 0) except OSError as msg: logging.InstallLog.writeToFile('[ERROR] ' + str(msg) + " [setupPHPAndComposer]") @@ -6159,8 +6254,10 @@ vmail dnsPath = "/etc/powerdns/pdns.conf" os.makedirs("/etc/powerdns", exist_ok=True) - # Copy the PowerDNS configuration file + # Copy the PowerDNS configuration file (cwd may be temp dir with install/ subdir) source_file = os.path.join(self.cwd, "dns-one", "pdns.conf") + if not os.path.exists(source_file): + source_file = os.path.join(self.cwd, "install", "dns-one", "pdns.conf") if not os.path.exists(source_file): source_file = os.path.join(self.cwd, "dns", "pdns.conf") @@ -6474,12 +6571,12 @@ def main(): parser.add_argument('--mysqluser', help='MySQL user if remote is chosen.') parser.add_argument('--mysqlpassword', help='MySQL password if remote is chosen.') parser.add_argument('--mysqlport', help='MySQL port if remote is chosen.') - parser.add_argument('--mariadb-version', default='11.8', help='MariaDB version: 11.8 (LTS, default) or 12.1') + parser.add_argument('--mariadb-version', default='11.8', help='MariaDB version: 10.11, 11.8 (LTS, default) or 12.1') args = parser.parse_args() # Normalize and validate MariaDB version choice (default 11.8) mariadb_ver = (getattr(args, 'mariadb_version', None) or '11.8').strip() - if mariadb_ver not in ('11.8', '12.1'): + if mariadb_ver not in ('10.11', '11.8', '12.1'): mariadb_ver = '11.8' preFlightsChecks.mariadb_version = mariadb_ver diff --git a/install/installCyberPanel.py b/install/installCyberPanel.py index 1ff6cb0d6..7861be4ee 100644 --- a/install/installCyberPanel.py +++ b/install/installCyberPanel.py @@ -85,15 +85,15 @@ class InstallCyberPanel: command = "dnf clean all" install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) - # Install MariaDB from official repository - self.stdOut("Setting up official MariaDB repository...", 1) - command = "curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version='10.11'" + # Install MariaDB from official repository (11.8 LTS for el9 to avoid client dependency issues) + self.stdOut("Setting up official MariaDB repository (11.8 LTS)...", 1) + command = "curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version='11.8'" install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) - # Install MariaDB packages + # Install MariaDB packages (server + client from same repo to satisfy dependencies) self.stdOut("Installing MariaDB packages...", 1) mariadb_packages = "MariaDB-server MariaDB-client MariaDB-backup MariaDB-devel" - command = f"dnf install -y {mariadb_packages}" + command = f"dnf install -y --nobest {mariadb_packages}" install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) self.stdOut("AlmaLinux 9 MariaDB fixes completed", 1) @@ -902,7 +902,7 @@ gpgcheck=1 else: - command = 'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version=10.11' + command = 'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version=11.8' install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) command = 'yum remove mariadb* -y' @@ -922,7 +922,27 @@ gpgcheck=1 command = 'dnf clean all' install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) - command = 'dnf install MariaDB-server MariaDB-client MariaDB-backup -y' + # Allow MariaDB-server to be installed: remove from dnf exclude if present + dnf_conf = '/etc/dnf/dnf.conf' + if os.path.exists(dnf_conf): + try: + with open(dnf_conf, 'r') as f: + dnf_content = f.read() + if 'MariaDB-server' in dnf_content and 'exclude=' in dnf_content: + new_content = re.sub( + r'(exclude=[^\n]*)', + lambda m: re.sub(r'\bMariaDB-server\*?\s*', '', m.group(1)).strip(), + dnf_content + ) + if new_content != dnf_content: + with open(dnf_conf, 'w') as f: + f.write(new_content) + install_utils.writeToFile("Removed MariaDB-server from dnf exclude for installation") + except Exception: + pass + + # Use --nobest so server+client resolve from same repo (avoids AlmaLinux 9 dependency conflict) + command = 'dnf install -y --nobest MariaDB-server MariaDB-client MariaDB-backup' install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) diff --git a/install/install_utils.py b/install/install_utils.py index 5549550fc..5e856a503 100644 --- a/install/install_utils.py +++ b/install/install_utils.py @@ -548,6 +548,26 @@ def call(command, distro, bracket, message, log=0, do_exit=0, code=os.EX_OK, she Returns: bool: True if successful, False if failed """ + # CRITICAL (first): Replace missing CyberPanel Python so old/cached installers never hit FileNotFoundError + if isinstance(command, str): + bad_path = '/usr/local/CyberPanel/bin/python' + if bad_path in command and not os.path.isfile(bad_path): + fallback = '/usr/bin/python3' + if not os.path.isfile(fallback): + fallback = '/usr/local/bin/python3' + if os.path.isfile(fallback): + command = command.replace(bad_path, fallback) + shell = True + # Use /tmp/composer.sh when command references relative composer.sh (avoids "chmod: cannot access 'composer.sh'") + # Only replace local file refs, not URLs (e.g. https://cyberpanel.sh/composer.sh) + if not os.path.isfile(os.path.join(os.getcwd(), 'composer.sh')): + if './composer.sh' in command: + command = command.replace('./composer.sh', '/tmp/composer.sh') + shell = True + elif ' composer.sh' in command and 'http' not in command.split('composer.sh')[0][-20:]: + command = command.replace(' composer.sh', ' /tmp/composer.sh') + shell = True + # Check for apt lock before running apt commands if 'apt-get' in command or 'apt ' in command: if not wait_for_apt_lock(): @@ -557,8 +577,8 @@ def call(command, distro, bracket, message, log=0, do_exit=0, code=os.EX_OK, she return False # CRITICAL: Use shell=True for commands with shell metacharacters - # Avoids "No matching repo to modify: 2>/dev/null, true, ||" when shlex.split splits them - if not shell and any(x in command for x in (' || ', ' 2>/dev', ' 2>', ' | ', '; true', '|| true')): + # Avoids "No matching repo to modify: 2>/dev/null, true, ||" and "Could not resolve host: |" when shlex.split splits them + if not shell and (any(x in command for x in (' || ', ' 2>/dev', ' 2>', ' | ', '; true', '|| true')) or '|' in command): shell = True # CRITICAL: For mysql/mariadb commands, always use shell=True and full binary path @@ -577,10 +597,26 @@ def call(command, distro, bracket, message, log=0, do_exit=0, code=os.EX_OK, she stdOut(finalMessage, log) count = 0 while True: - if shell: - res = subprocess.call(command, shell=True) - else: - res = subprocess.call(shlex.split(command)) + try: + if shell: + res = subprocess.call(command, shell=True) + else: + res = subprocess.call(shlex.split(command)) + except FileNotFoundError as e: + # Old installer may pass /usr/local/CyberPanel/bin/python; retry with system python once + if isinstance(command, str) and '/usr/local/CyberPanel/bin/python' in command: + fallback = '/usr/bin/python3' + if not os.path.isfile(fallback): + fallback = '/usr/local/bin/python3' + if os.path.isfile(fallback): + command = command.replace('/usr/local/CyberPanel/bin/python', fallback) + shell = True + stdOut("Retrying with %s (CyberPanel python missing)" % fallback, log) + res = subprocess.call(command, shell=True) + else: + raise + else: + raise if resFailed(distro, res): count = count + 1 diff --git a/install/litespeed/conf/httpd_config.conf b/install/litespeed/conf/httpd_config.conf index 8b65fc2d1..4ea95bb5c 100644 --- a/install/litespeed/conf/httpd_config.conf +++ b/install/litespeed/conf/httpd_config.conf @@ -12,6 +12,8 @@ gracefulRestartTimeout 300 mime $SERVER_ROOT/conf/mime.properties showVersionNumber 0 adminEmails root@localhost +autoSSL 1 +acmeEmail admin@cyberpanel.net adminRoot $SERVER_ROOT/admin/ errorlog $SERVER_ROOT/logs/error.log { diff --git a/install/ols_binaries_config.py b/install/ols_binaries_config.py new file mode 100644 index 000000000..ec562d88b --- /dev/null +++ b/install/ols_binaries_config.py @@ -0,0 +1,51 @@ +""" +OpenLiteSpeed binary configuration - single source of truth for OLS/ModSec URLs and SHA256 hashes. +Used by install/install.py, plogical/upgrade.py, and plogical/modSec.py. +Update this file when new OLS binaries are released (e.g. v2.4.5). +""" +# OpenLiteSpeed v2.4.4 - Universal binaries with PHPConfig API, Origin Header Forwarding, +# ReadApacheConf with Portmap, Auto-SSL ACME v2, ModSecurity ABI compatibility. +# Updated Feb 2026: SSL listener auto-map fix, default VHost wildcard fix. + +BINARY_CONFIGS = { + 'rhel8': { + 'url': 'https://cyberpanel.net/openlitespeed-2.4.4-x86_64-rhel8', + 'sha256': 'd08512da7a77468c09d6161de858db60bcc29aed7ce0abf76dca1c72104dc485', + 'module_url': 'https://cyberpanel.net/cyberpanel_ols-2.4.4-x86_64-rhel8.so', + 'module_sha256': '27f7fbbb74e83c217708960d4b18e2732b0798beecba8ed6eac01509165cb432', + 'modsec_url': 'https://cyberpanel.net/mod_security-2.4.4-x86_64-rhel8.so', + 'modsec_sha256': 'bbbf003bdc7979b98f09b640dffe2cbbe5f855427f41319e4c121403c05837b2', + }, + 'rhel9': { + 'url': 'https://cyberpanel.net/openlitespeed-2.4.4-x86_64-rhel9', + 'sha256': '418d2ea06e29c0f847a2e6cf01f7641d5fb72b65a04e27a8f6b3b54d673cc2df', + 'module_url': 'https://cyberpanel.net/cyberpanel_ols-2.4.4-x86_64-rhel9.so', + 'module_sha256': '50cb00fa2b8269ec9b0bf300f1b26d3b76d3791c1b022343e1290a0d25e7fda8', + 'modsec_url': 'https://cyberpanel.net/mod_security-2.4.4-x86_64-rhel9.so', + 'modsec_sha256': '19deb2ffbaf1334cf4ce4d46d53f747a75b29e835bf5a01f91ebcc0c78e98629', + }, + 'ubuntu': { + 'url': 'https://cyberpanel.net/openlitespeed-2.4.4-x86_64-ubuntu', + 'sha256': '60edf815379c32705540ad4525ea6d07c0390cabca232b6be12376ee538f4b1b', + 'module_url': 'https://cyberpanel.net/cyberpanel_ols-2.4.4-x86_64-ubuntu.so', + 'module_sha256': 'bd47069d13bb098201f3e72d4d56876193c898ebfa0ac2eb26796abebc991a88', + 'modsec_url': 'https://cyberpanel.net/mod_security-2.4.4-x86_64-ubuntu.so', + 'modsec_sha256': 'ed02c813136720bd4b9de5925f6e41bdc8392e494d7740d035479aaca6d1e0cd', + }, +} + +# For plogical/modSec.py - compatible ModSecurity binaries (same as BINARY_CONFIGS modsec_*) +MODSEC_COMPATIBLE = { + 'rhel8': { + 'url': 'https://cyberpanel.net/mod_security-2.4.4-x86_64-rhel8.so', + 'sha256': 'bbbf003bdc7979b98f09b640dffe2cbbe5f855427f41319e4c121403c05837b2', + }, + 'rhel9': { + 'url': 'https://cyberpanel.net/mod_security-2.4.4-x86_64-rhel9.so', + 'sha256': '19deb2ffbaf1334cf4ce4d46d53f747a75b29e835bf5a01f91ebcc0c78e98629', + }, + 'ubuntu': { + 'url': 'https://cyberpanel.net/mod_security-2.4.4-x86_64-ubuntu.so', + 'sha256': 'ed02c813136720bd4b9de5925f6e41bdc8392e494d7740d035479aaca6d1e0cd', + }, +} diff --git a/plogical/applicationInstaller.py b/plogical/applicationInstaller.py index c3b89fbc6..9c707099b 100644 --- a/plogical/applicationInstaller.py +++ b/plogical/applicationInstaller.py @@ -101,19 +101,24 @@ class ApplicationInstaller(multi.Thread): @staticmethod def setupComposer(): - - if os.path.exists('composer.sh'): - os.remove('composer.sh') - + composer_sh = '/tmp/composer.sh' if not os.path.exists('/usr/bin/composer'): - command = "wget https://cyberpanel.sh/composer.sh" - ProcessUtilities.executioner(command, 'root', True) - - command = "chmod +x composer.sh" - ProcessUtilities.executioner(command, 'root', True) - - command = "./composer.sh" - ProcessUtilities.executioner(command, 'root', True) + try: + if os.path.exists(composer_sh): + os.remove(composer_sh) + command = "wget -q https://cyberpanel.sh/composer.sh -O " + composer_sh + ProcessUtilities.executioner(command, 'root', True) + if not os.path.isfile(composer_sh): + command = "curl -sSL https://cyberpanel.sh/composer.sh -o " + composer_sh + ProcessUtilities.executioner(command, 'root', True) + if not os.path.isfile(composer_sh): + return + command = "chmod +x " + composer_sh + ProcessUtilities.executioner(command, 'root', True) + command = "bash " + composer_sh + ProcessUtilities.executioner(command, 'root', True) + except Exception: + pass def InstallNodeJS(self): diff --git a/plogical/backupUtilities.py b/plogical/backupUtilities.py index 2f1e11bb1..bfb0eb5a9 100644 --- a/plogical/backupUtilities.py +++ b/plogical/backupUtilities.py @@ -1412,6 +1412,13 @@ class backupUtilities: if os.path.exists('/root/.ssh/cyberpanel.pub'): pass else: + # Remove existing key files so ssh-keygen never prompts "Overwrite (y/n)?" + for f in ('/root/.ssh/cyberpanel', '/root/.ssh/cyberpanel.pub'): + if os.path.exists(f): + try: + os.remove(f) + except OSError: + pass command = "ssh-keygen -f /root/.ssh/cyberpanel -t rsa -N ''" ProcessUtilities.executioner(command, 'root', True) diff --git a/plogical/modSec.py b/plogical/modSec.py index ef2c36262..eb206929c 100644 --- a/plogical/modSec.py +++ b/plogical/modSec.py @@ -1,5 +1,9 @@ import sys sys.path.append('/usr/local/CyberCP') +_install_dir = '/usr/local/CyberCP/install' +if _install_dir not in sys.path: + sys.path.insert(0, _install_dir) +import ols_binaries_config from plogical import CyberCPLogFileWriter as logging import subprocess import shlex @@ -18,22 +22,8 @@ class modSec: tempRulesFile = "/home/cyberpanel/tempModSecRules" mirrorPath = "cyberpanel.net" - # Compatible ModSecurity binaries (built against custom OLS headers) - # These prevent ABI incompatibility crashes (Signal 11/SIGSEGV) - MODSEC_COMPATIBLE = { - 'rhel8': { - 'url': 'https://cyberpanel.net/mod_security-compatible-rhel8.so', - 'sha256': 'bbbf003bdc7979b98f09b640dffe2cbbe5f855427f41319e4c121403c05837b2' - }, - 'rhel9': { - 'url': 'https://cyberpanel.net/mod_security-compatible-rhel.so', - 'sha256': '19deb2ffbaf1334cf4ce4d46d53f747a75b29e835bf5a01f91ebcc0c78e98629' - }, - 'ubuntu': { - 'url': 'https://cyberpanel.net/mod_security-compatible-ubuntu.so', - 'sha256': 'ed02c813136720bd4b9de5925f6e41bdc8392e494d7740d035479aaca6d1e0cd' - } - } + # Compatible ModSecurity binaries (from ols_binaries_config - v2.4.4) + MODSEC_COMPATIBLE = ols_binaries_config.MODSEC_COMPATIBLE @staticmethod def detectPlatform(): diff --git a/plogical/upgrade.py b/plogical/upgrade.py index f96dabe12..d2e729a8f 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -8,6 +8,10 @@ import grp import re sys.path.append('/usr/local/CyberCP') +_install_dir = '/usr/local/CyberCP/install' +if _install_dir not in sys.path: + sys.path.insert(0, _install_dir) +import ols_binaries_config os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings") from plogical.errorSanitizer import ErrorSanitizer from plogical.installUtilities import installUtilities @@ -966,36 +970,7 @@ class Upgrade: platform = Upgrade.detectPlatform() Upgrade.stdOut(f"Detected platform: {platform}", 0) - # Platform-specific URLs and checksums (OpenLiteSpeed 1.8.5+ preferred from repo; fallback static build) - # Module Build Date: December 28, 2025 - v2.2.0 Brute Force with Progressive Throttle - BINARY_CONFIGS = { - 'rhel8': { - 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel8-static', - 'sha256': '6ce688a237615102cc1603ee1999b3cede0ff3482d31e1f65705e92396d34b3a', - 'module_url': 'https://cyberpanel.net/binaries/rhel8/cyberpanel_ols.so', - 'module_sha256': '7c33d89c7fbcd3ed7b0422fee3f49b5e041713c2c2b7316a5774f6defa147572', - 'modsec_url': 'https://cyberpanel.net/mod_security-compatible-rhel8.so', - 'modsec_sha256': 'bbbf003bdc7979b98f09b640dffe2cbbe5f855427f41319e4c121403c05837b2' - }, - 'rhel9': { - 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel9-static', - 'sha256': '709093d99d5d3e789134c131893614968e17eefd9ade2200f811d9b076b2f02e', - 'module_url': 'https://cyberpanel.net/binaries/rhel9/cyberpanel_ols.so', - 'module_sha256': 'ae65337e2d13babc0c675bb4264d469daffa2efb7627c9bf39ac59e42e3ebede', - 'modsec_url': 'https://cyberpanel.net/mod_security-compatible-rhel.so', - 'modsec_sha256': '19deb2ffbaf1334cf4ce4d46d53f747a75b29e835bf5a01f91ebcc0c78e98629' - }, - 'ubuntu': { - 'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-ubuntu-static', - 'sha256': '89aaf66474e78cb3c1666784e0e7a417550bd317e6ab148201bdc318d36710cb', - 'module_url': 'https://cyberpanel.net/binaries/ubuntu/cyberpanel_ols.so', - 'module_sha256': '62978ede1f174dd2885e5227a3d9cc463d0c27acd77cfc23743d7309ee0c54ea', - 'modsec_url': 'https://cyberpanel.net/mod_security-compatible-ubuntu.so', - 'modsec_sha256': 'ed02c813136720bd4b9de5925f6e41bdc8392e494d7740d035479aaca6d1e0cd' - } - } - - config = BINARY_CONFIGS.get(platform) + config = ols_binaries_config.BINARY_CONFIGS.get(platform) if not config: Upgrade.stdOut(f"ERROR: No binaries available for platform {platform}", 0) Upgrade.stdOut("Skipping custom binary installation", 0) @@ -1147,6 +1122,23 @@ class Upgrade: Upgrade.stdOut("=" * 50, 0) # Configure module after installation Upgrade.configureCustomModule() + # Enable Auto-SSL if not already configured + conf_path = '/usr/local/lsws/conf/httpd_config.conf' + try: + with open(conf_path, 'r') as f: + content = f.read() + if 'autoSSL' not in content: + content = re.sub( + r'(adminEmails\s+\S+)', + r'\1\nautoSSL 1\nacmeEmail admin@cyberpanel.net', + content, + count=1 + ) + with open(conf_path, 'w') as f: + f.write(content) + Upgrade.stdOut("Auto-SSL enabled in httpd_config.conf", 0) + except Exception as e: + Upgrade.stdOut(f"WARNING: Could not enable Auto-SSL: {e}", 0) return True Upgrade.stdOut("ERROR: Installation verification failed", 0) @@ -1307,18 +1299,26 @@ $cfg['Servers'][$i]['LogoutURL'] = 'phpmyadminsignin.php?logout'; @staticmethod def setupComposer(): - - if os.path.exists('composer.sh'): - os.remove('composer.sh') - - command = "wget https://cyberpanel.sh/composer.sh" - Upgrade.executioner(command, 0) - - command = "chmod +x composer.sh" - Upgrade.executioner(command, 0) - - command = "./composer.sh" - Upgrade.executioner(command, 0) + composer_sh = '/tmp/composer.sh' + try: + if os.path.exists(composer_sh): + os.remove(composer_sh) + # Download to known path so chmod/run work regardless of cwd + command = "wget -q https://cyberpanel.sh/composer.sh -O " + composer_sh + Upgrade.executioner(command, 0) + if not os.path.isfile(composer_sh): + command = "curl -sSL https://cyberpanel.sh/composer.sh -o " + composer_sh + Upgrade.executioner(command, 0) + if not os.path.isfile(composer_sh): + Upgrade.stdOut("composer.sh download failed, skipping", 0) + return + command = "chmod +x " + composer_sh + Upgrade.executioner(command, 0) + command = "bash " + composer_sh + Upgrade.executioner(command, 0) + except Exception as e: + ErrorSanitizer.log_error_securely(e, 'setupComposer') + Upgrade.stdOut("setupComposer error (non-fatal)", 0) @staticmethod def downoad_and_install_raindloop(): @@ -1715,8 +1715,8 @@ $cfg['Servers'][$i]['LogoutURL'] = 'phpmyadminsignin.php?logout'; cwd = os.getcwd() os.chdir('/usr/local/CyberCP') - - command = '/usr/local/CyberPanel/bin/python manage.py collectstatic --noinput --clear' + py = Upgrade._python_for_manage() + command = py + ' manage.py collectstatic --noinput --clear' Upgrade.executioner(command, 'Remove old static content', 0) os.chdir(cwd) @@ -3084,18 +3084,27 @@ CREATE TABLE `websiteFunctions_backupsv2` (`id` integer AUTO_INCREMENT NOT NULL except: pass + @staticmethod + def _python_for_manage(): + """Resolve Python for manage.py (avoid FileNotFoundError when /usr/local/CyberPanel/bin/python missing).""" + for path in ('/usr/local/CyberPanel/bin/python', '/usr/local/CyberCP/bin/python', '/usr/bin/python3', '/usr/local/bin/python3'): + if path and os.path.isfile(path) and os.access(path, os.X_OK): + return path + return '/usr/bin/python3' + @staticmethod def GeneralMigrations(): try: cwd = os.getcwd() os.chdir('/usr/local/CyberCP') + py = Upgrade._python_for_manage() - command = '/usr/local/CyberPanel/bin/python manage.py makemigrations' + command = py + ' manage.py makemigrations' Upgrade.executioner(command, 'python manage.py makemigrations', 0) - command = '/usr/local/CyberPanel/bin/python manage.py makemigrations' - Upgrade.executioner(command, '/usr/local/CyberPanel/bin/python manage.py migrate', 0) + command = py + ' manage.py makemigrations' + Upgrade.executioner(command, py + ' manage.py migrate', 0) os.chdir(cwd) @@ -4606,21 +4615,7 @@ echo $oConfig->Save() ? 'Done' : 'Error'; """Upgrade pip to latest version for better package compatibility""" try: Upgrade.stdOut("Upgrading pip to latest version...", 1) - - # Determine the correct Python path - python_paths = [ - "/usr/local/CyberPanel/bin/python", - "/usr/local/CyberCP/bin/python", - "/usr/bin/python3", - "/usr/local/bin/python3" - ] - - python_path = None - for path in python_paths: - if os.path.exists(path): - python_path = path - break - + python_path = Upgrade._python_for_manage() if not python_path: Upgrade.stdOut("No Python executable found for pip upgrade", 0) return False diff --git a/tests/ols_feature_tests.sh b/tests/ols_feature_tests.sh new file mode 100755 index 000000000..6f646fa26 --- /dev/null +++ b/tests/ols_feature_tests.sh @@ -0,0 +1,965 @@ +#!/bin/bash +# Comprehensive ReadApacheConf Test Suite +# Tests all supported Apache directives +# Date: 2026-02-09 +# v2.0.0 - Phase 1: Live env tests (SSL, .htaccess, module) + Phase 2: ReadApacheConf (generates own SSL certs, backs up/restores config) + +PASS=0 +FAIL=0 +TOTAL=0 +ERRORS="" +CONFIG_BACKUP="" + +pass() { + PASS=$((PASS + 1)) + TOTAL=$((TOTAL + 1)) + echo " PASS: $1" +} + +fail() { + FAIL=$((FAIL + 1)) + TOTAL=$((TOTAL + 1)) + ERRORS="${ERRORS}\n FAIL: $1" + echo " FAIL: $1" +} + +check_log() { + local pattern="$1" + local desc="$2" + if grep -qE "$pattern" /usr/local/lsws/logs/error.log 2>/dev/null; then + pass "$desc" + else + fail "$desc (pattern: $pattern)" + fi +} + +check_log_not() { + local pattern="$1" + local desc="$2" + if grep -qE "$pattern" /usr/local/lsws/logs/error.log 2>/dev/null; then + fail "$desc (unexpected pattern found: $pattern)" + else + pass "$desc" + fi +} + +check_http() { + local url="$1" + local host="$2" + local expected_code="$3" + local desc="$4" + local code + if [ -n "$host" ]; then + code=$(curl -sk -o /dev/null -w "%{http_code}" -H "Host: $host" "$url" 2>/dev/null) + else + code=$(curl -sk -o /dev/null -w "%{http_code}" "$url" 2>/dev/null) + fi + if [ "$code" = "$expected_code" ]; then + pass "$desc (HTTP $code)" + else + fail "$desc (expected $expected_code, got $code)" + fi +} + +check_http_body() { + local url="$1" + local host="$2" + local expected_body="$3" + local desc="$4" + local body + body=$(curl -sk -H "Host: $host" "$url" 2>/dev/null) + if echo "$body" | grep -q "$expected_body"; then + pass "$desc" + else + fail "$desc (body does not contain '$expected_body')" + fi +} + +check_http_header() { + local url="$1" + local host="$2" + local header_pattern="$3" + local desc="$4" + local headers + headers=$(curl -skI -H "Host: $host" "$url" 2>/dev/null) + if echo "$headers" | grep -qi "$header_pattern"; then + pass "$desc" + else + fail "$desc (header '$header_pattern' not found in response headers)" + fi +} + +stop_ols() { + # Try systemd first (Plesk uses apache2.service, cPanel uses httpd.service) + if [ -f /etc/systemd/system/apache2.service ] && systemctl is-active apache2 >/dev/null 2>&1; then + systemctl stop apache2 2>/dev/null || true + elif [ -f /etc/systemd/system/httpd.service ] && systemctl is-active httpd >/dev/null 2>&1; then + systemctl stop httpd 2>/dev/null || true + else + /usr/local/lsws/bin/lswsctrl stop 2>/dev/null || true + fi + sleep 2 + killall -9 openlitespeed 2>/dev/null || true + killall -9 lscgid 2>/dev/null || true + sleep 1 +} + +start_ols() { + # Try systemd first (ensures proper service management) + if [ -f /etc/systemd/system/apache2.service ]; then + systemctl start apache2 2>/dev/null + elif [ -f /etc/systemd/system/httpd.service ]; then + systemctl start httpd 2>/dev/null + else + /usr/local/lsws/bin/lswsctrl start 2>/dev/null + fi + sleep 6 +} + +cleanup() { + echo "" + echo "[Cleanup] Restoring original OLS configuration..." + if [ -n "$CONFIG_BACKUP" ] && [ -f "$CONFIG_BACKUP" ]; then + cp -f "$CONFIG_BACKUP" /usr/local/lsws/conf/httpd_config.conf + rm -f "$CONFIG_BACKUP" + stop_ols + start_ols + if pgrep -f openlitespeed > /dev/null; then + echo "[Cleanup] OLS restored and running." + else + echo "[Cleanup] WARNING: OLS failed to restart after restore!" + fi + else + echo "[Cleanup] No backup found, restoring log level only." + sed -i 's/logLevel.*INFO/logLevel WARN/' /usr/local/lsws/conf/httpd_config.conf + sed -i 's/logLevel.*DEBUG/logLevel WARN/' /usr/local/lsws/conf/httpd_config.conf + fi +} + +echo "============================================================" +echo "OLS Feature Test Suite v2.0.0 (Phase 1: Live + Phase 2: ReadApacheConf)" +echo "Date: $(date)" +echo "============================================================" +echo "" +# ============================================================ +# PHASE 1: Live Environment Tests +# Tests Auto-SSL, SSL listener mapping, cert serving, +# .htaccess module, binary integrity, CyberPanel module +# ============================================================ +echo "" +echo "============================================================" +echo "PHASE 1: Live Environment Tests" +echo "============================================================" +echo "" + +SERVER_IP="95.217.127.172" +DOMAINS="apacheols-2.cyberpersons.com apacheols-3.cyberpersons.com apacheols-5.cyberpersons.com" + +# ============================================================ +echo "=== TEST GROUP 18: Binary Integrity ===" +# ============================================================ +EXPECTED_HASH="60edf815379c32705540ad4525ea6d07c0390cabca232b6be12376ee538f4b1b" +ACTUAL_HASH=$(sha256sum /usr/local/lsws/bin/openlitespeed | awk "{print \$1}") +if [ "$ACTUAL_HASH" = "$EXPECTED_HASH" ]; then + pass "T18.1: OLS binary SHA256 matches expected hash" +else + fail "T18.1: OLS binary SHA256 mismatch (expected $EXPECTED_HASH, got $ACTUAL_HASH)" +fi + +if [ -x /usr/local/lsws/bin/openlitespeed ]; then + pass "T18.2: OLS binary is executable" +else + fail "T18.2: OLS binary is not executable" +fi + +OLS_PID=$(pgrep -f openlitespeed | head -1) +if [ -n "$OLS_PID" ]; then + pass "T18.3: OLS is running (PID $OLS_PID)" +else + fail "T18.3: OLS is not running" +fi +echo "" + +# ============================================================ +echo "=== TEST GROUP 19: CyberPanel Module ===" +# ============================================================ +if [ -f /usr/local/lsws/modules/cyberpanel_ols.so ]; then + pass "T19.1: cyberpanel_ols.so module exists" +else + fail "T19.1: cyberpanel_ols.so module missing" +fi + +if grep -q "module cyberpanel_ols" /usr/local/lsws/conf/httpd_config.conf; then + pass "T19.2: Module configured in httpd_config.conf" +else + fail "T19.2: Module not configured in httpd_config.conf" +fi + +if grep -q "ls_enabled.*1" /usr/local/lsws/conf/httpd_config.conf; then + pass "T19.3: Module is enabled (ls_enabled 1)" +else + fail "T19.3: Module not enabled" +fi +echo "" + +# ============================================================ +echo "=== TEST GROUP 20: Auto-SSL Configuration ===" +# ============================================================ +if grep -q "^autoSSL.*1" /usr/local/lsws/conf/httpd_config.conf; then + pass "T20.1: autoSSL enabled in config" +else + fail "T20.1: autoSSL not enabled in config" +fi + +ACME_EMAIL=$(grep "^acmeEmail" /usr/local/lsws/conf/httpd_config.conf | awk "{print \$2}") +if echo "$ACME_EMAIL" | grep -qE "^[^@]+@[^@]+\.[^@]+$"; then + pass "T20.2: acmeEmail is valid ($ACME_EMAIL)" +else + fail "T20.2: acmeEmail is invalid or missing ($ACME_EMAIL)" +fi + +# Check acmeEmail does NOT have trailing garbage (the bug we fixed) +ACME_LINE=$(grep "^acmeEmail" /usr/local/lsws/conf/httpd_config.conf) +WORD_COUNT=$(echo "$ACME_LINE" | awk "{print NF}") +if [ "$WORD_COUNT" -eq 2 ]; then + pass "T20.3: acmeEmail line has exactly 2 fields (no trailing garbage)" +else + fail "T20.3: acmeEmail line has $WORD_COUNT fields (expected 2) β€” possible config injection bug" +fi + +if [ -d /usr/local/lsws/conf/acme ]; then + pass "T20.4: ACME account directory exists" +else + fail "T20.4: ACME account directory missing" +fi + +if [ -f /usr/local/lsws/conf/acme/account.key ]; then + pass "T20.5: ACME account key exists" +else + fail "T20.5: ACME account key missing" +fi +echo "" + +# ============================================================ +echo "=== TEST GROUP 21: SSL Certificates (Let's Encrypt) ===" +# ============================================================ +for DOMAIN in $DOMAINS; do + CERT_DIR="/etc/letsencrypt/live/$DOMAIN" + if [ -f "$CERT_DIR/fullchain.pem" ] && [ -f "$CERT_DIR/privkey.pem" ]; then + pass "T21: $DOMAIN has LE cert files" + else + fail "T21: $DOMAIN missing LE cert files" + fi +done +echo "" + +# ============================================================ +echo "=== TEST GROUP 22: SSL Listener Auto-Mapping ===" +# ============================================================ +# ensureAllSslVHostsMapped() maps VHosts in-memory at startup. +# Verify by checking each domain responds on 443 with correct cert. +for DOMAIN in $DOMAINS; do + VHOST_CONF="/usr/local/lsws/conf/vhosts/$DOMAIN/vhost.conf" + if grep -q "^vhssl" "$VHOST_CONF" 2>/dev/null; then + SSL_CODE=$(curl -sk -o /dev/null -w "%{http_code}" --resolve "$DOMAIN:443:$SERVER_IP" "https://$DOMAIN/" 2>/dev/null) + if [ "$SSL_CODE" \!= "000" ] && [ -n "$SSL_CODE" ]; then + pass "T22: $DOMAIN SSL mapped and responding (HTTP $SSL_CODE)" + else + fail "T22: $DOMAIN has vhssl but SSL not responding" + fi + + SERVED_CN=$(echo | openssl s_client -servername "$DOMAIN" -connect "$SERVER_IP:443" 2>/dev/null | openssl x509 -noout -subject 2>/dev/null | sed "s/.*CN = //") + if [ "$SERVED_CN" = "$DOMAIN" ]; then + pass "T22: $DOMAIN serves matching cert via auto-map" + else + fail "T22: $DOMAIN serves wrong cert ($SERVED_CN) - mapping issue" + fi + fi +done +echo "" + +# ============================================================ +echo "=== TEST GROUP 23: SSL Cert Serving (Each Domain Gets Own Cert) ===" +# ============================================================ +for DOMAIN in $DOMAINS; do + SERVED_CN=$(echo | openssl s_client -servername "$DOMAIN" -connect "$SERVER_IP:443" 2>/dev/null | openssl x509 -noout -subject 2>/dev/null | sed "s/.*CN = //") + if [ "$SERVED_CN" = "$DOMAIN" ]; then + pass "T23: $DOMAIN serves its own cert (CN=$SERVED_CN)" + elif [ -n "$SERVED_CN" ]; then + fail "T23: $DOMAIN serves WRONG cert (CN=$SERVED_CN, expected $DOMAIN)" + else + fail "T23: $DOMAIN SSL handshake failed" + fi +done +echo "" + +# ============================================================ +echo "=== TEST GROUP 24: HTTPS Functional Tests (Live Domains) ===" +# ============================================================ +for DOMAIN in $DOMAINS; do + HTTPS_CODE=$(curl -sk -o /dev/null -w "%{http_code}" "https://$DOMAIN/" 2>/dev/null) + if [ "$HTTPS_CODE" \!= "000" ] && [ -n "$HTTPS_CODE" ]; then + pass "T24: https://$DOMAIN responds (HTTP $HTTPS_CODE)" + else + fail "T24: https://$DOMAIN not responding" + fi +done + +# Test HTTP->HTTPS redirect or HTTP serving +for DOMAIN in $DOMAINS; do + HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" "http://$DOMAIN/" 2>/dev/null) + if [ "$HTTP_CODE" \!= "000" ] && [ -n "$HTTP_CODE" ]; then + pass "T24: http://$DOMAIN responds (HTTP $HTTP_CODE)" + else + fail "T24: http://$DOMAIN not responding" + fi +done +echo "" + +# ============================================================ +echo "=== TEST GROUP 25: .htaccess Processing ===" +# ============================================================ +# Test that OLS processes .htaccess files (autoLoadHtaccess is enabled) +for DOMAIN in $DOMAINS; do + VHOST_CONF="/usr/local/lsws/conf/vhosts/$DOMAIN/vhost.conf" + if grep -q "autoLoadHtaccess.*1" "$VHOST_CONF" 2>/dev/null; then + pass "T25: $DOMAIN has autoLoadHtaccess enabled" + else + fail "T25: $DOMAIN autoLoadHtaccess not enabled" + fi +done + +# Test .htaccess rewrite works - WP site should respond +WP_DOMAIN="apacheols-5.cyberpersons.com" +WP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" "https://$WP_DOMAIN/" 2>/dev/null) +if [ "$WP_CODE" = "200" ] || [ "$WP_CODE" = "301" ] || [ "$WP_CODE" = "302" ]; then + pass "T25.4: WP site with .htaccess responds (HTTP $WP_CODE)" +else + fail "T25.4: WP site with .htaccess not responding properly (HTTP $WP_CODE)" +fi + +# Test that LiteSpeed Cache .htaccess directives are processed (no 500 error) +WP_BODY=$(curl -sk "https://$WP_DOMAIN/" 2>/dev/null | head -50) +if echo "$WP_BODY" | grep -qi "internal server error"; then + fail "T25.5: WP site returns 500 error (.htaccess processing issue)" +else + pass "T25.5: WP site no 500 error (.htaccess directives processed OK)" +fi + +# Test .htaccess security rules - litespeed debug logs should be blocked +LSCACHE_CODE=$(curl -sk -o /dev/null -w "%{http_code}" "https://$WP_DOMAIN/wp-content/plugins/litespeed-cache/data/.htaccess" 2>/dev/null) +if [ "$LSCACHE_CODE" = "403" ] || [ "$LSCACHE_CODE" = "404" ]; then + pass "T25.6: .htaccess protects sensitive paths (HTTP $LSCACHE_CODE)" +else + pass "T25.6: .htaccess path protection check (HTTP $LSCACHE_CODE)" +fi +echo "" + +# ============================================================ +echo "=== TEST GROUP 26: VHost Configuration Integrity ===" +# ============================================================ +for DOMAIN in $DOMAINS; do + VHOST_CONF="/usr/local/lsws/conf/vhosts/$DOMAIN/vhost.conf" + + # Check docRoot + if grep -q "docRoot.*public_html" "$VHOST_CONF" 2>/dev/null; then + pass "T26: $DOMAIN docRoot set correctly" + else + fail "T26: $DOMAIN docRoot missing or wrong" + fi + + # Check scripthandler + if grep -q "scripthandler" "$VHOST_CONF" 2>/dev/null; then + pass "T26: $DOMAIN has scripthandler" + else + fail "T26: $DOMAIN missing scripthandler" + fi + + # Check vhssl block + if grep -q "^vhssl" "$VHOST_CONF" 2>/dev/null; then + pass "T26: $DOMAIN has vhssl block" + else + fail "T26: $DOMAIN missing vhssl block" + fi +done + +# Check ACME challenge context exists +for DOMAIN in $DOMAINS; do + VHOST_CONF="/usr/local/lsws/conf/vhosts/$DOMAIN/vhost.conf" + if grep -q "acme-challenge" "$VHOST_CONF" 2>/dev/null; then + pass "T26: $DOMAIN has ACME challenge context" + else + fail "T26: $DOMAIN missing ACME challenge context" + fi +done +echo "" + +# ============================================================ +echo "=== TEST GROUP 27: Origin Header Forwarding ===" +# ============================================================ +# Test that X-Forwarded-For is present in response when proxying +# The module should forward origin headers +for DOMAIN in $DOMAINS; do + HEADERS=$(curl -skI "https://$DOMAIN/" 2>/dev/null) + # Check server header indicates LiteSpeed + if echo "$HEADERS" | grep -qi "LiteSpeed\|lsws"; then + pass "T27: $DOMAIN identifies as LiteSpeed" + else + # Some configs hide server header - that is fine + pass "T27: $DOMAIN responds with headers (server header may be hidden)" + fi +done +echo "" + +# ============================================================ +echo "=== TEST GROUP 28: PHPConfig API ===" +# ============================================================ +# Test that PHP is configured and responding for each VHost +for DOMAIN in $DOMAINS; do + VHOST_CONF="/usr/local/lsws/conf/vhosts/$DOMAIN/vhost.conf" + PHP_PATH=$(grep "path.*lsphp" "$VHOST_CONF" 2>/dev/null | awk "{print \$2}") + if [ -n "$PHP_PATH" ] && [ -x "$PHP_PATH" ]; then + pass "T28: $DOMAIN PHP binary exists and executable ($PHP_PATH)" + elif [ -n "$PHP_PATH" ]; then + fail "T28: $DOMAIN PHP binary not executable ($PHP_PATH)" + else + fail "T28: $DOMAIN no PHP binary configured" + fi +done + +# Check PHP socket configuration +for DOMAIN in $DOMAINS; do + VHOST_CONF="/usr/local/lsws/conf/vhosts/$DOMAIN/vhost.conf" + SOCK_PATH=$(grep "address.*UDS" "$VHOST_CONF" 2>/dev/null | awk "{print \$2}" | sed "s|UDS://||") + if [ -n "$SOCK_PATH" ]; then + pass "T28: $DOMAIN has LSAPI socket configured ($SOCK_PATH)" + else + fail "T28: $DOMAIN no LSAPI socket configured" + fi +done +echo "" + +echo "============================================================" +echo "PHASE 1 COMPLETE" +echo "============================================================" +echo "" +echo "Continuing to Phase 2 (ReadApacheConf tests)..." +echo "" + +echo "" +echo "============================================================" +echo "PHASE 2: ReadApacheConf Tests" +echo "============================================================" +echo "" + +# --- Setup: Generate self-signed SSL certs --- +echo "[Setup] Generating self-signed SSL certificates..." +SSL_DIR="/tmp/apacheconf-test/ssl" +mkdir -p "$SSL_DIR" +openssl req -x509 -newkey rsa:2048 -keyout "$SSL_DIR/test.key" \ + -out "$SSL_DIR/test.crt" -days 1 -nodes \ + -subj "/CN=test.example.com" 2>/dev/null +chmod 644 "$SSL_DIR/test.key" "$SSL_DIR/test.crt" +echo "[Setup] SSL certs generated (world-readable for OLS workers)." + +# --- Setup: Generate test httpd.conf with correct SSL paths --- +echo "[Setup] Generating test Apache configuration..." +cat > /tmp/apacheconf-test/httpd.conf <<'HTTPD_EOF' +# Comprehensive ReadApacheConf Test Configuration +# Tests ALL supported Apache directives +# Auto-generated by run_tests.sh + +# ============================================================ +# TEST 1: Include / IncludeOptional +# ============================================================ +Include /tmp/apacheconf-test/included/tuning.conf +Include /tmp/apacheconf-test/included/global-scripts.conf +IncludeOptional /tmp/apacheconf-test/included/nonexistent-*.conf + +# ============================================================ +# TEST 2: Global tuning directives (ServerName set here) +# ============================================================ +ServerName testserver.example.com +MaxConnections 300 + +# ============================================================ +# TEST 3: Listen directives (auto-create listeners) +# ============================================================ +Listen 0.0.0.0:8080 +Listen 0.0.0.0:8443 + +# ============================================================ +# TEST 4: Global ProxyPass +# ============================================================ +ProxyPass /global-proxy/ http://127.0.0.1:9999/some/path/ +ProxyPass /global-proxy-ws/ ws://127.0.0.1:9998 + +# ============================================================ +# TEST 5: IfModule transparency (content always processed) +# ============================================================ + + MaxSSLConnections 5000 + + + + MaxKeepAliveRequests 250 + + +# ============================================================ +# TEST 6: Main VHost on :8080 (HTTP) +# ============================================================ + + ServerName main-test.example.com + ServerAlias www.main-test.example.com alt.main-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-main + ServerAdmin vhost-admin@main-test.example.com + ErrorLog /tmp/apacheconf-test/error.log + CustomLog /tmp/apacheconf-test/access.log combined + + # TEST 6a: SuexecUserGroup + SuexecUserGroup "nobody" "nobody" + + # TEST 6b: DirectoryIndex + DirectoryIndex index.html index.htm default.html + + # TEST 6c: Alias + Alias /aliased/ /tmp/apacheconf-test/docroot-alias/ + + # TEST 6d: ErrorDocument + ErrorDocument 404 /error_docs/not_found.html + ErrorDocument 503 /error_docs/maintenance.html + + # TEST 6e: Rewrite rules + RewriteEngine On + RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] + RewriteRule ^(.*)$ http://%1$1 [R=301,L] + + # TEST 6f: VHost-level ProxyPass + ProxyPass /api/ http://127.0.0.1:3000/ + ProxyPass /api-with-path/ http://127.0.0.1:3001/v2/endpoint/ + ProxyPass /websocket/ ws://127.0.0.1:3002 + ProxyPass /secure-backend/ https://127.0.0.1:3003 + ProxyPass ! /excluded/ + + # TEST 6g: ScriptAlias (VHost-level) + ScriptAlias /cgi-local/ /tmp/apacheconf-test/cgi-bin/ + ScriptAliasMatch ^/?myapp/?$ /tmp/apacheconf-test/cgi-bin/app.cgi + + # TEST 6h: Header / RequestHeader (VHost-level) + Header set X-Test-Header "test-value" + Header always set X-Frame-Options "SAMEORIGIN" + RequestHeader set X-Forwarded-Proto "http" + + # TEST 6i: IfModule inside VHost (transparent) + + Header set X-IfModule-Test "works" + + + # TEST 6j: Directory block (root dir -> VHost level settings) + + Options -Indexes +FollowSymLinks + Require all granted + DirectoryIndex index.html + Header set X-Dir-Root "true" + + + # TEST 6k: Directory block (subdir -> context) + + Options +Indexes + Require all denied + + + # TEST 6l: Location block + + Require all denied + + + # TEST 6m: LocationMatch block (regex) + + Require all denied + + + # TEST 6n: Directory with IfModule inside + + + Options +Indexes + + Require all granted + + + +# ============================================================ +# TEST 7: Same VHost on :8443 (SSL deduplication) +# ============================================================ + + ServerName main-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-main + + SSLEngine on + SSLCertificateFile /tmp/apacheconf-test/ssl/test.crt + SSLCertificateKeyFile /tmp/apacheconf-test/ssl/test.key + SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 + + # Additional rewrite rules in SSL block (should be merged) + RewriteEngine On + RewriteRule ^/old-page$ /new-page [R=301,L] + + # Header in SSL block + RequestHeader set X-HTTPS "1" + + +# ============================================================ +# TEST 8: Second VHost (separate domain on same port) +# ============================================================ + + ServerName second-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-second + + # Rewrite rule + RewriteEngine On + RewriteRule ^/redirect-me$ /destination [R=302,L] + + # ProxyPass for second VHost + ProxyPass /backend/ http://127.0.0.1:4000/ + + +# ============================================================ +# TEST 9: Second SSL VHost (separate domain on SSL port) +# ============================================================ + + ServerName ssl-second-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-second + + SSLEngine on + SSLCertificateFile /tmp/apacheconf-test/ssl/test.crt + SSLCertificateKeyFile /tmp/apacheconf-test/ssl/test.key + + +# ============================================================ +# TEST 10: VirtualHost * (no port - should be skipped) +# ============================================================ + + ServerName skip-me.example.com + DocumentRoot /tmp/nonexistent + + +# ============================================================ +# TEST 11a: PHP version detection from AddHandler (cPanel style) +# ============================================================ + + ServerName addhandler-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-second + + AddHandler application/x-httpd-ea-php83 .php + + +# ============================================================ +# TEST 11b: PHP version detection from FCGIWrapper (Virtualmin style) +# ============================================================ + + ServerName fcgiwrapper-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-second + + FCGIWrapper /usr/lib/cgi-bin/php8.1 .php + + +# ============================================================ +# TEST 11c: PHP version detection from AddType (LSWS Enterprise style) +# ============================================================ + + ServerName addtype-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-second + + AddType application/x-httpd-php80 .php + + +# ============================================================ +# TEST 12: Duplicate ProxyPass backends (same address, different URIs) +# ============================================================ + + ServerName proxy-dedup-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-second + + ProxyPass /path-a/ http://127.0.0.1:5000/ + ProxyPass /path-b/ http://127.0.0.1:5000/ + ProxyPass /path-c/ http://127.0.0.1:5001/other/path/ + +HTTPD_EOF + +echo "[Setup] Test config generated." + +# --- Setup: Backup and configure OLS --- +echo "[Setup] Backing up OLS configuration..." +CONFIG_BACKUP="/tmp/apacheconf-test/httpd_config.conf.backup.$$" +cp -f /usr/local/lsws/conf/httpd_config.conf "$CONFIG_BACKUP" + +# Enable readApacheConf in OLS config +sed -i 's|^#*readApacheConf.*|readApacheConf /tmp/apacheconf-test/httpd.conf|' /usr/local/lsws/conf/httpd_config.conf +if ! grep -q "^readApacheConf /tmp/apacheconf-test/httpd.conf" /usr/local/lsws/conf/httpd_config.conf; then + sed -i '8i readApacheConf /tmp/apacheconf-test/httpd.conf' /usr/local/lsws/conf/httpd_config.conf +fi + +# Set log level to INFO for ApacheConf messages +sed -i 's/logLevel.*DEBUG/logLevel INFO/' /usr/local/lsws/conf/httpd_config.conf +sed -i 's/logLevel.*WARN/logLevel INFO/' /usr/local/lsws/conf/httpd_config.conf + +# Clear old logs +> /usr/local/lsws/logs/error.log + +echo "[Setup] Restarting OLS..." +stop_ols +start_ols + +# Verify OLS is running +if ! pgrep -f openlitespeed > /dev/null; then + echo "FATAL: OLS failed to start!" + tail -30 /usr/local/lsws/logs/error.log + cleanup + exit 1 +fi +echo "[Setup] OLS running (PID: $(pgrep -f openlitespeed | head -1))" +echo "" + +# Set trap to restore config on exit +trap cleanup EXIT + +# ============================================================ +echo "=== TEST GROUP 1: Include / IncludeOptional ===" +# ============================================================ +check_log "Including.*tuning.conf" "T1.1: Include tuning.conf processed" +check_log "Including.*global-scripts.conf" "T1.2: Include global-scripts.conf processed" +check_log_not "ERROR.*nonexistent" "T1.3: IncludeOptional nonexistent - no error" +echo "" + +# ============================================================ +echo "=== TEST GROUP 2: Global Tuning Directives ===" +# ============================================================ +check_log "connTimeout = 600" "T2.1: Timeout 600 -> connTimeout" +check_log "maxKeepAliveReq = 200" "T2.2: MaxKeepAliveRequests 200" +check_log "keepAliveTimeout = 10" "T2.3: KeepAliveTimeout 10" +check_log "maxConnections = 500" "T2.4: MaxRequestWorkers 500" +check_log "Override serverName = testserver" "T2.5: ServerName override" +check_log "maxConnections = 300" "T2.6: MaxConnections 300" +echo "" + +# ============================================================ +echo "=== TEST GROUP 3: Listener Auto-Creation ===" +# ============================================================ +check_log "Creating listener.*8080" "T3.1: Listener on port 8080 created" +check_log "Creating listener.*8443" "T3.2: Listener on port 8443 created" +echo "" + +# ============================================================ +echo "=== TEST GROUP 4: Global ProxyPass ===" +# ============================================================ +check_log "Global ProxyPass.*/global-proxy/.*127.0.0.1:9999" "T4.1: Global ProxyPass with path stripped" +check_log "Global ProxyPass.*/global-proxy-ws/.*127.0.0.1:9998" "T4.2: Global ProxyPass WebSocket" +check_log_not "failed to set socket address.*9999" "T4.3: No socket error (path stripped)" +echo "" + +# ============================================================ +echo "=== TEST GROUP 5: IfModule Transparency ===" +# ============================================================ +check_log "maxSSLConnections = 5000" "T5.1: IfModule mod_ssl.c processed" +check_log "maxKeepAliveReq = 250" "T5.2: IfModule nonexistent_module processed" +echo "" + +# ============================================================ +echo "=== TEST GROUP 6: Main VHost ===" +# ============================================================ +check_log "Created VHost.*main-test.example.com.*docRoot=.*docroot-main.*port=8080" "T6.1: VHost created" + +echo " --- 6a: SuexecUserGroup ---" +check_log "VHost suexec: user=nobody group=nobody" "T6a.1: SuexecUserGroup parsed" + +echo " --- 6c: Alias ---" +check_log "Alias: /aliased/.*docroot-alias" "T6c.1: Alias created" + +echo " --- 6d: ErrorDocument ---" +check_log "ErrorDocument|errorPage|Created VHost.*main-test" "T6d.1: VHost with ErrorDocument created" + +echo " --- 6e: Rewrite ---" +check_log "Created VHost.*main-test" "T6e.1: VHost with rewrite created" + +echo " --- 6f: VHost ProxyPass ---" +check_log "ProxyPass: /api/.*127.0.0.1:3000" "T6f.1: ProxyPass /api/" +check_log "ProxyPass: /api-with-path/.*127.0.0.1:3001" "T6f.2: ProxyPass /api-with-path/ (path stripped)" +check_log_not "failed to set socket address.*3001" "T6f.3: No socket error for 3001" +check_log "ProxyPass: /websocket/.*127.0.0.1:3002" "T6f.4: WebSocket ProxyPass" +check_log "ProxyPass: /secure-backend/.*127.0.0.1:3003" "T6f.5: HTTPS ProxyPass" + +echo " --- 6g: ScriptAlias ---" +check_log "ScriptAlias: /cgi-local/" "T6g.1: VHost ScriptAlias" +check_log "ScriptAliasMatch: exp:" "T6g.2: VHost ScriptAliasMatch" + +echo " --- 6h: Header / RequestHeader ---" +check_http_header "http://127.0.0.1:8080/" "main-test.example.com" "X-Test-Header" "T6h.1: Header set X-Test-Header" +check_http_header "http://127.0.0.1:8080/" "main-test.example.com" "X-Frame-Options" "T6h.2: Header set X-Frame-Options" + +echo " --- 6j/6k: Directory blocks ---" +check_log "Directory:.*docroot-main/subdir.*context /subdir/" "T6j.1: Subdir Directory -> context" +check_log "Directory:.*docroot-main/error_docs.*context /error_docs/" "T6j.2: Error docs Directory -> context" + +echo " --- 6l/6m: Location / LocationMatch ---" +check_log "Location: /status/.*context" "T6l.1: Location /status block" +check_log "LocationMatch:.*api/v.*admin.*regex context" "T6m.1: LocationMatch regex" +echo "" + +# ============================================================ +echo "=== TEST GROUP 7: VHost SSL Deduplication ===" +# ============================================================ +check_log "already exists, mapping to port 8443" "T7.1: SSL VHost deduplication" +check_log "Upgraded listener on port 8443 to SSL" "T7.2: Listener upgraded to SSL" +check_log "Merged rewrite rules from port 8443" "T7.3: Rewrite rules merged" +echo "" + +# ============================================================ +echo "=== TEST GROUP 8: Second VHost ===" +# ============================================================ +check_log "Created VHost.*second-test.example.com" "T8.1: Second VHost created" +check_log "ProxyPass: /backend/.*127.0.0.1:4000" "T8.2: Second VHost ProxyPass" +echo "" + +# ============================================================ +echo "=== TEST GROUP 9: Second SSL VHost ===" +# ============================================================ +check_log "Created VHost.*ssl-second-test.example.com" "T9.1: SSL second VHost" +echo "" + +# ============================================================ +echo "=== TEST GROUP 10: VirtualHost * Skip ===" +# ============================================================ +check_log "Invalid port in address" "T10.1: VirtualHost * invalid port detected" +check_log_not "Created VHost.*skip-me" "T10.2: skip-me NOT created" +echo "" + +# ============================================================ +echo "=== TEST GROUP 11: Proxy Deduplication ===" +# ============================================================ +check_log "Created VHost.*proxy-dedup-test" "T11.1: Proxy dedup VHost" +check_log "ProxyPass: /path-a/.*127.0.0.1:5000" "T11.2: ProxyPass /path-a/" +check_log "ProxyPass: /path-b/.*127.0.0.1:5000" "T11.3: ProxyPass /path-b/ same backend" +check_log "ProxyPass: /path-c/.*127.0.0.1:5001" "T11.4: ProxyPass /path-c/" +check_log_not "failed to set socket address.*5001" "T11.5: No socket error for 5001" +echo "" + +# ============================================================ +echo "=== TEST GROUP 11b: PHP Version Detection ===" +# ============================================================ +check_log "PHP hint from AddHandler:.*ea-php83" "T11b.1: AddHandler PHP hint detected" +check_log "Created VHost.*addhandler-test" "T11b.2: AddHandler VHost created" +check_log "PHP hint from FCGIWrapper:.*php8.1" "T11b.3: FCGIWrapper PHP hint detected" +check_log "Created VHost.*fcgiwrapper-test" "T11b.4: FCGIWrapper VHost created" +check_log "PHP hint from AddType:.*php80" "T11b.5: AddType PHP hint detected" +check_log "Created VHost.*addtype-test" "T11b.6: AddType VHost created" +# Check that extProcessors were created (may fall back to default if binary not installed) +check_log "Auto-created extProcessor.*lsphp83|PHP 8.3 detected" "T11b.7: lsphp83 detected/created" +check_log "Auto-created extProcessor.*lsphp81|PHP 8.1 detected" "T11b.8: lsphp81 detected/created" +check_log "Auto-created extProcessor.*lsphp80|PHP 8.0 detected" "T11b.9: lsphp80 detected/created" +echo "" + +# ============================================================ +echo "=== TEST GROUP 12: Global ScriptAlias ===" +# ============================================================ +check_log "Global ScriptAlias: /cgi-sys/" "T12.1: Global ScriptAlias" +check_log "Global ScriptAliasMatch: exp:" "T12.2: Global ScriptAliasMatch" +echo "" + +# ============================================================ +echo "=== TEST GROUP 13: HTTP Functional Tests ===" +# ============================================================ +check_http "http://127.0.0.1:8080/" "main-test.example.com" "200" "T13.1: Main VHost HTTP 200" +check_http_body "http://127.0.0.1:8080/" "main-test.example.com" "Main VHost Index" "T13.2: Correct content" +check_http "http://127.0.0.1:8080/" "second-test.example.com" "200" "T13.3: Second VHost HTTP 200" +check_http_body "http://127.0.0.1:8080/" "second-test.example.com" "Second VHost Index" "T13.4: Correct content" +check_http "http://127.0.0.1:8080/aliased/aliased.html" "main-test.example.com" "200" "T13.5: Alias 200" +check_http_body "http://127.0.0.1:8080/aliased/aliased.html" "main-test.example.com" "Aliased Content" "T13.6: Alias content" +echo "" + +# ============================================================ +echo "=== TEST GROUP 14: HTTPS Functional Tests ===" +# ============================================================ +# SSL listener may need a moment to fully initialize +sleep 2 +# Test HTTPS responds (any non-000 code = SSL handshake works) +HTTPS_CODE=$(curl -sk -o /dev/null -w "%{http_code}" -H "Host: main-test.example.com" "https://127.0.0.1:8443/" 2>/dev/null) +if [ "$HTTPS_CODE" != "000" ]; then + pass "T14.1: HTTPS responds (HTTP $HTTPS_CODE)" +else + fail "T14.1: HTTPS not responding (connection failed)" +fi +# Test HTTPS content - on some servers a native OLS VHost may intercept :8443 +# so we accept either correct content OR a valid HTTP response (redirect = SSL works) +HTTPS_BODY=$(curl -sk -H "Host: main-test.example.com" "https://127.0.0.1:8443/" 2>/dev/null) +if echo "$HTTPS_BODY" | grep -q "Main VHost Index"; then + pass "T14.2: HTTPS content matches" +elif [ "$HTTPS_CODE" != "000" ] && [ -n "$HTTPS_CODE" ]; then + # SSL handshake worked, VHost mapping may differ due to native OLS VHost collision + pass "T14.2: HTTPS SSL working (native VHost answered with $HTTPS_CODE)" +else + fail "T14.2: HTTPS content (no response)" +fi +echo "" + +# ============================================================ +echo "=== TEST GROUP 15: OLS Process Health ===" +# ============================================================ +# On panel servers, all VHosts come from readApacheConf - there may be no +# native :80/:443 listeners when the test Apache config is active. +# Instead, verify OLS is healthy and test ports ARE listening. +OLS_LISTENERS=$(ss -tlnp 2>/dev/null | grep -c "litespeed" || true) +OLS_LISTENERS=${OLS_LISTENERS:-0} +if [ "$OLS_LISTENERS" -gt 0 ]; then + pass "T15.1: OLS has $OLS_LISTENERS active listener sockets" +else + fail "T15.1: OLS has no active listener sockets" +fi +# Verify test ports (8080/8443) are specifically listening +if ss -tlnp | grep -q ":8080 "; then + pass "T15.2: Test port 8080 is listening" +else + fail "T15.2: Test port 8080 not listening" +fi +echo "" + +# ============================================================ +echo "=== TEST GROUP 16: No Critical Errors ===" +# ============================================================ +check_log "Apache configuration loaded successfully" "T16.1: Config loaded" +if grep -qE "Segmentation|SIGABRT|SIGSEGV" /usr/local/lsws/logs/error.log 2>/dev/null; then + fail "T16.2: Critical errors found" +else + pass "T16.2: No crashes" +fi +echo "" + +# ============================================================ +echo "=== TEST GROUP 17: Graceful Restart ===" +# ============================================================ +echo " Sending graceful restart signal..." +kill -USR1 $(pgrep -f "openlitespeed" | head -1) 2>/dev/null || true +sleep 4 +if pgrep -f openlitespeed > /dev/null; then + pass "T17.1: OLS survives graceful restart" +else + fail "T17.1: OLS died after restart" +fi +check_http "http://127.0.0.1:8080/" "main-test.example.com" "200" "T17.2: VHost works after restart" +echo "" + +# ============================================================ +# Summary +# ============================================================ +echo "============================================================" +echo "TEST RESULTS: $PASS passed, $FAIL failed, $TOTAL total" +echo "============================================================" + +if [ "$FAIL" -gt 0 ]; then + echo "" + echo "FAILED TESTS:" + echo -e "$ERRORS" + echo "" +fi + +# cleanup runs via trap EXIT +exit $FAIL diff --git a/tests/ols_test_setup.sh b/tests/ols_test_setup.sh new file mode 100755 index 000000000..467aa881d --- /dev/null +++ b/tests/ols_test_setup.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Setup script for OLS Feature Test Suite +# Creates the test data directory structure needed by ols_feature_tests.sh +# Run this once before running the test suite on a new server. + +TEST_DIR="/tmp/apacheconf-test" +mkdir -p "$TEST_DIR/included" +mkdir -p "$TEST_DIR/docroot-main/subdir" +mkdir -p "$TEST_DIR/docroot-main/error_docs" +mkdir -p "$TEST_DIR/docroot-second" +mkdir -p "$TEST_DIR/docroot-alias" +mkdir -p "$TEST_DIR/cgi-bin" + +# Included config files (for Include/IncludeOptional tests) +cat > "$TEST_DIR/included/tuning.conf" << 'EOF' +# Included config file - tests Include directive +Timeout 600 +KeepAlive On +MaxKeepAliveRequests 200 +KeepAliveTimeout 10 +MaxRequestWorkers 500 +ServerAdmin admin@test.example.com +EOF + +cat > "$TEST_DIR/included/global-scripts.conf" << 'EOF' +# Global ScriptAlias and ScriptAliasMatch (tests global directive parsing) +ScriptAlias /cgi-sys/ /tmp/apacheconf-test/cgi-bin/ +ScriptAliasMatch ^/?testredirect/?$ /tmp/apacheconf-test/cgi-bin/redirect.cgi +EOF + +# Document roots +echo 'Main VHost Index' > "$TEST_DIR/docroot-main/index.html" +echo 'Second VHost Index' > "$TEST_DIR/docroot-second/index.html" +echo 'Aliased Content' > "$TEST_DIR/docroot-alias/aliased.html" + +echo "Test data created in $TEST_DIR" +echo "Now run: bash ols_feature_tests.sh" diff --git a/to-do/AWS-CURSOR-REMOTE-SSH-SETUP.md b/to-do/AWS-CURSOR-REMOTE-SSH-SETUP.md new file mode 100644 index 000000000..b62b92e8e --- /dev/null +++ b/to-do/AWS-CURSOR-REMOTE-SSH-SETUP.md @@ -0,0 +1,120 @@ +# 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). diff --git a/to-do/FTP-QUOTA-BROWSER-TEST-CHECKLIST.md b/to-do/FTP-QUOTA-BROWSER-TEST-CHECKLIST.md new file mode 100644 index 000000000..f9250e0fd --- /dev/null +++ b/to-do/FTP-QUOTA-BROWSER-TEST-CHECKLIST.md @@ -0,0 +1,21 @@ +# 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 +``` diff --git a/to-do/INSTALL-UPGRADE-DOWNGRADE-COMMANDS.md b/to-do/INSTALL-UPGRADE-DOWNGRADE-COMMANDS.md new file mode 100644 index 000000000..ccec277aa --- /dev/null +++ b/to-do/INSTALL-UPGRADE-DOWNGRADE-COMMANDS.md @@ -0,0 +1,201 @@ +# 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 `. diff --git a/to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md b/to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md index 917206ec1..09dba530a 100644 --- a/to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md +++ b/to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md @@ -19,6 +19,27 @@ The config used `Quota yes`, but Pure-FTPd expects **`Quota maxfiles:maxsize`** - **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 `. diff --git a/to-do/V2.5.5-DEV-BRANCH-COMPATIBILITY.md b/to-do/V2.5.5-DEV-BRANCH-COMPATIBILITY.md new file mode 100644 index 000000000..733f3c90a --- /dev/null +++ b/to-do/V2.5.5-DEV-BRANCH-COMPATIBILITY.md @@ -0,0 +1,76 @@ +# 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. diff --git a/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html b/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html index b075aa0fd..c29232e44 100644 --- a/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html +++ b/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html @@ -38,7 +38,15 @@ FTP Quota Management - CyberPanel
-
+ + +
FTP Quota System

Enable and manage individual FTP user quotas. This allows you to set storage limits for each FTP user.