#!/usr/bin/env python3 """ GeoIP Shop Blocker Manager - DACH & Eurozone Version 2-Component System: PHP blocking + Python watcher (systemd service) 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: Rate-limit bots, shop remains globally accessible v3.5.0: Erweiterte Bot-Erkennung mit 300+ Bots und generischen Fallback-Patterns """ 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" CROWDSEC_QUEUE_FILE = "geoip_crowdsec_queue.log" RATELIMIT_DIR = "geoip_ratelimit" WATCHER_SCRIPT = "/usr/local/bin/geoip_crowdsec_watcher.py" SYSTEMD_SERVICE = "/etc/systemd/system/geoip-crowdsec-watcher.service" ACTIVE_SHOPS_FILE = "/var/lib/crowdsec/geoip_active_shops.json" # 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("/", "\\/") patterns.append(f"'{bot_name}' => '/{escaped_pattern}/i'") return ",\n ".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}'; $crowdsec_queue = __DIR__ . '/{crowdsec_queue}'; $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); @file_put_contents($crowdsec_queue, "$timestamp|$visitor_ip|{shop_name}\\n", FILE_APPEND | LOCK_EX); header('HTTP/1.1 403 Forbidden'); exit; }} ''' GEOIP_SCRIPT_TEMPLATE_PHP_ONLY = ''' $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 (FIXED: Bots under limit ALLOWED through) # ============================================================================= BOT_ONLY_SCRIPT_TEMPLATE = ''' $expiry_date) return; $log_file = __DIR__ . '/{log_file}'; $crowdsec_queue = __DIR__ . '/{crowdsec_queue}'; $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 $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'] ?? ''; // Create hash for this visitor (IP + User-Agent) $visitor_hash = md5($visitor_ip . '|' . $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); // === STEP 1: Check if visitor is banned === $ban_file = "$bans_dir/$visitor_hash.ban"; if (file_exists($ban_file)) {{ $ban_until = (int)@file_get_contents($ban_file); if (time() < $ban_until) {{ // Still banned - immediate 403 header('HTTP/1.1 403 Forbidden'); header('Retry-After: ' . ($ban_until - time())); exit; }} // Ban expired - remove file @unlink($ban_file); }} // === STEP 2: Bot detection === $bot_patterns = [ {bot_patterns} ]; 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; }} }} // Not a bot - allow through without any rate limiting if ($detected_bot === null) return; // === STEP 3: Rate-Limit Check for detected bot === $count_file = "$counts_dir/$visitor_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 4: Check if limit exceeded === 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); 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(); foreach (glob("$bans_dir/*.ban") as $f) {{ $ban_time = (int)@file_get_contents($f); 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; ''' BOT_ONLY_SCRIPT_TEMPLATE_NO_CROWDSEC = ''' $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 $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'] ?? ''; // Create hash for this visitor (IP + User-Agent) $visitor_hash = md5($visitor_ip . '|' . $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); // === STEP 1: Check if visitor is banned === $ban_file = "$bans_dir/$visitor_hash.ban"; if (file_exists($ban_file)) {{ $ban_until = (int)@file_get_contents($ban_file); if (time() < $ban_until) {{ header('HTTP/1.1 403 Forbidden'); header('Retry-After: ' . ($ban_until - time())); exit; }} @unlink($ban_file); }} // === STEP 2: Bot detection === $bot_patterns = [ {bot_patterns} ]; 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; }} }} // Not a bot - allow through if ($detected_bot === null) return; // === STEP 3: Rate-Limit Check === $count_file = "$counts_dir/$visitor_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) {{ $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 4: Check if limit exceeded === 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; }} // === 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(); foreach (glob("$bans_dir/*.ban") as $f) {{ $ban_time = (int)@file_get_contents($f); 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; ''' # ============================================================================= # WATCHER SERVICE # ============================================================================= WATCHER_SCRIPT_CONTENT = '''#!/usr/bin/env python3 import os, sys, time, subprocess, json from datetime import datetime VHOSTS_DIR = "/var/www/vhosts" QUEUE_FILE = "geoip_crowdsec_queue.log" ACTIVE_SHOPS_FILE = "/var/lib/crowdsec/geoip_active_shops.json" PROCESSED_IPS = {} CHECK_INTERVAL = 5 def log(msg): print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}", flush=True) def get_active_shops(): if not os.path.isfile(ACTIVE_SHOPS_FILE): return {} try: with open(ACTIVE_SHOPS_FILE, 'r') as f: return json.load(f) except: return {} def add_to_crowdsec(ip, shop): now = time.time() if ip in PROCESSED_IPS and (now - PROCESSED_IPS[ip]) < 3600: return True try: result = subprocess.run(['cscli', 'decisions', 'add', '--ip', ip, '--duration', '72h', '--type', 'ban', '--reason', f'GeoIP: blocked by {shop}'], capture_output=True, text=True, timeout=10) if result.returncode == 0: PROCESSED_IPS[ip] = now log(f"Added {ip} to CrowdSec (from {shop})") return True except: pass return False def process_queue_file(shop_path, shop): queue_file = os.path.join(shop_path, 'httpdocs', QUEUE_FILE) if not os.path.isfile(queue_file): return 0 processed = 0 try: with open(queue_file, 'r') as f: lines = f.readlines() if not lines: return 0 for line in lines: parts = line.strip().split('|') if len(parts) >= 2 and add_to_crowdsec(parts[1], shop): processed += 1 if processed > 0: with open(queue_file, 'w') as f: f.write('') except: pass return processed def main(): log("GeoIP CrowdSec Watcher started") while True: try: active_shops = get_active_shops() for shop, info in active_shops.items(): 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) if __name__ == "__main__": main() ''' SYSTEMD_SERVICE_CONTENT = '''[Unit] Description=GeoIP CrowdSec Watcher Service After=network.target crowdsec.service [Service] Type=simple ExecStart=/usr/bin/python3 /usr/local/bin/geoip_crowdsec_watcher.py Restart=always RestartSec=10 [Install] WantedBy=multi-user.target ''' # ============================================================================= # 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 check_crowdsec(): code, stdout, _ = run_command("systemctl is-active crowdsec") return code == 0 and stdout.strip() == "active" def install_watcher_service(): print(" 📦 Installiere CrowdSec-Watcher-Service...") with open(WATCHER_SCRIPT, 'w') as f: f.write(WATCHER_SCRIPT_CONTENT) os.chmod(WATCHER_SCRIPT, 0o755) with open(SYSTEMD_SERVICE, 'w') as f: f.write(SYSTEMD_SERVICE_CONTENT) run_command("systemctl daemon-reload") run_command("systemctl enable geoip-crowdsec-watcher.service") run_command("systemctl start geoip-crowdsec-watcher.service") time.sleep(1) code, stdout, _ = run_command("systemctl is-active geoip-crowdsec-watcher.service") if code == 0 and stdout.strip() == "active": print(" ✅ Service gestartet") return True print(" ⚠️ Service konnte nicht gestartet werden") return False def uninstall_watcher_service(): print(" 📦 Deinstalliere CrowdSec-Watcher-Service...") run_command("systemctl stop geoip-crowdsec-watcher.service") run_command("systemctl disable geoip-crowdsec-watcher.service") for f in [SYSTEMD_SERVICE, WATCHER_SCRIPT]: if os.path.isfile(f): os.remove(f) run_command("systemctl daemon-reload") print(" ✅ Service deinstalliert") def add_shop_to_active(shop, mode="php+crowdsec", geo_region="dach", 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 "php+crowdsec" try: with open(ACTIVE_SHOPS_FILE, 'r') as f: return json.load(f).get(shop, {}).get("mode", "php+crowdsec") except: return "php+crowdsec" 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 cleanup_crowdsec_decisions(shop): if not check_crowdsec(): return print(f" 🔍 Entferne CrowdSec-Decisions für {shop}...") total_removed = 0 for _ in range(50): code, stdout, _ = run_command(f"cscli decisions list -o raw --limit 0 | grep '{shop}'") if code != 0 or not stdout.strip(): break batch_count = 0 for line in stdout.strip().split('\n')[:100]: try: parts = line.split(',') if len(parts) >= 3: ip_field = parts[2].strip() ip = ip_field.split(':', 1)[1] if ':' in ip_field else ip_field if ip and run_command(f"cscli decisions delete --ip {ip}")[0] == 0: batch_count += 1 except: continue total_removed += batch_count if batch_count == 0: break print(f" ✅ {total_removed} Decisions entfernt" if total_removed else " ℹ️ Keine Decisions gefunden") 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 + CrowdSec (IPs werden an CrowdSec gemeldet)") 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" elif choice == "3": return "bot+crowdsec" elif choice == "4": return "bot-only" return "php+crowdsec" 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 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") print(f" ✅ Ban-Dauer: {ban_minutes} Minuten") return rate_limit, ban_seconds def get_mode_icon(mode): icons = { 'php+crowdsec': '🛡️', 'php-only': '📝', 'bot+crowdsec': '🤖🛡️', 'bot-only': '🤖' } return icons.get(mode, '❓') def get_mode_description(mode): descriptions = { 'php+crowdsec': 'GeoIP + CrowdSec', 'php-only': 'Nur GeoIP', 'bot+crowdsec': 'Bot-Rate-Limit + CrowdSec', 'bot-only': 'Nur Bot-Rate-Limit' } return descriptions.get(mode, mode) def is_bot_only_mode(mode): return mode in ['bot-only', 'bot+crowdsec'] def uses_crowdsec(mode): return mode in ['php+crowdsec', 'bot+crowdsec'] 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="php+crowdsec", 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) 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: 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: 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))] if not crowdsec_shops and check_crowdsec(): if not silent: print("\n[1/4] Installiere CrowdSec-Watcher-Service...") install_watcher_service() elif not silent: print("\n[1/4] CrowdSec-Watcher-Service bereits aktiv") elif not silent: print("\n[1/4] CrowdSec-Synchronisation deaktiviert") # 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): if 'declare(strict_types' in line: insert_line = i + 1 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}") else: rate_limit, ban_duration = get_shop_rate_limit_config(shop) print(f"🤖 Bot-Patterns: {len(BOT_PATTERNS)} aktiv") 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]: 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_php_blocks = 0 total_bans = 0 total_active_bans = 0 shop_php_stats = {} 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, 'req_min': req_min, 'ips': ips, 'bots': bots, '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': {}} 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_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") if code == 0 and stdout: for line in stdout.strip().split('\n')[1:]: for shop in active_shops: if shop in line: 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()): stats = shop_php_stats[shop] count = stats['blocks'] req_min = stats['req_min'] runtime = stats['runtime_minutes'] bar = "█" * min(int(req_min * 2), 20) if req_min > 0 else "" runtime_str = format_duration(runtime) if runtime > 0 else "?" 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']) top_ip_addr = top_ip[0] top_ip_count = top_ip[1]['count'] 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()): stats = shop_php_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']} 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] 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 v3.5.0") print(" 🇩🇪🇦🇹🇨🇭 DACH | 🇪🇺 Eurozone+GB | 🤖 Bot-Rate-Limiting") 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)") 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() is_bot_mode = is_bot_only_mode(mode) if is_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) 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 "" ban_str = f", {active_bans} aktive Bans" if active_bans > 0 else "" 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 "⚠️" 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 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)