#!/usr/bin/env python3 """ 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 two modes: - geoip: GeoIP blocking (only allowed regions can access) - bot: Rate-limit bots by bot-type, shop remains globally accessible v4.0.0: Bot-Rate-Limiting nach Bot-Typ (nicht IP), CrowdSec entfernt """ import os import sys import shutil import subprocess import json import time import re import socket from datetime import datetime, timedelta from pathlib import Path # ANSI Color Codes COLOR_GREEN = "\033[92m" COLOR_RED = "\033[91m" COLOR_YELLOW = "\033[93m" COLOR_BLUE = "\033[94m" COLOR_RESET = "\033[0m" COLOR_BOLD = "\033[1m" # Link11 IP LINK11_IP = "128.65.223.106" # Cache for DNS lookups (to avoid repeated lookups) DNS_CACHE = {} # Configuration VHOSTS_DIR = "/var/www/vhosts" BACKUP_SUFFIX = ".geoip_backup" BLOCKING_FILE = "geoip_blocking.php" CACHE_FILE = "geoip_ip_ranges.cache" LOG_FILE = "geoip_blocked.log" RATELIMIT_DIR = "geoip_ratelimit" ACTIVE_SHOPS_FILE = "/var/lib/geoip/active_shops.json" # Rate-Limit Defaults DEFAULT_RATE_LIMIT = 30 # Requests pro Minute DEFAULT_BAN_DURATION = 5 # Minuten # Minimum expected IP ranges per region (for validation) MIN_RANGES = { "dach": 1000, "eurozone": 5000 } # ============================================================================= # GEO REGIONS # ============================================================================= GEO_REGIONS = { "dach": { "name": "DACH", "countries": ["de", "at", "ch"], "description": "Deutschland, Österreich, Schweiz", "icon": "🇩🇪🇦🇹🇨🇭", "short": "DACH" }, "eurozone": { "name": "Eurozone + GB", "countries": [ "de", "at", "ch", "be", "cy", "ee", "es", "fi", "fr", "gb", "gr", "hr", "ie", "it", "lt", "lu", "lv", "mt", "nl", "pt", "si", "sk" ], "description": "22 Länder: DE, AT, CH, BE, CY, EE, ES, FI, FR, GB, GR, HR, IE, IT, LT, LU, LV, MT, NL, PT, SI, SK", "icon": "🇪🇺", "short": "EU+" }, "none": { "name": "Bot-Only", "countries": [], "description": "Nur Bot-Rate-Limiting, weltweit erreichbar", "icon": "🤖", "short": "BOT" } } # ============================================================================= # BOT DETECTION # ============================================================================= BOT_PATTERNS = { # ========================================================================= # AI/LLM SERVICES # ========================================================================= 'ChatGPT-User': r'chatgpt-user', 'ChatGPT-Operator': r'chatgpt-operator', 'ChatGPT-Agent': r'chatgpt agent', 'ChatGPT': r'chatgpt', 'GPTBot (OpenAI)': r'gptbot', 'OAI-SearchBot (OpenAI)': r'oai-searchbot', 'OpenAI': r'openai', 'ClaudeBot (Anthropic)': r'claudebot', 'Claude-User': r'claude-user', 'Claude-Web': r'claude-web', 'Claude-SearchBot': r'claude-searchbot', 'Anthropic-AI': r'anthropic-ai', 'Anthropic': r'anthropic', 'Gemini-Deep-Research': r'gemini-deep-research', 'Google-NotebookLM': r'google-notebooklm', 'NotebookLM': r'notebooklm', 'GoogleAgent-Mariner': r'googleagent-mariner', 'PerplexityBot': r'perplexitybot', 'Perplexity-User': r'perplexity-user', 'Perplexity': r'perplexity', 'Cohere-AI': r'cohere-ai', 'Cohere-Training-Crawler': r'cohere-training-data-crawler', 'Cohere': r'cohere', 'MistralAI-User': r'mistralai-user', 'MistralAI': r'mistralai', 'Mistral': r'mistral', 'DeepSeekBot': r'deepseekbot', 'DeepSeek': r'deepseek', 'Bytespider (TikTok/ByteDance)': r'bytespider', 'TikTokSpider': r'tiktokspider', 'ByteDance': r'bytedance', 'AI2Bot-DeepResearchEval': r'ai2bot-deepresearcheval', 'AI2Bot-Dolma': r'ai2bot-dolma', 'AI2Bot (Allen Institute)': r'ai2bot', 'CCBot (Common Crawl)': r'ccbot', 'Diffbot': r'diffbot', 'img2dataset': r'img2dataset', 'LAIONDownloader': r'laiondownloader', 'LAION-HuggingFace': r'laion-huggingface', 'LAION': r'laion', 'HuggingFace': r'huggingface', 'BedrockBot (AWS)': r'bedrockbot', 'DuckAssistBot': r'duckassistbot', 'PhindBot': r'phindbot', 'YouBot': r'youbot', 'iAskSpider': r'iaskspider', 'iAskBot': r'iaskbot', 'ChatGLM-Spider': r'chatglm-spider', 'Panscient': r'panscient', 'Devin (Cognition)': r'devin', 'Manus-User': r'manus-user', 'TwinAgent': r'twinagent', 'NovaAct': r'novaact', 'FirecrawlAgent': r'firecrawlagent', 'Firecrawl': r'firecrawl', 'Crawl4AI': r'crawl4ai', 'Crawlspace': r'crawlspace', 'Cloudflare-AutoRAG': r'cloudflare-autorag', 'TerraCotta': r'terracotta', 'Thinkbot': r'thinkbot', # ========================================================================= # SUCHMASCHINEN # ========================================================================= 'Googlebot-Image': r'googlebot-image', 'Googlebot-Video': r'googlebot-video', 'Googlebot-News': r'googlebot-news', 'Googlebot-Discovery': r'googlebot-discovery', 'Googlebot': r'googlebot', 'Google-Extended': r'google-extended', 'Google-CloudVertexBot': r'google-cloudvertexbot', 'Google-Firebase': r'google-firebase', 'Google-InspectionTool': r'google-inspectiontool', 'GoogleOther-Image': r'googleother-image', 'GoogleOther-Video': r'googleother-video', 'GoogleOther': r'googleother', 'Storebot-Google': r'storebot-google', 'AdsBot-Google': r'adsbot-google', 'Bingbot (Microsoft)': r'bingbot', 'BingPreview': r'bingpreview', 'MSNBot': r'msnbot', 'Baiduspider': r'baiduspider', 'Baidu': r'baidu', 'YandexBot': r'yandexbot', 'YandexAdditionalBot': r'yandexadditionalbot', 'YandexAdditional': r'yandexadditional', 'Yandex': r'yandex', 'DuckDuckBot': r'duckduckbot', 'DuckDuckGo': r'duckduckgo', 'Applebot-Extended': r'applebot-extended', 'Applebot': r'applebot', 'Yahoo Slurp': r'slurp', 'Sogou': r'sogou', 'Sosospider': r'sosospider', 'NaverBot': r'naverbot', 'Naver': r'naver', 'SeznamBot': r'seznambot', 'MojeekBot': r'mojeekbot', 'QwantBot': r'qwantbot', 'PetalBot (Huawei)': r'petalbot', 'CocCocBot': r'coccocbot', 'Exabot': r'exabot', 'BraveBot': r'bravebot', 'Bravest': r'bravest', 'SeekportBot': r'seekportbot', # ========================================================================= # SEO & MARKETING TOOLS # ========================================================================= 'AhrefsBot': r'ahrefsbot', 'Ahrefs': r'ahrefs', 'SemrushBot-OCOB': r'semrushbot-ocob', 'SemrushBot-SWA': r'semrushbot-swa', 'SemrushBot': r'semrushbot', 'Semrush': r'semrush', 'MJ12Bot (Majestic)': r'mj12bot', 'Majestic': r'majestic', 'DotBot (Moz)': r'dotbot', 'RogerBot (Moz)': r'rogerbot', 'Screaming Frog': r'screaming frog', 'BLEXBot': r'blexbot', 'DataForSEOBot': r'dataforseobot', 'Linkdex': r'linkdex', 'SearchmetricsBot': r'searchmetricsbot', # ========================================================================= # SOCIAL MEDIA # ========================================================================= 'Facebook External Hit': r'facebookexternalhit', 'FacebookBot': r'facebookbot', 'Facebot': r'facebot', 'Meta-ExternalAgent': r'meta-externalagent', 'Meta-ExternalFetcher': r'meta-externalfetcher', 'Meta-WebIndexer': r'meta-webindexer', 'Facebook': r'facebook', 'Twitterbot': r'twitterbot', 'Twitter': r'twitter', 'Instagram': r'instagram', 'LinkedInBot': r'linkedinbot', 'LinkedIn': r'linkedin', 'Pinterestbot': r'pinterestbot', 'Pinterest': r'pinterest', 'WhatsApp': r'whatsapp', 'TelegramBot': r'telegrambot', 'Telegram': r'telegram', 'DiscordBot': r'discordbot', 'Discord': r'discord', 'Slackbot': r'slackbot', 'Slack': r'slack', 'Quora-Bot': r'quora-bot', 'Snapchat': r'snapchat', 'RedditBot': r'redditbot', # ========================================================================= # E-COMMERCE & PREISVERGLEICH # ========================================================================= 'Amazonbot': r'amazonbot', 'Amazon-Kendra': r'amazon-kendra', 'AmazonBuyForMe': r'amazonbuyforme', 'AMZNKAssocBot': r'amznkassocbot', 'GeedoShopProductFinder': r'geedoshopproductfinder', 'Geedo': r'geedo', 'ShopWiki': r'shopwiki', 'PriceGrabber': r'pricegrabber', 'Shopify': r'shopify', 'Idealo': r'idealo', 'Guenstiger.de': r'guenstiger', 'Billiger.de': r'billiger', 'Ladenzeile': r'ladenzeile', 'Kelkoo': r'kelkoo', 'PriceRunner': r'pricerunner', # ========================================================================= # ARCHIV & RESEARCH # ========================================================================= 'Archive.org Bot': r'archive\.org_bot|archive-org-bot', 'Internet Archive': r'ia_archiver|ia-archiver', 'Wayback Machine': r'wayback', 'Heritrix': r'heritrix', 'Apache Nutch': r'nutch', 'Common Crawl': r'commoncrawl', # ========================================================================= # MONITORING & UPTIME # ========================================================================= 'UptimeRobot': r'uptimerobot', 'Pingdom': r'pingdom', 'StatusCake': r'statuscake', 'Site24x7': r'site24x7', 'NewRelic': r'newrelic', 'Datadog': r'datadog', 'GTmetrix': r'gtmetrix', 'PageSpeed Insights': r'pagespeed', 'Chrome Lighthouse': r'chrome-lighthouse', # ========================================================================= # DOWNLOAD & SCRAPER TOOLS # ========================================================================= 'HTTrack': r'httrack', 'Teleport Pro': r'teleportpro|teleport pro', 'Teleport': r'teleport', 'GetRight': r'getright', 'FlashGet': r'flashget', 'LeechFTP': r'leechftp', 'LeechGet': r'leechget', 'Leech': r'leech', 'Offline Explorer': r'offline explorer', 'Offline Navigator': r'offline navigator', 'Offline Tool': r'offline', 'WebCopier': r'webcopier', 'WebCopy': r'webcopy', 'WebRipper': r'webripper', 'WebReaper': r'webreaper', 'WebStripper': r'webstripper', 'WebSauger': r'websauger', 'WebZIP': r'webzip', 'WebWhacker': r'webwhacker', 'WebBandit': r'webbandit', 'SiteSucker': r'sitesucker', 'SiteSnagger': r'sitesnagger', 'BlackWidow': r'blackwidow', 'Mass Downloader': r'mass downloader', 'Download Demon': r'download demon', 'Download Ninja': r'download ninja', 'Download Master': r'download master', 'FreshDownload': r'freshdownload', 'SmartDownload': r'smartdownload', 'RealDownload': r'realdownload', 'StarDownloader': r'stardownloader', 'Net Vampire': r'net vampire', 'NetAnts': r'netants', 'NetZIP': r'netzip', 'Go!Zilla': r'go!zilla|gozilla', 'Grabber': r'grabber', 'PageGrabber': r'pagegrabber', 'EirGrabber': r'eirgrabber', 'EmailSiphon': r'emailsiphon', 'EmailCollector': r'emailcollector', 'EmailWolf': r'emailwolf', 'Email Extractor': r'email extractor', 'ExtractorPro': r'extractorpro', 'HarvestMan': r'harvestman', 'Harvest': r'harvest', 'Collector': r'collector', 'Vacuum': r'vacuum', 'WebVac': r'webvac', 'Zeus': r'zeus', 'ScrapeBox': r'scrapebox', 'Xenu Link Sleuth': r'xenu', 'Larbin': r'larbin', 'Grub': r'grub', # ========================================================================= # HTTP LIBRARIES & FRAMEWORKS # ========================================================================= 'Python-Requests': r'python-requests', 'Python-urllib': r'python-urllib', 'Python-HTTPX': r'python-httpx', 'Python HTTP': r'python/', 'aiohttp': r'aiohttp', 'HTTPX': r'httpx/', 'cURL': r'curl/|^curl', 'Wget': r'wget/|^wget', 'Go-HTTP-Client': r'go-http-client', 'Go HTTP': r'go http|go-http', 'Java HTTP Client': r'java/|java ', 'Apache-HttpClient': r'apache-httpclient', 'Jakarta Commons': r'jakarta', 'Axios': r'axios/|axios', 'Node-Fetch': r'node-fetch', 'Got (Node.js)': r'got/', 'libwww-perl': r'libwww-perl', 'LWP (Perl)': r'lwp::|lwp/', 'WWW-Mechanize': r'www-mechanize', 'Mechanize': r'mechanize', 'Scrapy': r'scrapy/|scrapy', 'HTTP.rb': r'http\.rb', 'Typhoeus': r'typhoeus', 'OkHttp': r'okhttp/|okhttp', 'CFNetwork': r'cfnetwork', 'WinHTTP': r'winhttp', 'Indy Library': r'indy library', 'Chilkat': r'chilkat', 'httplib': r'httplib', 'ApacheBench': r'apachebench', 'Guzzle (PHP)': r'guzzle', 'Requests': r'requests/', # ========================================================================= # SECURITY SCANNER # ========================================================================= 'Nessus': r'nessus', 'SQLMap': r'sqlmap', 'Netsparker': r'netsparker', 'Nikto': r'nikto', 'Acunetix': r'acunetix', 'Burp Suite': r'burpsuite|burp', 'OWASP ZAP': r'owasp zap', 'OpenVAS': r'openvas', 'Nmap': r'nmap', 'Masscan': r'masscan', 'WPScan': r'wpscan', # ========================================================================= # HEADLESS BROWSERS & AUTOMATION # ========================================================================= 'PhantomJS': r'phantomjs', 'Headless Chrome': r'headlesschrome', 'Headless Browser': r'headless', 'Selenium': r'selenium', 'Puppeteer': r'puppeteer', 'Playwright': r'playwright', 'Cypress': r'cypress', # ========================================================================= # FEED READER & RSS # ========================================================================= 'FeedFetcher': r'feedfetcher', 'FeedParser': r'feedparser', 'Feedly': r'feedly', 'Inoreader': r'inoreader', 'NewsBlur': r'newsblur', # ========================================================================= # WEITERE BEKANNTE BOTS # ========================================================================= 'OmgiliBot': r'omgilibot', 'Omgili': r'omgili', 'Webzio-Extended': r'webzio-extended', 'Webzio': r'webzio', 'Timpibot': r'timpibot', 'PanguBot': r'pangubot', 'ImagesiftBot': r'imagesiftbot', 'Kangaroo Bot': r'kangaroo bot', 'QualifiedBot': r'qualifiedbot', 'VelenPublicWebCrawler': r'velenpublicwebcrawler', 'Linguee Bot': r'linguee bot', 'Linguee': r'linguee', 'QuillBot': r'quillbot', 'TurnitinBot': r'turnitinbot', 'Turnitin': r'turnitin', 'ZanistaBot': r'zanistabot', 'WRTNBot': r'wrtnbot', 'WARDBot': r'wardbot', 'ShapBot': r'shapbot', 'LinerBot': r'linerbot', 'LinkupBot': r'linkupbot', 'KlaviyoAIBot': r'klaviyoaibot', 'KunatoCrawler': r'kunatocrawler', 'IbouBot': r'iboubot', 'BuddyBot': r'buddybot', 'BrightBot': r'brightbot', 'Channel3Bot': r'channel3bot', 'Andibot': r'andibot', 'Anomura': r'anomura', 'Awario': r'awario', 'BigSur.ai': r'bigsur', 'Cotoyogi': r'cotoyogi', 'AddSearchBot': r'addsearchbot', 'aiHitBot': r'aihitbot', 'Atlassian-Bot': r'atlassian-bot', 'RainBot': r'rainbot', 'TinyTestBot': r'tinytestbot', 'Brandwatch': r'brandwatch', 'Meltwater': r'meltwater', 'Netvibes': r'netvibes', 'BitlyBot': r'bitlybot', 'Mail.ru Bot': r'mail\.ru', 'YaK': r'yak', } # Generische Patterns (Fallback für unbekannte Bots) GENERIC_BOT_PATTERNS = [ 'bot', 'crawler', 'spider', 'scraper', 'fetch', 'scan', 'check', 'monitor', 'probe', 'index', 'archive', 'capture', 'reader', 'download', 'mirror', 'ripper', 'collector', 'extractor', 'siphon', 'copier', 'sucker', 'bandit', 'stripper', 'whacker', 'reaper', 'robot', 'agent', 'seeker', 'finder', 'walker', 'roam', 'snagger', ] def detect_bot(user_agent): """Erkennt Bots anhand des User-Agents. Gibt den Anzeigenamen zurück.""" if not user_agent or user_agent == 'Unknown': return 'Unbekannt' # Erst spezifische Patterns prüfen for bot_name, pattern in BOT_PATTERNS.items(): if re.search(pattern, user_agent, re.IGNORECASE): return bot_name # Dann generische Patterns als Fallback ua_lower = user_agent.lower() for pattern in GENERIC_BOT_PATTERNS: if pattern in ua_lower: return f'Bot ({pattern})' return 'Unbekannt' def check_link11(domain): global DNS_CACHE if domain in DNS_CACHE: return DNS_CACHE[domain] try: ip = socket.gethostbyname(domain) is_link11 = (ip == LINK11_IP) DNS_CACHE[domain] = {'is_link11': is_link11, 'ip': ip} return DNS_CACHE[domain] except socket.gaierror: DNS_CACHE[domain] = {'is_link11': False, 'ip': 'N/A'} return DNS_CACHE[domain] def format_shop_with_link11(shop, prefix="", show_index=None): link11_info = check_link11(shop) if link11_info['is_link11']: color, suffix = COLOR_GREEN, " [Link11]" else: color, suffix = COLOR_RED, " [Direkt]" if show_index is not None: return f" [{show_index}] {color}{shop}{suffix}{COLOR_RESET}" return f"{prefix}{color}{shop}{suffix}{COLOR_RESET}" def get_geo_region_info(geo_region): return GEO_REGIONS.get(geo_region, GEO_REGIONS["dach"]) def generate_php_countries_array(geo_region): region_info = get_geo_region_info(geo_region) return ", ".join([f"'{c}'" for c in region_info["countries"]]) def generate_php_bot_patterns(): patterns = [] for bot_name, pattern in BOT_PATTERNS.items(): escaped_pattern = pattern.replace("'", "\\'").replace("/", "\\/") safe_bot_name = bot_name.replace("'", "\\'") patterns.append(f"'{safe_bot_name}' => '/{escaped_pattern}/i'") return ",\n ".join(patterns) def generate_php_generic_patterns(): patterns = [] for pattern in GENERIC_BOT_PATTERNS: patterns.append(f"'{pattern}'") return ", ".join(patterns) # ============================================================================= # CACHE VALIDATION # ============================================================================= def generate_and_validate_cache(httpdocs_path, geo_region): cache_file = os.path.join(httpdocs_path, CACHE_FILE) region_info = get_geo_region_info(geo_region) countries = region_info["countries"] min_expected = MIN_RANGES.get(geo_region, 1000) php_script = f''' ['timeout' => 30]]); $content = @file_get_contents($url, false, $ctx); if ($content !== false) {{ $lines = explode("\\n", trim($content)); foreach ($lines as $line) {{ $line = trim($line); if (!empty($line) && strpos($line, '/') !== false) {{ $ranges[] = $line; }} }} }} }} if (count($ranges) >= {min_expected}) {{ file_put_contents("{cache_file}", serialize($ranges)); echo "OK:" . count($ranges); }} else {{ echo "FAIL:" . count($ranges); }} ''' temp_php = os.path.join(httpdocs_path, '_geoip_cache_gen.php') try: with open(temp_php, 'w') as f: f.write(php_script) result = subprocess.run(['php', temp_php], capture_output=True, text=True, timeout=120) output = result.stdout.strip() if output.startswith('OK:'): return True, int(output.split(':')[1]), None elif output.startswith('FAIL:'): return False, int(output.split(':')[1]), f"Nur {output.split(':')[1]} Ranges (min. {min_expected} erwartet)" return False, 0, f"Unerwartete Ausgabe: {output}" except subprocess.TimeoutExpired: return False, 0, "Timeout beim Laden der IP-Ranges" except Exception as e: return False, 0, str(e) finally: if os.path.exists(temp_php): os.remove(temp_php) def validate_existing_cache(httpdocs_path, geo_region): 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 - GEOIP # ============================================================================= GEOIP_SCRIPT_TEMPLATE = ''' $expiry_date) return; $visitor_ip = $_SERVER['REMOTE_ADDR'] ?? ''; if (empty($visitor_ip)) return; if (filter_var($visitor_ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) return; $cache_file = __DIR__ . '/{cache_file}'; $cache_duration = 86400; $log_file = __DIR__ . '/{log_file}'; $min_ranges = {min_ranges}; $allowed_countries = [{countries_array}]; function download_allowed_ranges($countries) {{ $ranges = []; foreach ($countries as $country) {{ $url = "https://www.ipdeny.com/ipblocks/data/aggregated/$country-aggregated.zone"; $ctx = stream_context_create(['http' => ['timeout' => 30]]); $content = @file_get_contents($url, false, $ctx); if ($content !== false) {{ foreach (explode("\\n", trim($content)) as $line) {{ $line = trim($line); if (!empty($line) && strpos($line, '/') !== false) $ranges[] = $line; }} }} }} return $ranges; }} function ip_in_range($ip, $cidr) {{ list($subnet, $mask) = explode('/', $cidr); $mask_long = -1 << (32 - (int)$mask); return (ip2long($ip) & $mask_long) == (ip2long($subnet) & $mask_long); }} $allowed_ranges = []; $cache_valid = false; if (file_exists($cache_file) && (time() - filemtime($cache_file)) < $cache_duration) {{ $cached_data = @file_get_contents($cache_file); if ($cached_data !== false) {{ $allowed_ranges = @unserialize($cached_data); if (is_array($allowed_ranges) && count($allowed_ranges) >= $min_ranges) {{ $cache_valid = true; }} else {{ @unlink($cache_file); $allowed_ranges = []; }} }} }} if (!$cache_valid) {{ $allowed_ranges = download_allowed_ranges($allowed_countries); if (is_array($allowed_ranges) && count($allowed_ranges) >= $min_ranges) {{ @file_put_contents($cache_file, serialize($allowed_ranges)); $cache_valid = true; }} else {{ error_log("GeoIP FAIL-OPEN: Could not load valid IP ranges (got " . count($allowed_ranges) . ", need $min_ranges)"); return; }} }} $is_allowed = false; foreach ($allowed_ranges as $range) {{ if (ip_in_range($visitor_ip, $range)) {{ $is_allowed = true; break; }} }} if (!$is_allowed) {{ $timestamp = date('Y-m-d H:i:s'); $ua = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown'; $uri = $_SERVER['REQUEST_URI'] ?? '/'; @file_put_contents($log_file, "[$timestamp] IP: $visitor_ip | UA: $ua | URI: $uri\\n", FILE_APPEND | LOCK_EX); header('HTTP/1.1 403 Forbidden'); exit; }} ''' # ============================================================================= # PHP TEMPLATES - BOT RATE-LIMITING (By Bot-Type, not IP) # ============================================================================= BOT_SCRIPT_TEMPLATE = ''' $expiry_date) return; $log_file = __DIR__ . '/{log_file}'; $ratelimit_dir = __DIR__ . '/{ratelimit_dir}'; $bans_dir = $ratelimit_dir . '/bans'; $counts_dir = $ratelimit_dir . '/counts'; // Rate-Limit Configuration $rate_limit = {rate_limit}; // Requests per minute for this bot-type $ban_duration = {ban_duration}; // Ban duration in seconds $window_size = 60; // Window size in seconds (1 minute) $cleanup_probability = 100; // 1 in X chance to run cleanup $visitor_ip = $_SERVER['REMOTE_ADDR'] ?? ''; $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; // Ensure directories exist if (!is_dir($bans_dir)) @mkdir($bans_dir, 0777, true); if (!is_dir($counts_dir)) @mkdir($counts_dir, 0777, true); // === Bot Detection === $bot_patterns = [ {bot_patterns} ]; $generic_patterns = [{generic_patterns}]; if (empty($user_agent)) return; $detected_bot = null; // Check specific patterns first foreach ($bot_patterns as $bot_name => $pattern) {{ if (preg_match($pattern, $user_agent)) {{ $detected_bot = $bot_name; break; }} }} // Check generic patterns as fallback if ($detected_bot === null) {{ $ua_lower = strtolower($user_agent); foreach ($generic_patterns as $pattern) {{ if (strpos($ua_lower, $pattern) !== false) {{ $detected_bot = "Bot ($pattern)"; break; }} }} }} // Not a bot - allow through without any rate limiting if ($detected_bot === null) return; // === Create hash based on BOT-TYPE only (not IP!) === $bot_hash = md5($detected_bot); // === STEP 1: Check if this bot-type is banned === $ban_file = "$bans_dir/$bot_hash.ban"; if (file_exists($ban_file)) {{ $ban_content = @file_get_contents($ban_file); $ban_parts = explode('|', $ban_content, 2); $ban_until = (int)$ban_parts[0]; if (time() < $ban_until) {{ // Bot-type is banned - log and block $timestamp = date('Y-m-d H:i:s'); $remaining = $ban_until - time(); @file_put_contents($log_file, "[$timestamp] BLOCKED (banned): $detected_bot | IP: $visitor_ip | Remaining: {{$remaining}}s\\n", FILE_APPEND | LOCK_EX); header('HTTP/1.1 403 Forbidden'); header('Retry-After: ' . $remaining); exit; }} // Ban expired - remove file @unlink($ban_file); }} // === STEP 2: Rate-Limit Check for this bot-type === $count_file = "$counts_dir/$bot_hash.count"; $current_time = time(); $count = 1; $window_start = $current_time; if (file_exists($count_file)) {{ $fp = @fopen($count_file, 'c+'); if ($fp && flock($fp, LOCK_EX)) {{ $content = fread($fp, 100); if (!empty($content)) {{ $parts = explode('|', $content); if (count($parts) === 2) {{ $window_start = (int)$parts[0]; $count = (int)$parts[1]; if ($current_time - $window_start > $window_size) {{ // New window $window_start = $current_time; $count = 1; }} else {{ $count++; }} }} }} ftruncate($fp, 0); rewind($fp); fwrite($fp, "$window_start|$count"); flock($fp, LOCK_UN); fclose($fp); }} }} else {{ @file_put_contents($count_file, "$window_start|$count", LOCK_EX); }} // === STEP 3: Check if limit exceeded === if ($count > $rate_limit) {{ // Create ban for this bot-type (store timestamp|botname) $ban_until = $current_time + $ban_duration; @file_put_contents($ban_file, "$ban_until|$detected_bot", 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 | Total requests: $count\\n", FILE_APPEND | LOCK_EX); // Block this request header('HTTP/1.1 403 Forbidden'); header('Retry-After: ' . $ban_duration); exit; }} // === STEP 4: 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 5: Probabilistic cleanup === if (rand(1, $cleanup_probability) === 1) {{ $now = time(); foreach (glob("$bans_dir/*.ban") as $f) {{ $ban_content = @file_get_contents($f); $ban_parts = explode('|', $ban_content, 2); $ban_time = (int)$ban_parts[0]; if ($now > $ban_time) @unlink($f); }} foreach (glob("$counts_dir/*.count") as $f) {{ if ($now - filemtime($f) > $window_size * 2) @unlink($f); }} }} // Bot is under rate limit - ALLOW through (no exit, no 403) return; ''' # ============================================================================= # HELPER FUNCTIONS # ============================================================================= def run_command(cmd, capture_output=True): try: if capture_output: result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30) return result.returncode, result.stdout, result.stderr return subprocess.run(cmd, shell=True, timeout=30).returncode, "", "" except Exception as e: return -1, "", str(e) def add_shop_to_active(shop, mode="geoip", geo_region="dach", rate_limit=None, ban_duration=None): os.makedirs(os.path.dirname(ACTIVE_SHOPS_FILE), exist_ok=True) shops = {} if os.path.isfile(ACTIVE_SHOPS_FILE): with open(ACTIVE_SHOPS_FILE, 'r') as f: shops = json.load(f) shop_data = { "activated": datetime.now().isoformat(), "expiry": (datetime.now() + timedelta(hours=72)).isoformat(), "mode": mode, "geo_region": geo_region } if rate_limit is not None: shop_data["rate_limit"] = rate_limit if ban_duration is not None: shop_data["ban_duration"] = ban_duration shops[shop] = shop_data with open(ACTIVE_SHOPS_FILE, 'w') as f: json.dump(shops, f, indent=2) def get_shop_mode(shop): if not os.path.isfile(ACTIVE_SHOPS_FILE): return "geoip" try: with open(ACTIVE_SHOPS_FILE, 'r') as f: return json.load(f).get(shop, {}).get("mode", "geoip") except: return "geoip" def get_shop_geo_region(shop): if not os.path.isfile(ACTIVE_SHOPS_FILE): return "dach" try: with open(ACTIVE_SHOPS_FILE, 'r') as f: return json.load(f).get(shop, {}).get("geo_region", "dach") except: return "dach" def get_shop_rate_limit_config(shop): if not os.path.isfile(ACTIVE_SHOPS_FILE): return None, None try: with open(ACTIVE_SHOPS_FILE, 'r') as f: shop_data = json.load(f).get(shop, {}) return shop_data.get("rate_limit"), shop_data.get("ban_duration") except: return None, None def get_shop_activation_time(shop): if not os.path.isfile(ACTIVE_SHOPS_FILE): return None try: with open(ACTIVE_SHOPS_FILE, 'r') as f: activated_str = json.load(f).get(shop, {}).get("activated") return datetime.fromisoformat(activated_str) if activated_str else None except: return None def format_duration(minutes): if minutes < 60: return f"{int(minutes)}m" hours = minutes / 60 if hours < 24: return f"{int(hours)}h {int(minutes % 60)}m" return f"{int(hours / 24)}d {int(hours % 24)}h" def remove_shop_from_active(shop): if not os.path.isfile(ACTIVE_SHOPS_FILE): return with open(ACTIVE_SHOPS_FILE, 'r') as f: shops = json.load(f) if shop in shops: del shops[shop] with open(ACTIVE_SHOPS_FILE, 'w') as f: json.dump(shops, f, indent=2) def get_available_shops(): shops = [] if not os.path.exists(VHOSTS_DIR): return shops for entry in os.listdir(VHOSTS_DIR): shop_path = os.path.join(VHOSTS_DIR, entry) if os.path.isdir(shop_path) and entry not in ['chroot', 'system', 'default']: httpdocs = os.path.join(shop_path, 'httpdocs') if os.path.isdir(httpdocs) and os.path.isfile(os.path.join(httpdocs, 'index.php')): shops.append(entry) return sorted(shops) def get_active_shops(): active = [] for shop in get_available_shops(): httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs') if os.path.isfile(os.path.join(httpdocs, BLOCKING_FILE)) or os.path.isfile(os.path.join(httpdocs, f'index.php{BACKUP_SUFFIX}')): active.append(shop) return active def select_geo_region(): print(f"\n🌍 Wähle die Geo-Region:") print(f" [1] {GEO_REGIONS['dach']['icon']} DACH - {GEO_REGIONS['dach']['description']}") print(f" [2] {GEO_REGIONS['eurozone']['icon']} Eurozone+GB - {len(GEO_REGIONS['eurozone']['countries'])} Länder") return "eurozone" if input(f"\nRegion wählen [1/2]: ").strip() == "2" else "dach" def select_mode(): print(f"\n🔧 Wähle den Blocking-Modus:") print(f" [1] 🌍 GeoIP-Blocking (nur erlaubte Regionen)") print(f" [2] 🤖 Bot-Rate-Limiting (weltweit erreichbar, Bots limitiert)") choice = input(f"\nModus wählen [1/2]: ").strip() if choice == "2": return "bot" return "geoip" def select_rate_limit(): print(f"\n🚦 Rate-Limit Konfiguration:") print(f" (0 = Bots sofort bannen, kein Rate-Limiting)") print(f" ⚠️ ACHTUNG: Limit gilt pro Bot-TYP, nicht pro IP!") rate_input = input(f" Requests pro Minute bevor Ban [{DEFAULT_RATE_LIMIT}]: ").strip() try: rate_limit = int(rate_input) if rate_input else DEFAULT_RATE_LIMIT if rate_limit < 0: print(f" ⚠️ Ungültiger Wert, verwende Default: {DEFAULT_RATE_LIMIT}") rate_limit = DEFAULT_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 if ban_minutes < 1: print(f" ⚠️ Ungültiger Wert, verwende Default: {DEFAULT_BAN_DURATION}") ban_minutes = DEFAULT_BAN_DURATION 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 pro Bot-Typ") print(f" ✅ Ban-Dauer: {ban_minutes} Minuten") return rate_limit, ban_seconds def get_mode_icon(mode): icons = { 'geoip': '🌍', 'bot': '🤖' } return icons.get(mode, '❓') def get_mode_description(mode): descriptions = { 'geoip': 'GeoIP-Blocking', 'bot': 'Bot-Rate-Limiting' } return descriptions.get(mode, mode) def is_bot_mode(mode): return mode == 'bot' def get_direct_shops(available_shops): direct_shops = [] for shop in available_shops: link11_info = check_link11(shop) if not link11_info['is_link11']: direct_shops.append(shop) return direct_shops def get_link11_shops(available_shops): link11_shops = [] for shop in available_shops: link11_info = check_link11(shop) if link11_info['is_link11']: link11_shops.append(shop) return link11_shops # ============================================================================= # MAIN FUNCTIONS # ============================================================================= def activate_blocking(shop, silent=False, mode="geoip", geo_region="dach", rate_limit=None, ban_duration=None): httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs') index_php = os.path.join(httpdocs, 'index.php') backup_php = os.path.join(httpdocs, f'index.php{BACKUP_SUFFIX}') blocking_file = os.path.join(httpdocs, BLOCKING_FILE) ratelimit_path = os.path.join(httpdocs, RATELIMIT_DIR) bot_mode = is_bot_mode(mode) if 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 bot_mode: print(f" Modus: Bot-Rate-Limiting nach Bot-Typ (weltweit erreichbar)") if rate_limit is not None and ban_duration is not None: print(f" Rate-Limit: {rate_limit} req/min pro Bot-Typ, Ban: {ban_duration // 60} min") else: print(f" Erlaubt: {region_info['description']}") print("=" * 60) # Step 1: PHP blocking if not silent: print("\n[1/3] Aktiviere PHP-Blocking...") shutil.copy2(index_php, backup_php) with open(index_php, 'r', encoding='utf-8') as f: content = f.read() lines = content.split('\n') insert_line = 0 for i, line in enumerate(lines): if 'declare(strict_types' in line: insert_line = i + 1 break elif ' 1 else "Unbekannt" if now < ban_until: active_bans += 1 banned_bots.append(bot_name) except: pass return log_entries, ips, bots, get_shop_activation_time(shop), bans, active_bans, banned_bots def show_logs(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) bot_mode = is_bot_mode(shop_mode) entries, ips, bots, activation_time, total_bans, active_bans, banned_bots = get_shop_log_stats(shop) if activation_time: runtime = (datetime.now() - activation_time).total_seconds() / 60 req_min = entries / runtime if runtime > 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: {entries} ({req_min:.1f} req/min)") if not bot_mode: valid, count, err = validate_existing_cache(httpdocs, shop_geo) print(f"✅ Cache: {count:,} Ranges" if valid else f"⚠️ Cache: {err}") else: rate_limit, ban_duration = get_shop_rate_limit_config(shop) print(f"🤖 Bot-Patterns: {len(BOT_PATTERNS)} + {len(GENERIC_BOT_PATTERNS)} generische") if rate_limit is not None and ban_duration is not None: print(f"🚦 Rate-Limit: {rate_limit} req/min PRO BOT-TYP, Ban: {ban_duration // 60} min") print(f"🚫 Bans: {total_bans} ausgelöst, {active_bans} Bot-Typen aktuell gebannt") if banned_bots: print(f" Gebannt: {', '.join(banned_bots)}") if bots: print(f"\n🤖 Bot-Statistik (nach Bot-Typ):") for bot_name, count in sorted(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 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]: bot = data.get('bot') or 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 ALLER SHOPS") print(f"{'═' * 70}") print(f" {COLOR_GREEN}Grün = hinter Link11{COLOR_RESET} | {COLOR_RED}Rot = Direkt{COLOR_RESET}") total_log_entries = 0 total_bans = 0 total_active_bans = 0 all_banned_bots = [] shop_stats = {} all_ips = {} all_bots = {} total_minutes = 0 for shop in active_shops: entries, ips, bots, activation_time, bans, active_bans, banned_bots = get_shop_log_stats(shop) total_log_entries += entries total_bans += bans total_active_bans += active_bans all_banned_bots.extend(banned_bots) if activation_time: runtime_minutes = (datetime.now() - activation_time).total_seconds() / 60 req_min = entries / runtime_minutes if runtime_minutes > 0 else 0 else: runtime_minutes = 0 req_min = 0 shop_stats[shop] = { 'entries': entries, 'runtime_minutes': runtime_minutes, 'req_min': req_min, 'ips': ips, 'bots': bots, 'bans': bans, 'active_bans': active_bans, 'banned_bots': banned_bots } if runtime_minutes > total_minutes: total_minutes = runtime_minutes for ip, data in ips.items(): if ip not in all_ips: all_ips[ip] = {'count': 0, 'ua': data['ua'], 'bot': data.get('bot'), 'shops': {}} all_ips[ip]['count'] += data['count'] all_ips[ip]['shops'][shop] = data['count'] if data['ua'] != 'Unknown' and all_ips[ip]['ua'] == 'Unknown': all_ips[ip]['ua'] = data['ua'] 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_log_entries / total_minutes if total_minutes > 0 else 0 # Zeige aktuell gebannte Bot-Typen ganz oben unique_banned_bots = list(set(all_banned_bots)) if unique_banned_bots: print(f"\n🚫 AKTUELL GEBANNTE BOT-TYPEN: {', '.join(sorted(unique_banned_bots))}") print(f"\n📝 Log-Einträge gesamt: {total_log_entries} (⌀ {total_req_min:.1f} req/min, Laufzeit: {format_duration(total_minutes)})") if shop_stats: for shop in sorted(shop_stats.keys()): stats = shop_stats[shop] count = stats['entries'] 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 "?" 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}") # Top 3 Bot-Typen für diesen Shop shop_bots = stats['bots'] if shop_bots and count > 0: top_bots = sorted(shop_bots.items(), key=lambda x: x[1], reverse=True)[:3] for i, (bot_name, bot_count) in enumerate(top_bots): bot_req_min = bot_count / runtime if runtime > 0 else 0 prefix = "└─➤" if i == len(top_bots) - 1 else "├─➤" print(f" │ {prefix} {bot_name}: {bot_count}x, {bot_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} Bot-Typen aktuell gebannt") for shop in sorted(shop_stats.keys()): stats = shop_stats[shop] if stats['bans'] > 0 or stats['active_bans'] > 0: link11_info = check_link11(shop) if link11_info['is_link11']: shop_colored = f"{COLOR_GREEN}{shop}{COLOR_RESET}" else: shop_colored = f"{COLOR_RED}{shop}{COLOR_RESET}" bar = "█" * min(stats['bans'] // 2, 20) if stats['bans'] > 0 else "" print(f" ├─ {shop_colored}: {stats['bans']} bans ({stats['active_bans']} Bot-Typen aktiv) {bar}") # Zeige gebannte Bot-Typen if stats['banned_bots']: for bot_name in stats['banned_bots']: print(f" │ └─🚫 {bot_name}") if all_bots: print(f"\n🤖 Bot-Statistik nach Bot-Typ (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] for ip, data in sorted_ips: count = data['count'] 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] top_shop_count = top_shop[1] if len(top_shop_name) > 25: top_shop_short = top_shop_name[:22] + '...' else: top_shop_short = top_shop_name link11_info = check_link11(top_shop_name) if link11_info['is_link11']: top_shop_display = f"{COLOR_GREEN}{top_shop_short}{COLOR_RESET}" else: top_shop_display = f"{COLOR_RED}{top_shop_short}{COLOR_RESET}" else: top_shop_display = "?" top_shop_count = 0 if bot_name == 'Unbekannt': display_name = (ua[:35] + '...') if len(ua) > 38 else ua if display_name == 'Unknown': display_name = 'Unbekannt' else: display_name = bot_name bar = "█" * min(count // 5, 20) if count > 0 else "█" print(f" {ip} ({display_name}): {count} ({ip_req_min:.1f} req/min) → {top_shop_display} [{top_shop_count}x] {bar}") print(f"\n{'═' * 70}") input("\nDrücke Enter um fortzufahren...") def main(): print("\n" + "=" * 60) print(" GeoIP Shop Blocker Manager v4.0.0") print(" 🇩🇪🇦🇹🇨🇭 DACH | 🇪🇺 Eurozone+GB | 🤖 Bot-Rate-Limiting") print(" 🛡️ Mit Cache-Validierung und Fail-Open") print(" 🚦 Rate-Limiting nach BOT-TYP (nicht IP)") print("=" * 60) while True: print("\n" + "-" * 40) print("[1] Aktivieren (einzeln)") print("[2] Deaktivieren (einzeln)") print("[3] Logs anzeigen") print("[4] Status") print("-" * 40) print("[5] 🚀 ALLE aktivieren") print("[6] 🛑 ALLE deaktivieren") print(f"[7] {COLOR_RED}🎯 Nur DIREKTE aktivieren (ohne Link11){COLOR_RESET}") print("-" * 40) print("[0] Beenden") choice = input("\nWähle: ").strip() if choice == "1": available = [s for s in get_available_shops() if s not in get_active_shops()] if not available: print("\n⚠️ Keine Shops verfügbar") continue print("\n📋 Verfügbare Shops:") for i, shop in enumerate(available, 1): print(format_shop_with_link11(shop, show_index=i)) try: idx = int(input("\nShop wählen: ").strip()) - 1 if 0 <= idx < len(available): mode = select_mode() bot_mode = is_bot_mode(mode) if bot_mode: geo = "none" region_info = get_geo_region_info("none") rate_limit, ban_duration = select_rate_limit() else: geo = select_geo_region() region_info = get_geo_region_info(geo) rate_limit, ban_duration = None, None confirm_msg = f"\n{region_info['icon']} {get_mode_description(mode)} aktivieren für '{available[idx]}'? (ja/nein): " if input(confirm_msg).lower() in ['ja', 'j']: activate_blocking(available[idx], mode=mode, geo_region=geo, rate_limit=rate_limit, ban_duration=ban_duration) except: print("❌ Ungültig") elif choice == "2": active = get_active_shops() if not active: print("\n⚠️ Keine aktiven Shops") continue print("\n📋 Aktive Shops:") for i, shop in enumerate(active, 1): region_info = get_geo_region_info(get_shop_geo_region(shop)) mode_icon = get_mode_icon(get_shop_mode(shop)) print(f" [{i}] {format_shop_with_link11(shop)} {region_info['icon']} {mode_icon}") try: idx = int(input("\nShop wählen: ").strip()) - 1 if 0 <= idx < len(active): if input(f"\nDeaktivieren für '{active[idx]}'? (ja/nein): ").lower() in ['ja', 'j']: deactivate_blocking(active[idx]) except: print("❌ Ungültig") elif choice == "3": active = get_active_shops() if not active: print("\n⚠️ Keine aktiven Shops") continue print("\n📋 Logs für:") print(" [0] 📊 ALLE") for i, shop in enumerate(active, 1): region_info = get_geo_region_info(get_shop_geo_region(shop)) mode_icon = get_mode_icon(get_shop_mode(shop)) print(f" [{i}] {format_shop_with_link11(shop)} {region_info['icon']} {mode_icon}") try: idx = int(input("\nWähle: ").strip()) if idx == 0: show_all_logs() elif 1 <= idx <= len(active): show_logs(active[idx - 1]) except: print("❌ Ungültig") elif choice == "4": shops = get_available_shops() active = get_active_shops() print(f"\n📊 {len(active)}/{len(shops)} Shops aktiv") for shop in active: region_info = get_geo_region_info(get_shop_geo_region(shop)) shop_mode = get_shop_mode(shop) mode_icon = get_mode_icon(shop_mode) bot_mode = is_bot_mode(shop_mode) entries, _, bots, activation_time, total_bans, active_bans, banned_bots = 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 bot_mode: rate_limit, ban_duration = get_shop_rate_limit_config(shop) rl_str = f", {rate_limit} req/min/Bot-Typ" if rate_limit else "" ban_str = "" if active_bans > 0: if banned_bots: ban_str = f", 🚫 {', '.join(banned_bots)}" else: ban_str = f", {active_bans} Bot-Typen gebannt" print(f" {entries} log entries, {format_duration(runtime)}, {len(BOT_PATTERNS)} Patterns{rl_str}{ban_str}") else: valid, count, _ = validate_existing_cache(httpdocs, get_shop_geo_region(shop)) cache_str = f"✅{count:,}" if valid else "⚠️" print(f" {entries} blocks, {format_duration(runtime)}, Cache: {cache_str}") elif choice == "5": activate_all_shops() elif choice == "6": deactivate_all_shops() elif choice == "7": activate_direct_shops_only() elif choice == "0": print("\n👋 Auf Wiedersehen!") break if __name__ == "__main__": if os.geteuid() != 0: print("❌ Als root ausführen!") sys.exit(1) try: main() except KeyboardInterrupt: print("\n\n👋 Abgebrochen") sys.exit(0)