From 3ebd1123a69777055914e2cd6378ceac69b0624c Mon Sep 17 00:00:00 2001 From: thomasciesla Date: Wed, 10 Dec 2025 10:11:54 +0100 Subject: [PATCH] geoip_shop_manager.py aktualisiert --- geoip_shop_manager.py | 348 +++++++++++++++++++++--------------------- 1 file changed, 174 insertions(+), 174 deletions(-) diff --git a/geoip_shop_manager.py b/geoip_shop_manager.py index 076d9a8..62282e6 100644 --- a/geoip_shop_manager.py +++ b/geoip_shop_manager.py @@ -10,7 +10,7 @@ Supports three modes: - php-only: GeoIP blocking without CrowdSec - bot-only: Rate-limit bots, shop remains globally accessible -v3.4.4: Rate-Limit 0 wird akzeptiert (= sofortiger Bot-Ban) +v3.4.5: Fix regex delimiter escape für curl pattern """ import os @@ -155,7 +155,7 @@ def generate_php_countries_array(geo_region): def generate_php_bot_patterns(): patterns = [] for bot_name, pattern in BOT_PATTERNS.items(): - escaped_pattern = pattern.replace("'", "\\'") + escaped_pattern = pattern.replace("'", "\\'").replace("/", "\\/") patterns.append(f"'{bot_name}' => '/{escaped_pattern}/i'") return ",\n ".join(patterns) @@ -169,7 +169,7 @@ def generate_and_validate_cache(httpdocs_path, geo_region): region_info = get_geo_region_info(geo_region) countries = region_info["countries"] min_expected = MIN_RANGES.get(geo_region, 1000) - + php_script = f'''= {min_expected}) {{ echo "FAIL:" . count($ranges); }} ''' - + temp_php = os.path.join(httpdocs_path, '_geoip_cache_gen.php') try: with open(temp_php, 'w') as f: @@ -218,10 +218,10 @@ if (count($ranges) >= {min_expected}) {{ def validate_existing_cache(httpdocs_path, geo_region): 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''' $window_size) {{ // New window $window_start = $current_time; @@ -510,7 +510,7 @@ if (file_exists($count_file)) {{ }} }} }} - + ftruncate($fp, 0); rewind($fp); fwrite($fp, "$window_start|$count"); @@ -526,13 +526,13 @@ if ($count > $rate_limit) {{ // Create ban $ban_until = $current_time + $ban_duration; @file_put_contents($ban_file, $ban_until, LOCK_EX); - + // Log the ban $timestamp = date('Y-m-d H:i:s'); $ban_minutes = $ban_duration / 60; @file_put_contents($log_file, "[$timestamp] BANNED: $detected_bot | IP: $visitor_ip | Exceeded $rate_limit req/min | Ban: {{$ban_minutes}}m | UA: $user_agent\\n", FILE_APPEND | LOCK_EX); @file_put_contents($crowdsec_queue, "$timestamp|$visitor_ip|{shop_name}\\n", FILE_APPEND | LOCK_EX); - + // Block this request header('HTTP/1.1 403 Forbidden'); header('Retry-After: ' . $ban_duration); @@ -639,7 +639,7 @@ if (file_exists($count_file)) {{ if (count($parts) === 2) {{ $window_start = (int)$parts[0]; $count = (int)$parts[1]; - + if ($current_time - $window_start > $window_size) {{ $window_start = $current_time; $count = 1; @@ -662,11 +662,11 @@ if (file_exists($count_file)) {{ if ($count > $rate_limit) {{ $ban_until = $current_time + $ban_duration; @file_put_contents($ban_file, $ban_until, LOCK_EX); - + $timestamp = date('Y-m-d H:i:s'); $ban_minutes = $ban_duration / 60; @file_put_contents($log_file, "[$timestamp] BANNED: $detected_bot | IP: $visitor_ip | Exceeded $rate_limit req/min | Ban: {{$ban_minutes}}m | UA: $user_agent\\n", FILE_APPEND | LOCK_EX); - + header('HTTP/1.1 403 Forbidden'); header('Retry-After: ' . $ban_duration); exit; @@ -967,7 +967,7 @@ def select_mode(): print(f" [2] 🌍 Nur GeoIP (keine CrowdSec-Synchronisation)") print(f" [3] 🤖 Bot-Rate-Limiting (weltweit erreichbar, mit CrowdSec)") print(f" [4] 🤖 Bot-Rate-Limiting (weltweit erreichbar, ohne CrowdSec)") - + choice = input(f"\nModus wählen [1/2/3/4]: ").strip() if choice == "2": return "php-only" @@ -981,7 +981,7 @@ def select_mode(): def select_rate_limit(): print(f"\n🚦 Rate-Limit Konfiguration:") print(f" (0 = Bots sofort bannen, kein Rate-Limiting)") - + rate_input = input(f" Requests pro Minute bevor Ban [{DEFAULT_RATE_LIMIT}]: ").strip() try: rate_limit = int(rate_input) if rate_input else DEFAULT_RATE_LIMIT @@ -991,7 +991,7 @@ def select_rate_limit(): except ValueError: print(f" ⚠️ Ungültiger Wert, verwende Default: {DEFAULT_RATE_LIMIT}") rate_limit = DEFAULT_RATE_LIMIT - + ban_input = input(f" Ban-Dauer in Minuten [{DEFAULT_BAN_DURATION}]: ").strip() try: ban_minutes = int(ban_input) if ban_input else DEFAULT_BAN_DURATION @@ -1001,15 +1001,15 @@ def select_rate_limit(): except ValueError: print(f" ⚠️ Ungültiger Wert, verwende Default: {DEFAULT_BAN_DURATION}") ban_minutes = DEFAULT_BAN_DURATION - + ban_seconds = ban_minutes * 60 - + if rate_limit == 0: print(f"\n 🚫 Rate-Limit: 0 (Bots werden SOFORT gebannt!)") else: print(f"\n ✅ Rate-Limit: {rate_limit} req/min") print(f" ✅ Ban-Dauer: {ban_minutes} Minuten") - + return rate_limit, ban_seconds @@ -1069,27 +1069,27 @@ 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) ratelimit_path = os.path.join(httpdocs, RATELIMIT_DIR) - + is_bot_mode = is_bot_only_mode(mode) - + if is_bot_mode: region_info = get_geo_region_info("none") geo_region = "none" else: region_info = get_geo_region_info(geo_region) - + min_ranges = MIN_RANGES.get(geo_region, 1000) - + if os.path.isfile(backup_php): if not silent: print(f"⚠️ Blocking bereits aktiv für {shop}") return False - + if not os.path.isfile(index_php): if not silent: print(f"❌ index.php nicht gefunden") return False - + if not silent: print(f"\n🔧 Aktiviere {region_info['icon']} {region_info['name']} für: {shop}") if is_bot_mode: @@ -1100,7 +1100,7 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach" print(f" Erlaubt: {region_info['description']}") print(f" CrowdSec: {'Ja' if uses_crowdsec(mode) else 'Nein'}") print("=" * 60) - + # Step 1: Watcher service if uses_crowdsec(mode): crowdsec_shops = [s for s in get_active_shops() if uses_crowdsec(get_shop_mode(s))] @@ -1112,16 +1112,16 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach" print("\n[1/4] CrowdSec-Watcher-Service bereits aktiv") elif not silent: print("\n[1/4] CrowdSec-Synchronisation deaktiviert") - + # Step 2: PHP blocking if not silent: print("\n[2/4] Aktiviere PHP-Blocking...") - + shutil.copy2(index_php, backup_php) - + with open(index_php, 'r', encoding='utf-8') as f: content = f.read() - + lines = content.split('\n') insert_line = 0 for i, line in enumerate(lines): @@ -1130,15 +1130,15 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach" break elif ' 0 else 0 else: runtime, req_min = 0, 0 - + print(f"\n{'═' * 70}") print(f"📊 {shop} | {region_info['icon']} {region_info['name']} {get_mode_icon(shop_mode)}") print(f"{'═' * 70}") print(f"⏱️ Laufzeit: {format_duration(runtime)}") print(f"📈 Log-Einträge: {blocks} ({req_min:.1f} req/min)") - + if not is_bot_mode: valid, count, err = validate_existing_cache(httpdocs, shop_geo) print(f"✅ Cache: {count:,} Ranges" if valid else f"⚠️ Cache: {err}") @@ -1771,19 +1771,19 @@ def show_logs(shop): if rate_limit and ban_duration: print(f"🚦 Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min") print(f"🚫 Bans: {total_bans} ausgelöst, {active_bans} aktiv") - + 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 Log-Einträge:") with open(log_file, 'r') as f: for line in f.readlines()[-30:]: print(line.rstrip()) - + if ips: print(f"\n🔥 Top 10 IPs:") for ip, data in sorted(ips.items(), key=lambda x: x[1]['count'], reverse=True)[:10]: @@ -1793,16 +1793,16 @@ def show_logs(shop): 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 ALLER SHOPS") print(f"{'═' * 70}") print(f" {COLOR_GREEN}Grün = hinter Link11{COLOR_RESET} | {COLOR_RED}Rot = Direkt{COLOR_RESET}") - + total_php_blocks = 0 total_bans = 0 total_active_bans = 0 @@ -1810,20 +1810,20 @@ def show_all_logs(): all_ips = {} all_bots = {} total_minutes = 0 - + for shop in active_shops: blocks, ips, bots, activation_time, bans, active_bans = get_shop_log_stats(shop) total_php_blocks += blocks total_bans += bans total_active_bans += active_bans - + if activation_time: runtime_minutes = (datetime.now() - activation_time).total_seconds() / 60 req_min = blocks / runtime_minutes if runtime_minutes > 0 else 0 else: runtime_minutes = 0 req_min = 0 - + shop_php_stats[shop] = { 'blocks': blocks, 'runtime_minutes': runtime_minutes, @@ -1833,10 +1833,10 @@ def show_all_logs(): 'bans': bans, 'active_bans': active_bans } - + if runtime_minutes > total_minutes: total_minutes = runtime_minutes - + for ip, data in ips.items(): if ip not in all_ips: all_ips[ip] = {'count': 0, 'ua': data['ua'], 'bot': data.get('bot'), 'shops': {}} @@ -1846,12 +1846,12 @@ def show_all_logs(): all_ips[ip]['ua'] = data['ua'] if data.get('bot') and not all_ips[ip].get('bot'): all_ips[ip]['bot'] = data.get('bot') - + for bot_name, count in bots.items(): all_bots[bot_name] = all_bots.get(bot_name, 0) + count - + total_req_min = total_php_blocks / total_minutes if total_minutes > 0 else 0 - + crowdsec_stats = {} if check_crowdsec(): code, stdout, _ = run_command("cscli decisions list -o raw --limit 0") @@ -1862,7 +1862,7 @@ def show_all_logs(): crowdsec_stats[shop] = crowdsec_stats.get(shop, 0) + 1 break total_crowdsec = sum(crowdsec_stats.values()) - + print(f"\n📝 Log-Einträge 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()): @@ -1872,20 +1872,20 @@ def show_all_logs(): 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 "?" - + geo_region = get_shop_geo_region(shop) region_info = get_geo_region_info(geo_region) geo_icon = region_info['icon'] mode_icon = get_mode_icon(get_shop_mode(shop)) - + link11_info = check_link11(shop) if link11_info['is_link11']: shop_colored = f"{COLOR_GREEN}{shop}{COLOR_RESET}" else: shop_colored = f"{COLOR_RED}{shop}{COLOR_RESET}" - + print(f" ├─ {shop_colored} {geo_icon} {mode_icon}: {count} ({req_min:.1f} req/min, seit {runtime_str}) {bar}") - + shop_ips = stats['ips'] if shop_ips and count > 0: top_ip = max(shop_ips.items(), key=lambda x: x[1]['count']) @@ -1894,14 +1894,14 @@ def show_all_logs(): top_ip_ua = top_ip[1]['ua'] top_ip_bot = top_ip[1].get('bot') or detect_bot(top_ip_ua) top_ip_req_min = top_ip_count / runtime if runtime > 0 else 0 - + if top_ip_bot == 'Unbekannt': display_name = (top_ip_ua[:40] + '...') if len(top_ip_ua) > 43 else top_ip_ua else: display_name = top_ip_bot - + print(f" │ └─➤ Top: {top_ip_addr} ({display_name}) - {top_ip_count}x, {top_ip_req_min:.1f} req/min") - + if total_bans > 0 or total_active_bans > 0: print(f"\n🚫 Rate-Limit Bans: {total_bans} ausgelöst, {total_active_bans} aktiv") for shop in sorted(shop_php_stats.keys()): @@ -1912,34 +1912,34 @@ def show_all_logs(): shop_colored = f"{COLOR_GREEN}{shop}{COLOR_RESET}" else: shop_colored = f"{COLOR_RED}{shop}{COLOR_RESET}" - + bar = "█" * min(stats['bans'] // 2, 20) if stats['bans'] > 0 else "" print(f" ├─ {shop_colored}: {stats['bans']} bans ({stats['active_bans']} aktiv) {bar}") - + 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 "" - + 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}: {count} {bar}") elif check_crowdsec(): print(" └─ Keine aktiven Bans") else: print(" └─ CrowdSec nicht verfügbar") - + 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}") - + if all_ips: print(f"\n🔥 Top 50 IPs (alle Shops):") sorted_ips = sorted(all_ips.items(), key=lambda x: x[1]['count'], reverse=True)[:50] @@ -1948,9 +1948,9 @@ def show_all_logs(): ua = data['ua'] bot_name = data.get('bot') or detect_bot(ua) shops_data = data['shops'] - + ip_req_min = count / total_minutes if total_minutes > 0 else 0 - + if shops_data: top_shop = max(shops_data.items(), key=lambda x: x[1]) top_shop_name = top_shop[0] @@ -1959,7 +1959,7 @@ def show_all_logs(): top_shop_short = top_shop_name[:22] + '...' else: top_shop_short = top_shop_name - + link11_info = check_link11(top_shop_name) if link11_info['is_link11']: top_shop_display = f"{COLOR_GREEN}{top_shop_short}{COLOR_RESET}" @@ -1968,17 +1968,17 @@ def show_all_logs(): else: top_shop_display = "?" top_shop_count = 0 - + if bot_name == 'Unbekannt': display_name = (ua[:35] + '...') if len(ua) > 38 else ua if display_name == 'Unknown': display_name = 'Unbekannt' else: display_name = bot_name - + bar = "█" * min(count // 5, 20) if count > 0 else "█" print(f" {ip} ({display_name}): {count} ({ip_req_min:.1f} req/min) → {top_shop_display} [{top_shop_count}x] {bar}") - + print(f"\n{'═' * 70}") input("\nDrücke Enter um fortzufahren...") @@ -1990,11 +1990,11 @@ def main(): print(" 🛡️ Mit Cache-Validierung und Fail-Open") print(" 🚦 Bots unter Rate-Limit werden durchgelassen") print("=" * 60) - + print(f" {'✅' if check_crowdsec() else '⚠️ '} CrowdSec") code, stdout, _ = run_command("systemctl is-active geoip-crowdsec-watcher.service") print(f" {'✅' if code == 0 and stdout.strip() == 'active' else '⚠️ '} Watcher-Service") - + while True: print("\n" + "-" * 40) print("[1] Aktivieren (einzeln)") @@ -2007,9 +2007,9 @@ def main(): print(f"[7] {COLOR_RED}🎯 Nur DIREKTE aktivieren (ohne Link11){COLOR_RESET}") print("-" * 40) print("[0] Beenden") - + choice = input("\nWähle: ").strip() - + if choice == "1": available = [s for s in get_available_shops() if s not in get_active_shops()] if not available: @@ -2023,7 +2023,7 @@ def main(): if 0 <= idx < len(available): mode = select_mode() is_bot_mode = is_bot_only_mode(mode) - + if is_bot_mode: geo = "none" region_info = get_geo_region_info("none") @@ -2032,13 +2032,13 @@ def main(): geo = select_geo_region() region_info = get_geo_region_info(geo) rate_limit, ban_duration = None, None - + confirm_msg = f"\n{region_info['icon']} {get_mode_description(mode)} aktivieren für '{available[idx]}'? (ja/nein): " if input(confirm_msg).lower() in ['ja', 'j']: activate_blocking(available[idx], mode=mode, geo_region=geo, rate_limit=rate_limit, ban_duration=ban_duration) except: print("❌ Ungültig") - + elif choice == "2": active = get_active_shops() if not active: @@ -2056,7 +2056,7 @@ def main(): deactivate_blocking(active[idx]) except: print("❌ Ungültig") - + elif choice == "3": active = get_active_shops() if not active: @@ -2076,7 +2076,7 @@ def main(): show_logs(active[idx - 1]) except: print("❌ Ungültig") - + elif choice == "4": shops = get_available_shops() active = get_active_shops() @@ -2088,11 +2088,11 @@ def main(): is_bot_mode = is_bot_only_mode(shop_mode) blocks, _, bots, activation_time, total_bans, active_bans = get_shop_log_stats(shop) runtime = (datetime.now() - activation_time).total_seconds() / 60 if activation_time else 0 - + httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs') - + print(f" {format_shop_with_link11(shop)} {region_info['icon']} {mode_icon}") - + if is_bot_mode: rate_limit, ban_duration = get_shop_rate_limit_config(shop) rl_str = f", {rate_limit} req/min" if rate_limit else "" @@ -2102,16 +2102,16 @@ def main(): valid, count, _ = validate_existing_cache(httpdocs, get_shop_geo_region(shop)) cache_str = f"✅{count:,}" if valid else "⚠️" print(f" {blocks} blocks, {format_duration(runtime)}, Cache: {cache_str}") - + elif choice == "5": activate_all_shops() - + elif choice == "6": deactivate_all_shops() - + elif choice == "7": activate_direct_shops_only() - + elif choice == "0": print("\n👋 Auf Wiedersehen!") break