From 172eed3090985532e144f6ea7a2db0e6f78b3850 Mon Sep 17 00:00:00 2001 From: thomasciesla Date: Tue, 9 Dec 2025 12:28:29 +0100 Subject: [PATCH] geoip_shop_manager.py aktualisiert --- geoip_shop_manager.py | 464 +++++++++++++++++++++++++++++++++--------- 1 file changed, 368 insertions(+), 96 deletions(-) diff --git a/geoip_shop_manager.py b/geoip_shop_manager.py index 6cebe0c..1a83204 100644 --- a/geoip_shop_manager.py +++ b/geoip_shop_manager.py @@ -5,8 +5,12 @@ 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) +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.1.0: Cache validation on activation + fail-open in PHP +v3.2.0: Added bot-only blocking mode """ import os @@ -71,6 +75,13 @@ GEO_REGIONS = { "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" } } @@ -136,6 +147,15 @@ def generate_php_countries_array(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 # ============================================================================= @@ -392,6 +412,82 @@ if (!$is_allowed) {{ }} ''' +BOT_ONLY_SCRIPT_TEMPLATE = ''' $expiry_date) return; + +$log_file = __DIR__ . '/{log_file}'; +$crowdsec_queue = __DIR__ . '/{crowdsec_queue}'; + +$bot_patterns = [ + {bot_patterns} +]; + +$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; +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; + }} +}} + +if ($detected_bot !== null) {{ + $timestamp = date('Y-m-d H:i:s'); + $visitor_ip = $_SERVER['REMOTE_ADDR'] ?? 'Unknown'; + $uri = $_SERVER['REQUEST_URI'] ?? '/'; + @file_put_contents($log_file, "[$timestamp] BOT: $detected_bot | IP: $visitor_ip | UA: $user_agent | 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; +}} +''' + +BOT_ONLY_SCRIPT_TEMPLATE_NO_CROWDSEC = ''' $expiry_date) return; + +$log_file = __DIR__ . '/{log_file}'; + +$bot_patterns = [ + {bot_patterns} +]; + +$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; +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; + }} +}} + +if ($detected_bot !== null) {{ + $timestamp = date('Y-m-d H:i:s'); + $visitor_ip = $_SERVER['REMOTE_ADDR'] ?? 'Unknown'; + $uri = $_SERVER['REQUEST_URI'] ?? '/'; + @file_put_contents($log_file, "[$timestamp] BOT: $detected_bot | IP: $visitor_ip | UA: $user_agent | URI: $uri\\n", FILE_APPEND | LOCK_EX); + header('HTTP/1.1 403 Forbidden'); + exit; +}} +''' + # ============================================================================= # WATCHER SERVICE # ============================================================================= @@ -446,9 +542,12 @@ def main(): while True: try: active_shops = get_active_shops() - for shop in active_shops: - shop_path = os.path.join(VHOSTS_DIR, shop) - if os.path.isdir(shop_path): process_queue_file(shop_path, shop) + 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) @@ -635,6 +734,11 @@ def get_active_shops(): 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']}") @@ -644,9 +748,51 @@ def select_geo_region(): def select_mode(): 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)") - return "php-only" if input(f"\nModus wählen [1/2]: ").strip() == "2" else "php+crowdsec" + 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 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'] # ============================================================================= @@ -659,12 +805,19 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach" 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) + 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"⚠️ GeoIP-Blocking bereits aktiv für {shop}") + print(f"⚠️ Blocking bereits aktiv für {shop}") return False if not os.path.isfile(index_php): @@ -673,14 +826,17 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach" return False if not silent: - print(f"\n🔧 Aktiviere {region_info['icon']} {region_info['name']} GeoIP-Blocking für: {shop}") - print(f" Erlaubt: {region_info['description']}") - print(f" Modus: {'PHP + CrowdSec' if mode == 'php+crowdsec' else 'Nur PHP'}") + 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)") + else: + print(f" Erlaubt: {region_info['description']}") + print(f" CrowdSec: {'Ja' if uses_crowdsec(mode) else 'Nein'}") print("=" * 60) # Step 1: Watcher service - if mode == "php+crowdsec": - crowdsec_shops = [s for s in get_active_shops() if get_shop_mode(s) == "php+crowdsec"] + 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...") @@ -688,7 +844,7 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach" 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)") + print("\n[1/4] CrowdSec-Synchronisation deaktiviert") # Step 2: PHP blocking if not silent: @@ -715,40 +871,61 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach" f.write('\n'.join(lines)) expiry = datetime.now() + timedelta(hours=72) - countries_array = generate_php_countries_array(geo_region) - 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 - ) + # Select appropriate template + if is_bot_mode: + if uses_crowdsec(mode): + template = BOT_ONLY_SCRIPT_TEMPLATE + else: + template = BOT_ONLY_SCRIPT_TEMPLATE_NO_CROWDSEC + + geoip_content = template.format( + expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'), + expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'), + log_file=LOG_FILE, + crowdsec_queue=CROWDSEC_QUEUE_FILE, + shop_name=shop, + bot_patterns=generate_php_bot_patterns() + ) + else: + countries_array = generate_php_countries_array(geo_region) + template = GEOIP_SCRIPT_TEMPLATE if uses_crowdsec(mode) 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(" ✅ PHP-Blocking aktiviert") - # Step 3: Generate cache - if not silent: - 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: + # Step 3: Generate cache (only for GeoIP modes) + if is_bot_mode: if not silent: - print(f" ✅ Cache generiert: {range_count:,} IP-Ranges") + print(f"\n[3/4] Cache-Generierung nicht erforderlich (Bot-Only)") + range_count = 0 else: if not silent: - print(f" ⚠️ Cache-Generierung: {error}") - print(f" ℹ️ Fail-Open aktiv - Cache wird beim ersten Request neu generiert") + 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: @@ -757,11 +934,15 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach" if not silent: print("\n" + "=" * 60) - print(f"✅ {region_info['icon']} {region_info['name']} GeoIP-Blocking aktiviert") + print(f"✅ {region_info['icon']} {region_info['name']} aktiviert") print(f" Shop: {shop}") - print(f" IP-Ranges: {range_count:,}") + print(f" Modus: {get_mode_description(mode)} {get_mode_icon(mode)}") + if not is_bot_mode: + print(f" IP-Ranges: {range_count:,}") + print(f" 🛡️ Fail-Open: Bei Cache-Fehlern wird Traffic durchgelassen") + else: + print(f" 🤖 {len(BOT_PATTERNS)} Bot-Patterns aktiv") print(f" Gültig bis: {expiry.strftime('%Y-%m-%d %H:%M:%S CET')}") - print(f" 🛡️ Fail-Open: Bei Cache-Fehlern wird Traffic durchgelassen") print("=" * 60) return True @@ -804,14 +985,14 @@ def deactivate_blocking(shop, silent=False): if not silent: print("\n[3/4] CrowdSec-Decisions entfernen...") - if shop_mode == "php+crowdsec" and check_crowdsec(): + if uses_crowdsec(shop_mode) and check_crowdsec(): cleanup_crowdsec_decisions(shop) elif not silent: print(" ℹ️ Keine CrowdSec-Synchronisation aktiv") if not silent: print("\n[4/4] Prüfe Watcher-Service...") - crowdsec_shops = [s for s in get_active_shops() if get_shop_mode(s) == "php+crowdsec"] + crowdsec_shops = [s for s in get_active_shops() if uses_crowdsec(get_shop_mode(s))] if not crowdsec_shops: uninstall_watcher_service() elif not silent: @@ -819,7 +1000,7 @@ def deactivate_blocking(shop, silent=False): if not silent: print("\n" + "=" * 60) - print(f"✅ GeoIP-Blocking deaktiviert für: {shop}") + print(f"✅ Blocking deaktiviert für: {shop}") print("=" * 60) return True @@ -833,18 +1014,25 @@ def activate_all_shops(): return print(f"\n{'=' * 60}") - print(f" GeoIP-Blocking für ALLE Shops aktivieren") + print(f" Blocking für ALLE Shops aktivieren") print(f"{'=' * 60}") print(f"\n📋 {len(available_shops)} Shop(s):") for shop in available_shops: print(format_shop_with_link11(shop, prefix=" • ")) - geo_region = select_geo_region() - region_info = get_geo_region_info(geo_region) mode = select_mode() + is_bot_mode = is_bot_only_mode(mode) - print(f"\n⚠️ Region: {region_info['icon']} {region_info['name']}") - print(f" Modus: {'PHP + CrowdSec 🛡️' if mode == 'php+crowdsec' else 'Nur PHP 📝'}") + if is_bot_mode: + geo_region = "none" + region_info = get_geo_region_info("none") + else: + geo_region = select_geo_region() + region_info = get_geo_region_info(geo_region) + + print(f"\n⚠️ Modus: {get_mode_description(mode)} {get_mode_icon(mode)}") + if not is_bot_mode: + print(f" Region: {region_info['icon']} {region_info['name']}") if input(f"\nFortfahren? (ja/nein): ").strip().lower() not in ['ja', 'j', 'yes', 'y']: print("\n❌ Abgebrochen") @@ -852,8 +1040,8 @@ def activate_all_shops(): print(f"\n{'=' * 60}") - if mode == "php+crowdsec" and check_crowdsec(): - if not [s for s in get_active_shops() if get_shop_mode(s) == "php+crowdsec"]: + if uses_crowdsec(mode) and check_crowdsec(): + if not [s for s in get_active_shops() if uses_crowdsec(get_shop_mode(s))]: print("\n📦 Installiere CrowdSec-Watcher-Service...") install_watcher_service() @@ -890,33 +1078,54 @@ def activate_all_shops(): f.write('\n'.join(lines)) expiry = datetime.now() + timedelta(hours=72) - 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=generate_php_countries_array(geo_region), - min_ranges=MIN_RANGES.get(geo_region, 1000) - ) + + if is_bot_mode: + if uses_crowdsec(mode): + template = BOT_ONLY_SCRIPT_TEMPLATE + else: + template = BOT_ONLY_SCRIPT_TEMPLATE_NO_CROWDSEC + + geoip_content = template.format( + expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'), + expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'), + log_file=LOG_FILE, + crowdsec_queue=CROWDSEC_QUEUE_FILE, + shop_name=shop, + bot_patterns=generate_php_bot_patterns() + ) + else: + template = GEOIP_SCRIPT_TEMPLATE if uses_crowdsec(mode) 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=generate_php_countries_array(geo_region), + min_ranges=MIN_RANGES.get(geo_region, 1000) + ) with open(blocking_file, 'w', encoding='utf-8') as f: f.write(geoip_content) - print(f" ⏳ Cache generieren...") - cache_ok, count, _ = generate_and_validate_cache(httpdocs, geo_region) + if is_bot_mode: + add_shop_to_active(shop, mode, geo_region) + print(f" ✅ Aktiviert ({len(BOT_PATTERNS)} Bot-Patterns)") + else: + print(f" ⏳ Cache generieren...") + cache_ok, count, _ = generate_and_validate_cache(httpdocs, geo_region) + add_shop_to_active(shop, mode, geo_region) + print(f" ✅ Aktiviert ({count:,} Ranges)" if cache_ok else f" ⚠️ Aktiviert (Cache-Warnung)") - add_shop_to_active(shop, mode, geo_region) - print(f" ✅ Aktiviert ({count:,} Ranges)" if cache_ok else f" ⚠️ Aktiviert (Cache-Warnung)") success_count += 1 print(f"\n{'=' * 60}") print(f" ✅ {success_count} Shop(s) aktiviert") - print(f" 🛡️ Fail-Open bei Cache-Fehlern aktiv") + if not is_bot_mode: + print(f" 🛡️ Fail-Open bei Cache-Fehlern aktiv") print(f"{'=' * 60}") @@ -928,12 +1137,13 @@ def deactivate_all_shops(): return print(f"\n{'=' * 60}") - print(f" GeoIP-Blocking für ALLE deaktivieren") + print(f" Blocking für ALLE deaktivieren") print(f"{'=' * 60}") print(f"\n📋 {len(active_shops)} Shop(s):") for shop in active_shops: region_info = get_geo_region_info(get_shop_geo_region(shop)) - print(f" • {format_shop_with_link11(shop)} {region_info['icon']}") + mode_icon = get_mode_icon(get_shop_mode(shop)) + print(f" • {format_shop_with_link11(shop)} {region_info['icon']} {mode_icon}") if input(f"\nFortfahren? (ja/nein): ").strip().lower() not in ['ja', 'j', 'yes', 'y']: print("\n❌ Abgebrochen") @@ -969,12 +1179,25 @@ def get_shop_log_stats(shop): log_file = os.path.join(httpdocs, LOG_FILE) php_blocks = 0 ips = {} + bots = {} # Track bot statistics + + shop_mode = get_shop_mode(shop) + is_bot_mode = is_bot_only_mode(shop_mode) if os.path.isfile(log_file): with open(log_file, 'r') as f: for line in f: php_blocks += 1 ip, ua = None, 'Unknown' + detected_bot = None + + # Parse bot-only format: BOT: botname | IP: ... + if 'BOT: ' in line: + try: + detected_bot = line.split('BOT: ')[1].split(' |')[0].strip() + except: + pass + if 'IP: ' in line: try: ip = line.split('IP: ')[1].split(' |')[0].strip() @@ -985,14 +1208,23 @@ def get_shop_log_stats(shop): 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 + + # Track bot statistics + if detected_bot: + bots[detected_bot] = bots.get(detected_bot, 0) + 1 + elif ua and ua != 'Unknown': + bot_name = detect_bot(ua) + if bot_name != 'Unbekannt': + bots[bot_name] = bots.get(bot_name, 0) + 1 - return php_blocks, ips, get_shop_activation_time(shop) + return php_blocks, ips, bots, get_shop_activation_time(shop) def show_logs(shop): @@ -1001,8 +1233,9 @@ def show_logs(shop): shop_mode = get_shop_mode(shop) shop_geo = get_shop_geo_region(shop) region_info = get_geo_region_info(shop_geo) + is_bot_mode = is_bot_only_mode(shop_mode) - blocks, ips, activation_time = get_shop_log_stats(shop) + blocks, ips, bots, activation_time = get_shop_log_stats(shop) if activation_time: runtime = (datetime.now() - activation_time).total_seconds() / 60 @@ -1011,13 +1244,23 @@ def show_logs(shop): runtime, req_min = 0, 0 print(f"\n{'═' * 70}") - print(f"📊 {shop} | {region_info['icon']} {region_info['name']}") + 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)") - valid, count, err = validate_existing_cache(httpdocs, shop_geo) - print(f"✅ Cache: {count:,} Ranges" if valid else f"⚠️ Cache: {err}") + 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: + print(f"🤖 Bot-Patterns: {len(BOT_PATTERNS)} 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:") @@ -1046,13 +1289,14 @@ def show_all_logs(): 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, 'runtime_minutes': float, 'req_min': float, 'ips': {}} + 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, activation_time = get_shop_log_stats(shop) + blocks, ips, bots, activation_time = get_shop_log_stats(shop) total_php_blocks += blocks # Calculate runtime and req/min @@ -1067,7 +1311,8 @@ def show_all_logs(): 'blocks': blocks, 'runtime_minutes': runtime_minutes, 'req_min': req_min, - 'ips': ips + 'ips': ips, + 'bots': bots } if runtime_minutes > total_minutes: @@ -1080,6 +1325,9 @@ def show_all_logs(): all_ips[ip]['shops'][shop] = data['count'] if data['ua'] != 'Unknown' and all_ips[ip]['ua'] == 'Unknown': all_ips[ip]['ua'] = data['ua'] + + 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 @@ -1097,7 +1345,7 @@ def show_all_logs(): total_crowdsec = sum(crowdsec_stats.values()) # 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)})") + 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] @@ -1107,10 +1355,11 @@ def show_all_logs(): 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 + # 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) @@ -1119,7 +1368,7 @@ def show_all_logs(): 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}") + 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'] @@ -1148,6 +1397,7 @@ def show_all_logs(): 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']: @@ -1155,12 +1405,19 @@ def show_all_logs(): else: shop_colored = f"{COLOR_RED}{shop}{COLOR_RESET}" - print(f" ├─ {shop_colored} {geo_icon}: {count} {bar}") + 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):") @@ -1209,8 +1466,8 @@ def show_all_logs(): def main(): print("\n" + "=" * 60) - print(" GeoIP Shop Blocker Manager v3.1.0") - print(" 🇩🇪🇦🇹🇨🇭 DACH | 🇪🇺 Eurozone+GB (22 Länder)") + print(" GeoIP Shop Blocker Manager v3.2.0") + print(" 🇩🇪🇦🇹🇨🇭 DACH | 🇪🇺 Eurozone+GB | 🤖 Bot-Only") print(" 🛡️ Mit Cache-Validierung und Fail-Open") print("=" * 60) @@ -1243,10 +1500,18 @@ def main(): try: idx = int(input("\nShop wählen: ").strip()) - 1 if 0 <= idx < len(available): - geo = select_geo_region() mode = select_mode() - 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']: + is_bot_mode = is_bot_only_mode(mode) + + if is_bot_mode: + geo = "none" + region_info = get_geo_region_info("none") + else: + geo = select_geo_region() + region_info = get_geo_region_info(geo) + + 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) except: print("❌ Ungültig") @@ -1259,7 +1524,7 @@ def main(): print("\n📋 Aktive Shops:") 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 "📝" + 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 @@ -1278,7 +1543,8 @@ def main(): 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']}") + 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: @@ -1294,16 +1560,22 @@ def main(): 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) + 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 = 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}") + + if is_bot_mode: + print(f" {blocks} blocks, {format_duration(runtime)}, {len(BOT_PATTERNS)} Bot-Patterns") + 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()