#!/usr/bin/env python3 """ GeoIP Shop Blocker Manager - DACH & Eurozone Version 2-Component System: PHP blocking + Python watcher (systemd service) Supports two geo regions: - DACH: Germany, Austria, Switzerland (3 countries) - Eurozone+GB: All Eurozone countries + GB + CH (22 countries) Supports three modes: - php+crowdsec: GeoIP blocking with CrowdSec integration - php-only: GeoIP blocking without CrowdSec - bot-only: Block only bots, shop remains globally accessible v3.4.2: Fixed directory permissions for rate-limit (777 for PHP access) """ import os import sys import shutil import subprocess import json import time import re import socket from datetime import datetime, timedelta from pathlib import Path # ANSI Color Codes COLOR_GREEN = "\033[92m" COLOR_RED = "\033[91m" COLOR_YELLOW = "\033[93m" COLOR_BLUE = "\033[94m" COLOR_RESET = "\033[0m" COLOR_BOLD = "\033[1m" # Link11 IP LINK11_IP = "128.65.223.106" # Cache for DNS lookups (to avoid repeated lookups) DNS_CACHE = {} # Configuration VHOSTS_DIR = "/var/www/vhosts" BACKUP_SUFFIX = ".geoip_backup" BLOCKING_FILE = "geoip_blocking.php" CACHE_FILE = "geoip_ip_ranges.cache" LOG_FILE = "geoip_blocked.log" CROWDSEC_QUEUE_FILE = "geoip_crowdsec_queue.log" RATELIMIT_DIR = "geoip_ratelimit" 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" # Rate-Limit Defaults DEFAULT_RATE_LIMIT = 30 # Requests pro Minute DEFAULT_BAN_DURATION = 5 # Minuten # Minimum expected IP ranges per region (for validation) MIN_RANGES = { "dach": 1000, # DE+AT+CH should have at least 1000 ranges "eurozone": 5000 # 22 countries should have at least 5000 ranges } # ============================================================================= # GEO REGIONS # ============================================================================= GEO_REGIONS = { "dach": { "name": "DACH", "countries": ["de", "at", "ch"], "description": "Deutschland, Österreich, Schweiz", "icon": "🇩🇪🇦🇹🇨🇭", "short": "DACH" }, "eurozone": { "name": "Eurozone + GB", "countries": [ "de", "at", "ch", "be", "cy", "ee", "es", "fi", "fr", "gb", "gr", "hr", "ie", "it", "lt", "lu", "lv", "mt", "nl", "pt", "si", "sk" ], "description": "22 Länder: DE, AT, CH, BE, CY, EE, ES, FI, FR, GB, GR, HR, IE, IT, LT, LU, LV, MT, NL, PT, SI, SK", "icon": "🇪🇺", "short": "EU+" }, "none": { "name": "Bot-Only", "countries": [], "description": "Nur Bot-Blocking, weltweit erreichbar", "icon": "🤖", "short": "BOT" } } # ============================================================================= # BOT DETECTION # ============================================================================= BOT_PATTERNS = { 'GPTBot': r'GPTBot', 'OAI-SearchBot': r'OAI-SearchBot', 'ChatGPT-User': r'ChatGPT-User', 'ClaudeBot': r'ClaudeBot', 'Claude-User': r'Claude-User', 'anthropic-ai': r'anthropic-ai', 'Googlebot': r'Googlebot', 'Google-Extended': r'Google-Extended', 'AdsBot-Google': r'AdsBot-Google', 'Bingbot': r'[Bb]ingbot', 'BingPreview': r'BingPreview', 'msnbot': r'msnbot', 'PerplexityBot': r'PerplexityBot', 'Applebot': r'Applebot', 'Amazonbot': r'Amazonbot', 'FacebookBot': r'facebookexternalhit|FacebookBot', 'Bytespider': r'Bytespider', 'DuckDuckBot': r'DuckDuckBot', 'YandexBot': r'YandexBot', 'Baiduspider': r'Baiduspider', 'AhrefsBot': r'AhrefsBot', 'SemrushBot': r'SemrushBot', 'MJ12bot': r'MJ12bot', 'DotBot': r'DotBot', 'PetalBot': r'PetalBot', 'DataForSeoBot': r'DataForSeoBot', 'LinkedInBot': r'LinkedInBot', 'Twitterbot': r'Twitterbot', 'Slackbot': r'Slackbot', 'UptimeRobot': r'UptimeRobot', 'Pingdom': r'Pingdom', 'curl': r'^curl/', 'python-requests': r'python-requests', 'Wget': r'Wget', 'Scrapy': r'Scrapy', } def detect_bot(user_agent): if not user_agent or user_agent == 'Unknown': return 'Unbekannt' for bot_name, pattern in BOT_PATTERNS.items(): if re.search(pattern, user_agent, re.IGNORECASE): return bot_name return 'Unbekannt' def check_link11(domain): global DNS_CACHE if domain in DNS_CACHE: return DNS_CACHE[domain] try: ip = socket.gethostbyname(domain) is_link11 = (ip == LINK11_IP) DNS_CACHE[domain] = {'is_link11': is_link11, 'ip': ip} return DNS_CACHE[domain] except socket.gaierror: DNS_CACHE[domain] = {'is_link11': False, 'ip': 'N/A'} return DNS_CACHE[domain] def format_shop_with_link11(shop, prefix="", show_index=None): link11_info = check_link11(shop) if link11_info['is_link11']: color, suffix = COLOR_GREEN, " [Link11]" else: color, suffix = COLOR_RED, " [Direkt]" if show_index is not None: return f" [{show_index}] {color}{shop}{suffix}{COLOR_RESET}" return f"{prefix}{color}{shop}{suffix}{COLOR_RESET}" def get_geo_region_info(geo_region): return GEO_REGIONS.get(geo_region, GEO_REGIONS["dach"]) def generate_php_countries_array(geo_region): region_info = get_geo_region_info(geo_region) return ", ".join([f"'{c}'" for c in region_info["countries"]]) def generate_php_bot_patterns(): """Generate PHP array of bot patterns""" patterns = [] for bot_name, pattern in BOT_PATTERNS.items(): escaped_pattern = pattern.replace("'", "\\'") patterns.append(f"'{bot_name}' => '/{escaped_pattern}/i'") return ",\n ".join(patterns) # ============================================================================= # CACHE VALIDATION # ============================================================================= def generate_and_validate_cache(httpdocs_path, geo_region): """Generate the IP ranges cache and validate it. Returns (success, range_count, error_message)""" cache_file = os.path.join(httpdocs_path, CACHE_FILE) region_info = get_geo_region_info(geo_region) countries = region_info["countries"] min_expected = MIN_RANGES.get(geo_region, 1000) php_script = f''' ['timeout' => 30]]); $content = @file_get_contents($url, false, $ctx); if ($content !== false) {{ $lines = explode("\\n", trim($content)); foreach ($lines as $line) {{ $line = trim($line); if (!empty($line) && strpos($line, '/') !== false) {{ $ranges[] = $line; }} }} }} }} if (count($ranges) >= {min_expected}) {{ file_put_contents("{cache_file}", serialize($ranges)); echo "OK:" . count($ranges); }} else {{ echo "FAIL:" . count($ranges); }} ''' temp_php = os.path.join(httpdocs_path, '_geoip_cache_gen.php') try: with open(temp_php, 'w') as f: f.write(php_script) result = subprocess.run(['php', temp_php], capture_output=True, text=True, timeout=120) output = result.stdout.strip() if output.startswith('OK:'): return True, int(output.split(':')[1]), None elif output.startswith('FAIL:'): return False, int(output.split(':')[1]), f"Nur {output.split(':')[1]} Ranges (min. {min_expected} erwartet)" return False, 0, f"Unerwartete Ausgabe: {output}" except subprocess.TimeoutExpired: return False, 0, "Timeout beim Laden der IP-Ranges" except Exception as e: return False, 0, str(e) finally: if os.path.exists(temp_php): os.remove(temp_php) def validate_existing_cache(httpdocs_path, geo_region): """Validate existing cache. Returns (valid, range_count, error_message)""" cache_file = os.path.join(httpdocs_path, CACHE_FILE) min_expected = MIN_RANGES.get(geo_region, 1000) if not os.path.exists(cache_file): return False, 0, "Cache-Datei existiert nicht" php_script = f'''= min_expected: return True, count, None return False, count, f"Nur {count} Ranges (min. {min_expected} erwartet)" return False, 0, "Cache-Datei ist korrupt" except Exception as e: return False, 0, str(e) # ============================================================================= # PHP TEMPLATES WITH FAIL-OPEN AND RATE-LIMITING # ============================================================================= GEOIP_SCRIPT_TEMPLATE = ''' $expiry_date) return; $visitor_ip = $_SERVER['REMOTE_ADDR'] ?? ''; if (empty($visitor_ip)) return; if (filter_var($visitor_ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) return; $cache_file = __DIR__ . '/{cache_file}'; $cache_duration = 86400; $log_file = __DIR__ . '/{log_file}'; $crowdsec_queue = __DIR__ . '/{crowdsec_queue}'; $min_ranges = {min_ranges}; $allowed_countries = [{countries_array}]; function download_allowed_ranges($countries) {{ $ranges = []; foreach ($countries as $country) {{ $url = "https://www.ipdeny.com/ipblocks/data/aggregated/$country-aggregated.zone"; $ctx = stream_context_create(['http' => ['timeout' => 30]]); $content = @file_get_contents($url, false, $ctx); if ($content !== false) {{ foreach (explode("\\n", trim($content)) as $line) {{ $line = trim($line); if (!empty($line) && strpos($line, '/') !== false) $ranges[] = $line; }} }} }} return $ranges; }} function ip_in_range($ip, $cidr) {{ list($subnet, $mask) = explode('/', $cidr); $mask_long = -1 << (32 - (int)$mask); return (ip2long($ip) & $mask_long) == (ip2long($subnet) & $mask_long); }} $allowed_ranges = []; $cache_valid = false; if (file_exists($cache_file) && (time() - filemtime($cache_file)) < $cache_duration) {{ $cached_data = @file_get_contents($cache_file); if ($cached_data !== false) {{ $allowed_ranges = @unserialize($cached_data); if (is_array($allowed_ranges) && count($allowed_ranges) >= $min_ranges) {{ $cache_valid = true; }} else {{ @unlink($cache_file); $allowed_ranges = []; }} }} }} if (!$cache_valid) {{ $allowed_ranges = download_allowed_ranges($allowed_countries); if (is_array($allowed_ranges) && count($allowed_ranges) >= $min_ranges) {{ @file_put_contents($cache_file, serialize($allowed_ranges)); $cache_valid = true; }} else {{ error_log("GeoIP FAIL-OPEN: Could not load valid IP ranges (got " . count($allowed_ranges) . ", need $min_ranges)"); return; }} }} $is_allowed = false; foreach ($allowed_ranges as $range) {{ if (ip_in_range($visitor_ip, $range)) {{ $is_allowed = true; break; }} }} if (!$is_allowed) {{ $timestamp = date('Y-m-d H:i:s'); $ua = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown'; $uri = $_SERVER['REQUEST_URI'] ?? '/'; @file_put_contents($log_file, "[$timestamp] IP: $visitor_ip | UA: $ua | URI: $uri\\n", FILE_APPEND | LOCK_EX); @file_put_contents($crowdsec_queue, "$timestamp|$visitor_ip|{shop_name}\\n", FILE_APPEND | LOCK_EX); header('HTTP/1.1 403 Forbidden'); exit; }} ''' GEOIP_SCRIPT_TEMPLATE_PHP_ONLY = ''' $expiry_date) return; $visitor_ip = $_SERVER['REMOTE_ADDR'] ?? ''; if (empty($visitor_ip)) return; if (filter_var($visitor_ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) return; $cache_file = __DIR__ . '/{cache_file}'; $cache_duration = 86400; $log_file = __DIR__ . '/{log_file}'; $min_ranges = {min_ranges}; $allowed_countries = [{countries_array}]; function download_allowed_ranges($countries) {{ $ranges = []; foreach ($countries as $country) {{ $url = "https://www.ipdeny.com/ipblocks/data/aggregated/$country-aggregated.zone"; $ctx = stream_context_create(['http' => ['timeout' => 30]]); $content = @file_get_contents($url, false, $ctx); if ($content !== false) {{ foreach (explode("\\n", trim($content)) as $line) {{ $line = trim($line); if (!empty($line) && strpos($line, '/') !== false) $ranges[] = $line; }} }} }} return $ranges; }} function ip_in_range($ip, $cidr) {{ list($subnet, $mask) = explode('/', $cidr); $mask_long = -1 << (32 - (int)$mask); return (ip2long($ip) & $mask_long) == (ip2long($subnet) & $mask_long); }} $allowed_ranges = []; $cache_valid = false; if (file_exists($cache_file) && (time() - filemtime($cache_file)) < $cache_duration) {{ $cached_data = @file_get_contents($cache_file); if ($cached_data !== false) {{ $allowed_ranges = @unserialize($cached_data); if (is_array($allowed_ranges) && count($allowed_ranges) >= $min_ranges) {{ $cache_valid = true; }} else {{ @unlink($cache_file); $allowed_ranges = []; }} }} }} if (!$cache_valid) {{ $allowed_ranges = download_allowed_ranges($allowed_countries); if (is_array($allowed_ranges) && count($allowed_ranges) >= $min_ranges) {{ @file_put_contents($cache_file, serialize($allowed_ranges)); $cache_valid = true; }} else {{ error_log("GeoIP FAIL-OPEN: Could not load valid IP ranges (got " . count($allowed_ranges) . ", need $min_ranges)"); return; }} }} $is_allowed = false; foreach ($allowed_ranges as $range) {{ if (ip_in_range($visitor_ip, $range)) {{ $is_allowed = true; break; }} }} if (!$is_allowed) {{ $timestamp = date('Y-m-d H:i:s'); $ua = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown'; $uri = $_SERVER['REQUEST_URI'] ?? '/'; @file_put_contents($log_file, "[$timestamp] IP: $visitor_ip | UA: $ua | URI: $uri\\n", FILE_APPEND | LOCK_EX); header('HTTP/1.1 403 Forbidden'); exit; }} ''' BOT_ONLY_SCRIPT_TEMPLATE = ''' $expiry_date) return; $log_file = __DIR__ . '/{log_file}'; $crowdsec_queue = __DIR__ . '/{crowdsec_queue}'; $ratelimit_dir = __DIR__ . '/{ratelimit_dir}'; $bans_dir = $ratelimit_dir . '/bans'; $counts_dir = $ratelimit_dir . '/counts'; // Rate-Limit Configuration $rate_limit = {rate_limit}; // Requests per minute $ban_duration = {ban_duration}; // Ban duration in seconds $window_size = 60; // Window size in seconds (1 minute) $cleanup_probability = 100; // 1 in X chance to run cleanup $visitor_ip = $_SERVER['REMOTE_ADDR'] ?? ''; $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; // Create hash for this visitor (IP + User-Agent) $visitor_hash = md5($visitor_ip . '|' . $user_agent); // Ensure directories exist if (!is_dir($bans_dir)) @mkdir($bans_dir, 0755, true); if (!is_dir($counts_dir)) @mkdir($counts_dir, 0755, true); // === STEP 1: Check if visitor is banned === $ban_file = "$bans_dir/$visitor_hash.ban"; if (file_exists($ban_file)) {{ $ban_until = (int)@file_get_contents($ban_file); if (time() < $ban_until) {{ // Still banned - immediate 403 header('HTTP/1.1 403 Forbidden'); header('Retry-After: ' . ($ban_until - time())); exit; }} // Ban expired - remove file @unlink($ban_file); }} // === STEP 2: Bot detection === $bot_patterns = [ {bot_patterns} ]; if (empty($user_agent)) return; $detected_bot = null; foreach ($bot_patterns as $bot_name => $pattern) {{ if (preg_match($pattern, $user_agent)) {{ $detected_bot = $bot_name; break; }} }} // Not a bot - allow through if ($detected_bot === null) return; // === STEP 3: Rate-Limit Check === $count_file = "$counts_dir/$visitor_hash.count"; $current_time = time(); $count = 1; $window_start = $current_time; if (file_exists($count_file)) {{ $fp = @fopen($count_file, 'c+'); if ($fp && flock($fp, LOCK_EX)) {{ $content = fread($fp, 100); if (!empty($content)) {{ $parts = explode('|', $content); if (count($parts) === 2) {{ $window_start = (int)$parts[0]; $count = (int)$parts[1]; if ($current_time - $window_start > $window_size) {{ // New window $window_start = $current_time; $count = 1; }} else {{ $count++; }} }} }} ftruncate($fp, 0); rewind($fp); fwrite($fp, "$window_start|$count"); flock($fp, LOCK_UN); fclose($fp); }} }} else {{ @file_put_contents($count_file, "$window_start|$count", LOCK_EX); }} // === STEP 4: Check if limit exceeded === $is_banned = false; if ($count > $rate_limit) {{ // Create ban $ban_until = $current_time + $ban_duration; @file_put_contents($ban_file, $ban_until, LOCK_EX); $is_banned = true; }} // === STEP 5: Log and block === $timestamp = date('Y-m-d H:i:s'); $uri = $_SERVER['REQUEST_URI'] ?? '/'; if ($is_banned) {{ $ban_minutes = $ban_duration / 60; @file_put_contents($log_file, "[$timestamp] BANNED: $detected_bot | IP: $visitor_ip | Exceeded $rate_limit req/min | Ban: {{$ban_minutes}}m | UA: $user_agent\\n", FILE_APPEND | LOCK_EX); }} else {{ @file_put_contents($log_file, "[$timestamp] BOT: $detected_bot | IP: $visitor_ip | Count: $count/$rate_limit | URI: $uri\\n", FILE_APPEND | LOCK_EX); }} @file_put_contents($crowdsec_queue, "$timestamp|$visitor_ip|{shop_name}\\n", FILE_APPEND | LOCK_EX); // === STEP 6: Probabilistic cleanup === if (rand(1, $cleanup_probability) === 1) {{ $now = time(); // Clean expired bans foreach (glob("$bans_dir/*.ban") as $f) {{ $ban_time = (int)@file_get_contents($f); if ($now > $ban_time) @unlink($f); }} // Clean old count files (older than 2x window) foreach (glob("$counts_dir/*.count") as $f) {{ if ($now - filemtime($f) > $window_size * 2) @unlink($f); }} }} header('HTTP/1.1 403 Forbidden'); if ($is_banned) {{ header('Retry-After: ' . $ban_duration); }} exit; ''' BOT_ONLY_SCRIPT_TEMPLATE_NO_CROWDSEC = ''' $expiry_date) return; $log_file = __DIR__ . '/{log_file}'; $ratelimit_dir = __DIR__ . '/{ratelimit_dir}'; $bans_dir = $ratelimit_dir . '/bans'; $counts_dir = $ratelimit_dir . '/counts'; // Rate-Limit Configuration $rate_limit = {rate_limit}; // Requests per minute $ban_duration = {ban_duration}; // Ban duration in seconds $window_size = 60; // Window size in seconds (1 minute) $cleanup_probability = 100; // 1 in X chance to run cleanup $visitor_ip = $_SERVER['REMOTE_ADDR'] ?? ''; $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; // Create hash for this visitor (IP + User-Agent) $visitor_hash = md5($visitor_ip . '|' . $user_agent); // Ensure directories exist if (!is_dir($bans_dir)) @mkdir($bans_dir, 0755, true); if (!is_dir($counts_dir)) @mkdir($counts_dir, 0755, true); // === STEP 1: Check if visitor is banned === $ban_file = "$bans_dir/$visitor_hash.ban"; if (file_exists($ban_file)) {{ $ban_until = (int)@file_get_contents($ban_file); if (time() < $ban_until) {{ // Still banned - immediate 403 header('HTTP/1.1 403 Forbidden'); header('Retry-After: ' . ($ban_until - time())); exit; }} // Ban expired - remove file @unlink($ban_file); }} // === STEP 2: Bot detection === $bot_patterns = [ {bot_patterns} ]; if (empty($user_agent)) return; $detected_bot = null; foreach ($bot_patterns as $bot_name => $pattern) {{ if (preg_match($pattern, $user_agent)) {{ $detected_bot = $bot_name; break; }} }} // Not a bot - allow through if ($detected_bot === null) return; // === STEP 3: Rate-Limit Check === $count_file = "$counts_dir/$visitor_hash.count"; $current_time = time(); $count = 1; $window_start = $current_time; if (file_exists($count_file)) {{ $fp = @fopen($count_file, 'c+'); if ($fp && flock($fp, LOCK_EX)) {{ $content = fread($fp, 100); if (!empty($content)) {{ $parts = explode('|', $content); if (count($parts) === 2) {{ $window_start = (int)$parts[0]; $count = (int)$parts[1]; if ($current_time - $window_start > $window_size) {{ // New window $window_start = $current_time; $count = 1; }} else {{ $count++; }} }} }} ftruncate($fp, 0); rewind($fp); fwrite($fp, "$window_start|$count"); flock($fp, LOCK_UN); fclose($fp); }} }} else {{ @file_put_contents($count_file, "$window_start|$count", LOCK_EX); }} // === STEP 4: Check if limit exceeded === $is_banned = false; if ($count > $rate_limit) {{ // Create ban $ban_until = $current_time + $ban_duration; @file_put_contents($ban_file, $ban_until, LOCK_EX); $is_banned = true; }} // === STEP 5: Log and block === $timestamp = date('Y-m-d H:i:s'); $uri = $_SERVER['REQUEST_URI'] ?? '/'; if ($is_banned) {{ $ban_minutes = $ban_duration / 60; @file_put_contents($log_file, "[$timestamp] BANNED: $detected_bot | IP: $visitor_ip | Exceeded $rate_limit req/min | Ban: {{$ban_minutes}}m | UA: $user_agent\\n", FILE_APPEND | LOCK_EX); }} else {{ @file_put_contents($log_file, "[$timestamp] BOT: $detected_bot | IP: $visitor_ip | Count: $count/$rate_limit | URI: $uri\\n", FILE_APPEND | LOCK_EX); }} // === STEP 6: Probabilistic cleanup === if (rand(1, $cleanup_probability) === 1) {{ $now = time(); // Clean expired bans foreach (glob("$bans_dir/*.ban") as $f) {{ $ban_time = (int)@file_get_contents($f); if ($now > $ban_time) @unlink($f); }} // Clean old count files (older than 2x window) foreach (glob("$counts_dir/*.count") as $f) {{ if ($now - filemtime($f) > $window_size * 2) @unlink($f); }} }} header('HTTP/1.1 403 Forbidden'); if ($is_banned) {{ header('Retry-After: ' . $ban_duration); }} exit; ''' # ============================================================================= # WATCHER SERVICE # ============================================================================= WATCHER_SCRIPT_CONTENT = '''#!/usr/bin/env python3 import os, sys, time, subprocess, json 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 = {} CHECK_INTERVAL = 5 def log(msg): print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}", flush=True) def get_active_shops(): 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): now = time.time() if ip in PROCESSED_IPS and (now - PROCESSED_IPS[ip]) < 3600: return True try: result = subprocess.run(['cscli', 'decisions', 'add', '--ip', ip, '--duration', '72h', '--type', 'ban', '--reason', f'GeoIP: blocked by {shop}'], 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 except: pass return False def process_queue_file(shop_path, shop): queue_file = os.path.join(shop_path, 'httpdocs', QUEUE_FILE) if not os.path.isfile(queue_file): return 0 processed = 0 try: with open(queue_file, 'r') as f: lines = f.readlines() if not lines: return 0 for line in lines: parts = line.strip().split('|') if len(parts) >= 2 and add_to_crowdsec(parts[1], shop): processed += 1 if processed > 0: with open(queue_file, 'w') as f: f.write('') except: pass return processed def main(): log("🚀 GeoIP CrowdSec Watcher started") while True: try: active_shops = get_active_shops() for shop, info in active_shops.items(): # Only process shops with CrowdSec enabled mode = info.get('mode', 'php+crowdsec') if mode in ['php+crowdsec', 'bot+crowdsec']: shop_path = os.path.join(VHOSTS_DIR, shop) if os.path.isdir(shop_path): process_queue_file(shop_path, shop) time.sleep(CHECK_INTERVAL) except KeyboardInterrupt: break except: time.sleep(CHECK_INTERVAL) if __name__ == "__main__": main() ''' SYSTEMD_SERVICE_CONTENT = '''[Unit] Description=GeoIP CrowdSec Watcher Service After=network.target crowdsec.service [Service] Type=simple ExecStart=/usr/bin/python3 /usr/local/bin/geoip_crowdsec_watcher.py Restart=always RestartSec=10 [Install] WantedBy=multi-user.target ''' # ============================================================================= # HELPER FUNCTIONS # ============================================================================= def run_command(cmd, capture_output=True): try: if capture_output: result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30) return result.returncode, result.stdout, result.stderr return subprocess.run(cmd, shell=True, timeout=30).returncode, "", "" except Exception as e: return -1, "", str(e) def check_crowdsec(): code, stdout, _ = run_command("systemctl is-active crowdsec") return code == 0 and stdout.strip() == "active" def install_watcher_service(): print(" 📦 Installiere CrowdSec-Watcher-Service...") with open(WATCHER_SCRIPT, 'w') as f: f.write(WATCHER_SCRIPT_CONTENT) os.chmod(WATCHER_SCRIPT, 0o755) with open(SYSTEMD_SERVICE, 'w') as f: f.write(SYSTEMD_SERVICE_CONTENT) run_command("systemctl daemon-reload") run_command("systemctl enable geoip-crowdsec-watcher.service") run_command("systemctl start geoip-crowdsec-watcher.service") time.sleep(1) code, stdout, _ = run_command("systemctl is-active geoip-crowdsec-watcher.service") if code == 0 and stdout.strip() == "active": print(" ✅ Service gestartet") return True print(" ⚠️ Service konnte nicht gestartet werden") return False def uninstall_watcher_service(): print(" 📦 Deinstalliere CrowdSec-Watcher-Service...") run_command("systemctl stop geoip-crowdsec-watcher.service") run_command("systemctl disable geoip-crowdsec-watcher.service") for f in [SYSTEMD_SERVICE, WATCHER_SCRIPT]: if os.path.isfile(f): os.remove(f) run_command("systemctl daemon-reload") print(" ✅ Service deinstalliert") def add_shop_to_active(shop, mode="php+crowdsec", geo_region="dach", rate_limit=None, ban_duration=None): 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) shop_data = { "activated": datetime.now().isoformat(), "expiry": (datetime.now() + timedelta(hours=72)).isoformat(), "mode": mode, "geo_region": geo_region } if rate_limit is not None: shop_data["rate_limit"] = rate_limit if ban_duration is not None: shop_data["ban_duration"] = ban_duration shops[shop] = shop_data with open(ACTIVE_SHOPS_FILE, 'w') as f: json.dump(shops, f, indent=2) def get_shop_mode(shop): if not os.path.isfile(ACTIVE_SHOPS_FILE): return "php+crowdsec" try: with open(ACTIVE_SHOPS_FILE, 'r') as f: return json.load(f).get(shop, {}).get("mode", "php+crowdsec") except: return "php+crowdsec" def get_shop_geo_region(shop): if not os.path.isfile(ACTIVE_SHOPS_FILE): return "dach" try: with open(ACTIVE_SHOPS_FILE, 'r') as f: return json.load(f).get(shop, {}).get("geo_region", "dach") except: return "dach" def get_shop_rate_limit_config(shop): """Get rate limit configuration for a shop""" if not os.path.isfile(ACTIVE_SHOPS_FILE): return None, None try: with open(ACTIVE_SHOPS_FILE, 'r') as f: shop_data = json.load(f).get(shop, {}) return shop_data.get("rate_limit"), shop_data.get("ban_duration") except: return None, None def get_shop_activation_time(shop): if not os.path.isfile(ACTIVE_SHOPS_FILE): return None try: with open(ACTIVE_SHOPS_FILE, 'r') as f: activated_str = json.load(f).get(shop, {}).get("activated") return datetime.fromisoformat(activated_str) if activated_str else None except: return None def format_duration(minutes): if minutes < 60: return f"{int(minutes)}m" hours = minutes / 60 if hours < 24: return f"{int(hours)}h {int(minutes % 60)}m" return f"{int(hours / 24)}d {int(hours % 24)}h" def remove_shop_from_active(shop): 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): if not check_crowdsec(): return print(f" 🔍 Entferne CrowdSec-Decisions für {shop}...") total_removed = 0 for _ in range(50): code, stdout, _ = run_command(f"cscli decisions list -o raw --limit 0 | grep '{shop}'") if code != 0 or not stdout.strip(): break batch_count = 0 for line in stdout.strip().split('\n')[:100]: try: parts = line.split(',') if len(parts) >= 3: ip_field = parts[2].strip() ip = ip_field.split(':', 1)[1] if ':' in ip_field else ip_field if ip and run_command(f"cscli decisions delete --ip {ip}")[0] == 0: batch_count += 1 except: continue total_removed += batch_count if batch_count == 0: break print(f" ✅ {total_removed} Decisions entfernt" if total_removed else " ℹ️ Keine Decisions gefunden") def get_available_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) and os.path.isfile(os.path.join(httpdocs, 'index.php')): shops.append(entry) return sorted(shops) def get_active_shops(): active = [] for shop in get_available_shops(): httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs') if os.path.isfile(os.path.join(httpdocs, BLOCKING_FILE)) or os.path.isfile(os.path.join(httpdocs, f'index.php{BACKUP_SUFFIX}')): active.append(shop) return active def get_crowdsec_modes(): """Return list of modes that use CrowdSec""" return ['php+crowdsec', 'bot+crowdsec'] def select_geo_region(): print(f"\n🌍 Wähle die Geo-Region:") print(f" [1] {GEO_REGIONS['dach']['icon']} DACH - {GEO_REGIONS['dach']['description']}") print(f" [2] {GEO_REGIONS['eurozone']['icon']} Eurozone+GB - {len(GEO_REGIONS['eurozone']['countries'])} Länder") return "eurozone" if input(f"\nRegion wählen [1/2]: ").strip() == "2" else "dach" def select_mode(): print(f"\n🔧 Wähle den Blocking-Modus:") print(f" [1] 🌍 GeoIP + CrowdSec (IPs werden an CrowdSec gemeldet)") print(f" [2] 🌍 Nur GeoIP (keine CrowdSec-Synchronisation)") print(f" [3] 🤖 Nur Bot-Blocking (weltweit erreichbar, mit CrowdSec)") print(f" [4] 🤖 Nur Bot-Blocking (weltweit erreichbar, ohne CrowdSec)") choice = input(f"\nModus wählen [1/2/3/4]: ").strip() if choice == "2": return "php-only" elif choice == "3": return "bot+crowdsec" elif choice == "4": return "bot-only" return "php+crowdsec" def select_rate_limit(): """Ask user for rate-limit configuration. Returns (rate_limit, ban_duration_seconds)""" print(f"\n🚦 Rate-Limit Konfiguration:") # Rate limit rate_input = input(f" Requests pro Minute bevor Ban [{DEFAULT_RATE_LIMIT}]: ").strip() try: rate_limit = int(rate_input) if rate_input else DEFAULT_RATE_LIMIT if rate_limit < 1: rate_limit = DEFAULT_RATE_LIMIT except ValueError: rate_limit = DEFAULT_RATE_LIMIT # Ban duration ban_input = input(f" Ban-Dauer in Minuten [{DEFAULT_BAN_DURATION}]: ").strip() try: ban_minutes = int(ban_input) if ban_input else DEFAULT_BAN_DURATION if ban_minutes < 1: ban_minutes = DEFAULT_BAN_DURATION except ValueError: ban_minutes = DEFAULT_BAN_DURATION ban_seconds = ban_minutes * 60 print(f"\n ✅ Rate-Limit: {rate_limit} req/min") print(f" ✅ Ban-Dauer: {ban_minutes} Minuten") return rate_limit, ban_seconds def get_mode_icon(mode): """Return icon for mode""" icons = { 'php+crowdsec': '🛡️', 'php-only': '📝', 'bot+crowdsec': '🤖🛡️', 'bot-only': '🤖' } return icons.get(mode, '❓') def get_mode_description(mode): """Return description for mode""" descriptions = { 'php+crowdsec': 'GeoIP + CrowdSec', 'php-only': 'Nur GeoIP', 'bot+crowdsec': 'Bot-Block + CrowdSec', 'bot-only': 'Nur Bot-Block' } return descriptions.get(mode, mode) def is_bot_only_mode(mode): """Check if mode is bot-only""" return mode in ['bot-only', 'bot+crowdsec'] def uses_crowdsec(mode): """Check if mode uses CrowdSec""" return mode in ['php+crowdsec', 'bot+crowdsec'] def get_direct_shops(available_shops): """Return list of shops that are NOT behind Link11 (direct exposure)""" direct_shops = [] for shop in available_shops: link11_info = check_link11(shop) if not link11_info['is_link11']: direct_shops.append(shop) return direct_shops def get_link11_shops(available_shops): """Return list of shops that ARE behind Link11""" link11_shops = [] for shop in available_shops: link11_info = check_link11(shop) if link11_info['is_link11']: link11_shops.append(shop) return link11_shops # ============================================================================= # MAIN FUNCTIONS # ============================================================================= def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach", rate_limit=None, ban_duration=None): 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) ratelimit_path = os.path.join(httpdocs, RATELIMIT_DIR) is_bot_mode = is_bot_only_mode(mode) if is_bot_mode: region_info = get_geo_region_info("none") geo_region = "none" else: region_info = get_geo_region_info(geo_region) min_ranges = MIN_RANGES.get(geo_region, 1000) if os.path.isfile(backup_php): if not silent: print(f"⚠️ Blocking bereits aktiv für {shop}") return False if not os.path.isfile(index_php): if not silent: print(f"❌ index.php nicht gefunden") return False if not silent: print(f"\n🔧 Aktiviere {region_info['icon']} {region_info['name']} für: {shop}") if is_bot_mode: print(f" Modus: Nur Bot-Blocking (weltweit erreichbar)") if rate_limit and ban_duration: print(f" Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min") else: print(f" Erlaubt: {region_info['description']}") print(f" CrowdSec: {'Ja' if uses_crowdsec(mode) else 'Nein'}") print("=" * 60) # Step 1: Watcher service if uses_crowdsec(mode): crowdsec_shops = [s for s in get_active_shops() if uses_crowdsec(get_shop_mode(s))] if not crowdsec_shops and check_crowdsec(): if not silent: print("\n[1/4] Installiere CrowdSec-Watcher-Service...") install_watcher_service() elif not silent: print("\n[1/4] CrowdSec-Watcher-Service bereits aktiv") elif not silent: print("\n[1/4] CrowdSec-Synchronisation deaktiviert") # Step 2: PHP blocking if not silent: print("\n[2/4] Aktiviere PHP-Blocking...") 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 ' 0 else 0 else: runtime, req_min = 0, 0 print(f"\n{'═' * 70}") print(f"📊 {shop} | {region_info['icon']} {region_info['name']} {get_mode_icon(shop_mode)}") print(f"{'═' * 70}") print(f"⏱️ Laufzeit: {format_duration(runtime)}") print(f"📈 Blocks: {blocks} ({req_min:.1f} req/min)") if not is_bot_mode: valid, count, err = validate_existing_cache(httpdocs, shop_geo) print(f"✅ Cache: {count:,} Ranges" if valid else f"⚠️ Cache: {err}") else: rate_limit, ban_duration = get_shop_rate_limit_config(shop) print(f"🤖 Bot-Patterns: {len(BOT_PATTERNS)} aktiv") if rate_limit and ban_duration: print(f"🚦 Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min") print(f"🚫 Bans: {total_bans} ausgelöst, {active_bans} aktiv") # Show bot statistics for bot-only mode if bots: print(f"\n🤖 Bot-Statistik:") for bot_name, count in sorted(bots.items(), key=lambda x: x[1], reverse=True)[:10]: bar = "█" * min(count // 5, 20) if count > 0 else "█" print(f" {bot_name}: {count}x {bar}") if os.path.isfile(log_file): print(f"\n📝 Letzte 30 Blocks:") with open(log_file, 'r') as f: for line in f.readlines()[-30:]: print(line.rstrip()) if ips: print(f"\n🔥 Top 10 IPs:") for ip, data in sorted(ips.items(), key=lambda x: x[1]['count'], reverse=True)[:10]: # Use stored bot name first, fallback to UA detection bot = data.get('bot') or detect_bot(data['ua']) print(f" {ip} ({bot}): {data['count']}x") def show_all_logs(): """Show combined logs for all active shops""" active_shops = get_active_shops() if not active_shops: print("\n⚠️ Keine aktiven Shops") return print(f"\n{'═' * 70}") print(" 📊 GESAMTÜBERSICHT ALLER SHOPS") print(f"{'═' * 70}") print(f" {COLOR_GREEN}Grün = hinter Link11{COLOR_RESET} | {COLOR_RED}Rot = Direkt{COLOR_RESET}") total_php_blocks = 0 total_bans = 0 total_active_bans = 0 shop_php_stats = {} # shop -> {'blocks': N, 'runtime_minutes': float, 'req_min': float, 'ips': {}, 'bots': {}} all_ips = {} # ip -> {'count': N, 'ua': user_agent, 'shops': {shop: count}} all_bots = {} # bot_name -> count total_minutes = 0 # Collect PHP stats for shop in active_shops: blocks, ips, bots, activation_time, bans, active_bans = get_shop_log_stats(shop) total_php_blocks += blocks total_bans += bans total_active_bans += active_bans # Calculate runtime and req/min if activation_time: runtime_minutes = (datetime.now() - activation_time).total_seconds() / 60 req_min = blocks / runtime_minutes if runtime_minutes > 0 else 0 else: runtime_minutes = 0 req_min = 0 shop_php_stats[shop] = { 'blocks': blocks, 'runtime_minutes': runtime_minutes, 'req_min': req_min, 'ips': ips, 'bots': bots, 'bans': bans, 'active_bans': active_bans } if runtime_minutes > total_minutes: total_minutes = runtime_minutes for ip, data in ips.items(): if ip not in all_ips: all_ips[ip] = {'count': 0, 'ua': data['ua'], 'bot': data.get('bot'), 'shops': {}} all_ips[ip]['count'] += data['count'] all_ips[ip]['shops'][shop] = data['count'] if data['ua'] != 'Unknown' and all_ips[ip]['ua'] == 'Unknown': all_ips[ip]['ua'] = data['ua'] # Update bot name if we have one if data.get('bot') and not all_ips[ip].get('bot'): all_ips[ip]['bot'] = data.get('bot') for bot_name, count in bots.items(): all_bots[bot_name] = all_bots.get(bot_name, 0) + count # Calculate total req/min total_req_min = total_php_blocks / total_minutes if total_minutes > 0 else 0 # Get CrowdSec stats crowdsec_stats = {} if check_crowdsec(): code, stdout, _ = run_command("cscli decisions list -o raw --limit 0") if code == 0 and stdout: for line in stdout.strip().split('\n')[1:]: for shop in active_shops: if shop in line: crowdsec_stats[shop] = crowdsec_stats.get(shop, 0) + 1 break total_crowdsec = sum(crowdsec_stats.values()) # Display PHP blocks with req/min and top IP per shop print(f"\n📝 Blocks gesamt: {total_php_blocks} (⌀ {total_req_min:.1f} req/min, Laufzeit: {format_duration(total_minutes)})") if shop_php_stats: for shop in sorted(shop_php_stats.keys()): stats = shop_php_stats[shop] count = stats['blocks'] req_min = stats['req_min'] runtime = stats['runtime_minutes'] bar = "█" * min(int(req_min * 2), 20) if req_min > 0 else "" runtime_str = format_duration(runtime) if runtime > 0 else "?" # Get geo region icon and mode icon geo_region = get_shop_geo_region(shop) region_info = get_geo_region_info(geo_region) geo_icon = region_info['icon'] mode_icon = get_mode_icon(get_shop_mode(shop)) # Color shop name based on Link11 status link11_info = check_link11(shop) if link11_info['is_link11']: shop_colored = f"{COLOR_GREEN}{shop}{COLOR_RESET}" else: shop_colored = f"{COLOR_RED}{shop}{COLOR_RESET}" print(f" ├─ {shop_colored} {geo_icon} {mode_icon}: {count} ({req_min:.1f} req/min, seit {runtime_str}) {bar}") # Show top IP for this shop shop_ips = stats['ips'] if shop_ips and count > 0: top_ip = max(shop_ips.items(), key=lambda x: x[1]['count']) top_ip_addr = top_ip[0] top_ip_count = top_ip[1]['count'] top_ip_ua = top_ip[1]['ua'] # Use stored bot name first, fallback to UA detection top_ip_bot = top_ip[1].get('bot') or detect_bot(top_ip_ua) top_ip_req_min = top_ip_count / runtime if runtime > 0 else 0 if top_ip_bot == 'Unbekannt': display_name = (top_ip_ua[:40] + '...') if len(top_ip_ua) > 43 else top_ip_ua else: display_name = top_ip_bot print(f" │ └─➤ Top: {top_ip_addr} ({display_name}) - {top_ip_count}x, {top_ip_req_min:.1f} req/min") # Display Rate-Limit Bans if total_bans > 0 or total_active_bans > 0: print(f"\n🚫 Rate-Limit Bans: {total_bans} ausgelöst, {total_active_bans} aktiv") for shop in sorted(shop_php_stats.keys()): stats = shop_php_stats[shop] if stats['bans'] > 0 or stats['active_bans'] > 0: link11_info = check_link11(shop) if link11_info['is_link11']: shop_colored = f"{COLOR_GREEN}{shop}{COLOR_RESET}" else: shop_colored = f"{COLOR_RED}{shop}{COLOR_RESET}" geo_region = get_shop_geo_region(shop) region_info = get_geo_region_info(geo_region) geo_icon = region_info['icon'] mode_icon = get_mode_icon(get_shop_mode(shop)) bar = "█" * min(stats['bans'] // 2, 20) if stats['bans'] > 0 else "" print(f" ├─ {shop_colored} {geo_icon} {mode_icon}: {stats['bans']} bans ({stats['active_bans']} aktiv) {bar}") # Display CrowdSec bans print(f"\n🛡️ CrowdSec-Bans gesamt: {total_crowdsec}") if crowdsec_stats: for shop in sorted(crowdsec_stats.keys()): count = crowdsec_stats[shop] bar = "█" * min(count // 10, 20) if count > 0 else "" geo_region = get_shop_geo_region(shop) region_info = get_geo_region_info(geo_region) geo_icon = region_info['icon'] mode_icon = get_mode_icon(get_shop_mode(shop)) link11_info = check_link11(shop) if link11_info['is_link11']: shop_colored = f"{COLOR_GREEN}{shop}{COLOR_RESET}" else: shop_colored = f"{COLOR_RED}{shop}{COLOR_RESET}" print(f" ├─ {shop_colored} {geo_icon} {mode_icon}: {count} {bar}") elif check_crowdsec(): print(" └─ Keine aktiven Bans") else: print(" └─ CrowdSec nicht verfügbar") # Display bot statistics if all_bots: print(f"\n🤖 Bot-Statistik (alle Shops):") for bot_name, count in sorted(all_bots.items(), key=lambda x: x[1], reverse=True)[:15]: bar = "█" * min(count // 5, 20) if count > 0 else "█" print(f" {bot_name}: {count}x {bar}") # Top 50 blocked IPs with bot detection, req/min, and top shop if all_ips: print(f"\n🔥 Top 50 blockierte IPs (alle Shops):") sorted_ips = sorted(all_ips.items(), key=lambda x: x[1]['count'], reverse=True)[:50] for ip, data in sorted_ips: count = data['count'] ua = data['ua'] # Use stored bot name first, fallback to UA detection bot_name = data.get('bot') or detect_bot(ua) shops_data = data['shops'] # Calculate req/min for this IP ip_req_min = count / total_minutes if total_minutes > 0 else 0 # Find top shop for this IP if shops_data: top_shop = max(shops_data.items(), key=lambda x: x[1]) top_shop_name = top_shop[0] top_shop_count = top_shop[1] if len(top_shop_name) > 25: top_shop_short = top_shop_name[:22] + '...' else: top_shop_short = top_shop_name link11_info = check_link11(top_shop_name) if link11_info['is_link11']: top_shop_display = f"{COLOR_GREEN}{top_shop_short}{COLOR_RESET}" else: top_shop_display = f"{COLOR_RED}{top_shop_short}{COLOR_RESET}" else: top_shop_display = "?" top_shop_count = 0 if bot_name == 'Unbekannt': display_name = (ua[:35] + '...') if len(ua) > 38 else ua if display_name == 'Unknown': display_name = 'Unbekannt' else: display_name = bot_name bar = "█" * min(count // 5, 20) if count > 0 else "█" print(f" {ip} ({display_name}): {count} ({ip_req_min:.1f} req/min) → {top_shop_display} [{top_shop_count}x] {bar}") print(f"\n{'═' * 70}") input("\nDrücke Enter um fortzufahren...") def main(): print("\n" + "=" * 60) print(" GeoIP Shop Blocker Manager v3.4.2") print(" 🇩🇪🇦🇹🇨🇭 DACH | 🇪🇺 Eurozone+GB | 🤖 Bot-Only") print(" 🛡️ Mit Cache-Validierung und Fail-Open") print(" 🚦 Mit File-basiertem Rate-Limiting") print("=" * 60) print(f" {'✅' if check_crowdsec() else '⚠️ '} CrowdSec") code, stdout, _ = run_command("systemctl is-active geoip-crowdsec-watcher.service") print(f" {'✅' if code == 0 and stdout.strip() == 'active' else '⚠️ '} Watcher-Service") while True: print("\n" + "-" * 40) print("[1] Aktivieren (einzeln)") print("[2] Deaktivieren (einzeln)") print("[3] Logs anzeigen") print("[4] Status") print("-" * 40) print("[5] 🚀 ALLE aktivieren") print("[6] 🛑 ALLE deaktivieren") print(f"[7] {COLOR_RED}🎯 Nur DIREKTE aktivieren (ohne Link11){COLOR_RESET}") print("-" * 40) print("[0] Beenden") choice = input("\nWähle: ").strip() if choice == "1": available = [s for s in get_available_shops() if s not in get_active_shops()] if not available: print("\n⚠️ Keine Shops verfügbar") continue print("\n📋 Verfügbare Shops:") for i, shop in enumerate(available, 1): print(format_shop_with_link11(shop, show_index=i)) try: idx = int(input("\nShop wählen: ").strip()) - 1 if 0 <= idx < len(available): mode = select_mode() is_bot_mode = is_bot_only_mode(mode) if is_bot_mode: geo = "none" region_info = get_geo_region_info("none") rate_limit, ban_duration = select_rate_limit() else: geo = select_geo_region() region_info = get_geo_region_info(geo) rate_limit, ban_duration = None, None confirm_msg = f"\n{region_info['icon']} {get_mode_description(mode)} aktivieren für '{available[idx]}'? (ja/nein): " if input(confirm_msg).lower() in ['ja', 'j']: activate_blocking(available[idx], mode=mode, geo_region=geo, rate_limit=rate_limit, ban_duration=ban_duration) except: print("❌ Ungültig") elif choice == "2": active = get_active_shops() if not active: print("\n⚠️ Keine aktiven Shops") continue print("\n📋 Aktive Shops:") for i, shop in enumerate(active, 1): region_info = get_geo_region_info(get_shop_geo_region(shop)) mode_icon = get_mode_icon(get_shop_mode(shop)) print(f" [{i}] {format_shop_with_link11(shop)} {region_info['icon']} {mode_icon}") try: idx = int(input("\nShop wählen: ").strip()) - 1 if 0 <= idx < len(active): if input(f"\nDeaktivieren für '{active[idx]}'? (ja/nein): ").lower() in ['ja', 'j']: deactivate_blocking(active[idx]) except: print("❌ Ungültig") elif choice == "3": active = get_active_shops() if not active: print("\n⚠️ Keine aktiven Shops") continue print("\n📋 Logs für:") print(" [0] 📊 ALLE") for i, shop in enumerate(active, 1): region_info = get_geo_region_info(get_shop_geo_region(shop)) mode_icon = get_mode_icon(get_shop_mode(shop)) print(f" [{i}] {format_shop_with_link11(shop)} {region_info['icon']} {mode_icon}") try: idx = int(input("\nWähle: ").strip()) if idx == 0: show_all_logs() elif 1 <= idx <= len(active): show_logs(active[idx - 1]) except: print("❌ Ungültig") elif choice == "4": shops = get_available_shops() active = get_active_shops() print(f"\n📊 {len(active)}/{len(shops)} Shops aktiv") for shop in active: region_info = get_geo_region_info(get_shop_geo_region(shop)) shop_mode = get_shop_mode(shop) mode_icon = get_mode_icon(shop_mode) is_bot_mode = is_bot_only_mode(shop_mode) blocks, _, bots, activation_time, total_bans, active_bans = get_shop_log_stats(shop) runtime = (datetime.now() - activation_time).total_seconds() / 60 if activation_time else 0 httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs') print(f" {format_shop_with_link11(shop)} {region_info['icon']} {mode_icon}") if is_bot_mode: rate_limit, ban_duration = get_shop_rate_limit_config(shop) rl_str = f", {rate_limit} req/min" if rate_limit else "" ban_str = f", {active_bans} aktive Bans" if active_bans > 0 else "" print(f" {blocks} blocks, {format_duration(runtime)}, {len(BOT_PATTERNS)} Bot-Patterns{rl_str}{ban_str}") else: valid, count, _ = validate_existing_cache(httpdocs, get_shop_geo_region(shop)) cache_str = f"✅{count:,}" if valid else "⚠️" print(f" {blocks} blocks, {format_duration(runtime)}, Cache: {cache_str}") elif choice == "5": activate_all_shops() elif choice == "6": deactivate_all_shops() elif choice == "7": activate_direct_shops_only() 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)