From 3533530eab1a941bc6784ed47c25628bf40aa67d Mon Sep 17 00:00:00 2001 From: thomasciesla Date: Fri, 28 Nov 2025 17:51:33 +0100 Subject: [PATCH] =?UTF-8?q?geoip=5Fshop=5Fmanager.py=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geoip_shop_manager.py | 771 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 771 insertions(+) create mode 100644 geoip_shop_manager.py diff --git a/geoip_shop_manager.py b/geoip_shop_manager.py new file mode 100644 index 0000000..bdc5cc7 --- /dev/null +++ b/geoip_shop_manager.py @@ -0,0 +1,771 @@ +#!/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) \ No newline at end of file