From cc90e59034095aabbc9def29c46a0b173de1fc2e Mon Sep 17 00:00:00 2001 From: thomasciesla Date: Tue, 9 Dec 2025 19:12:24 +0100 Subject: [PATCH] geoip_shop_manager.py aktualisiert --- geoip_shop_manager.py | 250 +++++++++++++++--------------------------- 1 file changed, 89 insertions(+), 161 deletions(-) diff --git a/geoip_shop_manager.py b/geoip_shop_manager.py index c8028a1..47c9a88 100644 --- a/geoip_shop_manager.py +++ b/geoip_shop_manager.py @@ -8,9 +8,9 @@ Supports two geo regions: 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 + - bot-only: Rate-limit bots, shop remains globally accessible -v3.4.2: Fixed directory permissions for rate-limit (777 for PHP access) +v3.4.3: Fixed rate-limit logic - bots under limit are allowed through """ import os @@ -56,8 +56,8 @@ DEFAULT_BAN_DURATION = 5 # Minuten # Minimum expected IP ranges per region (for validation) MIN_RANGES = { - "dach": 1000, # DE+AT+CH should have at least 1000 ranges - "eurozone": 5000 # 22 countries should have at least 5000 ranges + "dach": 1000, + "eurozone": 5000 } # ============================================================================= @@ -84,7 +84,7 @@ GEO_REGIONS = { "none": { "name": "Bot-Only", "countries": [], - "description": "Nur Bot-Blocking, weltweit erreichbar", + "description": "Nur Bot-Rate-Limiting, weltweit erreichbar", "icon": "🤖", "short": "BOT" } @@ -153,7 +153,6 @@ def generate_php_countries_array(geo_region): def generate_php_bot_patterns(): - """Generate PHP array of bot patterns""" patterns = [] for bot_name, pattern in BOT_PATTERNS.items(): escaped_pattern = pattern.replace("'", "\\'") @@ -166,7 +165,6 @@ def generate_php_bot_patterns(): # ============================================================================= 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"] @@ -218,7 +216,6 @@ if (count($ranges) >= {min_expected}) {{ 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) @@ -244,7 +241,7 @@ else {{ echo "OK:" . count($data); }} # ============================================================================= -# PHP TEMPLATES WITH FAIL-OPEN AND RATE-LIMITING +# PHP TEMPLATES - GEOIP # ============================================================================= GEOIP_SCRIPT_TEMPLATE = ''' $pattern) {{ }} }} -// Not a bot - allow through +// Not a bot - allow through without any rate limiting if ($detected_bot === null) return; -// === STEP 3: Rate-Limit Check === +// === STEP 3: Rate-Limit Check for detected bot === $count_file = "$counts_dir/$visitor_hash.count"; $current_time = time(); $count = 1; @@ -519,54 +522,52 @@ if (file_exists($count_file)) {{ }} // === STEP 4: Check if limit exceeded === -$is_banned = false; if ($count > $rate_limit) {{ // Create ban $ban_until = $current_time + $ban_duration; @file_put_contents($ban_file, $ban_until, LOCK_EX); - $is_banned = true; -}} - -// === STEP 5: Log and block === -$timestamp = date('Y-m-d H:i:s'); -$uri = $_SERVER['REQUEST_URI'] ?? '/'; - -if ($is_banned) {{ + + // 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); -}} else {{ - @file_put_contents($log_file, "[$timestamp] BOT: $detected_bot | IP: $visitor_ip | Count: $count/$rate_limit | URI: $uri\\n", FILE_APPEND | LOCK_EX); + @file_put_contents($crowdsec_queue, "$timestamp|$visitor_ip|{shop_name}\\n", FILE_APPEND | LOCK_EX); + + // Block this request + header('HTTP/1.1 403 Forbidden'); + header('Retry-After: ' . $ban_duration); + exit; }} -@file_put_contents($crowdsec_queue, "$timestamp|$visitor_ip|{shop_name}\\n", FILE_APPEND | LOCK_EX); +// === STEP 5: Under limit - log and ALLOW through === +$timestamp = date('Y-m-d H:i:s'); +$uri = $_SERVER['REQUEST_URI'] ?? '/'; +@file_put_contents($log_file, "[$timestamp] BOT: $detected_bot | IP: $visitor_ip | Count: $count/$rate_limit | URI: $uri\\n", FILE_APPEND | LOCK_EX); // === STEP 6: Probabilistic cleanup === if (rand(1, $cleanup_probability) === 1) {{ $now = time(); - // Clean expired bans foreach (glob("$bans_dir/*.ban") as $f) {{ $ban_time = (int)@file_get_contents($f); if ($now > $ban_time) @unlink($f); }} - // Clean old count files (older than 2x window) foreach (glob("$counts_dir/*.count") as $f) {{ if ($now - filemtime($f) > $window_size * 2) @unlink($f); }} }} -header('HTTP/1.1 403 Forbidden'); -if ($is_banned) {{ - header('Retry-After: ' . $ban_duration); -}} -exit; +// Bot is under rate limit - ALLOW through (no exit, no 403) +return; ''' BOT_ONLY_SCRIPT_TEMPLATE_NO_CROWDSEC = ''' $window_size) {{ - // New window $window_start = $current_time; $count = 1; }} else {{ @@ -650,7 +648,6 @@ if (file_exists($count_file)) {{ }} }} }} - ftruncate($fp, 0); rewind($fp); fwrite($fp, "$window_start|$count"); @@ -662,44 +659,38 @@ if (file_exists($count_file)) {{ }} // === STEP 4: Check if limit exceeded === -$is_banned = false; if ($count > $rate_limit) {{ - // Create ban $ban_until = $current_time + $ban_duration; @file_put_contents($ban_file, $ban_until, LOCK_EX); - $is_banned = true; -}} - -// === STEP 5: Log and block === -$timestamp = date('Y-m-d H:i:s'); -$uri = $_SERVER['REQUEST_URI'] ?? '/'; - -if ($is_banned) {{ + + $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); -}} else {{ - @file_put_contents($log_file, "[$timestamp] BOT: $detected_bot | IP: $visitor_ip | Count: $count/$rate_limit | URI: $uri\\n", FILE_APPEND | LOCK_EX); + + header('HTTP/1.1 403 Forbidden'); + header('Retry-After: ' . $ban_duration); + exit; }} +// === STEP 5: Under limit - log and ALLOW through === +$timestamp = date('Y-m-d H:i:s'); +$uri = $_SERVER['REQUEST_URI'] ?? '/'; +@file_put_contents($log_file, "[$timestamp] BOT: $detected_bot | IP: $visitor_ip | Count: $count/$rate_limit | URI: $uri\\n", FILE_APPEND | LOCK_EX); + // === STEP 6: Probabilistic cleanup === if (rand(1, $cleanup_probability) === 1) {{ $now = time(); - // Clean expired bans foreach (glob("$bans_dir/*.ban") as $f) {{ $ban_time = (int)@file_get_contents($f); if ($now > $ban_time) @unlink($f); }} - // Clean old count files (older than 2x window) foreach (glob("$counts_dir/*.count") as $f) {{ if ($now - filemtime($f) > $window_size * 2) @unlink($f); }} }} -header('HTTP/1.1 403 Forbidden'); -if ($is_banned) {{ - header('Retry-After: ' . $ban_duration); -}} -exit; +// Bot is under rate limit - ALLOW through (no exit, no 403) +return; ''' # ============================================================================= @@ -731,7 +722,7 @@ def add_to_crowdsec(ip, shop): 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})") + log(f"Added {ip} to CrowdSec (from {shop})") return True except: pass return False @@ -752,12 +743,11 @@ def process_queue_file(shop_path, shop): return processed def main(): - log("🚀 GeoIP CrowdSec Watcher started") + log("GeoIP CrowdSec Watcher started") while True: try: active_shops = get_active_shops() for shop, info in active_shops.items(): - # Only process shops with CrowdSec enabled mode = info.get('mode', 'php+crowdsec') if mode in ['php+crowdsec', 'bot+crowdsec']: shop_path = os.path.join(VHOSTS_DIR, shop) @@ -875,7 +865,6 @@ def get_shop_geo_region(shop): def get_shop_rate_limit_config(shop): - """Get rate limit configuration for a shop""" if not os.path.isfile(ACTIVE_SHOPS_FILE): return None, None try: @@ -965,11 +954,6 @@ 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']}") @@ -981,8 +965,8 @@ def select_mode(): print(f"\n🔧 Wähle den Blocking-Modus:") print(f" [1] 🌍 GeoIP + CrowdSec (IPs werden an CrowdSec gemeldet)") print(f" [2] 🌍 Nur GeoIP (keine CrowdSec-Synchronisation)") - print(f" [3] 🤖 Nur Bot-Blocking (weltweit erreichbar, mit CrowdSec)") - print(f" [4] 🤖 Nur Bot-Blocking (weltweit erreichbar, ohne CrowdSec)") + 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": @@ -995,10 +979,8 @@ def select_mode(): def select_rate_limit(): - """Ask user for rate-limit configuration. Returns (rate_limit, ban_duration_seconds)""" print(f"\n🚦 Rate-Limit Konfiguration:") - # Rate limit rate_input = input(f" Requests pro Minute bevor Ban [{DEFAULT_RATE_LIMIT}]: ").strip() try: rate_limit = int(rate_input) if rate_input else DEFAULT_RATE_LIMIT @@ -1007,7 +989,6 @@ def select_rate_limit(): except ValueError: rate_limit = DEFAULT_RATE_LIMIT - # Ban duration ban_input = input(f" Ban-Dauer in Minuten [{DEFAULT_BAN_DURATION}]: ").strip() try: ban_minutes = int(ban_input) if ban_input else DEFAULT_BAN_DURATION @@ -1025,7 +1006,6 @@ def select_rate_limit(): def get_mode_icon(mode): - """Return icon for mode""" icons = { 'php+crowdsec': '🛡️', 'php-only': '📝', @@ -1036,28 +1016,24 @@ def get_mode_icon(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' + 'bot+crowdsec': 'Bot-Rate-Limit + CrowdSec', + 'bot-only': 'Nur Bot-Rate-Limit' } return descriptions.get(mode, mode) def is_bot_only_mode(mode): - """Check if mode is bot-only""" return mode in ['bot-only', 'bot+crowdsec'] def uses_crowdsec(mode): - """Check if mode uses CrowdSec""" return mode in ['php+crowdsec', 'bot+crowdsec'] def get_direct_shops(available_shops): - """Return list of shops that are NOT behind Link11 (direct exposure)""" direct_shops = [] for shop in available_shops: link11_info = check_link11(shop) @@ -1067,7 +1043,6 @@ def get_direct_shops(available_shops): def get_link11_shops(available_shops): - """Return list of shops that ARE behind Link11""" link11_shops = [] for shop in available_shops: link11_info = check_link11(shop) @@ -1110,7 +1085,7 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach" if not silent: print(f"\n🔧 Aktiviere {region_info['icon']} {region_info['name']} für: {shop}") if is_bot_mode: - print(f" Modus: Nur Bot-Blocking (weltweit erreichbar)") + print(f" Modus: Bot-Rate-Limiting (weltweit erreichbar)") if rate_limit and ban_duration: print(f" Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min") else: @@ -1158,19 +1133,17 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach" # Select appropriate template if is_bot_mode: - # Create rate-limit directories with open permissions (PHP runs as different user) + # Create rate-limit directories with open permissions os.makedirs(os.path.join(ratelimit_path, 'bans'), mode=0o777, exist_ok=True) os.makedirs(os.path.join(ratelimit_path, 'counts'), mode=0o777, exist_ok=True) - # Ensure parent dir also has correct permissions os.chmod(ratelimit_path, 0o777) os.chmod(os.path.join(ratelimit_path, 'bans'), 0o777) os.chmod(os.path.join(ratelimit_path, 'counts'), 0o777) - # Use defaults if not specified if rate_limit is None: rate_limit = DEFAULT_RATE_LIMIT if ban_duration is None: - ban_duration = DEFAULT_BAN_DURATION * 60 # Convert to seconds + ban_duration = DEFAULT_BAN_DURATION * 60 if uses_crowdsec(mode): template = BOT_ONLY_SCRIPT_TEMPLATE @@ -1186,7 +1159,8 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach" shop_name=shop, bot_patterns=generate_php_bot_patterns(), rate_limit=rate_limit, - ban_duration=ban_duration + ban_duration=ban_duration, + ban_duration_min=ban_duration // 60 ) else: countries_array = generate_php_countries_array(geo_region) @@ -1248,6 +1222,7 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach" else: print(f" 🤖 {len(BOT_PATTERNS)} Bot-Patterns aktiv") print(f" 🚦 Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min") + print(f" ℹ️ Bots unter dem Limit werden durchgelassen") print(f" Gültig bis: {expiry.strftime('%Y-%m-%d %H:%M:%S CET')}") print("=" * 60) @@ -1268,7 +1243,6 @@ def deactivate_blocking(shop, silent=False): print(f"\n🔧 Deaktiviere {region_info['icon']} {region_info['name']} für: {shop}") print("=" * 60) - # Restore backup if not silent: print("\n[1/5] PHP-Blocking entfernen...") @@ -1282,12 +1256,10 @@ def deactivate_blocking(shop, silent=False): with open(index_php, 'w') as f: f.write('\n'.join(lines)) - # Remove files 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) - # Remove rate-limit directory if not silent: print("\n[2/5] Rate-Limit Daten entfernen...") if os.path.isdir(ratelimit_path): @@ -1355,6 +1327,7 @@ def activate_all_shops(): print(f" Region: {region_info['icon']} {region_info['name']}") else: print(f" Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min") + print(f" ℹ️ Bots unter dem Limit werden durchgelassen") if input(f"\nFortfahren? (ja/nein): ").strip().lower() not in ['ja', 'j', 'yes', 'y']: print("\n❌ Abgebrochen") @@ -1403,7 +1376,6 @@ def activate_all_shops(): expiry = datetime.now() + timedelta(hours=72) if is_bot_mode: - # Create rate-limit directories with open permissions (PHP runs as different user) os.makedirs(os.path.join(ratelimit_path, 'bans'), mode=0o777, exist_ok=True) os.makedirs(os.path.join(ratelimit_path, 'counts'), mode=0o777, exist_ok=True) os.chmod(ratelimit_path, 0o777) @@ -1424,7 +1396,8 @@ def activate_all_shops(): shop_name=shop, bot_patterns=generate_php_bot_patterns(), rate_limit=rate_limit, - ban_duration=ban_duration + ban_duration=ban_duration, + ban_duration_min=ban_duration // 60 ) else: template = GEOIP_SCRIPT_TEMPLATE if uses_crowdsec(mode) else GEOIP_SCRIPT_TEMPLATE_PHP_ONLY @@ -1461,27 +1434,22 @@ def activate_all_shops(): print(f" 🛡️ Fail-Open bei Cache-Fehlern aktiv") else: print(f" 🚦 Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min") + print(f" ℹ️ Bots unter dem Limit werden durchgelassen") print(f"{'=' * 60}") def activate_direct_shops_only(): - """Activate blocking only for shops that are NOT behind Link11 (direct exposure)""" 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") return - # Filter: only shops NOT behind Link11 direct_shops = get_direct_shops(available_shops) link11_shops = get_link11_shops(available_shops) if not direct_shops: print("\n⚠️ Keine direkten Shops gefunden (alle sind hinter Link11)") - if link11_shops: - print(f"\n📋 {len(link11_shops)} Shop(s) hinter Link11 (werden übersprungen):") - for shop in link11_shops: - print(f" {COLOR_GREEN}• {shop} [Link11]{COLOR_RESET}") return print(f"\n{'=' * 60}") @@ -1516,8 +1484,7 @@ def activate_direct_shops_only(): print(f" Region: {region_info['icon']} {region_info['name']}") else: print(f" Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min") - print(f" Aktiviert: {len(direct_shops)} direkte Shop(s)") - print(f" Übersprungen: {len(link11_shops)} Link11-Shop(s)") + print(f" ℹ️ Bots unter dem Limit werden durchgelassen") if input(f"\nFortfahren? (ja/nein): ").strip().lower() not in ['ja', 'j', 'yes', 'y']: print("\n❌ Abgebrochen") @@ -1566,7 +1533,6 @@ def activate_direct_shops_only(): expiry = datetime.now() + timedelta(hours=72) if is_bot_mode: - # Create rate-limit directories with open permissions (PHP runs as different user) os.makedirs(os.path.join(ratelimit_path, 'bans'), mode=0o777, exist_ok=True) os.makedirs(os.path.join(ratelimit_path, 'counts'), mode=0o777, exist_ok=True) os.chmod(ratelimit_path, 0o777) @@ -1587,7 +1553,8 @@ def activate_direct_shops_only(): shop_name=shop, bot_patterns=generate_php_bot_patterns(), rate_limit=rate_limit, - ban_duration=ban_duration + ban_duration=ban_duration, + ban_duration_min=ban_duration // 60 ) else: template = GEOIP_SCRIPT_TEMPLATE if uses_crowdsec(mode) else GEOIP_SCRIPT_TEMPLATE_PHP_ONLY @@ -1625,6 +1592,7 @@ def activate_direct_shops_only(): print(f" 🛡️ Fail-Open bei Cache-Fehlern aktiv") else: print(f" 🚦 Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min") + print(f" ℹ️ Bots unter dem Limit werden durchgelassen") print(f"{'=' * 60}") @@ -1663,7 +1631,6 @@ def deactivate_all_shops(): if os.path.isfile(f): os.remove(f) - # Remove rate-limit directory if os.path.isdir(ratelimit_path): shutil.rmtree(ratelimit_path) @@ -1684,11 +1651,8 @@ def get_shop_log_stats(shop): ratelimit_path = os.path.join(httpdocs, RATELIMIT_DIR) php_blocks = 0 ips = {} - bots = {} # Track bot statistics - bans = 0 # Track rate-limit bans - - shop_mode = get_shop_mode(shop) - is_bot_mode = is_bot_only_mode(shop_mode) + bots = {} + bans = 0 if os.path.isfile(log_file): with open(log_file, 'r') as f: @@ -1697,14 +1661,12 @@ def get_shop_log_stats(shop): ip, ua = None, 'Unknown' detected_bot = None - # Check if this is a ban line if 'BANNED: ' in line: bans += 1 try: detected_bot = line.split('BANNED: ')[1].split(' |')[0].strip() except: pass - # Parse bot-only format: BOT: botname | IP: ... elif 'BOT: ' in line: try: detected_bot = line.split('BOT: ')[1].split(' |')[0].strip() @@ -1726,14 +1688,11 @@ def get_shop_log_stats(shop): if ip not in ips: ips[ip] = {'count': 0, 'ua': ua, 'bot': None} ips[ip]['count'] += 1 - # Store bot name if detected (from BOT: or BANNED: in log) if detected_bot and not ips[ip]['bot']: ips[ip]['bot'] = detected_bot - # Fallback: try to detect from UA if we have one 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': @@ -1741,7 +1700,6 @@ def get_shop_log_stats(shop): if bot_name != 'Unbekannt': bots[bot_name] = bots.get(bot_name, 0) + 1 - # Count active bans from file system active_bans = 0 bans_dir = os.path.join(ratelimit_path, 'bans') if os.path.isdir(bans_dir): @@ -1779,7 +1737,7 @@ def show_logs(shop): 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)") + 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) @@ -1791,7 +1749,6 @@ def show_logs(shop): print(f"🚦 Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min") print(f"🚫 Bans: {total_bans} ausgelöst, {active_bans} aktiv") - # Show bot statistics for bot-only mode if bots: print(f"\n🤖 Bot-Statistik:") for bot_name, count in sorted(bots.items(), key=lambda x: x[1], reverse=True)[:10]: @@ -1799,7 +1756,7 @@ def show_logs(shop): print(f" {bot_name}: {count}x {bar}") if os.path.isfile(log_file): - print(f"\n📝 Letzte 30 Blocks:") + print(f"\n📝 Letzte 30 Log-Einträge:") with open(log_file, 'r') as f: for line in f.readlines()[-30:]: print(line.rstrip()) @@ -1807,13 +1764,11 @@ def show_logs(shop): if ips: print(f"\n🔥 Top 10 IPs:") for ip, data in sorted(ips.items(), key=lambda x: x[1]['count'], reverse=True)[:10]: - # Use stored bot name first, fallback to UA detection bot = data.get('bot') or detect_bot(data['ua']) print(f" {ip} ({bot}): {data['count']}x") def show_all_logs(): - """Show combined logs for all active shops""" active_shops = get_active_shops() if not active_shops: @@ -1828,19 +1783,17 @@ def show_all_logs(): total_php_blocks = 0 total_bans = 0 total_active_bans = 0 - shop_php_stats = {} # shop -> {'blocks': N, 'runtime_minutes': float, 'req_min': float, 'ips': {}, 'bots': {}} - all_ips = {} # ip -> {'count': N, 'ua': user_agent, 'shops': {shop: count}} - all_bots = {} # bot_name -> count + shop_php_stats = {} + all_ips = {} + all_bots = {} total_minutes = 0 - # Collect PHP stats for shop in active_shops: blocks, ips, bots, activation_time, bans, active_bans = get_shop_log_stats(shop) total_php_blocks += blocks total_bans += bans total_active_bans += active_bans - # Calculate runtime and req/min if activation_time: runtime_minutes = (datetime.now() - activation_time).total_seconds() / 60 req_min = blocks / runtime_minutes if runtime_minutes > 0 else 0 @@ -1868,17 +1821,14 @@ 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'] - # Update bot name if we have one if data.get('bot') and not all_ips[ip].get('bot'): all_ips[ip]['bot'] = data.get('bot') for bot_name, count in bots.items(): all_bots[bot_name] = all_bots.get(bot_name, 0) + count - # Calculate total req/min total_req_min = total_php_blocks / total_minutes if total_minutes > 0 else 0 - # Get CrowdSec stats crowdsec_stats = {} if check_crowdsec(): code, stdout, _ = run_command("cscli decisions list -o raw --limit 0") @@ -1890,8 +1840,7 @@ def show_all_logs(): break total_crowdsec = sum(crowdsec_stats.values()) - # Display PHP blocks with req/min and top IP per shop - print(f"\n📝 Blocks gesamt: {total_php_blocks} (⌀ {total_req_min:.1f} req/min, Laufzeit: {format_duration(total_minutes)})") + 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()): stats = shop_php_stats[shop] @@ -1901,13 +1850,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 and mode icon geo_region = get_shop_geo_region(shop) region_info = get_geo_region_info(geo_region) geo_icon = region_info['icon'] mode_icon = get_mode_icon(get_shop_mode(shop)) - # Color shop name based on Link11 status link11_info = check_link11(shop) if link11_info['is_link11']: shop_colored = f"{COLOR_GREEN}{shop}{COLOR_RESET}" @@ -1916,14 +1863,12 @@ def show_all_logs(): print(f" ├─ {shop_colored} {geo_icon} {mode_icon}: {count} ({req_min:.1f} req/min, seit {runtime_str}) {bar}") - # Show top IP for this shop shop_ips = stats['ips'] if shop_ips and count > 0: top_ip = max(shop_ips.items(), key=lambda x: x[1]['count']) top_ip_addr = top_ip[0] top_ip_count = top_ip[1]['count'] top_ip_ua = top_ip[1]['ua'] - # Use stored bot name first, fallback to UA detection top_ip_bot = top_ip[1].get('bot') or detect_bot(top_ip_ua) top_ip_req_min = top_ip_count / runtime if runtime > 0 else 0 @@ -1934,7 +1879,6 @@ def show_all_logs(): print(f" │ └─➤ Top: {top_ip_addr} ({display_name}) - {top_ip_count}x, {top_ip_req_min:.1f} req/min") - # Display Rate-Limit Bans if total_bans > 0 or total_active_bans > 0: print(f"\n🚫 Rate-Limit Bans: {total_bans} ausgelöst, {total_active_bans} aktiv") for shop in sorted(shop_php_stats.keys()): @@ -1946,60 +1890,44 @@ def show_all_logs(): else: shop_colored = f"{COLOR_RED}{shop}{COLOR_RESET}" - geo_region = get_shop_geo_region(shop) - region_info = get_geo_region_info(geo_region) - geo_icon = region_info['icon'] - mode_icon = get_mode_icon(get_shop_mode(shop)) - bar = "█" * min(stats['bans'] // 2, 20) if stats['bans'] > 0 else "" - print(f" ├─ {shop_colored} {geo_icon} {mode_icon}: {stats['bans']} bans ({stats['active_bans']} aktiv) {bar}") + print(f" ├─ {shop_colored}: {stats['bans']} bans ({stats['active_bans']} aktiv) {bar}") - # Display CrowdSec bans print(f"\n🛡️ CrowdSec-Bans gesamt: {total_crowdsec}") if crowdsec_stats: for shop in sorted(crowdsec_stats.keys()): count = crowdsec_stats[shop] bar = "█" * min(count // 10, 20) if count > 0 else "" - geo_region = get_shop_geo_region(shop) - region_info = get_geo_region_info(geo_region) - geo_icon = region_info['icon'] - mode_icon = get_mode_icon(get_shop_mode(shop)) - link11_info = check_link11(shop) if link11_info['is_link11']: shop_colored = f"{COLOR_GREEN}{shop}{COLOR_RESET}" else: shop_colored = f"{COLOR_RED}{shop}{COLOR_RESET}" - print(f" ├─ {shop_colored} {geo_icon} {mode_icon}: {count} {bar}") + print(f" ├─ {shop_colored}: {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):") + print(f"\n🔥 Top 50 IPs (alle Shops):") sorted_ips = sorted(all_ips.items(), key=lambda x: x[1]['count'], reverse=True)[:50] for ip, data in sorted_ips: count = data['count'] ua = data['ua'] - # Use stored bot name first, fallback to UA detection bot_name = data.get('bot') or detect_bot(ua) shops_data = data['shops'] - # Calculate req/min for this IP ip_req_min = count / total_minutes if total_minutes > 0 else 0 - # Find top shop for this IP if shops_data: top_shop = max(shops_data.items(), key=lambda x: x[1]) top_shop_name = top_shop[0] @@ -2034,10 +1962,10 @@ def show_all_logs(): def main(): print("\n" + "=" * 60) - print(" GeoIP Shop Blocker Manager v3.4.2") - print(" 🇩🇪🇦🇹🇨🇭 DACH | 🇪🇺 Eurozone+GB | 🤖 Bot-Only") + print(" GeoIP Shop Blocker Manager v3.4.3") + print(" 🇩🇪🇦🇹🇨🇭 DACH | 🇪🇺 Eurozone+GB | 🤖 Bot-Rate-Limiting") print(" 🛡️ Mit Cache-Validierung und Fail-Open") - print(" 🚦 Mit File-basiertem Rate-Limiting") + print(" 🚦 Bots unter Rate-Limit werden durchgelassen") print("=" * 60) print(f" {'✅' if check_crowdsec() else '⚠️ '} CrowdSec") @@ -2146,7 +2074,7 @@ def main(): rate_limit, ban_duration = get_shop_rate_limit_config(shop) rl_str = f", {rate_limit} req/min" if rate_limit else "" ban_str = f", {active_bans} aktive Bans" if active_bans > 0 else "" - print(f" {blocks} blocks, {format_duration(runtime)}, {len(BOT_PATTERNS)} Bot-Patterns{rl_str}{ban_str}") + print(f" {blocks} log entries, {format_duration(runtime)}, {len(BOT_PATTERNS)} Bot-Patterns{rl_str}{ban_str}") else: valid, count, _ = validate_existing_cache(httpdocs, get_shop_geo_region(shop)) cache_str = f"✅{count:,}" if valid else "⚠️"