#!/usr/bin/env python3 """ GeoIP Shop Blocker Manager - Final Fixed Version 2-Component System: PHP blocking + Python watcher (systemd service) """ import os import sys import shutil import subprocess import json import time from datetime import datetime, timedelta from pathlib import Path # Configuration VHOSTS_DIR = "/var/www/vhosts" BACKUP_SUFFIX = ".geoip_backup" BLOCKING_FILE = "geoip_blocking.php" CACHE_FILE = "de_ip_ranges.cache" LOG_FILE = "geoip_blocked.log" CROWDSEC_QUEUE_FILE = "geoip_crowdsec_queue.log" WATCHER_SCRIPT = "/usr/local/bin/geoip_crowdsec_watcher.py" SYSTEMD_SERVICE = "/etc/systemd/system/geoip-crowdsec-watcher.service" ACTIVE_SHOPS_FILE = "/var/lib/crowdsec/geoip_active_shops.json" # PHP GeoIP blocking script (no exec, just logging) GEOIP_SCRIPT = ''' $expiry_date) {{ return; // Script expired, allow all traffic }} // Get visitor IP $visitor_ip = $_SERVER['REMOTE_ADDR'] ?? ''; if (empty($visitor_ip)) {{ return; }} // Skip private IPs if (filter_var($visitor_ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {{ return; }} // Files $cache_file = __DIR__ . '/{cache_file}'; $cache_duration = 86400; // 24 hours $log_file = __DIR__ . '/{log_file}'; $crowdsec_queue = __DIR__ . '/{crowdsec_queue}'; // Function to download German IP ranges function download_de_ranges() {{ $ranges = []; $url = 'https://www.ipdeny.com/ipblocks/data/aggregated/de-aggregated.zone'; $content = @file_get_contents($url); if ($content !== false) {{ $lines = explode("\\n", trim($content)); foreach ($lines as $line) {{ $line = trim($line); if (!empty($line) && strpos($line, '/') !== false) {{ $ranges[] = $line; }} }} }} return $ranges; }} // Function to check if IP is in CIDR range function ip_in_range($ip, $cidr) {{ list($subnet, $mask) = explode('/', $cidr); $ip_long = ip2long($ip); $subnet_long = ip2long($subnet); $mask_long = -1 << (32 - (int)$mask); return ($ip_long & $mask_long) == ($subnet_long & $mask_long); }} // Load or download IP ranges $de_ranges = []; if (file_exists($cache_file) && (time() - filemtime($cache_file)) < $cache_duration) {{ $de_ranges = unserialize(file_get_contents($cache_file)); }} else {{ $de_ranges = download_de_ranges(); if (!empty($de_ranges)) {{ @file_put_contents($cache_file, serialize($de_ranges)); }} }} // Check if visitor IP is from Germany $is_german = false; foreach ($de_ranges as $range) {{ if (ip_in_range($visitor_ip, $range)) {{ $is_german = true; break; }} }} // Block non-German IPs if (!$is_german) {{ $timestamp = date('Y-m-d H:i:s'); $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown'; $request_uri = $_SERVER['REQUEST_URI'] ?? '/'; // Log for humans $log_entry = "[$timestamp] IP: $visitor_ip | UA: $user_agent | URI: $request_uri\\n"; @file_put_contents($log_file, $log_entry, FILE_APPEND | LOCK_EX); // Queue for CrowdSec (simple format) $queue_entry = "$timestamp|$visitor_ip|{shop_name}\\n"; @file_put_contents($crowdsec_queue, $queue_entry, FILE_APPEND | LOCK_EX); header('HTTP/1.1 403 Forbidden'); exit; }} ''' # Python watcher script (runs as systemd service) WATCHER_SCRIPT_CONTENT = '''#!/usr/bin/env python3 """ GeoIP CrowdSec Watcher Service Monitors queue files and adds blocked IPs to CrowdSec """ import os import sys import time import subprocess import json from pathlib import Path from datetime import datetime VHOSTS_DIR = "/var/www/vhosts" QUEUE_FILE = "geoip_crowdsec_queue.log" ACTIVE_SHOPS_FILE = "/var/lib/crowdsec/geoip_active_shops.json" PROCESSED_IPS = {} # In-memory cache to avoid re-adding same IP CHECK_INTERVAL = 5 # Check every 5 seconds def log(msg): print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}", flush=True) def get_active_shops(): """Get list of shops with active GeoIP blocking""" if not os.path.isfile(ACTIVE_SHOPS_FILE): return {} try: with open(ACTIVE_SHOPS_FILE, 'r') as f: return json.load(f) except: return {} def add_to_crowdsec(ip, shop): """Add IP to CrowdSec with 72h ban""" # Check if already processed recently (within last hour) now = time.time() if ip in PROCESSED_IPS and (now - PROCESSED_IPS[ip]) < 3600: return True cmd = [ 'cscli', 'decisions', 'add', '--ip', ip, '--duration', '72h', '--type', 'ban', '--reason', f'GeoIP: Non-DE IP blocked by {shop}' ] try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) if result.returncode == 0: PROCESSED_IPS[ip] = now log(f"✅ Added {ip} to CrowdSec (from {shop})") return True else: log(f"⚠️ Failed to add {ip}: {result.stderr.strip()}") return False except Exception as e: log(f"❌ Error adding {ip}: {e}") return False def process_queue_file(shop_path, shop): """Process queue file for a shop""" queue_file = os.path.join(shop_path, 'httpdocs', QUEUE_FILE) if not os.path.isfile(queue_file): return 0 processed = 0 try: # Read all lines with open(queue_file, 'r') as f: lines = f.readlines() if not lines: return 0 # Process each line for line in lines: line = line.strip() if not line: continue try: parts = line.split('|') if len(parts) >= 2: timestamp = parts[0] ip = parts[1] if add_to_crowdsec(ip, shop): processed += 1 except: continue # Clear the file after processing if processed > 0: with open(queue_file, 'w') as f: f.write('') except Exception as e: log(f"❌ Error processing {shop}: {e}") return processed def main(): log("🚀 GeoIP CrowdSec Watcher started") while True: try: active_shops = get_active_shops() if not active_shops: time.sleep(CHECK_INTERVAL) continue total_processed = 0 for shop, info in active_shops.items(): shop_path = os.path.join(VHOSTS_DIR, shop) if os.path.isdir(shop_path): count = process_queue_file(shop_path, shop) total_processed += count if total_processed > 0: log(f"📊 Processed {total_processed} IPs in this cycle") time.sleep(CHECK_INTERVAL) except KeyboardInterrupt: log("👋 Shutting down...") break except Exception as e: log(f"❌ Error in main loop: {e}") time.sleep(CHECK_INTERVAL) if __name__ == "__main__": main() ''' # Systemd service file SYSTEMD_SERVICE_CONTENT = '''[Unit] Description=GeoIP CrowdSec Watcher Service After=network.target crowdsec.service Wants=crowdsec.service [Service] Type=simple ExecStart=/usr/bin/python3 /usr/local/bin/geoip_crowdsec_watcher.py Restart=always RestartSec=10 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target ''' def run_command(cmd, capture_output=True): """Run a shell command""" try: if capture_output: result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30) return result.returncode, result.stdout, result.stderr else: result = subprocess.run(cmd, shell=True, timeout=30) return result.returncode, "", "" except Exception as e: return -1, "", str(e) def check_crowdsec(): """Check if CrowdSec is running""" code, stdout, stderr = run_command("systemctl is-active crowdsec") return code == 0 and stdout.strip() == "active" def install_watcher_service(): """Install the watcher script and systemd service""" print(" 📦 Installiere CrowdSec-Watcher-Service...") # Create watcher script with open(WATCHER_SCRIPT, 'w') as f: f.write(WATCHER_SCRIPT_CONTENT) os.chmod(WATCHER_SCRIPT, 0o755) print(f" ✅ Watcher-Script erstellt: {WATCHER_SCRIPT}") # Create systemd service with open(SYSTEMD_SERVICE, 'w') as f: f.write(SYSTEMD_SERVICE_CONTENT) print(f" ✅ Systemd-Service erstellt: {SYSTEMD_SERVICE}") # Reload systemd and start service run_command("systemctl daemon-reload") run_command("systemctl enable geoip-crowdsec-watcher.service") run_command("systemctl start geoip-crowdsec-watcher.service") # Check if started time.sleep(1) code, stdout, _ = run_command("systemctl is-active geoip-crowdsec-watcher.service") if code == 0 and stdout.strip() == "active": print(" ✅ Service gestartet und läuft") return True else: print(" ⚠️ Service konnte nicht gestartet werden") return False def uninstall_watcher_service(): """Uninstall the watcher script and systemd service""" print(" 📦 Deinstalliere CrowdSec-Watcher-Service...") # Stop and disable service run_command("systemctl stop geoip-crowdsec-watcher.service") run_command("systemctl disable geoip-crowdsec-watcher.service") # Remove files if os.path.isfile(SYSTEMD_SERVICE): os.remove(SYSTEMD_SERVICE) print(f" 🗑️ Service-Datei gelöscht") if os.path.isfile(WATCHER_SCRIPT): os.remove(WATCHER_SCRIPT) print(f" 🗑️ Watcher-Script gelöscht") run_command("systemctl daemon-reload") print(" ✅ Service deinstalliert") def add_shop_to_active(shop): """Add shop to active shops tracking""" os.makedirs(os.path.dirname(ACTIVE_SHOPS_FILE), exist_ok=True) shops = {} if os.path.isfile(ACTIVE_SHOPS_FILE): with open(ACTIVE_SHOPS_FILE, 'r') as f: shops = json.load(f) shops[shop] = { "activated": datetime.now().isoformat(), "expiry": (datetime.now() + timedelta(hours=72)).isoformat() } with open(ACTIVE_SHOPS_FILE, 'w') as f: json.dump(shops, f, indent=2) def remove_shop_from_active(shop): """Remove shop from active shops tracking""" if not os.path.isfile(ACTIVE_SHOPS_FILE): return with open(ACTIVE_SHOPS_FILE, 'r') as f: shops = json.load(f) if shop in shops: del shops[shop] with open(ACTIVE_SHOPS_FILE, 'w') as f: json.dump(shops, f, indent=2) def cleanup_crowdsec_decisions(shop): """Remove CrowdSec decisions for a shop""" if not check_crowdsec(): return print(f" 🔍 Entferne CrowdSec-Decisions für {shop}...") # Use the same reliable method as manual cleanup # This uses xargs to properly handle all IPs cleanup_cmd = f"cscli decisions list -o raw | grep '{shop}' | cut -d',' -f3 | cut -d':' -f2 | xargs -I {{}} cscli decisions delete --ip {{}}" code, stdout, stderr = run_command(cleanup_cmd) # Count how many were removed by checking the output if stdout: # Count "deleted" messages removed = stdout.count("decision(s) deleted") if removed > 0: print(f" ✅ {removed} Decisions entfernt") else: print(" ℹ️ Keine Decisions gefunden") else: print(" ℹ️ Keine Decisions gefunden") def get_available_shops(): """Get list of all shops""" shops = [] if not os.path.exists(VHOSTS_DIR): return shops for entry in os.listdir(VHOSTS_DIR): shop_path = os.path.join(VHOSTS_DIR, entry) if os.path.isdir(shop_path) and entry not in ['chroot', 'system', 'default']: httpdocs = os.path.join(shop_path, 'httpdocs') if os.path.isdir(httpdocs): index_php = os.path.join(httpdocs, 'index.php') if os.path.isfile(index_php): shops.append(entry) return sorted(shops) def get_active_shops(): """Get list of shops with active blocking""" active = [] shops = get_available_shops() for shop in shops: httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs') blocking_file = os.path.join(httpdocs, BLOCKING_FILE) backup_file = os.path.join(httpdocs, f'index.php{BACKUP_SUFFIX}') if os.path.isfile(blocking_file) or os.path.isfile(backup_file): active.append(shop) return active def activate_blocking(shop): """Activate GeoIP blocking""" httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs') index_php = os.path.join(httpdocs, 'index.php') backup_php = os.path.join(httpdocs, f'index.php{BACKUP_SUFFIX}') blocking_file = os.path.join(httpdocs, BLOCKING_FILE) if os.path.isfile(backup_php): print(f"⚠️ GeoIP-Blocking bereits aktiv für {shop}") return False if not os.path.isfile(index_php): print(f"❌ index.php nicht gefunden") return False print(f"\n🔧 Aktiviere Hybrid GeoIP-Blocking für: {shop}") print("=" * 60) # Step 1: Install watcher service if not exists active_shops = get_active_shops() if not active_shops: # First shop print("\n[1/3] Installiere CrowdSec-Watcher-Service...") if check_crowdsec(): install_watcher_service() else: print(" ⚠️ CrowdSec nicht verfügbar - nur PHP-Blocking") else: print("\n[1/3] CrowdSec-Watcher-Service bereits aktiv") # Step 2: PHP blocking print("\n[2/3] Aktiviere PHP-Blocking...") print(" 📋 Backup erstellen...") shutil.copy2(index_php, backup_php) with open(index_php, 'r', encoding='utf-8') as f: content = f.read() lines = content.split('\n') insert_line = 0 for i, line in enumerate(lines): if 'declare(strict_types' in line: insert_line = i + 1 break elif ' 8: # Column 2: ip (format "Ip:1.2.3.4") ip_field = parts[2].strip() if ':' in ip_field: ip = ip_field.split(':', 1)[1] else: ip = ip_field # Column 8: expiration expiry = parts[8].strip() print(f" 🚫 {ip} (bis {expiry})") if len(shop_decisions) > 20: print(f" ... und {len(shop_decisions) - 20} weitere") else: print("Keine aktiven CrowdSec-Bans für diesen Shop") else: print("Konnte Decisions nicht abrufen") print("=" * 80) def main(): """Main menu""" print("\n" + "=" * 60) print(" GeoIP Shop Blocker Manager - Final") print(" PHP + CrowdSec Watcher (systemd service)") print("=" * 60) if check_crowdsec(): print(" ✅ CrowdSec: Aktiv") else: print(" ⚠️ CrowdSec: Nicht verfügbar") # Check service status code, stdout, _ = run_command("systemctl is-active geoip-crowdsec-watcher.service") if code == 0 and stdout.strip() == "active": print(" ✅ Watcher-Service: Läuft") else: print(" ⚠️ Watcher-Service: Nicht aktiv") while True: print("\n[1] GeoIP-Blocking AKTIVIEREN") print("[2] GeoIP-Blocking DEAKTIVIEREN") print("[3] Logs anzeigen") print("[4] Status anzeigen") print("[0] Beenden") choice = input("\nWähle eine Option: ").strip() if choice == "1": shops = get_available_shops() active_shops = get_active_shops() available_shops = [s for s in shops if s not in active_shops] if not available_shops: print("\n⚠️ Keine Shops verfügbar") continue print("\n📋 Verfügbare Shops:") for i, shop in enumerate(available_shops, 1): print(f" [{i}] {shop}") shop_choice = input("\nWähle einen Shop: ").strip() try: shop_idx = int(shop_choice) - 1 if 0 <= shop_idx < len(available_shops): selected_shop = available_shops[shop_idx] confirm = input(f"\n⚠️ Aktivieren für '{selected_shop}'? (ja/nein): ").strip().lower() if confirm in ['ja', 'j', 'yes', 'y']: activate_blocking(selected_shop) else: print("❌ Ungültig") except ValueError: print("❌ Ungültig") elif choice == "2": active_shops = get_active_shops() if not active_shops: print("\n⚠️ Keine aktiven Shops") continue print("\n📋 Aktive Shops:") for i, shop in enumerate(active_shops, 1): print(f" [{i}] {shop}") shop_choice = input("\nWähle einen Shop: ").strip() try: shop_idx = int(shop_choice) - 1 if 0 <= shop_idx < len(active_shops): selected_shop = active_shops[shop_idx] confirm = input(f"\n⚠️ Deaktivieren für '{selected_shop}'? (ja/nein): ").strip().lower() if confirm in ['ja', 'j', 'yes', 'y']: deactivate_blocking(selected_shop) else: print("❌ Ungültig") except ValueError: print("❌ Ungültig") elif choice == "3": active_shops = get_active_shops() if not active_shops: print("\n⚠️ Keine aktiven Shops") continue print("\n📋 Shops mit Logs:") for i, shop in enumerate(active_shops, 1): print(f" [{i}] {shop}") shop_choice = input("\nWähle einen Shop: ").strip() try: shop_idx = int(shop_choice) - 1 if 0 <= shop_idx < len(active_shops): show_logs(active_shops[shop_idx]) except ValueError: pass elif choice == "4": shops = get_available_shops() active_shops = get_active_shops() print(f"\n📊 Status:") print(f" Shops gesamt: {len(shops)}") print(f" Aktive Blockings: {len(active_shops)}") if active_shops: for shop in active_shops: print(f" ✓ {shop}") elif choice == "0": print("\n👋 Auf Wiedersehen!") break if __name__ == "__main__": if os.geteuid() != 0: print("❌ Als root ausführen!") sys.exit(1) try: main() except KeyboardInterrupt: print("\n\n👋 Abgebrochen") sys.exit(0)