From 388e8e194fafd05969375c395a12bbf8ae42ae0e Mon Sep 17 00:00:00 2001 From: thomasciesla Date: Tue, 9 Dec 2025 11:36:35 +0100 Subject: [PATCH] geoip_shop_manager.py aktualisiert --- geoip_shop_manager.py | 1830 ++++++++++++----------------------------- 1 file changed, 546 insertions(+), 1284 deletions(-) diff --git a/geoip_shop_manager.py b/geoip_shop_manager.py index e6abd6a..6683b60 100644 --- a/geoip_shop_manager.py +++ b/geoip_shop_manager.py @@ -5,6 +5,8 @@ GeoIP Shop Blocker Manager - DACH & Eurozone Version Supports two geo regions: - DACH: Germany, Austria, Switzerland (3 countries) - Eurozone+GB: All Eurozone countries + GB + CH (22 countries) + +v3.1.0: Cache validation on activation + fail-open in PHP """ import os @@ -43,6 +45,12 @@ 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" +# 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 # ============================================================================= @@ -57,28 +65,8 @@ GEO_REGIONS = { "eurozone": { "name": "Eurozone + GB", "countries": [ - "de", # Deutschland - "at", # Österreich - "ch", # Schweiz - "be", # Belgien - "cy", # Zypern - "ee", # Estland - "es", # Spanien - "fi", # Finnland - "fr", # Frankreich - "gb", # Großbritannien - "gr", # Griechenland - "hr", # Kroatien - "ie", # Irland - "it", # Italien - "lt", # Litauen - "lu", # Luxemburg - "lv", # Lettland - "mt", # Malta - "nl", # Niederlande - "pt", # Portugal - "si", # Slowenien - "sk", # Slowakei + "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": "🇪🇺", @@ -87,159 +75,37 @@ GEO_REGIONS = { } # ============================================================================= -# BOT DETECTION - Comprehensive list of known bots/crawlers +# BOT DETECTION # ============================================================================= BOT_PATTERNS = { - # OpenAI - 'GPTBot': r'GPTBot', - 'OAI-SearchBot': r'OAI-SearchBot', - 'ChatGPT-User': r'ChatGPT-User', - - # Anthropic (Claude) - 'ClaudeBot': r'ClaudeBot', - 'Claude-User': r'Claude-User', - 'Claude-SearchBot': r'Claude-SearchBot', - 'anthropic-ai': r'anthropic-ai', - 'claude-web': r'claude-web', - - # Google - 'Googlebot': r'Googlebot', - 'Google-Extended': r'Google-Extended', - 'Googlebot-Image': r'Googlebot-Image', - 'Googlebot-Video': r'Googlebot-Video', - 'Googlebot-News': r'Googlebot-News', - 'Gemini-Deep-Research': r'Gemini-Deep-Research', - 'Google-CloudVertexBot': r'Google-CloudVertexBot', - 'AdsBot-Google': r'AdsBot-Google', - 'Mediapartners-Google': r'Mediapartners-Google', - 'FeedFetcher-Google': r'FeedFetcher-Google', - 'Google-InspectionTool': r'Google-InspectionTool', - - # Microsoft/Bing - 'Bingbot': r'[Bb]ingbot', - 'BingPreview': r'BingPreview', - 'msnbot': r'msnbot', - 'AdIdxBot': r'AdIdxBot', - - # Perplexity - 'PerplexityBot': r'PerplexityBot', - 'Perplexity-User': r'Perplexity-User', - - # Apple - 'Applebot': r'Applebot', - 'Applebot-Extended': r'Applebot-Extended', - - # Amazon - 'Amazonbot': r'Amazonbot', - - # Meta/Facebook - 'FacebookBot': r'facebookexternalhit|FacebookBot', - 'meta-externalagent': r'meta-externalagent', - 'Meta-WebIndexer': r'Meta-WebIndexer', - - # ByteDance/TikTok - 'Bytespider': r'Bytespider', - - # DuckDuckGo - 'DuckDuckBot': r'DuckDuckBot', - 'DuckAssistBot': r'DuckAssistBot', - - # Other AI/LLM - 'cohere-ai': r'cohere-ai', - 'YouBot': r'YouBot', - 'MistralAI-User': r'MistralAI-User', - 'AI2Bot': r'AI2Bot', - 'CCBot': r'CCBot', - 'Diffbot': r'Diffbot', - 'Timpibot': r'Timpibot', - 'omgili': r'omgili', - 'webzio': r'webzio', - 'ICC-Crawler': r'ICC-Crawler', - - # SEO Tools - 'AhrefsBot': r'AhrefsBot', - 'SemrushBot': r'SemrushBot', - 'MJ12bot': r'MJ12bot', - 'DotBot': r'DotBot', - 'BLEXBot': r'BLEXBot', - 'DataForSeoBot': r'DataForSeoBot', - 'SEOkicks': r'SEOkicks', - 'seoscanners': r'seoscanners', - 'Screaming Frog': r'Screaming Frog', - 'Sistrix': r'Sistrix', - 'JEEC2Bot': r'JEEC2Bot', - - # Other Search Engines - 'YandexBot': r'YandexBot', - 'YandexImages': r'YandexImages', - 'Baiduspider': r'Baiduspider', - 'PetalBot': r'PetalBot', - 'Sogou': r'Sogou', - 'Qwantify': r'Qwantify', - 'ia_archiver': r'ia_archiver', - - # Social Media - 'LinkedInBot': r'LinkedInBot', - 'Twitterbot': r'Twitterbot', - 'Pinterest': r'Pinterest', - 'Slackbot': r'Slackbot', - 'TelegramBot': r'TelegramBot', - 'WhatsApp': r'WhatsApp', - 'Discordbot': r'Discordbot', - - # Monitoring & Security - 'UptimeRobot': r'UptimeRobot', - 'Pingdom': r'Pingdom', - 'StatusCake': r'StatusCake', - 'GTmetrix': r'GTmetrix', - 'Site24x7': r'Site24x7', - - # Payment/E-Commerce - 'PayPal IPN': r'PayPal', - 'Stripe': r'Stripe', - 'Shopify': r'Shopify', - - # Feed Readers - 'Feedly': r'Feedly', - 'NewsBlur': r'NewsBlur', - - # Other known bots - 'SeznamBot': r'SeznamBot', - 'Exabot': r'Exabot', - 'archive.org_bot': r'archive\.org_bot', - 'Wget': r'Wget', - 'curl': r'^curl/', - 'python-requests': r'python-requests', - 'Go-http-client': r'Go-http-client', - 'Java': r'^Java/', - 'Apache-HttpClient': r'Apache-HttpClient', - 'okhttp': r'okhttp', - 'HeadlessChrome': r'HeadlessChrome', - 'PhantomJS': r'PhantomJS', - 'Scrapy': r'Scrapy', + '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): - """Detect bot name from user agent string""" 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): - """Check if domain resolves to Link11 IP""" global DNS_CACHE - - # Check cache first if domain in DNS_CACHE: return DNS_CACHE[domain] - try: ip = socket.gethostbyname(domain) is_link11 = (ip == LINK11_IP) @@ -251,686 +117,550 @@ def check_link11(domain): def format_shop_with_link11(shop, prefix="", show_index=None): - """Format shop name with Link11 color coding""" link11_info = check_link11(shop) - if link11_info['is_link11']: - color = COLOR_GREEN - suffix = " [Link11]" + color, suffix = COLOR_GREEN, " [Link11]" else: - color = COLOR_RED - suffix = " [Direkt]" - + color, suffix = COLOR_RED, " [Direkt]" if show_index is not None: return f" [{show_index}] {color}{shop}{suffix}{COLOR_RESET}" - else: - return f"{prefix}{color}{shop}{suffix}{COLOR_RESET}" + return f"{prefix}{color}{shop}{suffix}{COLOR_RESET}" def get_geo_region_info(geo_region): - """Get info for a geo region""" return GEO_REGIONS.get(geo_region, GEO_REGIONS["dach"]) def generate_php_countries_array(geo_region): - """Generate PHP array string for countries""" + region_info = get_geo_region_info(geo_region) + return ", ".join([f"'{c}'" for c in region_info["countries"]]) + + +# ============================================================================= +# 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"] - return ", ".join([f"'{c}'" for c in 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) -# PHP GeoIP blocking script template (with CrowdSec) +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 +# ============================================================================= + GEOIP_SCRIPT_TEMPLATE = ''' $expiry_date) {{ - return; // Script expired, allow all traffic -}} +if (time() > $expiry_date) return; -// Get visitor IP $visitor_ip = $_SERVER['REMOTE_ADDR'] ?? ''; -if (empty($visitor_ip)) {{ - return; -}} +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; -// 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 +$cache_duration = 86400; $log_file = __DIR__ . '/{log_file}'; $crowdsec_queue = __DIR__ . '/{crowdsec_queue}'; - -// Allowed countries +$min_ranges = {min_ranges}; $allowed_countries = [{countries_array}]; -// Function to download IP ranges for allowed countries function download_allowed_ranges($countries) {{ $ranges = []; - foreach ($countries as $country) {{ $url = "https://www.ipdeny.com/ipblocks/data/aggregated/$country-aggregated.zone"; - $content = @file_get_contents($url); - + $ctx = stream_context_create(['http' => ['timeout' => 30]]); + $content = @file_get_contents($url, false, $ctx); if ($content !== false) {{ - $lines = explode("\\n", trim($content)); - foreach ($lines as $line) {{ + foreach (explode("\\n", trim($content)) as $line) {{ $line = trim($line); - if (!empty($line) && strpos($line, '/') !== false) {{ - $ranges[] = $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); + return (ip2long($ip) & $mask_long) == (ip2long($subnet) & $mask_long); }} -// Load or download IP ranges $allowed_ranges = []; +$cache_valid = false; + if (file_exists($cache_file) && (time() - filemtime($cache_file)) < $cache_duration) {{ - $allowed_ranges = unserialize(file_get_contents($cache_file)); -}} else {{ - $allowed_ranges = download_allowed_ranges($allowed_countries); - if (!empty($allowed_ranges)) {{ - @file_put_contents($cache_file, serialize($allowed_ranges)); + $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; }} }} -// Check if visitor IP is from allowed region $is_allowed = false; foreach ($allowed_ranges as $range) {{ - if (ip_in_range($visitor_ip, $range)) {{ - $is_allowed = true; - break; - }} + if (ip_in_range($visitor_ip, $range)) {{ $is_allowed = true; break; }} }} -// Block non-allowed IPs if (!$is_allowed) {{ $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); - + $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; }} ''' -# PHP GeoIP blocking script template - PHP ONLY (no CrowdSec queue) GEOIP_SCRIPT_TEMPLATE_PHP_ONLY = ''' $expiry_date) {{ - return; // Script expired, allow all traffic -}} +if (time() > $expiry_date) return; -// Get visitor IP $visitor_ip = $_SERVER['REMOTE_ADDR'] ?? ''; -if (empty($visitor_ip)) {{ - return; -}} +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; -// 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 +$cache_duration = 86400; $log_file = __DIR__ . '/{log_file}'; - -// Allowed countries +$min_ranges = {min_ranges}; $allowed_countries = [{countries_array}]; -// Function to download IP ranges for allowed countries function download_allowed_ranges($countries) {{ $ranges = []; - foreach ($countries as $country) {{ $url = "https://www.ipdeny.com/ipblocks/data/aggregated/$country-aggregated.zone"; - $content = @file_get_contents($url); - + $ctx = stream_context_create(['http' => ['timeout' => 30]]); + $content = @file_get_contents($url, false, $ctx); if ($content !== false) {{ - $lines = explode("\\n", trim($content)); - foreach ($lines as $line) {{ + foreach (explode("\\n", trim($content)) as $line) {{ $line = trim($line); - if (!empty($line) && strpos($line, '/') !== false) {{ - $ranges[] = $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); + return (ip2long($ip) & $mask_long) == (ip2long($subnet) & $mask_long); }} -// Load or download IP ranges $allowed_ranges = []; +$cache_valid = false; + if (file_exists($cache_file) && (time() - filemtime($cache_file)) < $cache_duration) {{ - $allowed_ranges = unserialize(file_get_contents($cache_file)); -}} else {{ - $allowed_ranges = download_allowed_ranges($allowed_countries); - if (!empty($allowed_ranges)) {{ - @file_put_contents($cache_file, serialize($allowed_ranges)); + $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; }} }} -// Check if visitor IP is from allowed region $is_allowed = false; foreach ($allowed_ranges as $range) {{ - if (ip_in_range($visitor_ip, $range)) {{ - $is_allowed = true; - break; - }} + if (ip_in_range($visitor_ip, $range)) {{ $is_allowed = true; break; }} }} -// Block non-allowed IPs if (!$is_allowed) {{ $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); - + $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; }} ''' -# 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 -""" +# ============================================================================= +# WATCHER SERVICE +# ============================================================================= -import os -import sys -import time -import subprocess -import json -from pathlib import Path +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 = {} # In-memory cache to avoid re-adding same IP -CHECK_INTERVAL = 5 # Check every 5 seconds +PROCESSED_IPS = {} +CHECK_INTERVAL = 5 -def log(msg): - print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}", flush=True) +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 {} + if not os.path.isfile(ACTIVE_SHOPS_FILE): return {} try: - with open(ACTIVE_SHOPS_FILE, 'r') as f: - return json.load(f) - except: - return {} + 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-allowed IP blocked by {shop}' - ] - + if ip in PROCESSED_IPS and (now - PROCESSED_IPS[ip]) < 3600: return True try: - result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + 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 - else: - log(f"⚠️ Failed to add {ip}: {result.stderr.strip()}") - return False - except Exception as e: - log(f"❌ Error adding {ip}: {e}") - return False + except: pass + 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 - + 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 + with open(queue_file, 'r') as f: lines = f.readlines() + if not lines: return 0 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 + 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 Exception as e: - log(f"❌ Error processing {shop}: {e}") - + 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() - - if not active_shops: - time.sleep(CHECK_INTERVAL) - continue - - total_processed = 0 - - for shop, info in active_shops.items(): + for shop in active_shops: 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}") + 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() +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 ''' +# ============================================================================= +# HELPER FUNCTIONS +# ============================================================================= + 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, "", "" + return subprocess.run(cmd, shell=True, timeout=30).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") + code, stdout, _ = 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") + print(" ✅ Service gestartet") return True - else: - print(" ⚠️ Service konnte nicht gestartet werden") - return False + 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") - + 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"): - """Add shop to active shops tracking with mode and geo region""" 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(), - "mode": mode, # "php+crowdsec" or "php-only" - "geo_region": geo_region # "dach" or "eurozone" + "mode": mode, + "geo_region": geo_region } - with open(ACTIVE_SHOPS_FILE, 'w') as f: json.dump(shops, f, indent=2) def get_shop_mode(shop): - """Get the blocking mode for a shop""" if not os.path.isfile(ACTIVE_SHOPS_FILE): return "php+crowdsec" - try: with open(ACTIVE_SHOPS_FILE, 'r') as f: - shops = json.load(f) - return shops.get(shop, {}).get("mode", "php+crowdsec") + return json.load(f).get(shop, {}).get("mode", "php+crowdsec") except: return "php+crowdsec" def get_shop_geo_region(shop): - """Get the geo region for a shop""" if not os.path.isfile(ACTIVE_SHOPS_FILE): return "dach" - try: with open(ACTIVE_SHOPS_FILE, 'r') as f: - shops = json.load(f) - return shops.get(shop, {}).get("geo_region", "dach") + return json.load(f).get(shop, {}).get("geo_region", "dach") except: return "dach" def get_shop_activation_time(shop): - """Get the activation timestamp for a shop""" if not os.path.isfile(ACTIVE_SHOPS_FILE): return None - try: with open(ACTIVE_SHOPS_FILE, 'r') as f: - shops = json.load(f) - activated_str = shops.get(shop, {}).get("activated") - if activated_str: - return datetime.fromisoformat(activated_str) + activated_str = json.load(f).get(shop, {}).get("activated") + return datetime.fromisoformat(activated_str) if activated_str else None except: - pass - return None + return None def format_duration(minutes): - """Format minutes as human readable duration""" if minutes < 60: return f"{int(minutes)}m" hours = minutes / 60 if hours < 24: return f"{int(hours)}h {int(minutes % 60)}m" - days = hours / 24 - remaining_hours = hours % 24 - return f"{int(days)}d {int(remaining_hours)}h" + return f"{int(hours / 24)}d {int(hours % 24)}h" 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}...") - total_removed = 0 - max_iterations = 50 # Safety limit - iteration = 0 - - while iteration < max_iterations: - iteration += 1 - - # Get all decisions with --limit 0 (no pagination) - list_cmd = f"cscli decisions list -o raw --limit 0 | grep '{shop}'" - code, stdout, stderr = run_command(list_cmd) - + 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 # No more decisions found - - # Extract IPs and delete them (process in batches of 100) - lines = stdout.strip().split('\n') + break batch_count = 0 - - for line in lines[:100]: # Process max 100 per iteration + for line in stdout.strip().split('\n')[:100]: try: parts = line.split(',') if len(parts) >= 3: ip_field = parts[2].strip() - if ':' in ip_field: - ip = ip_field.split(':', 1)[1] - else: - ip = ip_field - - if ip: - del_cmd = f"cscli decisions delete --ip {ip}" - del_code, _, _ = run_command(del_cmd) - if del_code == 0: - batch_count += 1 + 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 # Nothing deleted in this iteration - - # Small progress indicator for large cleanups - if iteration > 1: - print(f" ... {total_removed} Decisions entfernt (Durchlauf {iteration})") - - if total_removed > 0: - print(f" ✅ {total_removed} Decisions entfernt") - else: - print(" ℹ️ Keine Decisions gefunden") + break + print(f" ✅ {total_removed} Decisions entfernt" if total_removed else " ℹ️ 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) - + 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(): - """Get list of shops with active blocking""" active = [] - shops = get_available_shops() - - for shop in shops: + for shop in get_available_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): + 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 select_geo_region(): - """Interactive geo region selection""" 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") - - choice = input(f"\nRegion wählen [1/2]: ").strip() - - if choice == "2": - return "eurozone" - else: - return "dach" + return "eurozone" if input(f"\nRegion wählen [1/2]: ").strip() == "2" else "dach" def select_mode(): - """Interactive mode selection""" print(f"\n🔧 Wähle den Blocking-Modus:") print(f" [1] PHP + CrowdSec (IPs werden an CrowdSec gemeldet)") print(f" [2] Nur PHP (keine CrowdSec-Synchronisation)") - - choice = input(f"\nModus wählen [1/2]: ").strip() - - if choice == "2": - return "php-only" - else: - return "php+crowdsec" + return "php-only" if input(f"\nModus wählen [1/2]: ").strip() == "2" else "php+crowdsec" +# ============================================================================= +# MAIN FUNCTIONS +# ============================================================================= + def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach"): - """Activate GeoIP blocking for a single shop""" 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) 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: @@ -948,30 +678,21 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach" print(f" Modus: {'PHP + CrowdSec' if mode == 'php+crowdsec' else 'Nur PHP'}") print("=" * 60) - # Step 1: Install watcher service if not exists (only for php+crowdsec mode) + # Step 1: Watcher service if mode == "php+crowdsec": - active_shops = get_active_shops() - # Check if any shop uses crowdsec mode - crowdsec_shops = [s for s in active_shops if get_shop_mode(s) == "php+crowdsec"] - if not crowdsec_shops: # First shop with crowdsec + crowdsec_shops = [s for s in get_active_shops() if get_shop_mode(s) == "php+crowdsec"] + if not crowdsec_shops and check_crowdsec(): if not silent: - print("\n[1/3] Installiere CrowdSec-Watcher-Service...") - if check_crowdsec(): - install_watcher_service() - else: - if not silent: - print(" ⚠️ CrowdSec nicht verfügbar - nur PHP-Blocking") - else: - if not silent: - print("\n[1/3] CrowdSec-Watcher-Service bereits aktiv") - else: - if not silent: - print("\n[1/3] CrowdSec-Synchronisation deaktiviert (nur PHP-Modus)") + 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 (nur PHP-Modus)") # Step 2: PHP blocking if not silent: - print("\n[2/3] Aktiviere PHP-Blocking...") - print(" 📋 Backup erstellen...") + print("\n[2/4] Aktiviere PHP-Blocking...") shutil.copy2(index_php, backup_php) @@ -980,7 +701,6 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach" lines = content.split('\n') insert_line = 0 - for i, line in enumerate(lines): if 'declare(strict_types' in line: insert_line = i + 1 @@ -989,139 +709,113 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach" insert_line = i + 1 require_statement = f"require_once __DIR__ . '/{BLOCKING_FILE}';" - if require_statement not in content: lines.insert(insert_line, require_statement) with open(index_php, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) - if not silent: - print(" ✏️ index.php modifiziert") expiry = datetime.now() + timedelta(hours=72) countries_array = generate_php_countries_array(geo_region) - # Generate PHP script based on mode - if mode == "php+crowdsec": - geoip_content = GEOIP_SCRIPT_TEMPLATE.format( - region_name=region_info['name'], - region_description=region_info['description'], - expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'), - expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'), - cache_file=CACHE_FILE, - log_file=LOG_FILE, - crowdsec_queue=CROWDSEC_QUEUE_FILE, - shop_name=shop, - countries_array=countries_array - ) - else: - geoip_content = GEOIP_SCRIPT_TEMPLATE_PHP_ONLY.format( - region_name=region_info['name'], - region_description=region_info['description'], - expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'), - expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'), - cache_file=CACHE_FILE, - log_file=LOG_FILE, - shop_name=shop, - countries_array=countries_array - ) + template = GEOIP_SCRIPT_TEMPLATE if mode == "php+crowdsec" else GEOIP_SCRIPT_TEMPLATE_PHP_ONLY + geoip_content = template.format( + region_name=region_info['name'], + region_description=region_info['description'], + expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'), + expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'), + cache_file=CACHE_FILE, + log_file=LOG_FILE, + crowdsec_queue=CROWDSEC_QUEUE_FILE, + shop_name=shop, + countries_array=countries_array, + min_ranges=min_ranges + ) with open(blocking_file, 'w', encoding='utf-8') as f: f.write(geoip_content) if not silent: - print(" 📝 geoip_blocking.php erstellt") + print(" ✅ PHP-Blocking aktiviert") - # Step 3: Register shop + # Step 3: Generate cache if not silent: - print("\n[3/3] Registriere Shop...") + print(f"\n[3/4] Generiere IP-Range-Cache ({len(region_info['countries'])} Länder)...") + + success, range_count, error = generate_and_validate_cache(httpdocs, geo_region) + + if success: + if not silent: + print(f" ✅ Cache generiert: {range_count:,} IP-Ranges") + else: + if not silent: + print(f" ⚠️ Cache-Generierung: {error}") + print(f" ℹ️ Fail-Open aktiv - Cache wird beim ersten Request neu generiert") + + # Step 4: Register + if not silent: + print("\n[4/4] Registriere Shop...") add_shop_to_active(shop, mode, geo_region) - if not silent: - print(" ✅ Shop registriert") if not silent: print("\n" + "=" * 60) - print(f"✅ {region_info['icon']} {region_info['name']} GeoIP-Blocking aktiviert für: {shop}") - print(f" Erlaubte Länder: {region_info['description']}") - print(f" Modus: {'PHP + CrowdSec 🛡️' if mode == 'php+crowdsec' else 'Nur PHP 📝'}") + print(f"✅ {region_info['icon']} {region_info['name']} GeoIP-Blocking aktiviert") + print(f" Shop: {shop}") + print(f" IP-Ranges: {range_count:,}") print(f" Gültig bis: {expiry.strftime('%Y-%m-%d %H:%M:%S CET')}") - print(f" PHP-Log: {os.path.join(httpdocs, LOG_FILE)}") - if mode == "php+crowdsec": - print(f" CrowdSec-Queue: {os.path.join(httpdocs, CROWDSEC_QUEUE_FILE)}") - print(f"\n 🔄 Der Watcher-Service synchronisiert blockierte IPs zu CrowdSec") + print(f" 🛡️ Fail-Open: Bei Cache-Fehlern wird Traffic durchgelassen") print("=" * 60) return True def deactivate_blocking(shop, silent=False): - """Deactivate GeoIP blocking for a single shop""" 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) - cache_file = os.path.join(httpdocs, CACHE_FILE) - log_file = os.path.join(httpdocs, LOG_FILE) - queue_file = os.path.join(httpdocs, CROWDSEC_QUEUE_FILE) - # Get mode and geo region before removing from tracking shop_mode = get_shop_mode(shop) shop_geo = get_shop_geo_region(shop) region_info = get_geo_region_info(shop_geo) if not silent: - print(f"\n🔧 Deaktiviere {region_info['icon']} {region_info['name']} GeoIP-Blocking für: {shop}") - print(f" Modus war: {'PHP + CrowdSec' if shop_mode == 'php+crowdsec' else 'Nur PHP'}") + print(f"\n🔧 Deaktiviere {region_info['icon']} {region_info['name']} für: {shop}") print("=" * 60) - # Step 1: Remove PHP blocking + # Restore backup if not silent: print("\n[1/4] PHP-Blocking entfernen...") if os.path.isfile(backup_php): shutil.move(backup_php, index_php) - if not silent: - print(" 📋 index.php wiederhergestellt") else: if os.path.isfile(index_php): with open(index_php, 'r') as f: content = f.read() - lines = [line for line in content.split('\n') if BLOCKING_FILE not in line] + lines = [l for l in content.split('\n') if BLOCKING_FILE not in l] with open(index_php, 'w') as f: f.write('\n'.join(lines)) - for f in [blocking_file, cache_file, log_file, queue_file]: + for f in [os.path.join(httpdocs, x) for x in [BLOCKING_FILE, CACHE_FILE, LOG_FILE, CROWDSEC_QUEUE_FILE]]: if os.path.isfile(f): os.remove(f) - if not silent: - print(" 🗑️ PHP-Dateien gelöscht") - # Step 2: Remove from tracking if not silent: print("\n[2/4] Deregistriere Shop...") remove_shop_from_active(shop) - if not silent: - print(" ✅ Shop deregistriert") - # Step 3: Clean CrowdSec decisions (only if mode was php+crowdsec) if not silent: print("\n[3/4] CrowdSec-Decisions entfernen...") if shop_mode == "php+crowdsec" and check_crowdsec(): cleanup_crowdsec_decisions(shop) - else: - if not silent: - print(" ℹ️ Keine CrowdSec-Synchronisation aktiv (PHP-only Modus)") + elif not silent: + print(" ℹ️ Keine CrowdSec-Synchronisation aktiv") - # Step 4: Uninstall service if no more crowdsec shops if not silent: print("\n[4/4] Prüfe Watcher-Service...") - remaining_shops = get_active_shops() - crowdsec_shops = [s for s in remaining_shops if get_shop_mode(s) == "php+crowdsec"] + crowdsec_shops = [s for s in get_active_shops() if get_shop_mode(s) == "php+crowdsec"] if not crowdsec_shops: - if not silent: - print(" ℹ️ Keine Shops mit CrowdSec-Modus mehr - deinstalliere Service") uninstall_watcher_service() - else: - if not silent: - print(f" ℹ️ Service bleibt aktiv ({len(crowdsec_shops)} Shop(s) mit CrowdSec-Modus)") + elif not silent: + print(f" ℹ️ Service bleibt aktiv ({len(crowdsec_shops)} Shop(s))") if not silent: print("\n" + "=" * 60) @@ -1132,781 +826,350 @@ def deactivate_blocking(shop, silent=False): def activate_all_shops(): - """Activate GeoIP blocking for all available shops""" - shops = get_available_shops() - active_shops = get_active_shops() - available_shops = [s for s in shops if s not in active_shops] + available_shops = [s for s in get_available_shops() if s not in get_active_shops()] if not available_shops: print("\n⚠️ Keine Shops zum Aktivieren verfügbar") - print(" Alle Shops haben bereits aktives GeoIP-Blocking") return print(f"\n{'=' * 60}") print(f" GeoIP-Blocking für ALLE Shops aktivieren") print(f"{'=' * 60}") - print(f"\n📋 Folgende {len(available_shops)} Shop(s) werden aktiviert:") - print(f" {COLOR_GREEN}Grün = hinter Link11{COLOR_RESET} | {COLOR_RED}Rot = Direkt{COLOR_RESET}\n") - + print(f"\n📋 {len(available_shops)} Shop(s):") for shop in available_shops: - link11_info = check_link11(shop) - color = COLOR_GREEN if link11_info['is_link11'] else COLOR_RED - link11_tag = "[Link11]" if link11_info['is_link11'] else "[Direkt]" - print(f" • {color}{shop} {link11_tag}{COLOR_RESET}") + print(format_shop_with_link11(shop, prefix=" • ")) - # Ask for geo region geo_region = select_geo_region() region_info = get_geo_region_info(geo_region) - - # Ask for mode mode = select_mode() - mode_display = "PHP + CrowdSec 🛡️" if mode == "php+crowdsec" else "Nur PHP 📝" - print(f"\n⚠️ Dies aktiviert den Schutz für alle oben genannten Shops!") - print(f" Region: {region_info['icon']} {region_info['name']}") - print(f" Modus: {mode_display}") - confirm = input(f"\nFortfahren? (ja/nein): ").strip().lower() + print(f"\n⚠️ Region: {region_info['icon']} {region_info['name']}") + print(f" Modus: {'PHP + CrowdSec 🛡️' if mode == 'php+crowdsec' else 'Nur PHP 📝'}") - if confirm not in ['ja', 'j', 'yes', 'y']: + if input(f"\nFortfahren? (ja/nein): ").strip().lower() not in ['ja', 'j', 'yes', 'y']: print("\n❌ Abgebrochen") return print(f"\n{'=' * 60}") - print(f" Starte Aktivierung ({region_info['icon']} {region_info['name']}, {mode_display})...") - print(f"{'=' * 60}") - success_count = 0 - failed_count = 0 - failed_shops = [] - - # Install watcher service first if needed (only for php+crowdsec mode) - if mode == "php+crowdsec": - crowdsec_shops = [s for s in active_shops if get_shop_mode(s) == "php+crowdsec"] - if not crowdsec_shops and check_crowdsec(): + if mode == "php+crowdsec" and check_crowdsec(): + if not [s for s in get_active_shops() if get_shop_mode(s) == "php+crowdsec"]: print("\n📦 Installiere CrowdSec-Watcher-Service...") install_watcher_service() + success_count = 0 for i, shop in enumerate(available_shops, 1): - print(f"\n[{i}/{len(available_shops)}] Aktiviere: {shop}") + print(f"\n[{i}/{len(available_shops)}] {shop}") - try: - # Use silent mode but show progress - 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" ⚠️ Bereits aktiv - überspringe") - continue - - if not os.path.isfile(index_php): - print(f" ❌ index.php nicht gefunden") - failed_count += 1 - failed_shops.append(shop) - continue - - # Create backup - shutil.copy2(index_php, backup_php) - - # Modify index.php - with open(index_php, 'r', encoding='utf-8') as f: - content = f.read() - - lines = content.split('\n') - insert_line = 0 - - for idx, line in enumerate(lines): - if 'declare(strict_types' in line: - insert_line = idx + 1 - break - elif ' {'count': N, 'ua': user_agent} - - if os.path.isfile(log_file): - with open(log_file, 'r') as f: - for line in f: - php_blocks += 1 - # Extract IP and User-Agent from log line - # Format: [timestamp] IP: x.x.x.x | UA: user_agent | URI: /path - ip = None - ua = 'Unknown' - - if 'IP: ' in line: - try: - ip = line.split('IP: ')[1].split(' |')[0].strip() - except: - pass - - if 'UA: ' in line: - try: - ua = line.split('UA: ')[1].split(' |')[0].strip() - except: - pass - - if ip: - if ip not in ips: - ips[ip] = {'count': 0, 'ua': ua} - ips[ip]['count'] += 1 - # Update UA if we have a better one (not Unknown) - if ua != 'Unknown' and ips[ip]['ua'] == 'Unknown': - ips[ip]['ua'] = ua - - # Get activation time - activation_time = get_shop_activation_time(shop) - - return php_blocks, ips, activation_time - - -def get_crowdsec_stats_by_shop(): - """Get CrowdSec decision counts grouped by shop""" - if not check_crowdsec(): - return {} - - stats = {} - code, stdout, _ = run_command("cscli decisions list -o raw --limit 0") - - if code == 0 and stdout: - lines = stdout.strip().split('\n') - for line in lines[1:]: # Skip header - # Find shop name in reason field - for shop in get_active_shops(): - if shop in line: - stats[shop] = stats.get(shop, 0) + 1 - break - - return stats - - -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 - shop_php_stats = {} # shop -> {'blocks': N, 'activation': datetime, 'req_min': float, 'ips': {}} - all_ips = {} # ip -> {'count': N, 'ua': user_agent, 'shops': {shop: count}} - total_minutes = 0 - - # Collect PHP stats + print(f"\n{'=' * 60}") + print(f" GeoIP-Blocking für ALLE deaktivieren") + print(f"{'=' * 60}") + print(f"\n📋 {len(active_shops)} Shop(s):") for shop in active_shops: - blocks, ips, activation_time = get_shop_log_stats(shop) - total_php_blocks += blocks + region_info = get_geo_region_info(get_shop_geo_region(shop)) + print(f" • {format_shop_with_link11(shop)} {region_info['icon']}") + + if input(f"\nFortfahren? (ja/nein): ").strip().lower() not in ['ja', 'j', 'yes', 'y']: + print("\n❌ Abgebrochen") + return + + for i, shop in enumerate(active_shops, 1): + print(f"\n[{i}/{len(active_shops)}] {shop}") - # 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 + httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs') + backup_php = os.path.join(httpdocs, f'index.php{BACKUP_SUFFIX}') + index_php = os.path.join(httpdocs, 'index.php') - shop_php_stats[shop] = { - 'blocks': blocks, - 'activation': activation_time, - 'runtime_minutes': runtime_minutes, - 'req_min': req_min, - 'ips': ips # Store IPs per shop for top IP display - } + if os.path.isfile(backup_php): + shutil.move(backup_php, index_php) - if runtime_minutes > total_minutes: - total_minutes = runtime_minutes + for f in [os.path.join(httpdocs, x) for x in [BLOCKING_FILE, CACHE_FILE, LOG_FILE, CROWDSEC_QUEUE_FILE]]: + if os.path.isfile(f): + os.remove(f) - for ip, data in ips.items(): - if ip not in all_ips: - all_ips[ip] = {'count': 0, 'ua': data['ua'], 'shops': {}} - all_ips[ip]['count'] += data['count'] - all_ips[ip]['shops'][shop] = data['count'] - # Keep the most informative UA - if data['ua'] != 'Unknown' and all_ips[ip]['ua'] == 'Unknown': - all_ips[ip]['ua'] = data['ua'] + remove_shop_from_active(shop) + if check_crowdsec(): + cleanup_crowdsec_decisions(shop) + print(f" ✅ Deaktiviert") - # Calculate total req/min - total_req_min = total_php_blocks / total_minutes if total_minutes > 0 else 0 + uninstall_watcher_service() + print(f"\n{'=' * 60}") + print(f" ✅ Alle Shops deaktiviert") + print(f"{'=' * 60}") + + +def get_shop_log_stats(shop): + httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs') + log_file = os.path.join(httpdocs, LOG_FILE) + php_blocks = 0 + ips = {} - # Get CrowdSec stats - crowdsec_stats = get_crowdsec_stats_by_shop() - total_crowdsec = sum(crowdsec_stats.values()) + if os.path.isfile(log_file): + with open(log_file, 'r') as f: + for line in f: + php_blocks += 1 + ip, ua = None, 'Unknown' + if 'IP: ' in line: + try: + ip = line.split('IP: ')[1].split(' |')[0].strip() + except: + pass + if 'UA: ' in line: + try: + ua = line.split('UA: ')[1].split(' |')[0].strip() + except: + pass + if ip: + if ip not in ips: + ips[ip] = {'count': 0, 'ua': ua} + ips[ip]['count'] += 1 + if ua != 'Unknown' and ips[ip]['ua'] == 'Unknown': + ips[ip]['ua'] = ua - # Display PHP blocks with req/min and top IP per shop - print(f"\n📝 PHP-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 - geo_region = get_shop_geo_region(shop) - region_info = get_geo_region_info(geo_region) - geo_icon = region_info['icon'] - - # 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}: {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'] - top_ip_bot = detect_bot(top_ip_ua) - top_ip_req_min = top_ip_count / runtime if runtime > 0 else 0 - - # Show bot name or shortened UA if unknown - 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 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 "" - - # Get geo region icon - geo_region = get_shop_geo_region(shop) - region_info = get_geo_region_info(geo_region) - geo_icon = region_info['icon'] - - # 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}: {count} {bar}") - elif check_crowdsec(): - print(" └─ Keine aktiven Bans") - else: - print(" └─ CrowdSec nicht verfügbar") - - # Top blocked IPs with bot detection, req/min, and top shop - if all_ips: - print(f"\n🔥 Top 100 blockierte IPs (alle Shops):") - sorted_ips = sorted(all_ips.items(), key=lambda x: x[1]['count'], reverse=True)[:100] - for ip, data in sorted_ips: - count = data['count'] - ua = data['ua'] - bot_name = 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] - # Shorten shop name if too long - if len(top_shop_name) > 25: - top_shop_short = top_shop_name[:22] + '...' - else: - top_shop_short = top_shop_name - - # Color shop name based on Link11 status - 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 - - # Show bot name or shortened UA if unknown - 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}") - - # Wait for user - input("\nDrücke Enter um fortzufahren...") + return php_blocks, ips, get_shop_activation_time(shop) def show_logs(shop): - """Show logs for a single shop""" httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs') log_file = os.path.join(httpdocs, LOG_FILE) shop_mode = get_shop_mode(shop) shop_geo = get_shop_geo_region(shop) region_info = get_geo_region_info(shop_geo) - # Get stats blocks, ips, activation_time = get_shop_log_stats(shop) - # 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 - runtime_str = format_duration(runtime_minutes) - activation_str = activation_time.strftime('%Y-%m-%d %H:%M:%S') + runtime = (datetime.now() - activation_time).total_seconds() / 60 + req_min = blocks / runtime if runtime > 0 else 0 else: - runtime_minutes = 0 - req_min = 0 - runtime_str = "unbekannt" - activation_str = "unbekannt" - - mode_display = "PHP + CrowdSec 🛡️" if shop_mode == "php+crowdsec" else "Nur PHP 📝" + runtime, req_min = 0, 0 print(f"\n{'═' * 70}") - print(f"📊 Logs für {shop}") - print(f" {region_info['icon']} {region_info['name']} | {mode_display}") + print(f"📊 {shop} | {region_info['icon']} {region_info['name']}") print(f"{'═' * 70}") - print(f"\n⏱️ Aktiviert: {activation_str}") - print(f"⏱️ Laufzeit: {runtime_str}") + print(f"⏱️ Laufzeit: {format_duration(runtime)}") print(f"📈 Blocks: {blocks} ({req_min:.1f} req/min)") - if os.path.isfile(log_file): - print(f"\n📝 Letzte 50 PHP-Blocks:") - print("=" * 70) - with open(log_file, 'r') as f: - lines = f.readlines() - for line in lines[-50:]: - print(line.rstrip()) - print("=" * 70) - print(f"Gesamt: {len(lines)}") - - # Show top IPs with bot detection and req/min - if ips: - print(f"\n🔥 Top 20 blockierte IPs:") - sorted_ips = sorted(ips.items(), key=lambda x: x[1]['count'], reverse=True)[:20] - for ip, data in sorted_ips: - count = data['count'] - ua = data['ua'] - bot_name = detect_bot(ua) - ip_req_min = count / runtime_minutes if runtime_minutes > 0 else 0 - - # Show bot name or shortened UA if unknown - if bot_name == 'Unbekannt': - display_name = (ua[:40] + '...') if len(ua) > 43 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) {bar}") - else: - print(f"\nℹ️ Keine PHP-Logs für {shop}") + valid, count, err = validate_existing_cache(httpdocs, shop_geo) + print(f"✅ Cache: {count:,} Ranges" if valid else f"⚠️ Cache: {err}") - # Only show CrowdSec decisions if mode is php+crowdsec - if shop_mode == "php+crowdsec" and check_crowdsec(): - print(f"\n🛡️ CrowdSec Decisions:") - print("=" * 70) + 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()) - # Use raw output with --limit 0 (no pagination) - code, stdout, _ = run_command("cscli decisions list -o raw --limit 0") - if code == 0 and stdout: - lines = stdout.strip().split('\n') - shop_decisions = [] - - for line in lines[1:]: # Skip header - if shop in line: - shop_decisions.append(line) - - if shop_decisions: - print(f"Aktive Bans: {len(shop_decisions)}") - print("\nLetzte 20 Bans:") - for line in shop_decisions[:20]: - parts = line.split(',') - if len(parts) > 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") + if ips: + print(f"\n🔥 Top 10 IPs:") + for ip, data in sorted(ips.items(), key=lambda x: x[1]['count'], reverse=True)[:10]: + bot = detect_bot(data['ua']) + print(f" {ip} ({bot}): {data['count']}x") + + +def show_all_logs(): + active_shops = get_active_shops() + if not active_shops: + print("\n⚠️ Keine aktiven Shops") + return + + print(f"\n{'═' * 70}") + print(" 📊 GESAMTÜBERSICHT") + print(f"{'═' * 70}") + + total_blocks = 0 + all_ips = {} + + for shop in active_shops: + blocks, ips, activation_time = get_shop_log_stats(shop) + total_blocks += blocks + region_info = get_geo_region_info(get_shop_geo_region(shop)) - print("=" * 70) - elif shop_mode == "php-only": - print(f"\n📝 CrowdSec-Synchronisation ist für diesen Shop deaktiviert (PHP-only Modus)") + runtime = (datetime.now() - activation_time).total_seconds() / 60 if activation_time else 0 + req_min = blocks / runtime if runtime > 0 else 0 + + link11_info = check_link11(shop) + color = COLOR_GREEN if link11_info['is_link11'] else COLOR_RED + + print(f" {color}{shop}{COLOR_RESET} {region_info['icon']}: {blocks} ({req_min:.1f} req/min)") + + for ip, data in ips.items(): + if ip not in all_ips: + all_ips[ip] = {'count': 0, 'ua': data['ua']} + all_ips[ip]['count'] += data['count'] + + print(f"\n📊 Gesamt: {total_blocks} Blocks") + + if all_ips: + print(f"\n🔥 Top 20 IPs:") + for ip, data in sorted(all_ips.items(), key=lambda x: x[1]['count'], reverse=True)[:20]: + print(f" {ip} ({detect_bot(data['ua'])}): {data['count']}x") + + input("\nEnter zum Fortfahren...") def main(): - """Main menu""" print("\n" + "=" * 60) - print(" GeoIP Shop Blocker Manager") - print(" Regionen: 🇩🇪🇦🇹🇨🇭 DACH | 🇪🇺 Eurozone+GB (22 Länder)") - print(" PHP + CrowdSec Watcher (systemd service)") + print(" GeoIP Shop Blocker Manager v3.1.0") + print(" 🇩🇪🇦🇹🇨🇭 DACH | 🇪🇺 Eurozone+GB (22 Länder)") + print(" 🛡️ Mit Cache-Validierung und Fail-Open") print("=" * 60) - if check_crowdsec(): - print(" ✅ CrowdSec: Aktiv") - else: - print(" ⚠️ CrowdSec: Nicht verfügbar") - - # Check service status + print(f" {'✅' if check_crowdsec() else '⚠️ '} CrowdSec") 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") + print(f" {'✅' if code == 0 and stdout.strip() == 'active' else '⚠️ '} Watcher-Service") while True: print("\n" + "-" * 40) - print("[1] GeoIP-Blocking AKTIVIEREN (einzeln)") - print("[2] GeoIP-Blocking DEAKTIVIEREN (einzeln)") + print("[1] Aktivieren (einzeln)") + print("[2] Deaktivieren (einzeln)") print("[3] Logs anzeigen") - print("[4] Status anzeigen") + print("[4] Status") print("-" * 40) - print("[5] 🚀 ALLE Shops aktivieren") - print("[6] 🛑 ALLE Shops deaktivieren") + print("[5] 🚀 ALLE aktivieren") + print("[6] 🛑 ALLE deaktivieren") print("-" * 40) print("[0] Beenden") - choice = input("\nWähle eine Option: ").strip() + choice = input("\nWähle: ").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: + 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:") - print(f" {COLOR_GREEN}Grün = hinter Link11{COLOR_RESET} | {COLOR_RED}Rot = Direkt{COLOR_RESET}") - for i, shop in enumerate(available_shops, 1): + for i, shop in enumerate(available, 1): print(format_shop_with_link11(shop, show_index=i)) - - 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] - - # Ask for geo region - geo_region = select_geo_region() - region_info = get_geo_region_info(geo_region) - - # Ask for mode + idx = int(input("\nShop wählen: ").strip()) - 1 + if 0 <= idx < len(available): + geo = select_geo_region() mode = select_mode() - mode_display = "PHP + CrowdSec" if mode == "php+crowdsec" else "Nur PHP" - - confirm = input(f"\n⚠️ {region_info['icon']} {region_info['name']}-Blocking ({mode_display}) aktivieren für '{selected_shop}'? (ja/nein): ").strip().lower() - if confirm in ['ja', 'j', 'yes', 'y']: - activate_blocking(selected_shop, mode=mode, geo_region=geo_region) - else: - print("❌ Ungültig") - except ValueError: + region_info = get_geo_region_info(geo) + if input(f"\n{region_info['icon']} aktivieren für '{available[idx]}'? (ja/nein): ").lower() in ['ja', 'j']: + activate_blocking(available[idx], mode=mode, geo_region=geo) + except: print("❌ Ungültig") elif choice == "2": - active_shops = get_active_shops() - - if not active_shops: + active = get_active_shops() + if not active: print("\n⚠️ Keine aktiven Shops") continue - print("\n📋 Aktive Shops:") - print(f" {COLOR_GREEN}Grün = hinter Link11{COLOR_RESET} | {COLOR_RED}Rot = Direkt{COLOR_RESET}") - for i, shop in enumerate(active_shops, 1): - mode = get_shop_mode(shop) - geo_region = get_shop_geo_region(shop) - region_info = get_geo_region_info(geo_region) - mode_icon = "🛡️" if mode == "php+crowdsec" else "📝" - link11_info = check_link11(shop) - color = COLOR_GREEN if link11_info['is_link11'] else COLOR_RED - link11_tag = "[Link11]" if link11_info['is_link11'] else "[Direkt]" - print(f" [{i}] {color}{shop} {link11_tag}{COLOR_RESET} {region_info['icon']} {mode_icon}") - - shop_choice = input("\nWähle einen Shop: ").strip() + for i, shop in enumerate(active, 1): + region_info = get_geo_region_info(get_shop_geo_region(shop)) + mode_icon = "🛡️" if get_shop_mode(shop) == "php+crowdsec" else "📝" + print(f" [{i}] {format_shop_with_link11(shop)} {region_info['icon']} {mode_icon}") 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: + 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_shops = get_active_shops() - if not active_shops: + active = get_active_shops() + if not active: print("\n⚠️ Keine aktiven Shops") continue - - print("\n📋 Logs anzeigen für:") - print(f" {COLOR_GREEN}Grün = hinter Link11{COLOR_RESET} | {COLOR_RED}Rot = Direkt{COLOR_RESET}") - print(f" [0] 📊 ALLE Shops (Zusammenfassung)") - for i, shop in enumerate(active_shops, 1): - mode = get_shop_mode(shop) - geo_region = get_shop_geo_region(shop) - region_info = get_geo_region_info(geo_region) - mode_icon = "🛡️" if mode == "php+crowdsec" else "📝" - link11_info = check_link11(shop) - color = COLOR_GREEN if link11_info['is_link11'] else COLOR_RED - link11_tag = "[Link11]" if link11_info['is_link11'] else "[Direkt]" - print(f" [{i}] {color}{shop} {link11_tag}{COLOR_RESET} {region_info['icon']} {mode_icon}") - - shop_choice = input("\nWähle eine Option: ").strip() + 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)) + print(f" [{i}] {format_shop_with_link11(shop)} {region_info['icon']}") try: - shop_idx = int(shop_choice) - if shop_idx == 0: + idx = int(input("\nWähle: ").strip()) + if idx == 0: show_all_logs() - elif 1 <= shop_idx <= len(active_shops): - show_logs(active_shops[shop_idx - 1]) - else: - print("❌ Ungültig") - except ValueError: + elif 1 <= idx <= len(active): + show_logs(active[idx - 1]) + except: print("❌ Ungültig") 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 GeoIP-Blockings: {len(active_shops)}") - print(f" {COLOR_GREEN}Grün = hinter Link11{COLOR_RESET} | {COLOR_RED}Rot = Direkt{COLOR_RESET}") - if active_shops: - for shop in active_shops: - mode = get_shop_mode(shop) - geo_region = get_shop_geo_region(shop) - region_info = get_geo_region_info(geo_region) - mode_icon = "🛡️" if mode == "php+crowdsec" else "📝" - mode_text = "PHP+CS" if mode == "php+crowdsec" else "PHP" - - # Get stats - blocks, _, activation_time = get_shop_log_stats(shop) - if activation_time: - runtime_minutes = (datetime.now() - activation_time).total_seconds() / 60 - req_min = blocks / runtime_minutes if runtime_minutes > 0 else 0 - runtime_str = format_duration(runtime_minutes) - else: - req_min = 0 - runtime_str = "?" - - # Link11 check - link11_info = check_link11(shop) - color = COLOR_GREEN if link11_info['is_link11'] else COLOR_RED - link11_tag = "[Link11]" if link11_info['is_link11'] else "[Direkt]" - - print(f" ✓ {color}{shop} {link11_tag}{COLOR_RESET} {region_info['icon']} [{mode_text}] {mode_icon} - {blocks} blocks ({req_min:.1f} req/min, {runtime_str})") + 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)) + mode_icon = "🛡️" if get_shop_mode(shop) == "php+crowdsec" else "📝" + blocks, _, activation_time = 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') + valid, count, _ = validate_existing_cache(httpdocs, get_shop_geo_region(shop)) + cache_str = f"✅{count:,}" if valid else "⚠️" + + print(f" {format_shop_with_link11(shop)} {region_info['icon']} {mode_icon}") + print(f" {blocks} blocks, {format_duration(runtime)}, Cache: {cache_str}") elif choice == "5": activate_all_shops() @@ -1923,7 +1186,6 @@ if __name__ == "__main__": if os.geteuid() != 0: print("❌ Als root ausführen!") sys.exit(1) - try: main() except KeyboardInterrupt: