diff --git a/CyberCP/csrfMiddleware.py b/CyberCP/csrfMiddleware.py new file mode 100644 index 000000000..7fbe6bfa9 --- /dev/null +++ b/CyberCP/csrfMiddleware.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +""" +Custom CSRF middleware that exempts /phpmyadmin/ and /snappymail/ so their +PHP sign-in forms (POST) do not get 403 CSRF verification failed. +""" +from django.middleware.csrf import CsrfViewMiddleware + + +class CsrfExemptPhpMyAdminMiddleware(CsrfViewMiddleware): + """CSRF middleware that skips verification for phpMyAdmin and SnappyMail paths.""" + + EXEMPT_PREFIXES = ('/phpmyadmin/', '/snappymail/') + + def process_view(self, request, callback, callback_args, callback_kwargs): + if request.path.startswith(self.EXEMPT_PREFIXES): + return None # Skip CSRF check + return super().process_view(request, callback, callback_args, callback_kwargs) diff --git a/CyberCP/urls.py b/CyberCP/urls.py index 25eb74f3f..fcac1536d 100644 --- a/CyberCP/urls.py +++ b/CyberCP/urls.py @@ -20,20 +20,27 @@ from django.conf import settings from django.conf.urls.static import static from django.views.static import serve from django.views.generic import RedirectView +from django.views.decorators.csrf import csrf_exempt from firewall import views as firewall_views + +@csrf_exempt +def serve_phpmyadmin(request, path): + """Serve phpMyAdmin files; CSRF exempt so sign-in form POST does not get 403.""" + return serve(request, path, document_root=os.path.join(settings.PUBLIC_ROOT, 'phpmyadmin')) + # Plugin routes are no longer hardcoded here; pluginHolder.urls dynamically # includes each installed plugin (under /plugins//) so settings and # other plugin pages work for any installed plugin. # Optional app: may be missing after clean clone or git clean -fd (not in all repo trees). -# When missing or broken, register a placeholder so {% url 'emailMarketing' %} in templates never raises Reverse not found. +# When missing or broken, register a placeholder so {% url 'emailMarketing' %} in templates never raises Reverse not found. Redirect to Plugin Store. _optional_email_marketing = [] try: _optional_email_marketing.append(path('emailMarketing/', include('emailMarketing.urls'))) except (ModuleNotFoundError, ImportError, AttributeError): _optional_email_marketing.append( - path('emailMarketing/', RedirectView.as_view(url='/base/', permanent=False), name='emailMarketing') + path('emailMarketing/', RedirectView.as_view(url='/plugins/installed?view=store', permanent=False), name='emailMarketing') ) urlpatterns = [ @@ -43,7 +50,7 @@ urlpatterns = [ re_path(r'^snappymail/?$', RedirectView.as_view(url='/snappymail/index.php', permanent=False)), re_path(r'^snappymail/(?P.*)$', serve, {'document_root': os.path.join(settings.PUBLIC_ROOT, 'snappymail')}), re_path(r'^phpmyadmin/?$', RedirectView.as_view(url='/phpmyadmin/index.php', permanent=False)), - re_path(r'^phpmyadmin/(?P.*)$', serve, {'document_root': os.path.join(settings.PUBLIC_ROOT, 'phpmyadmin')}), + re_path(r'^phpmyadmin/(?P.*)$', serve_phpmyadmin), path('base/', include('baseTemplate.urls')), path('imunifyav/', firewall_views.imunifyAV, name='imunifyav_root'), path('ImunifyAV/', firewall_views.imunifyAV, name='imunifyav_root_legacy'), diff --git a/fix-phpmyadmin-install.sh b/fix-phpmyadmin-install.sh new file mode 100755 index 000000000..790fbfff7 --- /dev/null +++ b/fix-phpmyadmin-install.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Install/fix phpMyAdmin under /usr/local/CyberCP/public/phpmyadmin (creates signin + full app) +set -e +PUBLIC=/usr/local/CyberCP/public +PMA_DIR=$PUBLIC/phpmyadmin +VERSION=5.2.3 +TARBALL=$PUBLIC/phpmyadmin.tar.gz + +echo "[$(date -Iseconds)] Installing phpMyAdmin to $PMA_DIR ..." +sudo mkdir -p "$PUBLIC" +sudo rm -rf "$PMA_DIR" +sudo wget -q -O "$TARBALL" "https://files.phpmyadmin.net/phpMyAdmin/${VERSION}/phpMyAdmin-${VERSION}-all-languages.tar.gz" || { echo "Download failed"; exit 1; } +[ -f "$TARBALL" ] && [ $(stat -c%s "$TARBALL") -gt 1000000 ] || { echo "Tarball missing or too small"; exit 1; } +sudo tar -xzf "$TARBALL" -C "$PUBLIC" +if [ -d "$PUBLIC/phpMyAdmin-${VERSION}-all-languages" ]; then + sudo mv "$PUBLIC/phpMyAdmin-${VERSION}-all-languages" "$PMA_DIR" +else + sudo mv "$PUBLIC/phpMyAdmin-"*"-all-languages" "$PMA_DIR" 2>/dev/null || true +fi +sudo rm -f "$TARBALL" + +[ -d "$PMA_DIR" ] || { echo "phpmyadmin dir not created"; exit 1; } + +# Config: use sample if present, then ensure signon block +BLOWFISH=$(openssl rand -hex 16) +if [ -f "$PMA_DIR/config.sample.inc.php" ]; then + sudo cp "$PMA_DIR/config.sample.inc.php" "$PMA_DIR/config.inc.php" + sudo sed -i "s|blowfish_secret.*|blowfish_secret'] = '${BLOWFISH}';|" "$PMA_DIR/config.inc.php" 2>/dev/null || true +fi +sudo bash -c 'cat >> '"$PMA_DIR"'/config.inc.php << "PMACONF" + +$i = 0; +$i++; +$cfg["Servers"][$i]["AllowNoPassword"] = false; +$cfg["Servers"][$i]["auth_type"] = "signon"; +$cfg["Servers"][$i]["SignonSession"] = "SignonSession"; +$cfg["Servers"][$i]["SignonURL"] = "phpmyadminsignin.php"; +$cfg["Servers"][$i]["LogoutURL"] = "phpmyadminsignin.php?logout"; +$cfg["Servers"][$i]["host"] = "127.0.0.1"; +$cfg["Servers"][$i]["port"] = "3306"; +$cfg["TempDir"] = "/usr/local/CyberCP/public/phpmyadmin/tmp"; +PMACONF' +sudo mkdir -p "$PMA_DIR/tmp" +sudo cp /usr/local/CyberCP/plogical/phpmyadminsignin.php "$PMA_DIR/phpmyadminsignin.php" +sudo chown -R lscpd:lscpd "$PMA_DIR" +echo "[$(date -Iseconds)] phpMyAdmin install done. Test: https://YOUR_IP:2087/phpmyadmin/phpmyadminsignin.php" +exit 0 diff --git a/loginSystem/views.py b/loginSystem/views.py index fd5a9f98a..067c0e20a 100644 --- a/loginSystem/views.py +++ b/loginSystem/views.py @@ -165,6 +165,36 @@ def verifyLogin(request): @ensure_csrf_cookie def loadLoginPage(request): + try: + return _loadLoginPage(request) + except Exception as e: + try: + from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging + import traceback + logging.writeToFile("loadLoginPage error: %s\n%s" % (str(e), traceback.format_exc())) + except Exception: + pass + # User-friendly message for database connection errors + from django.db.utils import OperationalError + err_str = str(e).lower() + if isinstance(e, OperationalError) or 'access denied' in err_str or '1045' in err_str: + msg = ( + "Database connection failed (Access denied for user 'cyberpanel'@'localhost'). " + "Check: 1) MariaDB is running (systemctl status mariadb). " + "2) Password in /etc/cyberpanel/mysqlPassword matches the MySQL user used by the panel. " + "3) User exists: mysql -u root -p -e \"SELECT User,Host FROM mysql.user WHERE User='cyberpanel';\"" + ) + return HttpResponse(msg, status=503, content_type="text/plain; charset=utf-8") + try: + # Minimal cosmetic so template does not break (login.html uses cosmetic.MainDashboardCSS) + class _MinimalCosmetic: + MainDashboardCSS = '' + return render(request, 'loginSystem/login.html', {'cosmetic': _MinimalCosmetic()}) + except Exception: + return HttpResponse("Server error. Check /home/cyberpanel/error-logs.txt", status=500, content_type="text/plain") + + +def _loadLoginPage(request): try: userID = request.session['userID'] currentACL = ACLManager.loadedACL(userID)