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

V2.5.5 dev
This commit is contained in:
Master3395
2026-04-10 20:33:05 +02:00
committed by GitHub
14 changed files with 2103 additions and 186 deletions

View File

@@ -2559,7 +2559,7 @@
<script src="{% static 'managePHP/managePHP.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'serverLogs/serverLogs.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'serverStatus/serverStatus.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'firewall/firewall.js' %}?v={{ CP_VERSION }}&fw={{ FIREWALL_STATIC_VERSION|default:CP_VERSION }}&cb=4" data-cfasync="false"></script>
<script src="{% static 'firewall/firewall.js' %}?v={{ CP_VERSION }}&fw={{ FIREWALL_STATIC_VERSION|default:CP_VERSION }}&cb=6" data-cfasync="false"></script>
<script src="{% static 'emailPremium/emailPremium.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'manageServices/manageServices.js' %}?v={{ CP_VERSION }}&msModal=20260402d" data-cfasync="false"></script>
<script src="{% static 'CLManager/CLManager.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>

View File

@@ -0,0 +1,47 @@
#!/bin/bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WEBSITE_FUNCTIONS="${BASE_DIR}/website-functions.sh"
HELPER="${BASE_DIR}/ols-apache-backend-setup.sh"
pass() { echo "[PASS] $1"; }
fail() { echo "[FAIL] $1"; exit 1; }
[[ -f "$WEBSITE_FUNCTIONS" ]] || fail "website-functions.sh missing"
[[ -f "$HELPER" ]] || fail "ols-apache-backend-setup.sh missing"
[[ -x "$HELPER" ]] || fail "helper is not executable"
bash -n "$WEBSITE_FUNCTIONS" || fail "website-functions.sh syntax invalid"
pass "website-functions syntax"
bash -n "$HELPER" || fail "helper syntax invalid"
pass "helper syntax"
if ! rg -q "Enable Additional Feature: OpenLiteSpeed \+ Apache backend" "$WEBSITE_FUNCTIONS"; then
fail "create flow prompt for OLS+Apache backend missing"
fi
pass "create flow prompt exists"
if ! rg -q "setup_ols_apache_backend_if_enabled" "$WEBSITE_FUNCTIONS"; then
fail "setup function wiring missing from website flow"
fi
pass "setup hook exists"
if ! rg -q "cyberpanel createChild" "$WEBSITE_FUNCTIONS"; then
fail "child domain create function not wired"
fi
pass "child domain flow exists"
if ! rg -q "httpd -t" "$HELPER"; then
fail "apache validation gate missing"
fi
pass "apache validation gate exists"
if ! rg -q "health_check_domain" "$HELPER"; then
fail "domain health-check gate missing"
fi
pass "domain health-check gate exists"
echo "All OLS+Apache backend automation checks passed."

View File

@@ -0,0 +1,291 @@
#!/bin/bash
# OLS + Apache backend auto-config for CyberPanel domains/sub-domains.
# Idempotent: safe to run repeatedly for same domain.
set -u
LOG_FILE="/var/log/cyberpanel_ols_apache_backend.log"
MAX_RETRIES=3
log_msg() {
local level="$1"
local module="$2"
local message="$3"
local retry="${4:-0}"
local ts
ts="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$ts] [$level] [$module] retry=$retry domain=${TARGET_DOMAIN:-unknown} - $message" | tee -a "$LOG_FILE"
}
fail() {
log_msg "ERROR" "setup" "$1" "${2:-0}"
exit 1
}
require_root() {
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
fail "This script must run as root"
fi
}
domain_valid() {
local d="$1"
[[ "$d" =~ ^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]
}
detect_docroot() {
local domain="$1"
local vhost_conf="/usr/local/lsws/conf/vhosts/${domain}/vhost.conf"
local docroot=""
if [[ -f "$vhost_conf" ]]; then
docroot="$(awk '$1=="docRoot"{print $2; exit}' "$vhost_conf" 2>/dev/null)"
fi
if [[ -z "$docroot" ]]; then
docroot="/home/${domain}"
fi
echo "$docroot"
}
ensure_apache_conf() {
local domain="$1"
local docroot="$2"
local owner="${3:-newst3922}"
local group="${4:-newst3922}"
local apache_conf="/etc/httpd/conf.d/${domain}.ols-apache-backend.conf"
local cert_dir="/etc/letsencrypt/live/${domain}"
local cert_file="${cert_dir}/fullchain.pem"
local key_file="${cert_dir}/privkey.pem"
if [[ ! -f "$cert_file" || ! -f "$key_file" ]]; then
cert_file="/etc/httpd/conf.d/ssl/.fullchain.pem"
key_file="/etc/httpd/conf.d/ssl/.privkey.pem"
fi
mkdir -p "$(dirname "$apache_conf")" || return 1
cat > "$apache_conf" <<EOF
<VirtualHost *:8083>
ServerName ${domain}
ServerAlias www.${domain}
ServerAdmin root@localhost
SuexecUserGroup ${owner} ${group}
DocumentRoot ${docroot}
<FilesMatch \\.php$>
SetHandler "proxy:unix:/var/run/php-fpm/${domain}.sock|fcgi://localhost"
</FilesMatch>
<Directory ${docroot}>
Options Indexes FollowSymLinks
AllowOverride all
Require all granted
DirectoryIndex index.php index.html
</Directory>
</VirtualHost>
<VirtualHost *:8082>
ServerName ${domain}
ServerAlias www.${domain}
ServerAdmin root@localhost
SuexecUserGroup ${owner} ${group}
DocumentRoot ${docroot}
<FilesMatch \\.php$>
SetHandler "proxy:unix:/var/run/php-fpm/${domain}.sock|fcgi://localhost"
</FilesMatch>
<Directory ${docroot}>
Options Indexes FollowSymLinks
AllowOverride all
Require all granted
DirectoryIndex index.php index.html
</Directory>
SSLEngine on
SSLVerifyClient none
SSLCertificateFile ${cert_file}
SSLCertificateKeyFile ${key_file}
</VirtualHost>
EOF
return 0
}
ensure_ols_proxy_rewrite() {
local domain="$1"
local vhost_conf="/usr/local/lsws/conf/vhosts/${domain}/vhost.conf"
local tmp_file="${vhost_conf}.tmp.$$"
local marker="AUTO_OLS_APACHE_BACKEND"
[[ -f "$vhost_conf" ]] || return 1
awk '
BEGIN { in_rewrite=0; depth=0 }
{
if ($1=="rewrite" && $2=="{") {
in_rewrite=1;
depth=1;
next;
}
if (in_rewrite==1) {
if (index($0, "{")>0) depth++;
if (index($0, "}")>0) depth--;
if (depth<=0) { in_rewrite=0; next; }
next;
}
print $0;
}
' "$vhost_conf" > "$tmp_file" || return 1
cat >> "$tmp_file" <<'EOF'
rewrite {
enable 1
autoLoadHtaccess 0
rules <<<END_rules
RewriteEngine On
# AUTO_OLS_APACHE_BACKEND
RewriteCond %{HTTPS} !=on
RewriteRule ^(.*)$ HTTP://apachebackend/$1 [P,L]
RewriteRule ^(.*)$ HTTP://proxyApacheBackendSSL/$1 [P,L]
END_rules
}
EOF
mv "$tmp_file" "$vhost_conf" || return 1
return 0
}
ensure_httpd_running() {
if ! systemctl is-active --quiet httpd; then
systemctl start httpd || return 1
fi
systemctl enable httpd >/dev/null 2>&1 || true
return 0
}
check_ports() {
local has_8082
local has_8083
has_8082="$(ss -tln 2>/dev/null | awk '$4 ~ /:8082$/ {print "1"; exit}')"
has_8083="$(ss -tln 2>/dev/null | awk '$4 ~ /:8083$/ {print "1"; exit}')"
[[ "$has_8082" == "1" && "$has_8083" == "1" ]] || return 1
return 0
}
health_check_domain() {
local domain="$1"
local code
code="$(curl -k -sS -o /dev/null -w "%{http_code}" "https://${domain}/" 2>/dev/null || true)"
[[ "$code" != "503" && "$code" != "000" && "$code" != "" ]]
}
rollback_file_if_needed() {
local src="$1"
local backup="$2"
if [[ -f "$backup" ]]; then
cp -f "$backup" "$src" >/dev/null 2>&1 || true
fi
}
main() {
require_root
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
echo "Usage: $0 --domain <domain> [--owner <user>] [--group <group>] [--docroot <path>]"
exit 0
fi
local owner="newst3922"
local group="newst3922"
local docroot=""
local domain=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) domain="${2:-}"; shift 2 ;;
--owner) owner="${2:-}"; shift 2 ;;
--group) group="${2:-}"; shift 2 ;;
--docroot) docroot="${2:-}"; shift 2 ;;
*) fail "Unknown argument: $1" ;;
esac
done
TARGET_DOMAIN="$domain"
domain_valid "$domain" || fail "Invalid or missing domain"
[[ -n "$docroot" ]] || docroot="$(detect_docroot "$domain")"
[[ -d "$docroot" ]] || fail "Docroot does not exist: $docroot"
local vhost_conf="/usr/local/lsws/conf/vhosts/${domain}/vhost.conf"
local apache_conf="/etc/httpd/conf.d/${domain}.ols-apache-backend.conf"
[[ -f "$vhost_conf" ]] || fail "OLS vhost config not found: $vhost_conf"
local vhost_bak="${vhost_conf}.bak.$(date +%s)"
local apache_bak="${apache_conf}.bak.$(date +%s)"
cp -f "$vhost_conf" "$vhost_bak" || fail "Failed to backup OLS vhost"
[[ -f "$apache_conf" ]] && cp -f "$apache_conf" "$apache_bak" || true
log_msg "INFO" "setup" "Starting OLS+Apache backend setup"
local attempt=1
while [[ $attempt -le $MAX_RETRIES ]]; do
if ! ensure_apache_conf "$domain" "$docroot" "$owner" "$group"; then
log_msg "WARN" "apache" "Failed writing Apache config" "$attempt"
attempt=$((attempt + 1))
sleep 1
continue
fi
if ! ensure_ols_proxy_rewrite "$domain"; then
log_msg "WARN" "ols" "Failed writing OLS proxy rewrite" "$attempt"
attempt=$((attempt + 1))
sleep 1
continue
fi
if ! httpd -t >/dev/null 2>&1; then
log_msg "WARN" "validate" "Apache syntax validation failed" "$attempt"
rollback_file_if_needed "$apache_conf" "$apache_bak"
attempt=$((attempt + 1))
sleep 1
continue
fi
if ! ensure_httpd_running; then
log_msg "WARN" "service" "Could not start/enable httpd" "$attempt"
attempt=$((attempt + 1))
sleep 1
continue
fi
if ! systemctl restart lsws >/dev/null 2>&1; then
log_msg "WARN" "service" "LSWS restart failed" "$attempt"
rollback_file_if_needed "$vhost_conf" "$vhost_bak"
attempt=$((attempt + 1))
sleep 1
continue
fi
if ! check_ports; then
log_msg "WARN" "validate" "Ports 8082/8083 are not listening" "$attempt"
attempt=$((attempt + 1))
sleep 1
continue
fi
if ! health_check_domain "$domain"; then
log_msg "WARN" "health" "Domain health check failed (503/000)" "$attempt"
attempt=$((attempt + 1))
sleep 1
continue
fi
log_msg "INFO" "setup" "Apache backend configured, ports verified, vhost health check passed"
exit 0
done
rollback_file_if_needed "$vhost_conf" "$vhost_bak"
rollback_file_if_needed "$apache_conf" "$apache_bak"
fail "Failed to configure OLS+Apache backend after ${MAX_RETRIES} retries" "$MAX_RETRIES"
}
main "$@"

View File

@@ -0,0 +1,49 @@
# OLS + Apache Backend Auto-Setup
## What this does
When creating a website or child domain via `website-functions.sh`, users can now select:
- `Enable Additional Feature: OpenLiteSpeed + Apache backend (YES/NO)`
If set to `YES`, the script automatically runs:
- `ols-apache-backend-setup.sh --domain <domain>`
## Automated actions
1. Detects docroot from LiteSpeed vhost config.
2. Writes Apache backend vhost config at:
- `/etc/httpd/conf.d/<domain>.ols-apache-backend.conf`
- Includes both `:8083` (HTTP backend) and `:8082` (HTTPS backend).
3. Rewrites LiteSpeed vhost config to proxy through:
- `apachebackend` (`127.0.0.1:8083`)
- `proxyApacheBackendSSL` (`127.0.0.1:8082`)
4. Runs validation and service gates:
- `httpd -t`
- ensures `httpd` is running/enabled
- restarts `lsws`
- verifies ports `8082/8083` are listening
- verifies site health is not 503
5. Rolls back to backups if setup fails after retries.
## Logs
- Main setup log: `/var/log/cyberpanel_ols_apache_backend.log`
- Existing user CLI logs are still used for create/delete actions.
## Failure handling
- Retries up to 3 times.
- Every error line includes timestamp, module, domain, and retry count.
- On hard failure, prior config snapshots are restored.
## Test
Run:
```bash
/home/cyberpanel-mods/user-management/Test/test-ols-apache-backend-setup.sh
```
This validates wiring and safety gates for the new feature.

View File

@@ -0,0 +1,413 @@
#!/bin/bash
# Website Management Functions for CyberPanel
# Part of CyberPanel Mods - Enhanced Repository
## Website Functions
setup_ols_apache_backend_if_enabled() {
local domain="$1"
local enabled="$2"
local helper
local yn
yn="$(echo "${enabled:-NO}" | tr '[:lower:]' '[:upper:]')"
if [[ "$yn" != "YES" && "$yn" != "Y" && "$yn" != "1" && "$yn" != "ON" && "$yn" != "TRUE" ]]; then
return 0
fi
helper="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ols-apache-backend-setup.sh"
if [[ ! -x "$helper" ]]; then
echo "[!] OLS+Apache backend helper not found or not executable: $helper"
if command -v log_message >/dev/null 2>&1; then
log_message "OLS+Apache backend helper missing for domain: $domain"
fi
return 1
fi
echo "[*] Applying OpenLiteSpeed + Apache backend auto-setup for: $domain"
if ! "$helper" --domain "$domain"; then
echo "[!] OLS+Apache backend setup failed for: $domain"
if command -v log_message >/dev/null 2>&1; then
log_message "OLS+Apache backend setup failed for domain: $domain"
fi
return 1
fi
echo "[*] OLS+Apache backend setup completed for: $domain"
if command -v log_message >/dev/null 2>&1; then
log_message "OLS+Apache backend setup completed for domain: $domain"
fi
return 0
}
function createWebsite()
{
echo -e $YELLOW
echo "***********************************************************************"
echo "* Website Functions - Create Website *"
echo "***********************************************************************"
echo -e $TEXTRESET
read -p "Enter Package (Default | admin_Standard | admin_Professional | admin_Enterprise): " Package
echo $Package | grep -w $package_pattern >> /dev/null && is_package="true"
if [[ $is_package != "true" ]]; then
echo "[!] Package Choice Cannot be found!"
echo "[*] Reverting...."
sleep 2
clear
createWebsite
fi
unset is_package
read -p "Enter Username of the Web Owner (No Special Chars Except Email Pattern): " userName
echo $userName | grep -P $username_special_chars >> /dev/null || is_special="true"
if [[ $is_special != "true" ]]; then
echo "[!] Not a valid username!"
echo "[*] Reverting...."
sleep 2
clear
createWebsite
fi
unset is_special
read -p "Enter the website domain to be use: " Domain
echo $Domain | egrep -e $domain_pattern >> /dev/null && is_domain="true"
if [[ $is_domain != "true" ]]; then
echo "[!] Not a valid domain name!"
echo "[*] Reverting...."
sleep 2
clear
createWebsite
fi
unset is_domain
read -p "Enter Email Address: " email
echo $email | egrep $email_pattern >> /dev/null && is_email="true"
if [[ $is_email != "true" ]]; then
echo "[!] Not a valid email address!"
echo "[*] Reverting...."
sleep 2
clear
createWebsite
fi
unset is_email
echo -e "Available PHP Versions: 5.3 | 5.4 | 5.5 | 5.6 | 7.0 | 7.1 | 7.2 | 7.3 | 7.4 | 8.0 | 8.1 | 8.2 | 8.3"
read -p "Enter PHP Version: " php_version
echo $php_version | grep -w $php_version_pattern >> /dev/null && is_php="true"
if [[ $is_php != "true" ]]; then
echo "[!] Not a valid PHP Version!"
echo "[*] Reverting...."
sleep 2
clear
createWebsite
fi
unset is_php
read -p "Do you want to enable SSL (YES/NO)?: " Ssl
Ssl=`echo $Ssl | tr [:lower:] [:upper:]`
if [[ $Ssl = "YES" ]]; then
ssl_support=1
else
ssl_support=0
fi
read -p "Do you want to enable DKIM (YES/NO)?: " Dkim
Dkim=`echo $Dkim | tr [:lower:] [:upper:]`
if [[ $Dkim = "YES" ]]; then
dkim_support=1
else
dkim_support=0
fi
read -p "Do you want to enable openBasedir Protection (YES/NO)?: " Openbase
Openbase=`echo $Openbase | tr [:lower:] [:upper:]`
if [[ $Openbase = "YES" ]]; then
openbase_support=1
else
openbase_support=0
fi
read -p "Enable Additional Feature: OpenLiteSpeed + Apache backend (YES/NO)?: " OlsApacheBackend
OlsApacheBackend=`echo $OlsApacheBackend | tr [:lower:] [:upper:]`
if [[ -z "$OlsApacheBackend" ]]; then
OlsApacheBackend="NO"
fi
echo -e $YELLOW
echo "***********************************************************************"
echo "* Details You Entered *"
echo "***********************************************************************"
echo "Package: $Package"
echo "Username (Owner): $userName"
echo "Domain: $Domain"
echo "Email: $email"
echo "PHP Version: $php_version"
echo "SSL Support: $Ssl"
echo "DKIM Support: $Dkim"
echo "Open Base Directory Protection Enable: $Openbase"
echo "OLS + Apache Backend Feature: $OlsApacheBackend"
read -p "Are you sure about this? [Y/N]: " choice
choice=`echo $choice | tr [:lower:] [:upper:]`
if [ $choice = "Y" ] ; then
cyberpanel createWebsite --package $Package --owner $userName --domainName $Domain --email $email --php $php_version --ssl $ssl_support --dkim $dkim_support --openBasedir $openbase_support
if [[ $? -eq 0 ]]; then
setup_ols_apache_backend_if_enabled "$Domain" "$OlsApacheBackend"
fi
log_message "Created website: $Domain for user: $userName"
unset Package userName Domain email php_version Ssl Dkim Openbase OlsApacheBackend choice
exit 0
else
echo -e "[*] Reverting..."
unset Package userName Domain email php_version Ssl Dkim Openbase OlsApacheBackend choice
sleep 2
clear
createWebsite
fi
}
function createChildDomain()
{
echo -e $YELLOW
echo "***********************************************************************"
echo "* Website Functions - Create Child Domain *"
echo "***********************************************************************"
echo -e $TEXTRESET
read -p "Enter Master Domain: " masterDomain
echo $masterDomain | egrep -e $domain_pattern >> /dev/null && is_master_domain="true"
if [[ $is_master_domain != "true" ]]; then
echo "[!] Not a valid master domain name!"
echo "[*] Reverting...."
sleep 2
clear
createChildDomain
fi
unset is_master_domain
read -p "Enter Child Domain (full): " childDomain
echo $childDomain | egrep -e $domain_pattern >> /dev/null && is_child_domain="true"
if [[ $is_child_domain != "true" ]]; then
echo "[!] Not a valid child domain name!"
echo "[*] Reverting...."
sleep 2
clear
createChildDomain
fi
unset is_child_domain
read -p "Enter Username of the Web Owner: " userName
echo $userName | grep -P $username_special_chars >> /dev/null || is_special="true"
if [[ $is_special != "true" ]]; then
echo "[!] Not a valid username!"
echo "[*] Reverting...."
sleep 2
clear
createChildDomain
fi
unset is_special
echo -e "Available PHP Versions: 5.3 | 5.4 | 5.5 | 5.6 | 7.0 | 7.1 | 7.2 | 7.3 | 7.4 | 8.0 | 8.1 | 8.2 | 8.3"
read -p "Enter PHP Version: " php_version
echo $php_version | grep -w $php_version_pattern >> /dev/null && is_php="true"
if [[ $is_php != "true" ]]; then
echo "[!] Not a valid PHP Version!"
echo "[*] Reverting...."
sleep 2
clear
createChildDomain
fi
unset is_php
read -p "Do you want to enable SSL (YES/NO)?: " Ssl
Ssl=`echo $Ssl | tr [:lower:] [:upper:]`
if [[ $Ssl = "YES" ]]; then
ssl_support=1
else
ssl_support=0
fi
read -p "Do you want to enable DKIM (YES/NO)?: " Dkim
Dkim=`echo $Dkim | tr [:lower:] [:upper:]`
if [[ $Dkim = "YES" ]]; then
dkim_support=1
else
dkim_support=0
fi
read -p "Do you want to enable openBasedir Protection (YES/NO)?: " Openbase
Openbase=`echo $Openbase | tr [:lower:] [:upper:]`
if [[ $Openbase = "YES" ]]; then
openbase_support=1
else
openbase_support=0
fi
read -p "Enable Additional Feature: OpenLiteSpeed + Apache backend (YES/NO)?: " OlsApacheBackend
OlsApacheBackend=`echo $OlsApacheBackend | tr [:lower:] [:upper:]`
if [[ -z "$OlsApacheBackend" ]]; then
OlsApacheBackend="NO"
fi
echo -e $YELLOW
echo "***********************************************************************"
echo "* Details You Entered *"
echo "***********************************************************************"
echo "Master Domain: $masterDomain"
echo "Child Domain: $childDomain"
echo "Owner: $userName"
echo "PHP Version: $php_version"
echo "SSL Support: $Ssl"
echo "DKIM Support: $Dkim"
echo "Open Base Directory Protection Enable: $Openbase"
echo "OLS + Apache Backend Feature: $OlsApacheBackend"
read -p "Are you sure about this? [Y/N]: " choice
choice=`echo $choice | tr [:lower:] [:upper:]`
if [ "$choice" = "Y" ] ; then
cyberpanel createChild --masterDomain "$masterDomain" --childDomain "$childDomain" --owner "$userName" --php "$php_version" --ssl "$ssl_support" --dkim "$dkim_support" --openBasedir "$openbase_support"
if [[ $? -eq 0 ]]; then
setup_ols_apache_backend_if_enabled "$childDomain" "$OlsApacheBackend"
log_message "Created child domain: $childDomain under: $masterDomain"
else
log_message "Failed to create child domain: $childDomain under: $masterDomain"
fi
unset masterDomain childDomain userName php_version Ssl Dkim Openbase OlsApacheBackend choice
exit 0
else
echo -e "[*] Reverting..."
unset masterDomain childDomain userName php_version Ssl Dkim Openbase OlsApacheBackend choice
sleep 2
clear
createChildDomain
fi
}
function deleteChildDomain()
{
echo -e $YELLOW
echo "***********************************************************************"
echo "* Website Functions - Delete Child Domain *"
echo "***********************************************************************"
echo -e $TEXTRESET
read -p "Enter Child Domain to delete: " childDomain
echo $childDomain | egrep -e $domain_pattern >> /dev/null && is_child_domain="true"
if [[ $is_child_domain != "true" ]]; then
echo "[!] Not a valid child domain name!"
echo "[*] Reverting...."
sleep 2
clear
deleteChildDomain
fi
unset is_child_domain
read -p "Are you sure about this? [Y/N]: " choice
choice=`echo $choice | tr [:lower:] [:upper:]`
if [ "$choice" = "Y" ] ; then
cyberpanel deleteChild --childDomain "$childDomain"
log_message "Deleted child domain: $childDomain"
unset childDomain choice
exit 0
else
unset childDomain choice
deleteChildDomain
fi
}
function deleteWebsite()
{
echo -e $YELLOW
echo "***********************************************************************"
echo "* Website Functions - Delete Website *"
echo "***********************************************************************"
echo -e $TEXTRESET
echo "[*] Do not enter a child domain"
read -p "Enter the website domain to be deleted: " Domain
echo $Domain | egrep -e $domain_pattern >> /dev/null && is_domain="true"
if [[ $is_domain != "true" ]]; then
echo "[!] Not a valid domain name!"
echo "[*] Reverting...."
sleep 2
clear
deleteWebsite
fi
unset is_domain
echo -e $YELLOW
echo "***********************************************************************"
echo "* Details You Entered *"
echo "***********************************************************************"
echo "Domain: $Domain"
read -p "Are you sure about this? [Y/N]: " choice
choice=`echo $choice | tr [:lower:] [:upper:]`
if [ $choice = "Y" ] ; then
cyberpanel deleteWebsite --domainName $Domain
log_message "Deleted website: $Domain"
unset Domain choice
echo "Done..."
exit 0
else
echo -e "[*] Reverting..."
unset Domain choice
deleteWebsite
fi
}
function listWebsite()
{
echo -e $YELLOW
echo "***********************************************************************"
echo "* Website Functions - List Website *"
echo "***********************************************************************"
echo -e $TEXTRESET
cyberpanel listWebsitesPretty
}
function changePHP()
{
echo -e $YELLOW
echo "***********************************************************************"
echo "* Website Functions - Change PHP Version *"
echo "***********************************************************************"
echo -e $TEXTRESET
echo "[*] Do not enter a child domain"
read -p "Enter the website master domain to be use: " Domain
echo $Domain | egrep -e $domain_pattern >> /dev/null && is_domain="true"
if [[ $is_domain != "true" ]]; then
echo "[!] Not a valid domain name!"
echo "[*] Reverting...."
sleep 2
clear
changePHP
fi
unset is_domain
echo -e "Available PHP Versions: 5.3 | 5.4 | 5.5 | 5.6 | 7.0 | 7.1 | 7.2 | 7.3 | 7.4 | 8.0 | 8.1 | 8.2 | 8.3"
read -p "Enter PHP Version: " php_version
echo $php_version | grep -w $php_version_pattern >> /dev/null && is_php="true"
if [[ $is_php != "true" ]]; then
echo "[!] Not a valid PHP Version!"
echo "[*] Reverting...."
sleep 2
clear
changePHP
fi
unset is_php
echo -e $YELLOW
echo "***********************************************************************"
echo "* Details You Entered *"
echo "***********************************************************************"
echo "Domain: $Domain"
echo "PHP Version: $php_version"
read -p "Are you sure about this? [Y/N]: " choice
choice=`echo $choice | tr [:lower:] [:upper:]`
if [ $choice = "Y" ] ; then
cyberpanel changePHP --domainName $Domain --php $php_version
log_message "Changed PHP version for $Domain to $php_version"
unset Domain php_version choice
echo "Done..."
exit 0
else
echo -e "[*] Reverting..."
unset Domain php_version choice
changePHP
fi
}

View File

@@ -89,6 +89,15 @@
flex-wrap: wrap;
margin-bottom: 25px;
}
/* Anchor styled as button (Open Web UI link) — same visual weight as <button class="action-btn"> */
.action-buttons a.action-btn {
display: inline-flex;
align-items: center;
text-decoration: none;
box-sizing: border-box;
vertical-align: middle;
}
.action-btn {
background: #5856d6;
@@ -636,7 +645,11 @@
<div class="content-section">
<h2 class="section-title">{% trans "Rspamd Management" %}</h2>
<div class="action-buttons">
<button type="button" data-toggle="modal" data-target="#ViewRspamdlog" ng-click="FetchRspamdLog()" class="action-btn primary">
<a href="{{ rspamd_ui_url }}" target="_blank" rel="noopener noreferrer" class="action-btn primary">
<i class="fas fa-window-maximize"></i>
{% trans "Open Rspamd Web UI" %}
</a>
<button type="button" data-toggle="modal" data-target="#ViewRspamdlog" ng-click="FetchRspamdLog()" class="action-btn secondary">
<i class="fas fa-file-alt"></i>
{% trans "View Rspamd Logs" %}
</button>
@@ -649,6 +662,23 @@
{% trans "Uninstall Rspamd" %}
</button>
</div>
<p style="color: var(--text-secondary, #64748b); font-size: 14px; margin-top: 16px; line-height: 1.6;">
{% trans "Opens the official Rspamd web interface in a new tab (path /emailPremium/Rspamd/ui/ — proxied through CyberPanel, admin session required)." %}
</p>
</div>
<div class="content-section">
<h2 class="section-title">{% trans "Alternative: SSH tunnel" %}</h2>
<p style="color: var(--text-secondary, #64748b); font-size: 14px; line-height: 1.6;">
{% trans "If the proxied UI misbehaves, connect to port 11334 on the server via SSH and use your local browser." %}
</p>
<pre style="background: var(--bg-secondary,#f8f9ff); padding: 12px; border-radius: 8px; font-size: 13px; overflow-x: auto; border: 1px solid var(--border-light, #e8e9ff);">ssh -N -L 11334:127.0.0.1:11334 root@{{ ipAddress }}</pre>
<p style="margin-top: 12px;">
<a href="http://127.0.0.1:11334/" target="_blank" rel="noopener noreferrer" class="action-btn secondary">
<i class="fas fa-external-link-alt"></i>
{% trans "Open Rspamd UI (when tunnel is active)" %}
</a>
</p>
</div>
<!-- ClamAV Configuration -->

View File

@@ -33,7 +33,8 @@ urlpatterns = [
path('installMailScanner', views.installMailScanner, name='installMailScanner'),
path('installStatusMailScanner', views.installStatusMailScanner, name='installStatusMailScanner'),
# Rspamd
# Rspamd (proxied controller UI — must stay above catch-all domain route)
re_path(r'^Rspamd/ui(?:/(?P<subpath>.*))?$', views.rspamd_ui_proxy, name='RspamdUI'),
path('Rspamd', views.Rspamd, name='Rspamd'),
path('installRspamd', views.installRspamd, name='installRspamd'),
path('installStatusRspamd', views.installStatusRspamd, name='installStatusRspamd'),

View File

@@ -2337,6 +2337,18 @@ class FirewallManager:
final_dic = {'status': 0, 'error_message': 'Invalid IP address format', 'error': 'Invalid IP address format'}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
try:
from plogical.sshSecurityWhitelistUtilities import SSHSecurityWhitelistUtilities
if SSHSecurityWhitelistUtilities.is_whitelisted(ip):
final_dic = {
'status': 0,
'error_message': 'This IP is on the SSH Security trusted list and cannot be banned. Remove it from Trusted IPs first.',
'error': 'SSH Security whitelist',
}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
except Exception:
pass
current_time = time.time()
duration_map = {
'1h': 3600,

View File

@@ -23,6 +23,8 @@ function getCookie(name) {
app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.rulesLoading = true;
/** Incremented on each rules fetch; stale HTTP responses must not touch rulesLoading. */
var rulesFetchGen = 0;
$scope.actionFailed = true;
$scope.actionSuccess = true;
$scope.showExportFormatModal = false;
@@ -40,9 +42,12 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
// Initialize rules array - prevents "Cannot read 'length' of undefined" when template evaluates rules.length before API loads
$scope.rules = [];
// Banned IPs variables tab from hash so we stay on /firewall/ (avoids 404 on servers without /firewall/firewall-rules/)
/* Use window.location.hash only. Angular $location can disagree with the fragment on /firewall/#… and wrongly map to "rules". */
function tabFromHash() {
var h = (window.location.hash || '').replace(/^#/, '');
return (h === 'banned-ips') ? 'banned' : 'rules';
var h = String(window.location.hash || '').replace(/^#/, '').toLowerCase();
if (h === 'banned-ips' || h === 'banned') return 'banned';
if (h === 'trusted-ips' || h === 'ssh-whitelist') return 'trusted';
return 'rules';
}
$scope.activeTab = tabFromHash();
$scope.bannedIPs = []; // Initialize as empty array
@@ -52,7 +57,9 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
var tab = tabFromHash();
if ($scope.activeTab !== tab) {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
if (tab === 'banned') { populateBannedIPs(); }
else if (tab === 'trusted') { populateTrustedSSHWhitelist(); }
else { populateCurrentRecords(); }
if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); }
}
}
@@ -63,23 +70,51 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
window.addEventListener('load', function() { $timeout(applyTabFromHash, 0); });
}
// Sync tab with hash and load that tab's data on switch
$scope.setFirewallTab = function(tab) {
// Sync tab with hash and load that tab's data on switch (single source of truth from ng-click).
$scope.setFirewallTab = function(tab, $event) {
if ($event) {
try {
$event.stopPropagation();
} catch (ignoreErr) {}
}
$timeout(function() {
$scope.activeTab = tab;
window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules';
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
function setHashIfNeeded(frag) {
try {
if ((window.location.hash || '') === frag) {
return;
}
var path = window.location.pathname + window.location.search + frag;
if (window.history && typeof window.history.replaceState === 'function') {
window.history.replaceState(null, '', path);
} else {
window.location.hash = frag;
}
} catch (ignoreHash) {}
}
if (tab === 'banned') {
setHashIfNeeded('#banned-ips');
populateBannedIPs();
} else if (tab === 'trusted') {
setHashIfNeeded('#trusted-ips');
populateTrustedSSHWhitelist();
} else {
setHashIfNeeded('#rules');
populateCurrentRecords();
}
}, 0);
};
// Back/forward or direct hash change: sync tab and load its data
function syncTabFromHash() {
var tab = tabFromHash();
if ($scope.activeTab !== tab) {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); }
}
$scope.$evalAsync(function() {
if ($scope.activeTab !== tab) {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); }
else if (tab === 'trusted') { populateTrustedSSHWhitelist(); }
else { populateCurrentRecords(); }
}
});
}
window.addEventListener('hashchange', syncTabFromHash);
@@ -108,6 +143,9 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.banIP = '';
$scope.banReason = '';
$scope.banDuration = '24h';
$scope.trustedSSHWhitelist = [];
$scope.trustedForm = { ip: '', label: '' };
$scope.trustedSSHLoading = false;
$scope.bannedIPSearch = '';
$scope.searchBannedIPFilter = function(item) {
var q = ($scope.bannedIPSearch || '').toLowerCase().trim();
@@ -142,6 +180,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$timeout(function() {
try {
if (newVal === 'banned' && typeof populateBannedIPs === 'function') populateBannedIPs();
else if (newVal === 'trusted' && typeof populateTrustedSSHWhitelist === 'function') populateTrustedSSHWhitelist();
else if (newVal === 'rules' && typeof populateCurrentRecords === 'function') populateCurrentRecords();
} catch (e) {}
}, 0);
@@ -246,6 +285,156 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
);
}
function populateTrustedSSHWhitelist() {
$scope.trustedSSHLoading = true;
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') || '' } };
$http.post('/base/sshSecurityWhitelistList', {}, config).then(
function(response) {
$scope.trustedSSHLoading = false;
var res = response.data;
if (typeof res === 'string') {
try { res = JSON.parse(res); } catch (e) { res = {}; }
}
if (res && res.status === 1) {
var ent = res.entries || [];
$scope.trustedSSHWhitelist = ent.map(function(e) {
return {
ip: e.ip,
label: e.label || '',
updated: e.updated || 0,
_l: e.label || '',
_nip: ''
};
});
} else {
$scope.trustedSSHWhitelist = [];
var errMsg = (res && res.error) ? res.error : 'Could not load trusted IPs';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: errMsg, type: 'error', delay: 6000 });
}
}
},
function(error) {
$scope.trustedSSHLoading = false;
$scope.trustedSSHWhitelist = [];
var msg = (error.data && error.data.error) ? error.data.error : 'Request failed';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: msg, type: 'error', delay: 6000 });
}
}
);
}
$scope.populateTrustedSSHWhitelist = function() {
populateTrustedSSHWhitelist();
};
$scope.addTrustedSSHWhitelist = function() {
var ip = ($scope.trustedForm.ip || '').trim();
var label = ($scope.trustedForm.label || '').trim();
if (!ip) {
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: 'Enter an IP address', type: 'warning', delay: 5000 });
}
return;
}
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') || '' } };
$http.post('/base/sshSecurityWhitelistAdd', { ip: ip, label: label }, config).then(
function(response) {
var res = response.data;
if (typeof res === 'string') {
try { res = JSON.parse(res); } catch (e) { res = {}; }
}
if (res && res.status === 1) {
$scope.trustedForm.ip = '';
$scope.trustedForm.label = '';
populateTrustedSSHWhitelist();
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: 'IP added to trusted list', type: 'success', delay: 4000 });
}
} else {
var errAdd = (res && res.error) ? res.error : 'Failed to add';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: errAdd, type: 'error', delay: 6000 });
}
}
},
function(err) {
var em = (err.data && err.data.error) ? err.data.error : 'Request failed';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: em, type: 'error', delay: 6000 });
}
}
);
};
$scope.removeTrustedSSHWhitelist = function(ip) {
if (!ip) return;
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') || '' } };
$http.post('/base/sshSecurityWhitelistRemove', { ip: ip }, config).then(
function(response) {
var res = response.data;
if (typeof res === 'string') {
try { res = JSON.parse(res); } catch (e) { res = {}; }
}
if (res && res.status === 1) {
populateTrustedSSHWhitelist();
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: 'IP removed', type: 'success', delay: 4000 });
}
} else {
var errRm = (res && res.error) ? res.error : 'Failed to remove';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: errRm, type: 'error', delay: 6000 });
}
}
},
function(err) {
var em2 = (err.data && err.data.error) ? err.data.error : 'Request failed';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: em2, type: 'error', delay: 6000 });
}
}
);
};
$scope.saveTrustedSSHWhitelistRow = function(row) {
if (!row || !row.ip) return;
var payload = { ip: row.ip, label: row._l };
if (row._nip && String(row._nip).trim()) {
payload.new_ip = String(row._nip).trim();
}
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') || '' } };
$http.post('/base/sshSecurityWhitelistUpdate', payload, config).then(
function(response) {
var res = response.data;
if (typeof res === 'string') {
try { res = JSON.parse(res); } catch (e) { res = {}; }
}
var ok = res && (res.status === 1 || res.status === '1');
if (ok) {
populateTrustedSSHWhitelist();
if (typeof PNotify !== 'undefined') {
var unchanged = res.unchanged === true || res.unchanged === 'true' || res.unchanged === 1;
var msgOk = (res.message && String(res.message).length) ? res.message : (unchanged ? 'No changes to save.' : 'Entry updated');
new PNotify({ title: 'Trusted IPs', text: msgOk, type: unchanged ? 'info' : 'success', delay: 4000 });
}
} else {
var errUp = (res && res.error) ? res.error : 'Failed to update';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: errUp, type: 'error', delay: 6000 });
}
}
},
function(err) {
var em3 = (err.data && err.data.error) ? err.data.error : 'Request failed';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: em3, type: 'error', delay: 6000 });
}
}
);
};
// Expose to scope for template access
$scope.populateBannedIPs = function() {
@@ -301,7 +490,9 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
window.__firewallLoadTab = function(tab) {
$scope.$evalAsync(function() {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
if (tab === 'banned') { populateBannedIPs(); }
else if (tab === 'trusted') { populateTrustedSSHWhitelist(); }
else { populateCurrentRecords(); }
});
};
}
@@ -365,7 +556,6 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
populateCurrentRecords();
$scope.rulesLoading = true;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -377,7 +567,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
else {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -393,7 +583,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
function cantLoadInitialDatas(response) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -407,8 +597,8 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
};
function populateCurrentRecords() {
$scope.rulesLoading = false;
var gen = ++rulesFetchGen;
$scope.rulesLoading = true;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -426,21 +616,44 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data;
if (res && res.fetchStatus === 1) {
$scope.rules = typeof res.data === 'string' ? JSON.parse(res.data) : (res.data || []);
$scope.rulesTotalCount = res.total_count != null ? res.total_count : ($scope.rules ? $scope.rules.length : 0);
$scope.rulesPage = Math.max(1, res.page != null ? res.page : 1);
$scope.rulesPageSize = res.page_size != null ? res.page_size : 10;
$scope.rulesLoading = true;
if (gen !== rulesFetchGen) {
return;
}
else {
$scope.rulesLoading = true;
$scope.errorMessage = (res && res.error_message) ? res.error_message : '';
try {
var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data;
if (res && res.fetchStatus == 1) {
var parsedRules = [];
if (typeof res.data === 'string') {
try {
parsedRules = JSON.parse(res.data);
} catch (parseErr) {
parsedRules = [];
$scope.errorMessage = (res && res.error_message) ? res.error_message : 'Invalid rules data';
}
} else {
parsedRules = res.data || [];
}
$scope.rules = parsedRules;
$scope.rulesTotalCount = res.total_count != null ? res.total_count : ($scope.rules ? $scope.rules.length : 0);
$scope.rulesPage = Math.max(1, res.page != null ? res.page : 1);
$scope.rulesPageSize = res.page_size != null ? res.page_size : 10;
} else {
$scope.errorMessage = (res && res.error_message) ? res.error_message : '';
}
} catch (e) {
$scope.errorMessage = 'Could not load firewall rules.';
} finally {
if (gen === rulesFetchGen) {
$scope.rulesLoading = false;
}
}
}
function cantLoadInitialDatas(response) {
if (gen !== rulesFetchGen) {
return;
}
$scope.rulesLoading = false;
$scope.couldNotConnect = false;
}
}
@@ -519,7 +732,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.errorMessage = 'Port is required';
return;
}
$scope.rulesLoading = false;
$scope.rulesLoading = true;
var url = '/firewall/modifyRule';
var data = {
id: d.id,
@@ -530,19 +743,19 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
};
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
$http.post(url, data, config).then(function(response) {
$scope.rulesLoading = true;
if (response.data && response.data.status === 1) {
$scope.closeModifyRuleModal();
$scope.actionFailed = true;
$scope.actionSuccess = false;
populateCurrentRecords();
} else {
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = (response.data && response.data.error_message) || 'Modify failed';
}
}, function() {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = 'Could not connect to server. Please refresh this page.';
@@ -551,7 +764,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.deleteRule = function (id, proto, port, ruleIP) {
$scope.rulesLoading = false;
$scope.rulesLoading = true;
url = "/firewall/deleteRule";
@@ -579,7 +792,6 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
populateCurrentRecords();
$scope.rulesLoading = true;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -591,7 +803,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
else {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -599,7 +811,6 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.ruleAdded = true;
$scope.couldNotConnect = true;
$scope.rulesLoading = true;
$scope.errorMessage = response.data.error_message;
@@ -609,7 +820,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
function cantLoadInitialDatas(response) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -656,7 +867,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
if (response.data.reload_status == 1) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = false;
@@ -668,7 +879,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
else {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
@@ -685,7 +896,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
function cantLoadInitialDatas(response) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -731,7 +942,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
if (response.data.start_status == 1) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = false;
@@ -747,7 +958,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
else {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
@@ -764,7 +975,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
function cantLoadInitialDatas(response) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -811,7 +1022,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
if (response.data.stop_status == 1) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = false;
@@ -827,7 +1038,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
else {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
@@ -844,7 +1055,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
function cantLoadInitialDatas(response) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -1309,6 +1520,8 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
}
$scope.populateCurrentRecords = populateCurrentRecords;
});
@@ -3254,18 +3467,20 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window)
function syncFirewallTabFromHash() {
var nav = document.getElementById('firewall-tab-nav');
if (!nav) return;
var h = (window.location.hash || '').replace(/^#/, '');
var tab = (h === 'banned-ips') ? 'banned' : 'rules';
var h = (window.location.hash || '').replace(/^#/, '').toLowerCase();
var tab = 'rules';
if (h === 'banned-ips' || h === 'banned') tab = 'banned';
else if (h === 'trusted-ips' || h === 'ssh-whitelist') tab = 'trusted';
if (window.__firewallLoadTab) {
try { window.__firewallLoadTab(tab); } catch (e) {}
}
}
/* Initial sync only — hashchange is handled by Angular syncTabFromHash in firewallController
(multiple listeners were racing and could reset #trusted-ips to #rules). */
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', syncFirewallTabFromHash);
} else {
syncFirewallTabFromHash();
}
setTimeout(syncFirewallTabFromHash, 100);
window.addEventListener('hashchange', syncFirewallTabFromHash);
})();

View File

@@ -1257,15 +1257,19 @@
</div>
<!-- Tab Navigation: buttons with native fallback so clicks always work -->
<div class="tab-navigation" role="tablist" id="firewall-tab-nav">
<button type="button" class="tab-button" role="tab" data-tab="rules" title="{% trans 'Firewall Rules' %}" ng-class="{'tab-active': activeTab === 'rules'}" ng-click="setFirewallTab('rules')">
<div class="tab-navigation" id="firewall-tab-nav">
<button type="button" class="tab-button" data-tab="rules" title="{% trans 'Firewall Rules' %}" ng-class="{'tab-active': activeTab === 'rules'}" ng-click="setFirewallTab('rules', $event)">
<i class="fas fa-list-alt"></i>
{% trans "Firewall Rules" %}
</button>
<button type="button" class="tab-button" role="tab" data-tab="banned" title="{% trans 'Banned IPs' %}" ng-class="{'tab-active': activeTab === 'banned'}" ng-click="setFirewallTab('banned')">
<button type="button" class="tab-button" data-tab="banned" title="{% trans 'Banned IPs' %}" ng-class="{'tab-active': activeTab === 'banned'}" ng-click="setFirewallTab('banned', $event)">
<i class="fas fa-ban"></i>
{% trans "Banned IPs" %}
</button>
<button type="button" class="tab-button" data-tab="trusted" title="{% trans 'SSH trusted IPs' %}" ng-class="{'tab-active': activeTab === 'trusted'}" ng-click="setFirewallTab('trusted', $event)">
<i class="fas fa-shield-alt"></i>
{% trans "SSH trusted IPs" %}
</button>
</div>
<!-- Rules Panel (ng-show so panel is always in DOM; visibility toggled by tab) -->
@@ -1768,13 +1772,75 @@
</div>
</div>
</div>
<!-- SSH Security trusted IPs (same list as Dashboard → SSH Security Analysis) -->
<div class="banned-ips-panel trusted-ssh-panel" ng-show="activeTab === 'trusted'">
<div class="panel-header">
<div class="panel-title">
<div class="panel-icon">
<i class="fas fa-shield-alt"></i>
</div>
{% trans "SSH trusted IPs (never block)" %}
</div>
<div ng-show="trustedSSHLoading" class="loading-spinner"></div>
</div>
<div style="padding: 0 2rem 1rem; color: var(--text-secondary, #64748b); font-size: 0.95rem; line-height: 1.5;">
{% trans "These addresses are skipped in SSH Security Analysis and cannot be added under Banned IPs. Use your home or office public IP to avoid accidental lockouts." %}
</div>
<div class="add-banned-section" style="padding-top: 0;">
<form class="banned-form" onsubmit="return false;">
<div class="form-group">
<label class="form-label">{% trans "IP address" %}</label>
<input type="text" class="form-control" ng-model="trustedForm.ip" placeholder="{% trans 'Public IPv4 / IPv6' %}" autocomplete="off">
</div>
<div class="form-group">
<label class="form-label">{% trans "Label (optional)" %}</label>
<input type="text" class="form-control" ng-model="trustedForm.label" placeholder="{% trans 'e.g. Home PC' %}" autocomplete="off">
</div>
<button type="button" ng-click="addTrustedSSHWhitelist()" class="btn-add" style="background: var(--primary, #5b5fcf);">
<i class="fas fa-plus"></i>
{% trans "Add trusted IP" %}
</button>
</form>
</div>
<div class="table-responsive" style="padding: 0 2rem 2rem;">
<table class="rules-table" ng-if="trustedSSHWhitelist.length > 0">
<thead>
<tr>
<th>{% trans "IP" %}</th>
<th>{% trans "Label" %}</th>
<th>{% trans "Replace IP (optional)" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in trustedSSHWhitelist track by row.ip">
<td><code>{$ row.ip $}</code></td>
<td><input type="text" class="form-control" ng-model="row._l" style="min-width: 12rem;"></td>
<td><input type="text" class="form-control" ng-model="row._nip" placeholder="{% trans 'New IP' %}" style="min-width: 10rem;"></td>
<td>
<button type="button" class="btn-modify" ng-click="saveTrustedSSHWhitelistRow(row)">
<i class="fas fa-save"></i> {% trans "Save" %}
</button>
<button type="button" class="btn-delete" ng-click="removeTrustedSSHWhitelist(row.ip)">
<i class="fas fa-times"></i> {% trans "Remove" %}
</button>
</td>
</tr>
</tbody>
</table>
<div ng-if="!trustedSSHWhitelist || trustedSSHWhitelist.length === 0" class="empty-state" style="margin-top: 1rem;">
<i class="fas fa-user-shield empty-icon"></i>
<h3 class="empty-title">{% trans "No trusted IPs yet" %}</h3>
<p class="empty-text">{% trans "Add at least the public IP you use to manage this server." %}</p>
</div>
</div>
</div>
</div>
<script>
(function(){
var nav = document.getElementById('firewall-tab-nav');
if (!nav) return;
/* Bootstrapping only: race Angular controller init. Tab *clicks* use ng-click only (no duplicate nav listeners). */
function loadTabViaAngularScope(tab) {
if (!window.angular) return false;
var container = document.querySelector('.modern-container[ng-controller="firewallController"]') || document.querySelector('.modern-container');
@@ -1785,6 +1851,7 @@
scope.$evalAsync(function() {
scope.activeTab = tab;
if (tab === 'banned' && scope.populateBannedIPs) scope.populateBannedIPs();
else if (tab === 'trusted' && scope.populateTrustedSSHWhitelist) scope.populateTrustedSSHWhitelist();
else if (tab === 'rules' && scope.populateCurrentRecords) scope.populateCurrentRecords();
});
return true;
@@ -1794,7 +1861,7 @@
}
function loadTab(tab) {
if (!tab || (tab !== 'rules' && tab !== 'banned')) return;
if (!tab || (tab !== 'rules' && tab !== 'banned' && tab !== 'trusted')) return;
var done = false;
if (window.__firewallLoadTab) {
try { window.__firewallLoadTab(tab); done = true; } catch (e) {}
@@ -1811,38 +1878,18 @@
if (window.__firewallLoadTab) { try { window.__firewallLoadTab(tab); } catch (e) {} }
else { loadTabViaAngularScope(tab); }
}, 200);
setTimeout(function() {
if (window.__firewallLoadTab) { try { window.__firewallLoadTab(tab); } catch (e) {} }
else { loadTabViaAngularScope(tab); }
}, 500);
}
}
function onTabButtonClick(btn) {
var tab = btn && btn.getAttribute('data-tab');
if (!tab) return;
window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules';
loadTab(tab);
}
nav.addEventListener('click', function(e) {
var btn = (e.target && e.target.closest) ? e.target.closest('.tab-button[data-tab]') : null;
if (btn && nav.contains(btn)) onTabButtonClick(btn);
}, false);
nav.addEventListener('mousedown', function(e) {
var btn = (e.target && e.target.closest) ? e.target.closest('.tab-button[data-tab]') : null;
if (btn && nav.contains(btn)) onTabButtonClick(btn);
}, false);
function loadTabFromHash() {
var h = (window.location.hash || '').replace(/^#/, '');
var tab = (h === 'banned-ips') ? 'banned' : 'rules';
var h = (window.location.hash || '').replace(/^#/, '').toLowerCase();
var tab = 'rules';
if (h === 'banned-ips' || h === 'banned') tab = 'banned';
else if (h === 'trusted-ips' || h === 'ssh-whitelist') tab = 'trusted';
loadTab(tab);
}
var h = (window.location.hash || '').replace(/^#/, '');
if (h === 'banned-ips') loadTabFromHash();
window.addEventListener('hashchange', loadTabFromHash);
setTimeout(loadTabFromHash, 150);
setTimeout(loadTabFromHash, 500);
var h = (window.location.hash || '').replace(/^#/, '').toLowerCase();
if (h === 'banned-ips' || h === 'banned' || h === 'trusted-ips' || h === 'ssh-whitelist') loadTabFromHash();
})();
</script>

View File

@@ -145,6 +145,11 @@ def verifyLogin(request):
request.session['ipAddr'] = ipAddr
request.session.set_expiry(43200)
try:
from plogical.sshSecurityWhitelistUtilities import SSHSecurityWhitelistUtilities
SSHSecurityWhitelistUtilities.on_successful_panel_login(request, admin)
except Exception:
pass
data = {'userID': admin.pk, 'loginStatus': 1, 'error_message': "None"}
json_data = json.dumps(data)
response.write(json_data)

View File

@@ -0,0 +1,377 @@
#!/usr/local/CyberCP/bin/python
# -*- coding: utf-8 -*-
"""
SSH Security Analysis — IPs that must never be blocked (firewalld drop / Banned IPs).
Stored in /usr/local/CyberCP/data/ssh_security_whitelist.json as a JSON array of objects:
[{"ip": "203.0.113.10", "label": "Office", "updated": 1710000000}, ...]
"""
from __future__ import annotations
import ipaddress
import json
import os
import time
from typing import Any, Dict, List, Optional, Set
WHITELIST_PATH = '/usr/local/CyberCP/data/ssh_security_whitelist.json'
PUBLIC_IP_CACHE_PATH = '/usr/local/CyberCP/data/ssh_whitelist_public_ipv4.cache.json'
FIRST_ADMIN_LOGIN_FLAG_PATH = '/usr/local/CyberCP/data/ssh_whitelist_first_admin_login.recorded'
LABEL_SERVER_AUTO = 'CyberPanel server public IPv4 (auto)'
LABEL_FIRST_ADMIN_AUTO = 'First CyberPanel admin login (auto)'
class SSHSecurityWhitelistUtilities:
"""Load/save trusted IPs for SSH security analysis and firewall ban protection."""
@staticmethod
def _ensure_data_dir() -> None:
d = os.path.dirname(WHITELIST_PATH)
if d:
try:
os.makedirs(d, mode=0o750, exist_ok=True)
except OSError:
pass
@staticmethod
def normalize_ip(ip: str) -> str:
raw = (ip or '').strip()
if not raw:
return ''
host = raw.split('/')[0].strip()
if '%' in host:
host = host.split('%', 1)[0]
if host.startswith('[') and host.endswith(']'):
host = host[1:-1]
try:
return str(ipaddress.ip_address(host))
except ValueError:
return ''
@staticmethod
def normalized_ip_in_whitelist(raw_ip: str, wl_set: Set[str]) -> bool:
"""True if raw IP from a log line normalizes to an address in wl_set."""
if not raw_ip or not wl_set:
return False
norm = SSHSecurityWhitelistUtilities.normalize_ip(raw_ip)
return bool(norm and norm in wl_set)
@staticmethod
def validate_ip(ip: str) -> Optional[str]:
norm = SSHSecurityWhitelistUtilities.normalize_ip(ip)
if not norm:
return None
try:
obj = ipaddress.ip_address(norm)
except ValueError:
return None
if (
obj.is_private
or obj.is_loopback
or obj.is_link_local
or obj.is_multicast
or obj.is_reserved
):
return None
return norm
@staticmethod
def load_entries() -> List[Dict[str, Any]]:
SSHSecurityWhitelistUtilities._ensure_data_dir()
if not os.path.isfile(WHITELIST_PATH):
return []
try:
with open(WHITELIST_PATH, 'r', encoding='utf-8', errors='replace') as f:
data = json.load(f)
if not isinstance(data, list):
return []
out: List[Dict[str, Any]] = []
for item in data:
if not isinstance(item, dict):
continue
ip = item.get('ip') or item.get('address')
norm = SSHSecurityWhitelistUtilities.normalize_ip(str(ip) if ip else '')
if not norm:
continue
label = (item.get('label') or item.get('note') or '').strip()
updated = item.get('updated') or item.get('modified') or 0
try:
updated = int(updated)
except (TypeError, ValueError):
updated = 0
out.append({'ip': norm, 'label': label, 'updated': updated})
return out
except (OSError, json.JSONDecodeError):
return []
@staticmethod
def save_entries(entries: List[Dict[str, Any]]) -> bool:
SSHSecurityWhitelistUtilities._ensure_data_dir()
try:
serializable = []
for e in entries:
serializable.append({
'ip': e['ip'],
'label': e.get('label') or '',
'updated': int(e.get('updated') or 0),
})
tmp = WHITELIST_PATH + '.tmp'
with open(tmp, 'w', encoding='utf-8') as f:
json.dump(serializable, f, indent=2, ensure_ascii=False)
f.write('\n')
os.replace(tmp, WHITELIST_PATH)
try:
os.chmod(WHITELIST_PATH, 0o640)
except OSError:
pass
try:
import pwd
import grp
uid = pwd.getpwnam('cyberpanel').pw_uid
gid = grp.getgrnam('cyberpanel').gr_gid
os.chown(WHITELIST_PATH, uid, gid)
except (OSError, KeyError, ImportError):
pass
return True
except OSError:
return False
@staticmethod
def ip_set() -> Set[str]:
return {e['ip'] for e in SSHSecurityWhitelistUtilities.load_entries()}
@staticmethod
def is_whitelisted(ip: str) -> bool:
norm = SSHSecurityWhitelistUtilities.normalize_ip(ip)
if not norm:
return False
return norm in SSHSecurityWhitelistUtilities.ip_set()
@staticmethod
def add_entry(ip: str, label: str = '') -> tuple:
v = SSHSecurityWhitelistUtilities.validate_ip(ip)
if not v:
return False, 'Invalid or non-public IP address'
entries = SSHSecurityWhitelistUtilities.load_entries()
label = (label or '').strip()[:200]
now = int(time.time())
for e in entries:
if e['ip'] == v:
e['label'] = label
e['updated'] = now
if not SSHSecurityWhitelistUtilities.save_entries(entries):
return False, 'Failed to save whitelist'
return True, v
entries.append({'ip': v, 'label': label, 'updated': now})
if not SSHSecurityWhitelistUtilities.save_entries(entries):
return False, 'Failed to save whitelist'
return True, v
@staticmethod
def remove_entry(ip: str) -> tuple:
norm = SSHSecurityWhitelistUtilities.normalize_ip(ip)
if not norm:
return False, 'Invalid IP address'
entries = SSHSecurityWhitelistUtilities.load_entries()
new_list = [e for e in entries if e['ip'] != norm]
if len(new_list) == len(entries):
return False, 'IP not found in whitelist'
if not SSHSecurityWhitelistUtilities.save_entries(new_list):
return False, 'Failed to save whitelist'
return True, norm
@staticmethod
def update_entry(ip: str, new_ip: Optional[str] = None, label: Optional[str] = None) -> tuple:
"""
Update whitelist row. Returns (ok, ip_or_error_message, unchanged).
unchanged is True when nothing differed from stored values (still success).
"""
norm = SSHSecurityWhitelistUtilities.normalize_ip(ip)
if not norm:
return False, 'Invalid IP address', False
entries = SSHSecurityWhitelistUtilities.load_entries()
idx = next((i for i, e in enumerate(entries) if e['ip'] == norm), -1)
if idx < 0:
return False, 'IP not found in whitelist', False
now = int(time.time())
target_ip = norm
changed = False
current_label = str(entries[idx].get('label') or '').strip()[:200]
if new_ip is not None and str(new_ip).strip():
v = SSHSecurityWhitelistUtilities.validate_ip(str(new_ip).strip())
if not v:
return False, 'Invalid or non-public new IP address', False
if any(e['ip'] == v for e in entries if e['ip'] != norm):
return False, 'New IP already listed', False
if v != norm:
entries[idx]['ip'] = v
target_ip = v
changed = True
if label is not None:
new_l = str(label).strip()[:200]
if new_l != current_label:
entries[idx]['label'] = new_l
changed = True
if not changed:
return True, norm, True
entries[idx]['updated'] = now
if not SSHSecurityWhitelistUtilities.save_entries(entries):
return False, 'Failed to save whitelist', False
return True, target_ip, False
@staticmethod
def client_ip_from_request(request: Any) -> str:
"""Best-effort client IP for whitelisting (CF header or REMOTE_ADDR)."""
if request is None:
return ''
try:
meta = getattr(request, 'META', None) or {}
raw = meta.get('HTTP_CF_CONNECTING_IP') or meta.get('REMOTE_ADDR') or ''
raw = str(raw).split(',')[0].strip()
if '%' in raw:
raw = raw.split('%')[0]
return raw
except Exception:
return ''
@staticmethod
def upsert_whitelist_ip_if_absent(ip: str, label: str) -> bool:
"""
Add public IP to whitelist only if not already listed (does not overwrite labels).
"""
v = SSHSecurityWhitelistUtilities.validate_ip(ip)
if not v:
return False
entries = SSHSecurityWhitelistUtilities.load_entries()
label = (label or '').strip()[:200]
now = int(time.time())
for e in entries:
if e['ip'] == v:
return True
entries.append({'ip': v, 'label': label, 'updated': now})
return SSHSecurityWhitelistUtilities.save_entries(entries)
@staticmethod
def _fetch_ipv4_public_ip() -> str:
import re
import urllib.request
urls = (
'https://ipv4.icanhazip.com',
'https://api.ipify.org',
'https://checkip.amazonaws.com',
)
for url in urls:
try:
with urllib.request.urlopen(url, timeout=8) as resp:
ip = resp.read().decode('utf-8', errors='replace').strip()
if re.match(r'^(\d{1,3}\.){3}\d{1,3}$', ip):
return ip
except Exception:
continue
return ''
@staticmethod
def ensure_cyberpanel_public_ip_whitelisted(max_cache_age: int = 3600) -> None:
"""
Ensure this machine's outbound/public IPv4 is on the whitelist (SSH + ban protection).
Uses a short-lived cache to avoid HTTP self-lookups on every request.
"""
SSHSecurityWhitelistUtilities._ensure_data_dir()
now = int(time.time())
cached_ip = ''
cache_ts = 0
try:
if os.path.isfile(PUBLIC_IP_CACHE_PATH):
with open(PUBLIC_IP_CACHE_PATH, 'r', encoding='utf-8', errors='replace') as fc:
cj = json.load(fc)
if isinstance(cj, dict):
cached_ip = str(cj.get('ip') or '').strip()
try:
cache_ts = int(cj.get('ts') or 0)
except (TypeError, ValueError):
cache_ts = 0
except (OSError, json.JSONDecodeError):
pass
ip_to_try = ''
if cached_ip and (now - cache_ts) < max_cache_age:
ip_to_try = cached_ip
else:
detected = SSHSecurityWhitelistUtilities._fetch_ipv4_public_ip()
if detected:
ip_to_try = detected
try:
tmp = PUBLIC_IP_CACHE_PATH + '.tmp'
with open(tmp, 'w', encoding='utf-8') as ft:
json.dump({'ip': detected, 'ts': now}, ft, indent=2)
ft.write('\n')
os.replace(tmp, PUBLIC_IP_CACHE_PATH)
try:
os.chmod(PUBLIC_IP_CACHE_PATH, 0o640)
except OSError:
pass
except OSError:
pass
elif cached_ip:
ip_to_try = cached_ip
if not ip_to_try:
return
SSHSecurityWhitelistUtilities.upsert_whitelist_ip_if_absent(ip_to_try, LABEL_SERVER_AUTO)
@staticmethod
def maybe_whitelist_first_admin_login(client_ip: str) -> None:
"""
Once per installation: whitelist the client IP of the first successful login
by a user with admin ACL (adminStatus == 1).
"""
if os.path.isfile(FIRST_ADMIN_LOGIN_FLAG_PATH):
return
v = SSHSecurityWhitelistUtilities.validate_ip(client_ip)
if not v:
return
SSHSecurityWhitelistUtilities.upsert_whitelist_ip_if_absent(v, LABEL_FIRST_ADMIN_AUTO)
try:
tmp = FIRST_ADMIN_LOGIN_FLAG_PATH + '.tmp'
with open(tmp, 'w', encoding='utf-8') as tf:
tf.write(v + '\n')
os.replace(tmp, FIRST_ADMIN_LOGIN_FLAG_PATH)
try:
os.chmod(FIRST_ADMIN_LOGIN_FLAG_PATH, 0o640)
except OSError:
pass
try:
import grp
import pwd
uid = pwd.getpwnam('cyberpanel').pw_uid
gid = grp.getgrnam('cyberpanel').gr_gid
os.chown(FIRST_ADMIN_LOGIN_FLAG_PATH, uid, gid)
except (OSError, KeyError, ImportError):
pass
except OSError:
pass
@staticmethod
def on_successful_panel_login(request: Any, admin: Any) -> None:
"""
Called after a successful CyberPanel login. Safe to call on every login; errors swallowed.
"""
try:
SSHSecurityWhitelistUtilities.ensure_cyberpanel_public_ip_whitelisted()
except Exception:
pass
try:
acl = getattr(admin, 'acl', None)
if acl is None:
return
if int(getattr(acl, 'adminStatus', 0) or 0) != 1:
return
ip = SSHSecurityWhitelistUtilities.client_ip_from_request(request)
SSHSecurityWhitelistUtilities.maybe_whitelist_first_admin_login(ip)
except Exception:
pass

View File

@@ -23,6 +23,8 @@ function getCookie(name) {
app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.rulesLoading = true;
/** Incremented on each rules fetch; stale HTTP responses must not touch rulesLoading. */
var rulesFetchGen = 0;
$scope.actionFailed = true;
$scope.actionSuccess = true;
$scope.showExportFormatModal = false;
@@ -40,9 +42,12 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
// Initialize rules array - prevents "Cannot read 'length' of undefined" when template evaluates rules.length before API loads
$scope.rules = [];
// Banned IPs variables tab from hash so we stay on /firewall/ (avoids 404 on servers without /firewall/firewall-rules/)
/* Use window.location.hash only. Angular $location can disagree with the fragment on /firewall/#… and wrongly map to "rules". */
function tabFromHash() {
var h = (window.location.hash || '').replace(/^#/, '');
return (h === 'banned-ips') ? 'banned' : 'rules';
var h = String(window.location.hash || '').replace(/^#/, '').toLowerCase();
if (h === 'banned-ips' || h === 'banned') return 'banned';
if (h === 'trusted-ips' || h === 'ssh-whitelist') return 'trusted';
return 'rules';
}
$scope.activeTab = tabFromHash();
$scope.bannedIPs = []; // Initialize as empty array
@@ -52,7 +57,9 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
var tab = tabFromHash();
if ($scope.activeTab !== tab) {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
if (tab === 'banned') { populateBannedIPs(); }
else if (tab === 'trusted') { populateTrustedSSHWhitelist(); }
else { populateCurrentRecords(); }
if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); }
}
}
@@ -63,23 +70,51 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
window.addEventListener('load', function() { $timeout(applyTabFromHash, 0); });
}
// Sync tab with hash and load that tab's data on switch
$scope.setFirewallTab = function(tab) {
// Sync tab with hash and load that tab's data on switch (single source of truth from ng-click).
$scope.setFirewallTab = function(tab, $event) {
if ($event) {
try {
$event.stopPropagation();
} catch (ignoreErr) {}
}
$timeout(function() {
$scope.activeTab = tab;
window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules';
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
function setHashIfNeeded(frag) {
try {
if ((window.location.hash || '') === frag) {
return;
}
var path = window.location.pathname + window.location.search + frag;
if (window.history && typeof window.history.replaceState === 'function') {
window.history.replaceState(null, '', path);
} else {
window.location.hash = frag;
}
} catch (ignoreHash) {}
}
if (tab === 'banned') {
setHashIfNeeded('#banned-ips');
populateBannedIPs();
} else if (tab === 'trusted') {
setHashIfNeeded('#trusted-ips');
populateTrustedSSHWhitelist();
} else {
setHashIfNeeded('#rules');
populateCurrentRecords();
}
}, 0);
};
// Back/forward or direct hash change: sync tab and load its data
function syncTabFromHash() {
var tab = tabFromHash();
if ($scope.activeTab !== tab) {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); }
}
$scope.$evalAsync(function() {
if ($scope.activeTab !== tab) {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); }
else if (tab === 'trusted') { populateTrustedSSHWhitelist(); }
else { populateCurrentRecords(); }
}
});
}
window.addEventListener('hashchange', syncTabFromHash);
@@ -108,6 +143,9 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.banIP = '';
$scope.banReason = '';
$scope.banDuration = '24h';
$scope.trustedSSHWhitelist = [];
$scope.trustedForm = { ip: '', label: '' };
$scope.trustedSSHLoading = false;
$scope.bannedIPSearch = '';
$scope.searchBannedIPFilter = function(item) {
var q = ($scope.bannedIPSearch || '').toLowerCase().trim();
@@ -142,6 +180,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$timeout(function() {
try {
if (newVal === 'banned' && typeof populateBannedIPs === 'function') populateBannedIPs();
else if (newVal === 'trusted' && typeof populateTrustedSSHWhitelist === 'function') populateTrustedSSHWhitelist();
else if (newVal === 'rules' && typeof populateCurrentRecords === 'function') populateCurrentRecords();
} catch (e) {}
}, 0);
@@ -246,6 +285,156 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
);
}
function populateTrustedSSHWhitelist() {
$scope.trustedSSHLoading = true;
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') || '' } };
$http.post('/base/sshSecurityWhitelistList', {}, config).then(
function(response) {
$scope.trustedSSHLoading = false;
var res = response.data;
if (typeof res === 'string') {
try { res = JSON.parse(res); } catch (e) { res = {}; }
}
if (res && res.status === 1) {
var ent = res.entries || [];
$scope.trustedSSHWhitelist = ent.map(function(e) {
return {
ip: e.ip,
label: e.label || '',
updated: e.updated || 0,
_l: e.label || '',
_nip: ''
};
});
} else {
$scope.trustedSSHWhitelist = [];
var errMsg = (res && res.error) ? res.error : 'Could not load trusted IPs';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: errMsg, type: 'error', delay: 6000 });
}
}
},
function(error) {
$scope.trustedSSHLoading = false;
$scope.trustedSSHWhitelist = [];
var msg = (error.data && error.data.error) ? error.data.error : 'Request failed';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: msg, type: 'error', delay: 6000 });
}
}
);
}
$scope.populateTrustedSSHWhitelist = function() {
populateTrustedSSHWhitelist();
};
$scope.addTrustedSSHWhitelist = function() {
var ip = ($scope.trustedForm.ip || '').trim();
var label = ($scope.trustedForm.label || '').trim();
if (!ip) {
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: 'Enter an IP address', type: 'warning', delay: 5000 });
}
return;
}
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') || '' } };
$http.post('/base/sshSecurityWhitelistAdd', { ip: ip, label: label }, config).then(
function(response) {
var res = response.data;
if (typeof res === 'string') {
try { res = JSON.parse(res); } catch (e) { res = {}; }
}
if (res && res.status === 1) {
$scope.trustedForm.ip = '';
$scope.trustedForm.label = '';
populateTrustedSSHWhitelist();
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: 'IP added to trusted list', type: 'success', delay: 4000 });
}
} else {
var errAdd = (res && res.error) ? res.error : 'Failed to add';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: errAdd, type: 'error', delay: 6000 });
}
}
},
function(err) {
var em = (err.data && err.data.error) ? err.data.error : 'Request failed';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: em, type: 'error', delay: 6000 });
}
}
);
};
$scope.removeTrustedSSHWhitelist = function(ip) {
if (!ip) return;
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') || '' } };
$http.post('/base/sshSecurityWhitelistRemove', { ip: ip }, config).then(
function(response) {
var res = response.data;
if (typeof res === 'string') {
try { res = JSON.parse(res); } catch (e) { res = {}; }
}
if (res && res.status === 1) {
populateTrustedSSHWhitelist();
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: 'IP removed', type: 'success', delay: 4000 });
}
} else {
var errRm = (res && res.error) ? res.error : 'Failed to remove';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: errRm, type: 'error', delay: 6000 });
}
}
},
function(err) {
var em2 = (err.data && err.data.error) ? err.data.error : 'Request failed';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: em2, type: 'error', delay: 6000 });
}
}
);
};
$scope.saveTrustedSSHWhitelistRow = function(row) {
if (!row || !row.ip) return;
var payload = { ip: row.ip, label: row._l };
if (row._nip && String(row._nip).trim()) {
payload.new_ip = String(row._nip).trim();
}
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') || '' } };
$http.post('/base/sshSecurityWhitelistUpdate', payload, config).then(
function(response) {
var res = response.data;
if (typeof res === 'string') {
try { res = JSON.parse(res); } catch (e) { res = {}; }
}
var ok = res && (res.status === 1 || res.status === '1');
if (ok) {
populateTrustedSSHWhitelist();
if (typeof PNotify !== 'undefined') {
var unchanged = res.unchanged === true || res.unchanged === 'true' || res.unchanged === 1;
var msgOk = (res.message && String(res.message).length) ? res.message : (unchanged ? 'No changes to save.' : 'Entry updated');
new PNotify({ title: 'Trusted IPs', text: msgOk, type: unchanged ? 'info' : 'success', delay: 4000 });
}
} else {
var errUp = (res && res.error) ? res.error : 'Failed to update';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: errUp, type: 'error', delay: 6000 });
}
}
},
function(err) {
var em3 = (err.data && err.data.error) ? err.data.error : 'Request failed';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: em3, type: 'error', delay: 6000 });
}
}
);
};
// Expose to scope for template access
$scope.populateBannedIPs = function() {
@@ -301,7 +490,9 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
window.__firewallLoadTab = function(tab) {
$scope.$evalAsync(function() {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
if (tab === 'banned') { populateBannedIPs(); }
else if (tab === 'trusted') { populateTrustedSSHWhitelist(); }
else { populateCurrentRecords(); }
});
};
}
@@ -365,7 +556,6 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
populateCurrentRecords();
$scope.rulesLoading = true;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -377,7 +567,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
else {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -393,7 +583,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
function cantLoadInitialDatas(response) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -407,8 +597,8 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
};
function populateCurrentRecords() {
$scope.rulesLoading = false;
var gen = ++rulesFetchGen;
$scope.rulesLoading = true;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -426,21 +616,44 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data;
if (res && res.fetchStatus === 1) {
$scope.rules = typeof res.data === 'string' ? JSON.parse(res.data) : (res.data || []);
$scope.rulesTotalCount = res.total_count != null ? res.total_count : ($scope.rules ? $scope.rules.length : 0);
$scope.rulesPage = Math.max(1, res.page != null ? res.page : 1);
$scope.rulesPageSize = res.page_size != null ? res.page_size : 10;
$scope.rulesLoading = true;
if (gen !== rulesFetchGen) {
return;
}
else {
$scope.rulesLoading = true;
$scope.errorMessage = (res && res.error_message) ? res.error_message : '';
try {
var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data;
if (res && res.fetchStatus == 1) {
var parsedRules = [];
if (typeof res.data === 'string') {
try {
parsedRules = JSON.parse(res.data);
} catch (parseErr) {
parsedRules = [];
$scope.errorMessage = (res && res.error_message) ? res.error_message : 'Invalid rules data';
}
} else {
parsedRules = res.data || [];
}
$scope.rules = parsedRules;
$scope.rulesTotalCount = res.total_count != null ? res.total_count : ($scope.rules ? $scope.rules.length : 0);
$scope.rulesPage = Math.max(1, res.page != null ? res.page : 1);
$scope.rulesPageSize = res.page_size != null ? res.page_size : 10;
} else {
$scope.errorMessage = (res && res.error_message) ? res.error_message : '';
}
} catch (e) {
$scope.errorMessage = 'Could not load firewall rules.';
} finally {
if (gen === rulesFetchGen) {
$scope.rulesLoading = false;
}
}
}
function cantLoadInitialDatas(response) {
if (gen !== rulesFetchGen) {
return;
}
$scope.rulesLoading = false;
$scope.couldNotConnect = false;
}
}
@@ -519,7 +732,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.errorMessage = 'Port is required';
return;
}
$scope.rulesLoading = false;
$scope.rulesLoading = true;
var url = '/firewall/modifyRule';
var data = {
id: d.id,
@@ -530,19 +743,19 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
};
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
$http.post(url, data, config).then(function(response) {
$scope.rulesLoading = true;
if (response.data && response.data.status === 1) {
$scope.closeModifyRuleModal();
$scope.actionFailed = true;
$scope.actionSuccess = false;
populateCurrentRecords();
} else {
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = (response.data && response.data.error_message) || 'Modify failed';
}
}, function() {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = 'Could not connect to server. Please refresh this page.';
@@ -551,7 +764,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.deleteRule = function (id, proto, port, ruleIP) {
$scope.rulesLoading = false;
$scope.rulesLoading = true;
url = "/firewall/deleteRule";
@@ -579,7 +792,6 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
populateCurrentRecords();
$scope.rulesLoading = true;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -591,7 +803,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
else {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -599,7 +811,6 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.ruleAdded = true;
$scope.couldNotConnect = true;
$scope.rulesLoading = true;
$scope.errorMessage = response.data.error_message;
@@ -609,7 +820,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
function cantLoadInitialDatas(response) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -656,7 +867,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
if (response.data.reload_status == 1) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = false;
@@ -668,7 +879,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
else {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
@@ -685,7 +896,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
function cantLoadInitialDatas(response) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -731,7 +942,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
if (response.data.start_status == 1) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = false;
@@ -747,7 +958,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
else {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
@@ -764,7 +975,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
function cantLoadInitialDatas(response) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -811,7 +1022,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
if (response.data.stop_status == 1) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = false;
@@ -827,7 +1038,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
else {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
@@ -844,7 +1055,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
function cantLoadInitialDatas(response) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -1309,6 +1520,8 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
}
$scope.populateCurrentRecords = populateCurrentRecords;
});
@@ -3254,18 +3467,20 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window)
function syncFirewallTabFromHash() {
var nav = document.getElementById('firewall-tab-nav');
if (!nav) return;
var h = (window.location.hash || '').replace(/^#/, '');
var tab = (h === 'banned-ips') ? 'banned' : 'rules';
var h = (window.location.hash || '').replace(/^#/, '').toLowerCase();
var tab = 'rules';
if (h === 'banned-ips' || h === 'banned') tab = 'banned';
else if (h === 'trusted-ips' || h === 'ssh-whitelist') tab = 'trusted';
if (window.__firewallLoadTab) {
try { window.__firewallLoadTab(tab); } catch (e) {}
}
}
/* Initial sync only — hashchange is handled by Angular syncTabFromHash in firewallController
(multiple listeners were racing and could reset #trusted-ips to #rules). */
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', syncFirewallTabFromHash);
} else {
syncFirewallTabFromHash();
}
setTimeout(syncFirewallTabFromHash, 100);
window.addEventListener('hashchange', syncFirewallTabFromHash);
})();

View File

@@ -23,6 +23,8 @@ function getCookie(name) {
app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.rulesLoading = true;
/** Incremented on each rules fetch; stale HTTP responses must not touch rulesLoading. */
var rulesFetchGen = 0;
$scope.actionFailed = true;
$scope.actionSuccess = true;
$scope.showExportFormatModal = false;
@@ -40,9 +42,12 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
// Initialize rules array - prevents "Cannot read 'length' of undefined" when template evaluates rules.length before API loads
$scope.rules = [];
// Banned IPs variables tab from hash so we stay on /firewall/ (avoids 404 on servers without /firewall/firewall-rules/)
/* Use window.location.hash only. Angular $location can disagree with the fragment on /firewall/#… and wrongly map to "rules". */
function tabFromHash() {
var h = (window.location.hash || '').replace(/^#/, '');
return (h === 'banned-ips') ? 'banned' : 'rules';
var h = String(window.location.hash || '').replace(/^#/, '').toLowerCase();
if (h === 'banned-ips' || h === 'banned') return 'banned';
if (h === 'trusted-ips' || h === 'ssh-whitelist') return 'trusted';
return 'rules';
}
$scope.activeTab = tabFromHash();
$scope.bannedIPs = []; // Initialize as empty array
@@ -52,7 +57,9 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
var tab = tabFromHash();
if ($scope.activeTab !== tab) {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
if (tab === 'banned') { populateBannedIPs(); }
else if (tab === 'trusted') { populateTrustedSSHWhitelist(); }
else { populateCurrentRecords(); }
if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); }
}
}
@@ -63,23 +70,51 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
window.addEventListener('load', function() { $timeout(applyTabFromHash, 0); });
}
// Sync tab with hash and load that tab's data on switch
$scope.setFirewallTab = function(tab) {
// Sync tab with hash and load that tab's data on switch (single source of truth from ng-click).
$scope.setFirewallTab = function(tab, $event) {
if ($event) {
try {
$event.stopPropagation();
} catch (ignoreErr) {}
}
$timeout(function() {
$scope.activeTab = tab;
window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules';
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
function setHashIfNeeded(frag) {
try {
if ((window.location.hash || '') === frag) {
return;
}
var path = window.location.pathname + window.location.search + frag;
if (window.history && typeof window.history.replaceState === 'function') {
window.history.replaceState(null, '', path);
} else {
window.location.hash = frag;
}
} catch (ignoreHash) {}
}
if (tab === 'banned') {
setHashIfNeeded('#banned-ips');
populateBannedIPs();
} else if (tab === 'trusted') {
setHashIfNeeded('#trusted-ips');
populateTrustedSSHWhitelist();
} else {
setHashIfNeeded('#rules');
populateCurrentRecords();
}
}, 0);
};
// Back/forward or direct hash change: sync tab and load its data
function syncTabFromHash() {
var tab = tabFromHash();
if ($scope.activeTab !== tab) {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); }
}
$scope.$evalAsync(function() {
if ($scope.activeTab !== tab) {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); }
else if (tab === 'trusted') { populateTrustedSSHWhitelist(); }
else { populateCurrentRecords(); }
}
});
}
window.addEventListener('hashchange', syncTabFromHash);
@@ -108,6 +143,9 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.banIP = '';
$scope.banReason = '';
$scope.banDuration = '24h';
$scope.trustedSSHWhitelist = [];
$scope.trustedForm = { ip: '', label: '' };
$scope.trustedSSHLoading = false;
$scope.bannedIPSearch = '';
$scope.searchBannedIPFilter = function(item) {
var q = ($scope.bannedIPSearch || '').toLowerCase().trim();
@@ -142,6 +180,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$timeout(function() {
try {
if (newVal === 'banned' && typeof populateBannedIPs === 'function') populateBannedIPs();
else if (newVal === 'trusted' && typeof populateTrustedSSHWhitelist === 'function') populateTrustedSSHWhitelist();
else if (newVal === 'rules' && typeof populateCurrentRecords === 'function') populateCurrentRecords();
} catch (e) {}
}, 0);
@@ -246,6 +285,156 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
);
}
function populateTrustedSSHWhitelist() {
$scope.trustedSSHLoading = true;
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') || '' } };
$http.post('/base/sshSecurityWhitelistList', {}, config).then(
function(response) {
$scope.trustedSSHLoading = false;
var res = response.data;
if (typeof res === 'string') {
try { res = JSON.parse(res); } catch (e) { res = {}; }
}
if (res && res.status === 1) {
var ent = res.entries || [];
$scope.trustedSSHWhitelist = ent.map(function(e) {
return {
ip: e.ip,
label: e.label || '',
updated: e.updated || 0,
_l: e.label || '',
_nip: ''
};
});
} else {
$scope.trustedSSHWhitelist = [];
var errMsg = (res && res.error) ? res.error : 'Could not load trusted IPs';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: errMsg, type: 'error', delay: 6000 });
}
}
},
function(error) {
$scope.trustedSSHLoading = false;
$scope.trustedSSHWhitelist = [];
var msg = (error.data && error.data.error) ? error.data.error : 'Request failed';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: msg, type: 'error', delay: 6000 });
}
}
);
}
$scope.populateTrustedSSHWhitelist = function() {
populateTrustedSSHWhitelist();
};
$scope.addTrustedSSHWhitelist = function() {
var ip = ($scope.trustedForm.ip || '').trim();
var label = ($scope.trustedForm.label || '').trim();
if (!ip) {
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: 'Enter an IP address', type: 'warning', delay: 5000 });
}
return;
}
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') || '' } };
$http.post('/base/sshSecurityWhitelistAdd', { ip: ip, label: label }, config).then(
function(response) {
var res = response.data;
if (typeof res === 'string') {
try { res = JSON.parse(res); } catch (e) { res = {}; }
}
if (res && res.status === 1) {
$scope.trustedForm.ip = '';
$scope.trustedForm.label = '';
populateTrustedSSHWhitelist();
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: 'IP added to trusted list', type: 'success', delay: 4000 });
}
} else {
var errAdd = (res && res.error) ? res.error : 'Failed to add';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: errAdd, type: 'error', delay: 6000 });
}
}
},
function(err) {
var em = (err.data && err.data.error) ? err.data.error : 'Request failed';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: em, type: 'error', delay: 6000 });
}
}
);
};
$scope.removeTrustedSSHWhitelist = function(ip) {
if (!ip) return;
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') || '' } };
$http.post('/base/sshSecurityWhitelistRemove', { ip: ip }, config).then(
function(response) {
var res = response.data;
if (typeof res === 'string') {
try { res = JSON.parse(res); } catch (e) { res = {}; }
}
if (res && res.status === 1) {
populateTrustedSSHWhitelist();
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: 'IP removed', type: 'success', delay: 4000 });
}
} else {
var errRm = (res && res.error) ? res.error : 'Failed to remove';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: errRm, type: 'error', delay: 6000 });
}
}
},
function(err) {
var em2 = (err.data && err.data.error) ? err.data.error : 'Request failed';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: em2, type: 'error', delay: 6000 });
}
}
);
};
$scope.saveTrustedSSHWhitelistRow = function(row) {
if (!row || !row.ip) return;
var payload = { ip: row.ip, label: row._l };
if (row._nip && String(row._nip).trim()) {
payload.new_ip = String(row._nip).trim();
}
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') || '' } };
$http.post('/base/sshSecurityWhitelistUpdate', payload, config).then(
function(response) {
var res = response.data;
if (typeof res === 'string') {
try { res = JSON.parse(res); } catch (e) { res = {}; }
}
var ok = res && (res.status === 1 || res.status === '1');
if (ok) {
populateTrustedSSHWhitelist();
if (typeof PNotify !== 'undefined') {
var unchanged = res.unchanged === true || res.unchanged === 'true' || res.unchanged === 1;
var msgOk = (res.message && String(res.message).length) ? res.message : (unchanged ? 'No changes to save.' : 'Entry updated');
new PNotify({ title: 'Trusted IPs', text: msgOk, type: unchanged ? 'info' : 'success', delay: 4000 });
}
} else {
var errUp = (res && res.error) ? res.error : 'Failed to update';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: errUp, type: 'error', delay: 6000 });
}
}
},
function(err) {
var em3 = (err.data && err.data.error) ? err.data.error : 'Request failed';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Trusted IPs', text: em3, type: 'error', delay: 6000 });
}
}
);
};
// Expose to scope for template access
$scope.populateBannedIPs = function() {
@@ -301,7 +490,9 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
window.__firewallLoadTab = function(tab) {
$scope.$evalAsync(function() {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
if (tab === 'banned') { populateBannedIPs(); }
else if (tab === 'trusted') { populateTrustedSSHWhitelist(); }
else { populateCurrentRecords(); }
});
};
}
@@ -365,7 +556,6 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
populateCurrentRecords();
$scope.rulesLoading = true;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -377,7 +567,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
else {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -393,7 +583,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
function cantLoadInitialDatas(response) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -407,8 +597,8 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
};
function populateCurrentRecords() {
$scope.rulesLoading = false;
var gen = ++rulesFetchGen;
$scope.rulesLoading = true;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -426,21 +616,44 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data;
if (res && res.fetchStatus === 1) {
$scope.rules = typeof res.data === 'string' ? JSON.parse(res.data) : (res.data || []);
$scope.rulesTotalCount = res.total_count != null ? res.total_count : ($scope.rules ? $scope.rules.length : 0);
$scope.rulesPage = Math.max(1, res.page != null ? res.page : 1);
$scope.rulesPageSize = res.page_size != null ? res.page_size : 10;
$scope.rulesLoading = true;
if (gen !== rulesFetchGen) {
return;
}
else {
$scope.rulesLoading = true;
$scope.errorMessage = (res && res.error_message) ? res.error_message : '';
try {
var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data;
if (res && res.fetchStatus == 1) {
var parsedRules = [];
if (typeof res.data === 'string') {
try {
parsedRules = JSON.parse(res.data);
} catch (parseErr) {
parsedRules = [];
$scope.errorMessage = (res && res.error_message) ? res.error_message : 'Invalid rules data';
}
} else {
parsedRules = res.data || [];
}
$scope.rules = parsedRules;
$scope.rulesTotalCount = res.total_count != null ? res.total_count : ($scope.rules ? $scope.rules.length : 0);
$scope.rulesPage = Math.max(1, res.page != null ? res.page : 1);
$scope.rulesPageSize = res.page_size != null ? res.page_size : 10;
} else {
$scope.errorMessage = (res && res.error_message) ? res.error_message : '';
}
} catch (e) {
$scope.errorMessage = 'Could not load firewall rules.';
} finally {
if (gen === rulesFetchGen) {
$scope.rulesLoading = false;
}
}
}
function cantLoadInitialDatas(response) {
if (gen !== rulesFetchGen) {
return;
}
$scope.rulesLoading = false;
$scope.couldNotConnect = false;
}
}
@@ -519,7 +732,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.errorMessage = 'Port is required';
return;
}
$scope.rulesLoading = false;
$scope.rulesLoading = true;
var url = '/firewall/modifyRule';
var data = {
id: d.id,
@@ -530,19 +743,19 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
};
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
$http.post(url, data, config).then(function(response) {
$scope.rulesLoading = true;
if (response.data && response.data.status === 1) {
$scope.closeModifyRuleModal();
$scope.actionFailed = true;
$scope.actionSuccess = false;
populateCurrentRecords();
} else {
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = (response.data && response.data.error_message) || 'Modify failed';
}
}, function() {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = 'Could not connect to server. Please refresh this page.';
@@ -551,7 +764,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.deleteRule = function (id, proto, port, ruleIP) {
$scope.rulesLoading = false;
$scope.rulesLoading = true;
url = "/firewall/deleteRule";
@@ -579,7 +792,6 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
populateCurrentRecords();
$scope.rulesLoading = true;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -591,7 +803,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
else {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -599,7 +811,6 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.ruleAdded = true;
$scope.couldNotConnect = true;
$scope.rulesLoading = true;
$scope.errorMessage = response.data.error_message;
@@ -609,7 +820,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
function cantLoadInitialDatas(response) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -656,7 +867,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
if (response.data.reload_status == 1) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = false;
@@ -668,7 +879,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
else {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
@@ -685,7 +896,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
function cantLoadInitialDatas(response) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -731,7 +942,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
if (response.data.start_status == 1) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = false;
@@ -747,7 +958,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
else {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
@@ -764,7 +975,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
function cantLoadInitialDatas(response) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -811,7 +1022,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
if (response.data.stop_status == 1) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = false;
@@ -827,7 +1038,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
else {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
@@ -844,7 +1055,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
function cantLoadInitialDatas(response) {
$scope.rulesLoading = true;
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -1309,6 +1520,8 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
}
$scope.populateCurrentRecords = populateCurrentRecords;
});
@@ -3254,18 +3467,20 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window)
function syncFirewallTabFromHash() {
var nav = document.getElementById('firewall-tab-nav');
if (!nav) return;
var h = (window.location.hash || '').replace(/^#/, '');
var tab = (h === 'banned-ips') ? 'banned' : 'rules';
var h = (window.location.hash || '').replace(/^#/, '').toLowerCase();
var tab = 'rules';
if (h === 'banned-ips' || h === 'banned') tab = 'banned';
else if (h === 'trusted-ips' || h === 'ssh-whitelist') tab = 'trusted';
if (window.__firewallLoadTab) {
try { window.__firewallLoadTab(tab); } catch (e) {}
}
}
/* Initial sync only — hashchange is handled by Angular syncTabFromHash in firewallController
(multiple listeners were racing and could reset #trusted-ips to #rules). */
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', syncFirewallTabFromHash);
} else {
syncFirewallTabFromHash();
}
setTimeout(syncFirewallTabFromHash, 100);
window.addEventListener('hashchange', syncFirewallTabFromHash);
})();