urrverse untzy

This commit is contained in:
Jing Kohn 2025-11-08 15:41:43 +10:00
parent 8bc16e4d4e
commit 252d5ccf98
4 changed files with 457 additions and 1715 deletions

View File

@ -1,27 +0,0 @@
██████ ██████ █████ █████ ████ ██████ █████████ ███████ ████████
░░██████ ██████ ███░░░███░░████ ░░██ ███░░░███░█░░░██░░█░░██░░░█░░███░░███
░███░█████░███ ███ ░░███░██░██ ░██ ░███ ░░░ ░ ░██ ░ ░██ █ ░███ ░███
░███░░███ ░███░███ ░███░██ ░██░██ ░░███████ ░██ ░████ ░██████
░███ ░░░ ░███░███ ░███░██ ░████ ░░░░███ ░██ ░██░█ ░███░░███
░███ ░███░░███ ███ ░███ ░███ ███ ░███ ░██ ░██ █ ░███ ░███
█████ █████░░░█████░ ████ ░████░░███████ █████ ███████ ████ ████
░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░ ░░░░░░░ ░░░░ ░░░░░░ ░░░░ ░░░░
███████████ ███████ ███████████ ███████ ███████████
░░███░░░░░███ ███░░░░░███░░███░░░░░███ ███░░░░░███ ░█░░░███░░░█
░███ ░███ ███ ░░███░███ ░███ ███ ░░███░ ░███ ░
░██████████ ░███ ░███░██████████ ░███ ░███ ░███
░███░░░░░███░███ ░███░███░░░░░███░███ ░███ ░███
░███ ░███░░███ ███ ░███ ░███░░███ ███ ░███
█████ █████░░░███████░ ███████████ ░░░███████░ █████
░░░░░ ░░░░░ ░░░░░░░ ░░░░░░░░░░░ ░░░░░░░ ░░░░░
█████████ ███████ ███████████ ███████████
███░░░░░███ ███░░░░░███░░███░░░░░░█░█░░░███░░░█
░███ ░░░ ███ ░░███░███ █ ░ ░ ░███ ░
░░█████████ ░███ ░███░███████ ░███
░░░░░░░░███░███ ░███░███░░░█ ░███
███ ░███░░███ ███ ░███ ░ ░███
░░█████████ ░░░███████░ █████ █████
░░░░░░░░░ ░░░░░░░ ░░░░░ ░░░░░

View File

@ -1,218 +0,0 @@
Goal:
Expand an Ubuntu bash installer script so that it configures Cloudflare Firewall rules automatically using the users Zone ID and API Token.
The script should support three security levels — Light, Moderate, and Extreme — all using Cloudflares free-tier Firewall Rules API.
🪟 User Inputs (interactive prompt in terminal)
1. CLOUDFLARE_EMAIL (optional, only for logging)
2. ZONE_ID
3. API_TOKEN
4. DOMAIN_NAME (used for referer-safe rules)
5. SECURITY_LEVEL (choose: light, moderate, or extreme)
🧰 Dependencies (for a clean Ubuntu install)
sudo apt update
sudo apt install -y curl jq
(jq is for parsing JSON responses and checking rule creation success.)
🌩️ Cloudflare API Base Endpoint
https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/firewall/rules
Headers:
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json"
🪄 Security Levels
🟢 LIGHT (basic protection, no admin lockdown)
• Block /xmlrpc.php
• Block /wp-content/ or /wp-includes/ requests not from your domain
Expression:
(http.request.uri.path contains "/xmlrpc.php") or
((http.request.uri.path eq "/wp-content/" or http.request.uri.path eq "/wp-includes/") and not http.referer contains "${DOMAIN_NAME}")
🟡 MODERATE (adds wp-admin/wp-login limits)
Includes Light rules +
Block access to /wp-login.php or /wp-admin/ (except /wp-admin/admin-ajax.php) from outside AU, US, UK.
Expression:
(
http.request.uri.path contains "/wp-login.php" or
(http.request.uri.path contains "/wp-admin/" and http.request.uri.path ne "/wp-admin/admin-ajax.php")
)
and not (ip.geoip.country in {"AU" "US" "UK"})
🔴 EXTREME (maximum free-tier hardening)
Includes Moderate rules +
Block entire site access from sketchy regions (RU, CN, KP, IR, etc.), except known search engine bots (Googlebot, Bingbot).
Expression:
(
ip.geoip.country in {"RU" "CN" "KP" "IR" "VN" "TR" "IN" "NG"}
)
and not cf.client.bot
🧩 Example Bash Logic
Heres the conceptual structure for your installer:
read -p "Enter your Cloudflare Zone ID: " ZONE_ID
read -p "Enter your Cloudflare API Token: " API_TOKEN
read -p "Enter your domain (e.g. example.com): " DOMAIN
read -p "Choose security level (light/moderate/extreme): " LEVEL
API="https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/firewall/rules"
create_rule() {
local desc="$1"
local expr="$2"
echo "Creating rule: $desc..."
curl -s -X POST "$API" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
--data "[
{
\"description\": \"${desc}\",
\"action\": \"block\",
\"filter\": {\"expression\": \"${expr}\"}
}
]" | jq .
}
if [[ "$LEVEL" == "light" || "$LEVEL" == "moderate" || "$LEVEL" == "extreme" ]]; then
create_rule "Block XMLRPC and unauthorized includes" "(http.request.uri.path contains \"/xmlrpc.php\") or ((http.request.uri.path eq \"/wp-content/\" or http.request.uri.path eq \"/wp-includes/\") and not http.referer contains \"${DOMAIN}\")"
fi
if [[ "$LEVEL" == "moderate" || "$LEVEL" == "extreme" ]]; then
create_rule "Restrict wp-login/wp-admin to AU, US, UK" "((http.request.uri.path contains \"/wp-login.php\" or (http.request.uri.path contains \"/wp-admin/\" and http.request.uri.path ne \"/wp-admin/admin-ajax.php\")) and not (ip.geoip.country in {\"AU\" \"US\" \"UK\"}))"
fi
if [[ "$LEVEL" == "extreme" ]]; then
create_rule "Block access from high-risk countries" "((ip.geoip.country in {\"RU\" \"CN\" \"KP\" \"IR\" \"VN\" \"TR\" \"IN\" \"NG\"}) and not cf.client.bot)"
fi
echo "✅ Cloudflare security rules applied for ${DOMAIN} (${LEVEL} mode)"
extra concept to factor in...
💀 “Mega Bot Block” Expansion for Cloudflare Setup Script
This will:
• Keep Googlebot, Bingbot, DuckDuckGo, Applebot ✅ allowed.
• Block AI scrapers, SEO bots, curl/wget libraries, and other nuisance crawlers.
• Skip blocking /robots.txt so legitimate search engines can still fetch it.
• Use the same ZONE_ID and API_TOKEN you already input earlier.
⚙️ Integration Plan
In your Cloudflare bash installer (from before), add a 4th rule group triggered if user enables “bot blocking.”
At the prompt, include:
read -p "Enable advanced bot blocking? (y/n): " BOTBLOCK
If y, it will add a rule called:
“AI Crawl Control - Block AI & SEO Bots”
💬 Expression (copy-ready for API)
(http.request.uri.path ne "/robots.txt") and (
(http.user_agent contains "SemrushBot") or
(http.user_agent contains "AhrefsBot") or
(http.user_agent contains "Barkrowler") or
(http.user_agent contains "MJ12bot") or
(http.user_agent contains "DotBot") or
(http.user_agent contains "Exabot") or
(http.user_agent contains "Sogou") or
(http.user_agent contains "YandexBot") or
(http.user_agent contains "Baidu") or
(http.user_agent contains "ia_archiver") or
(http.user_agent contains "SemanticaBot") or
(http.user_agent contains "SiteLockSpider") or
(http.user_agent contains "SeznamBot") or
(http.user_agent contains "OpenLinkProfiler") or
(http.user_agent contains "SiteBot") or
(http.user_agent contains "Screaming Frog") or
(http.user_agent contains "DataForSeoBot") or
(http.user_agent contains "SEOkicks") or
(http.user_agent contains "BLEXBot") or
(http.user_agent contains "MegaIndex") or
(http.user_agent contains "MegaIndex.ru") or
(http.user_agent contains "NetcraftSurveyAgent") or
(http.user_agent contains "Apache-HttpClient") or
(http.user_agent contains "Python-urllib") or
(http.user_agent contains "python-requests") or
(http.user_agent contains "libwww-perl") or
(http.user_agent contains "Curl") or
(http.user_agent contains "wget") or
(http.user_agent contains "CensysInspect") or
(http.user_agent contains "ZoominfoBot") or
(http.user_agent contains "MauiBot") or
(http.user_agent contains "spbot") or
(http.user_agent contains "GPTBot") or
(http.user_agent contains "ClaudeBot") or
(http.user_agent contains "Claude-SearchBot") or
(http.user_agent contains "Claude-User") or
(http.user_agent contains "Bytespider") or
(http.user_agent contains "MistralAI-User") or
(http.user_agent contains "Perplexity-User") or
(http.user_agent contains "ProRataInc") or
(http.user_agent contains "Novellum") or
(http.user_agent contains "OAI-SearchBot") or
(http.user_agent contains "Meta-ExternalFetcher") or
(http.user_agent contains "Meta-ExternalAgent")
)
🧠 Add this function into the script
if [[ "$BOTBLOCK" =~ ^[Yy]$ ]]; then
create_rule "AI Crawl Control - Block AI & SEO Bots" \
'(http.request.uri.path ne "/robots.txt") and ((http.user_agent contains "SemrushBot") or (http.user_agent contains "AhrefsBot") or (http.user_agent contains "Barkrowler") or (http.user_agent contains "MJ12bot") or (http.user_agent contains "DotBot") or (http.user_agent contains "Exabot") or (http.user_agent contains "Sogou") or (http.user_agent contains "YandexBot") or (http.user_agent contains "Baidu") or (http.user_agent contains "ia_archiver") or (http.user_agent contains "SemanticaBot") or (http.user_agent contains "SiteLockSpider") or (http.user_agent contains "SeznamBot") or (http.user_agent contains "OpenLinkProfiler") or (http.user_agent contains "SiteBot") or (http.user_agent contains "Screaming Frog") or (http.user_agent contains "DataForSeoBot") or (http.user_agent contains "SEOkicks") or (http.user_agent contains "BLEXBot") or (http.user_agent contains "MegaIndex") or (http.user_agent contains "MegaIndex.ru") or (http.user_agent contains "NetcraftSurveyAgent") or (http.user_agent contains "Apache-HttpClient") or (http.user_agent contains "Python-urllib") or (http.user_agent contains "python-requests") or (http.user_agent contains "libwww-perl") or (http.user_agent contains "Curl") or (http.user_agent contains "wget") or (http.user_agent contains "CensysInspect") or (http.user_agent contains "ZoominfoBot") or (http.user_agent contains "MauiBot") or (http.user_agent contains "spbot") or (http.user_agent contains "GPTBot") or (http.user_agent contains "ClaudeBot") or (http.user_agent contains "Claude-SearchBot") or (http.user_agent contains "Claude-User") or (http.user_agent contains "Bytespider") or (http.user_agent contains "MistralAI-User") or (http.user_agent contains "Perplexity-User") or (http.user_agent contains "ProRataInc") or (http.user_agent contains "Novellum") or (http.user_agent contains "OAI-SearchBot") or (http.user_agent contains "Meta-ExternalFetcher") or (http.user_agent contains "Meta-ExternalAgent"))'
fi
✅ Summary of What This Adds
Rule Type Action Notes
/xmlrpc.php & unauthorized includes Block Basic protection
wp-login & wp-admin by region Block Optional Moderate/Extreme
Country blocks (RU, CN, KP, etc.) Block Optional Extreme
Bot/AI/SEO crawler block Block Optional, keeps search engines allowed

File diff suppressed because it is too large Load Diff

View File

@ -1875,6 +1875,12 @@ PKG_DESC[gamemode]="Optimize gaming performance [APT]"
PKG_METHOD[gamemode]="apt"
PKG_CATEGORY[gamemode]="gaming-platforms"
# Gargoyle (Source Build)
PACKAGES[gargoyle-source]="gargoyle"
PKG_DESC[gargoyle-source]="Gargoyle interactive fiction player [SOURCE BUILD]"
PKG_METHOD[gargoyle-source]="custom"
PKG_CATEGORY[gargoyle-source]="gaming-platforms"
PACKAGES[gimp]="gimp"
PKG_DESC[gimp]="GIMP image editor [APT]"
PKG_METHOD[gimp]="apt"
@ -2246,6 +2252,17 @@ PKG_DESC[n8n]="Workflow automation tool (self-hosted Zapier alternative) [NPM]"
PKG_METHOD[n8n]="custom"
PKG_CATEGORY[n8n]="dev"
# ==============================================================================
# GAMING - Z-CODE / INTERACTIVE FICTION
# ==============================================================================
PACKAGES[frotz]="frotz"
PKG_DESC[frotz]="Frotz Z-machine interpreter [APT]"
PKG_METHOD[frotz]="apt"
PKG_CATEGORY[frotz]="gaming-platforms"
## tmux is already defined under 'core' category above; avoid redefining here
# ==============================================================================
# SINGLE BOARD COMPUTERS & MICROCONTROLLERS
# ==============================================================================
@ -6876,6 +6893,130 @@ remove_esp-idf() {
# BUNTAGE MANAGEMENT DISPATCHER
# ==============================================================================
# Build and install Gargoyle from source
install_gargoyle() {
log "Starting Gargoyle (source build) installation"
# Dependencies needed for building Gargoyle
local deps=(
build-essential
cmake
pkg-config
git
libgtk-3-dev
libglade2-dev
libgtkglext1-dev
libglk-dev
libsdl2-dev
libsdl2-mixer-dev
libfreetype6-dev
libjpeg-dev
libfontconfig1-dev
)
apt_update
# Try to install each dependency (skip gracefully if unavailable)
for d in "${deps[@]}"; do
if apt-cache show "$d" >/dev/null 2>&1; then
install_apt_package "$d" || log_error "Failed to install dependency: $d"
else
log "Dependency not found in APT and will be skipped: $d"
fi
done
# Prepare source directories
local src_root="$HOME/.ultrabunt-src"
local repo_root="$src_root/gargoyle"
local repo_dir="$repo_root/garglk"
local build_dir="$repo_dir/build"
mkdir -p "$repo_root" || {
ui_msg "Error" "Could not create source directory at $repo_root"
return 1
}
# Clone or update repository
if [[ -d "$repo_dir/.git" ]]; then
log "Updating Gargoyle repository"
(cd "$repo_dir" && git pull --rebase) 2>&1 | tee -a "$LOGFILE" || true
else
log "Cloning Gargoyle repository"
(cd "$repo_root" && git clone https://github.com/garglk/garglk.git) 2>&1 | tee -a "$LOGFILE" || {
ui_msg "Clone Failed" "Failed to clone Gargoyle repo. Check logs at $LOGFILE."
return 1
}
fi
# Configure and build
mkdir -p "$build_dir"
ui_msg "Building Gargoyle" "Configuring and compiling Gargoyle...\n\n⏳ This may take several minutes."
local jobs
jobs=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1)
(
cd "$build_dir" && cmake .. && make -j"$jobs"
) 2>&1 | tee -a "$LOGFILE" || {
ui_msg "Build Failed" "Compiling Gargoyle failed. See $LOGFILE for details."
return 1
}
# Install
if ui_yesno "Install Gargoyle" "Install Gargoyle system-wide (sudo make install)?"; then
(cd "$build_dir" && sudo make install) 2>&1 | tee -a "$LOGFILE" || {
ui_msg "Install Failed" "Installation failed. See $LOGFILE for details."
return 1
}
# Ensure the system can find libgarglk.so (Ubuntu often misses /usr/local/lib)
local garg_conf="/etc/ld.so.conf.d/gargoyle.conf"
if [[ -w "/etc/ld.so.conf.d" ]]; then
echo "/usr/local/lib" | sudo tee "$garg_conf" >/dev/null
sudo ldconfig 2>&1 | tee -a "$LOGFILE" || true
log "Updated ld.so configuration at $garg_conf and refreshed cache via ldconfig"
else
# Fallback message if directory is not writable (unlikely due to sudo)
ui_msg "Manual Step Required" "Add /usr/local/lib to the library path and run ldconfig:\n\n echo '/usr/local/lib' | sudo tee /etc/ld.so.conf.d/gargoyle.conf\n sudo ldconfig"
fi
else
ui_msg "Build Complete" "Gargoyle built successfully at:\n$build_dir\n\nYou can run from the build directory or install later."
return 0
fi
ui_msg "Gargoyle Installed" "Gargoyle installed successfully. Try 'gargoyle --paths' or '--edit-config'."
return 0
}
remove_gargoyle() {
# Minimal helper; source builds rarely have automatic uninstall unless provided
local src_root="$HOME/.ultrabunt-src"
local repo_root="$src_root/gargoyle"
local repo_dir="$repo_root/garglk"
local build_dir="$repo_dir/build"
if ui_yesno "Remove Gargoyle?" "Attempt to remove Gargoyle.\n\nNote: Source builds may not support automatic uninstall.\nIf available, we will run 'sudo make uninstall' from:\n$build_dir\n\nOtherwise, we remove the main binary and known assets."; then
local removed=0
if [[ -d "$build_dir" ]] && grep -q uninstall "${build_dir}/Makefile" 2>/dev/null; then
(cd "$build_dir" && sudo make uninstall) 2>&1 | tee -a "$LOGFILE" && removed=1
fi
# Fallback: remove common install locations
if [[ $removed -eq 0 ]]; then
sudo rm -f /usr/local/bin/gargoyle 2>/dev/null || true
sudo rm -f /usr/bin/gargoyle 2>/dev/null || true
sudo rm -f /usr/local/share/applications/gargoyle.desktop 2>/dev/null || true
sudo rm -rf /usr/local/share/gargoyle 2>/dev/null || true
sudo rm -f /etc/garglk.ini 2>/dev/null || true
log "Removed common Gargoyle files"
fi
ui_msg "Gargoyle Removed" "Removal attempted. Some files may remain if the build system did not provide an uninstall target."
return 0
fi
return 1
}
install_package() {
local name="$1"
local pkg="${PACKAGES[$name]}"
@ -6993,6 +7134,7 @@ install_package() {
arduino-ide) install_arduino-ide; install_result=$? ;;
arduino-cli) install_arduino-cli; install_result=$? ;;
esp-idf) install_esp-idf; install_result=$? ;;
gargoyle-source) install_gargoyle; install_result=$? ;;
*) log_error "Unknown custom installer: $name"; install_result=1 ;;
esac
;;
@ -7097,6 +7239,7 @@ remove_package() {
arduino-ide) remove_arduino-ide; remove_result=$? ;;
arduino-cli) remove_arduino-cli; remove_result=$? ;;
esp-idf) remove_esp-idf; remove_result=$? ;;
gargoyle-source) remove_gargoyle; remove_result=$? ;;
*) log_error "Unknown custom remover: $name"; remove_result=1 ;;
esac
;;
@ -7232,6 +7375,13 @@ is_package_installed() {
result=1
fi
;;
gargoyle-source)
if is_binary_available "gargoyle"; then
result=0
else
result=1
fi
;;
*)
log "WARNING: Unknown custom buntage '$name'"
result=1
@ -7306,6 +7456,7 @@ show_category_menu() {
menu_items+=("database-management" "(.Y.) Database Management")
menu_items+=("swappiness" "(.Y.) Swappiness Tuning")
menu_items+=("wordpress-setup" "(.Y.) WordPress Management")
menu_items+=("reverse-proxy-addons" "(.Y.) Reverse-Proxy & Add-ons")
menu_items+=("keyboard-layout" "(.Y.) Keyboard Layout Configuration")
menu_items+=("php-settings" "(.Y.) PHP Configuration")
menu_items+=("log-viewer" "(.Y.) Log Viewer")
@ -7489,6 +7640,12 @@ show_category_menu() {
ui_msg "Error" "Failed to display WordPress setup menu. Please check the logs."
}
;;
reverse-proxy-addons)
show_reverse_proxy_addons_menu || {
log "ERROR: show_reverse_proxy_addons_menu failed"
ui_msg "Error" "Failed to display Reverse-Proxy & Add-ons menu. Please check the logs."
}
;;
php-settings)
show_php_settings_menu || {
log "ERROR: show_php_settings_menu failed"
@ -7642,6 +7799,11 @@ show_buntage_list() {
done
menu_items=("${sorted_items[@]}")
# Inject Z-Code submenu entry inside Gaming Platforms category
if [[ "$category" == "gaming-platforms" ]]; then
menu_items+=("zcode-menu" "(.Y.) Z-Code & Interactive Fiction")
fi
menu_items+=("" "(_*_)")
menu_items+=("zback" "(B) ← Back to Categories")
@ -7653,11 +7815,41 @@ show_buntage_list() {
case "$choice" in
back|zback|z|"") break ;;
zcode-menu)
show_zcode_menu
;;
*) show_buntage_actions "$choice" ;;
esac
done
}
# Submenu for Z-Code & Interactive Fiction tools
show_zcode_menu() {
while true; do
local menu_items=()
menu_items+=("manage-frotz" "(.Y.) Manage Frotz (Install/Remove/Info)")
menu_items+=("manage-gargoyle" "(.Y.) Build & Install Gargoyle (Source)")
menu_items+=("zback" "(B) ← Back to Gaming Platforms")
local choice
choice=$(ui_menu "Z-Code & Interactive Fiction" \
"Manage classic interactive fiction interpreters and frontends" \
$DIALOG_HEIGHT $DIALOG_WIDTH $DIALOG_MENU_HEIGHT "${menu_items[@]}") || break
case "$choice" in
manage-frotz)
show_buntage_actions "frotz"
;;
manage-gargoyle)
show_buntage_actions "gargoyle-source"
;;
back|zback|z|"")
break
;;
esac
done
}
show_buntage_actions() {
local name="$1"
@ -10304,8 +10496,11 @@ wordpress_quick_setup() {
# Show completion message
ui_msg "Step 6/6" "Finalizing WordPress installation...\n\nPreparing completion summary with all details.\n\n⏳ This process is automatic - please wait..."
show_wordpress_completion "$domain" "$db_info"
ui_msg "Installation Complete!" "🎉 WordPress installation finished successfully!\n\nYour site is ready at: http://$domain\n\nNext steps:\n1. Visit your site to complete WordPress setup\n2. Create your admin account\n3. Choose your theme and plugins\n\n💡 Tip: Use the WordPress Cleanup option in the WordPress menu to remove default content and optimize your site."
# Apply shared ownership permissions and restart services per requested policy
finalize_wordpress_setup "$domain" "$web_server"
}
wordpress_custom_setup() {
@ -10361,6 +10556,9 @@ wordpress_custom_setup() {
# Show completion
show_wordpress_completion "$domain" "$db_info"
# Apply shared ownership permissions and restart services per requested policy
finalize_wordpress_setup "$domain" "$web_server" "$site_dir"
}
wordpress_custom_database_setup() {
@ -10537,6 +10735,42 @@ install_wordpress_prerequisites() {
log "WordPress prerequisites installed successfully"
}
# Finalize WordPress setup by granting shared ownership and restarting services
finalize_wordpress_setup() {
local domain="$1"
local web_server="$2"
local site_dir="${3:-/var/www/$domain}"
log "Finalizing WordPress setup for $domain at $site_dir (web server: $web_server)"
# Grant shared ownership to current user and www-data group
sudo usermod -aG www-data "$(whoami)" 2>&1 | tee -a "$LOGFILE" || true
sudo chown -R "$(whoami)":www-data "$site_dir" 2>&1 | tee -a "$LOGFILE" || true
# Set directory permissions to 2775 and file permissions to 0664
sudo find "$site_dir" -type d -exec chmod 2775 {} \; 2>&1 | tee -a "$LOGFILE" || true
sudo find "$site_dir" -type f -exec chmod 0664 {} \; 2>&1 | tee -a "$LOGFILE" || true
# Ensure wp-config.php has group-writable permissions per requested policy
if [[ -f "$site_dir/wp-config.php" ]]; then
sudo chmod 0664 "$site_dir/wp-config.php" 2>&1 | tee -a "$LOGFILE" || true
fi
# Restart appropriate services
if [[ "$web_server" == "nginx" ]]; then
sudo systemctl restart php${PHP_VER}-fpm 2>&1 | tee -a "$LOGFILE" || true
sudo systemctl restart nginx 2>&1 | tee -a "$LOGFILE" || true
else
sudo systemctl restart apache2 2>&1 | tee -a "$LOGFILE" || true
# Restart PHP-FPM if present on Apache setups
if systemctl list-unit-files | grep -q "php${PHP_VER}-fpm.service"; then
sudo systemctl restart php${PHP_VER}-fpm 2>&1 | tee -a "$LOGFILE" || true
fi
fi
ui_msg "Permissions Applied" "✅ Shared ownership granted for:\n$site_dir\n\nOwner: $(whoami)\nGroup: www-data\nDirectories: 2775\nFiles: 0664\nwp-config.php: 0664\n\n🔄 Services restarted: ${web_server^} and PHP-FPM ${PHP_VER} (if applicable)."
}
check_wordpress_prerequisites() {
local web_server="$1"
@ -12656,6 +12890,7 @@ show_individual_site_management() {
"repair-db" "🔧 Repair Database Connection"
"" "(_*_)"
"file-access" "📁 Grant File Access to Current User"
"grant-root" "🔑 Grant Shared Ownership to Root"
"enable-site" "🔗 Enable/Disable Site"
"test-site" "🧪 Test Site Accessibility"
"" "(_*_)"
@ -12711,6 +12946,9 @@ show_individual_site_management() {
file-access)
grant_file_access_to_user "$site"
;;
grant-root)
finalize_wordpress_setup "$site" "$web_server" "$site_dir"
;;
enable-site)
manage_site_enablement "$site" "$web_server"
;;
@ -16372,4 +16610,222 @@ main() {
# Run main program
main "$@"
# ---------------------------------------------------------------------------
# REVERSE-PROXY & ADD-ONS (Mailcow + Umami behind Nginx)
# ---------------------------------------------------------------------------
# Helper: list WordPress sites (returns array of site names)
__wp_sites() {
local sites=()
if [[ -d /var/www ]]; then
while IFS= read -r -d '' d; do
local n=$(basename "$d")
[[ -f "$d/wp-config.php" ]] && sites+=("$n")
done < <(find /var/www -maxdepth 1 -type d -print0 2>/dev/null)
fi
printf '%s\n' "${sites[@]}"
}
# Helper: returns 0 if the site appears to be Internet-facing (has real DNS)
__site_is_live() {
local domain="$1"
# crude checks: 1) not localhost 2) DNS resolves to something != 127.x / private-only
[[ "$domain" =~ ^(localhost|127\.|192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.) ]] && return 1
if ! host "$domain" >/dev/null 2>&1; then
return 1 # no DNS at all
fi
return 0
}
# Menu entry point
show_reverse_proxy_addons_menu() {
log "Reverse-Proxy & Add-ons menu opened"
while true; do
local wp
mapfile -t wp < <(__wp_sites)
local info="Reverse-Proxy & Add-ons\n\n"
if [[ ${#wp[@]} -eq 0 ]]; then
info+="❌ No WordPress sites found.\n Please install WordPress first.\n"
else
info+="📁 Detected WordPress sites:\n"
for w in "${wp[@]}"; do info+="$w\n"; done
fi
local items=()
[[ ${#wp[@]} -gt 0 ]] && items+=("mailcow" "📧 Install Mailcow + proxy it")
[[ ${#wp[@]} -gt 0 ]] && items+=("umami" "📊 Install Umami + proxy it")
items+=("" "(_*_)")
items+=("zback" "(Z) ← Back to Main Menu")
local c; c=$(ui_menu "Reverse-Proxy & Add-ons" "$info" 20 80 10 "${items[@]}") || break
case "$c" in
mailcow) _do_mailcow_setup ;;
umami) _do_umami_setup ;;
zback|back|z|"") break ;;
esac
done
}
# --------------------------------------------------
# Mailcow (Docker) + Nginx reverse-proxy
# --------------------------------------------------
_do_mailcow_setup() {
local all; mapfile -t all < <(__wp_sites)
[[ ${#all[@]} -eq 0 ]] && { ui_msg "No WP" "No WordPress sites available."; return; }
# build menu: only live sites selectable
local items=() w
for w in "${all[@]}"; do
if __site_is_live "$w"; then
items+=("$w" "")
else
items+=("$w" "(localhost not usable for Mailcow)" "off")
fi
done
[[ ${#items[@]} -eq 0 ]] && { ui_msg "No live sites" "No Internet-facing WordPress sites found."; return; }
local target_domain
target_domain=$(ui_menu "Select site" "Which WordPress domain is the BASE domain?\n(e.g. example.com → mail.example.com)" 18 70 8 "${items[@]}" "") || return
local mail_domain; mail_domain="mail.$target_domain"
ui_yesno "Confirm" "Mailcow will be reachable at:\nhttps://$mail_domain\n\nContinue?" || return
# 1. Install Docker + Compose if missing
if ! command -v docker &>/dev/null; then
ui_msg "Docker" "Installing Docker & docker-compose…"
sudo apt update && sudo apt install -y docker.io docker-compose
sudo usermod -aG docker "$USER"
sudo systemctl enable --now docker
fi
# 2. Clone Mailcow repo
local mailcow_dir="/opt/mailcow-dockerized"
if [[ -d "$mailcow_dir" ]]; then
ui_msg "Exists" "Mailcow directory already present. Skipping clone."
else
sudo git clone https://github.com/mailcow/mailcow-dockerized "$mailcow_dir"
fi
cd "$mailcow_dir"
# 3. Generate mailcow.conf interactively (skip if exists)
[[ -f mailcow.conf ]] || sudo ./generate_config.sh
# 4. Start Mailcow
sudo docker-compose pull
sudo docker-compose up -d
# 5. Nginx reverse-proxy snippet
local ngx_conf="/etc/nginx/sites-available/mailcow.conf"
sudo tee "$ngx_conf" >/dev/null <<EOF
server {
listen 80;
listen [::]:80;
server_name $mail_domain;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
}
EOF
sudo ln -sf "$ngx_conf" /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# 6. Print DNS checklist
ui_info "Next steps" "🎉 Mailcow is running behind the proxy!\n\nAdd these DNS records in Cloudflare (or your DNS):\n\nType: A | Name: mail | Content: <your-server-ip>\nType: MX | Name: @ | Content: $mail_domain | Priority: 10\n\n(Replace <your-server-ip> with the IP of this server.)"
}
# --------------------------------------------------
# Umami (Docker) + Nginx reverse-proxy
# --------------------------------------------------
_do_umami_setup() {
local all; mapfile -t all < <(__wp_sites)
[[ ${#all[@]} -eq 0 ]] && { ui_msg "No WP" "No WordPress sites available."; return; }
# build menu: only live sites selectable
local items=() w
for w in "${all[@]}"; do
if __site_is_live "$w"; then
items+=("$w" "")
else
items+=("$w" "(localhost not usable for Umami)" "off")
fi
done
[[ ${#items[@]} -eq 0 ]] && { ui_msg "No live sites" "No Internet-facing WordPress sites found."; return; }
local target_domain
target_domain=$(ui_menu "Select site" "Which WordPress domain is the BASE domain?\n(e.g. example.com → analytics.example.com)" 18 70 8 "${items[@]}" "") || return
local analytics_domain; analytics_domain="analytics.$target_domain"
ui_yesno "Confirm" "Umami will be reachable at:\nhttps://$analytics_domain\n\nContinue?" || return
# 1. Docker (same as Mailcow)
if ! command -v docker &>/dev/null; then
ui_msg "Docker" "Installing Docker & docker-compose…"
sudo apt update && sudo apt install -y docker.io docker-compose
sudo usermod -aG docker "$USER"
sudo systemctl enable --now docker
fi
# 2. Folder & compose file
local umami_dir="/opt/umami"
sudo mkdir -p "$umami_dir"
sudo tee "$umami_dir/docker-compose.yml" >/dev/null <<'EOF'
version: '3.9'
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://umami:umami@db:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: replace-me-with-a-random-string
depends_on:
- db
restart: unless-stopped
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: umami
volumes:
- umami-db:/var/lib/postgresql/data
restart: unless-stopped
volumes:
umami-db:
EOF
# 3. Start Umami
cd "$umami_dir"
sudo docker-compose up -d
# 4. Nginx reverse-proxy snippet
local ngx_conf="/etc/nginx/sites-available/umami.conf"
sudo tee "$ngx_conf" >/dev/null <<EOF
server {
listen 80;
listen [::]:80;
server_name $analytics_domain;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
}
EOF
sudo ln -sf "$ngx_conf" /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
ui_info "Next steps" "🎉 Umami is running behind the proxy!\n\nAdd this DNS record in Cloudflare (or your DNS):\n\nType: A | Name: analytics | Content: <your-server-ip>\n\nDefault login: admin / umami\n(Change password immediately after first login.)"
}
exit 0