Files
geoip_shop_manager/geoip_shop_manager.py

2303 lines
84 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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.2.0: Korrekte Ownership für erstellte Dateien (nicht mehr root:root)
v4.1.0: IP-basierte Bot-Erkennung (für Bots die sich tarnen)
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
import ipaddress
from datetime import datetime, timedelta
from pathlib import Path
from collections import Counter
# 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 IP RANGES - Für Bots die sich mit normalem User-Agent tarnen
# =============================================================================
BOT_IP_RANGES = {
# Alibaba Cloud / Alibaba Spider - tarnt sich oft mit normalem UA
# Große zusammengefasste Blöcke basierend auf APNIC/WHOIS-Daten
'Alibaba-Bot': [
# === Alibaba Cloud Singapore (ASEPL-SG) - Hauptblöcke ===
'43.0.0.0/9', # 43.0.0.0 - 43.127.255.255 (8.3 Mio IPs)
'8.128.0.0/10', # 8.128.0.0 - 8.191.255.255 (4.2 Mio IPs)
'8.208.0.0/12', # 8.208.0.0 - 8.223.255.255 (1 Mio IPs)
# === Alibaba Cloud Singapore - Weitere Blöcke ===
'47.74.0.0/15', # 47.74.0.0 - 47.75.255.255
'47.76.0.0/16', # 47.76.0.0 - 47.76.255.255
'47.88.0.0/15', # 47.88.0.0 - 47.89.255.255
'47.90.0.0/15', # 47.90.0.0 - 47.91.255.255
'47.241.0.0/16', # 47.241.0.0 - 47.241.255.255
'47.52.0.0/15', # 47.52.0.0 - 47.53.255.255 (HK)
'47.56.0.0/15', # 47.56.0.0 - 47.57.255.255 (HK)
'149.129.0.0/16', # 149.129.0.0 - 149.129.255.255
'161.117.0.0/16', # 161.117.0.0 - 161.117.255.255
'170.33.0.0/16', # 170.33.0.0 - 170.33.255.255
# === Alibaba Cloud China (Aliyun) ===
'39.96.0.0/13', # 39.96.0.0 - 39.103.255.255
'39.104.0.0/14', # 39.104.0.0 - 39.107.255.255
'39.108.0.0/16', # 39.108.0.0 - 39.108.255.255
'101.132.0.0/15', # 101.132.0.0 - 101.133.255.255
'106.14.0.0/15', # 106.14.0.0 - 106.15.255.255
'112.124.0.0/16', # 112.124.0.0 - 112.124.255.255
'114.55.0.0/16', # 114.55.0.0 - 114.55.255.255
'115.28.0.0/15', # 115.28.0.0 - 115.29.255.255
'116.62.0.0/16', # 116.62.0.0 - 116.62.255.255
'118.31.0.0/16', # 118.31.0.0 - 118.31.255.255
'119.23.0.0/16', # 119.23.0.0 - 119.23.255.255
'120.24.0.0/14', # 120.24.0.0 - 120.27.255.255
'120.55.0.0/16', # 120.55.0.0 - 120.55.255.255
'120.76.0.0/14', # 120.76.0.0 - 120.79.255.255
'121.40.0.0/14', # 121.40.0.0 - 121.43.255.255
'121.196.0.0/14', # 121.196.0.0 - 121.199.255.255
'139.196.0.0/16', # 139.196.0.0 - 139.196.255.255
'139.224.0.0/16', # 139.224.0.0 - 139.224.255.255
'140.205.0.0/16', # 140.205.0.0 - 140.205.255.255
'182.92.0.0/16', # 182.92.0.0 - 182.92.255.255
# === Alibaba Sonstige ===
'203.107.0.0/16', # Alibaba DNS
'103.206.40.0/22', # Alibaba Cloud SG
'185.218.176.0/22', # Alibaba Cloud
],
}
# =============================================================================
# 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',
# Alibaba - auch per User-Agent erkennbar (zusätzlich zu IP-Erkennung)
'Alibaba-Bot': r'alibaba|alibabagroup|aliyun|alicdn|alimama|taobao|tmall|1688\.com',
'AlibabaSpider': r'alibabaspider',
'Aliyun': r'aliyun',
'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',
]
# =============================================================================
# OWNERSHIP HELPER FUNCTIONS
# =============================================================================
def get_most_common_owner(httpdocs_path):
"""
Ermittelt die häufigste uid:gid-Kombination im httpdocs-Verzeichnis.
Gibt (uid, gid) zurück oder (None, None) wenn nicht ermittelbar.
"""
if not os.path.isdir(httpdocs_path):
return None, None
owner_counts = Counter()
try:
for entry in os.listdir(httpdocs_path):
entry_path = os.path.join(httpdocs_path, entry)
try:
stat_info = os.stat(entry_path)
owner_counts[(stat_info.st_uid, stat_info.st_gid)] += 1
except (OSError, IOError):
continue
except (OSError, IOError):
return None, None
if not owner_counts:
return None, None
# Häufigste Kombination zurückgeben
most_common = owner_counts.most_common(1)[0][0]
return most_common
def set_owner(path, uid, gid, recursive=False):
"""
Setzt Owner und Gruppe für eine Datei oder einen Ordner.
Optional rekursiv für Verzeichnisse.
"""
if uid is None or gid is None:
return
try:
os.chown(path, uid, gid)
if recursive and os.path.isdir(path):
for root, dirs, files in os.walk(path):
for d in dirs:
try:
os.chown(os.path.join(root, d), uid, gid)
except (OSError, IOError):
pass
for f in files:
try:
os.chown(os.path.join(root, f), uid, gid)
except (OSError, IOError):
pass
except (OSError, IOError):
pass
# =============================================================================
# EXISTING HELPER FUNCTIONS
# =============================================================================
def ip_in_cidr(ip_str, cidr_str):
"""Prüft ob eine IP in einem CIDR-Netz liegt."""
try:
ip = ipaddress.ip_address(ip_str)
network = ipaddress.ip_network(cidr_str, strict=False)
return ip in network
except ValueError:
return False
def detect_bot(user_agent, ip=None):
"""
Erkennt Bots anhand des User-Agents und/oder der IP.
IP-basierte Erkennung hat Priorität (für getarnte Bots).
Gibt den Anzeigenamen zurück.
"""
# SCHRITT 1: IP-basierte Erkennung (höchste Priorität)
if ip:
for bot_name, ip_ranges in BOT_IP_RANGES.items():
for cidr in ip_ranges:
if ip_in_cidr(ip, cidr):
return bot_name
if not user_agent or user_agent == 'Unknown':
return 'Unbekannt'
# SCHRITT 2: Spezifische User-Agent Patterns
for bot_name, pattern in BOT_PATTERNS.items():
if re.search(pattern, user_agent, re.IGNORECASE):
return bot_name
# SCHRITT 3: 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)
def generate_php_bot_ip_ranges():
"""Generiert PHP-Array für IP-basierte Bot-Erkennung."""
lines = []
for bot_name, ip_ranges in BOT_IP_RANGES.items():
safe_bot_name = bot_name.replace("'", "\\'")
ranges_str = ", ".join([f"'{r}'" for r in ip_ranges])
lines.append(f"'{safe_bot_name}' => [{ranges_str}]")
return ",\n ".join(lines)
# =============================================================================
# CACHE VALIDATION
# =============================================================================
def generate_and_validate_cache(httpdocs_path, geo_region, uid=None, gid=None):
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'''<?php
$countries = {json.dumps(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) {{
$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:'):
# Cache-Datei wurde erstellt, Ownership setzen
if os.path.isfile(cache_file):
set_owner(cache_file, uid, gid)
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'''<?php
$data = @unserialize(@file_get_contents("{cache_file}"));
if ($data === false || !is_array($data)) {{ echo "CORRUPT:0"; }}
else {{ echo "OK:" . count($data); }}
'''
try:
result = subprocess.run(['php', '-r', php_script], capture_output=True, text=True, timeout=10)
output = result.stdout.strip()
if output.startswith('OK:'):
count = int(output.split(':')[1])
if count >= 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 = '''<?php
/**
* GeoIP Blocking Script - {region_name}
* Valid until: {expiry_date}
* FAIL-OPEN: If cache is corrupt/empty, traffic is allowed through
*/
$expiry_date = strtotime('{expiry_timestamp}');
if (time() > $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)
# Mit IP-basierter Bot-Erkennung für getarnte Bots
# =============================================================================
BOT_SCRIPT_TEMPLATE = '''<?php
/**
* Bot Rate-Limiting Script - By Bot-Type
* Valid until: {expiry_date}
* Rate-limits known bots/crawlers BY BOT-TYPE (not by IP)
* All requests from the same bot-type share ONE counter
* Includes IP-based detection for bots that disguise their User-Agent
* Rate-Limit: {rate_limit} req/min, Ban: {ban_duration_min} min
*/
$expiry_date = strtotime('{expiry_timestamp}');
if (time() > $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);
// === IP-in-CIDR Check Function ===
function ip_in_cidr($ip, $cidr) {{
if (strpos($cidr, '/') === false) return false;
list($subnet, $mask) = explode('/', $cidr);
$ip_long = ip2long($ip);
$subnet_long = ip2long($subnet);
if ($ip_long === false || $subnet_long === false) return false;
$mask_long = -1 << (32 - (int)$mask);
return ($ip_long & $mask_long) === ($subnet_long & $mask_long);
}}
// === Bot IP Ranges (für getarnte Bots) ===
$bot_ip_ranges = [
{bot_ip_ranges}
];
// === Bot Detection Patterns (User-Agent) ===
$bot_patterns = [
{bot_patterns}
];
$generic_patterns = [{generic_patterns}];
// === STEP 0: IP-basierte Bot-Erkennung (höchste Priorität) ===
$detected_bot = null;
if (!empty($visitor_ip)) {{
foreach ($bot_ip_ranges as $bot_name => $ip_ranges) {{
foreach ($ip_ranges as $cidr) {{
if (ip_in_cidr($visitor_ip, $cidr)) {{
$detected_bot = $bot_name;
break 2; // Aus beiden Schleifen ausbrechen
}}
}}
}}
}}
// === STEP 1: User-Agent-basierte Erkennung (falls IP nicht erkannt) ===
if ($detected_bot === null && !empty($user_agent)) {{
// 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 2: 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 3: 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 4: 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 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_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
# Ermittle den häufigsten Owner im httpdocs-Verzeichnis
uid, gid = get_most_common_owner(httpdocs)
if not silent and uid is not None:
import pwd
import grp
try:
user_name = pwd.getpwuid(uid).pw_name
group_name = grp.getgrgid(gid).gr_name
print(f"\n👤 Ownership: {user_name}:{group_name} (uid={uid}, gid={gid})")
except (KeyError, ImportError):
print(f"\n👤 Ownership: uid={uid}, gid={gid}")
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)
set_owner(backup_php, uid, gid)
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 '<?php' in line and insert_line == 0:
insert_line = i + 1
require_statement = f"require_once __DIR__ . '/{BLOCKING_FILE}';"
if require_statement not in content:
lines.insert(insert_line, require_statement)
with open(index_php, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
set_owner(index_php, uid, gid)
expiry = datetime.now() + timedelta(hours=72)
# Select appropriate template
if bot_mode:
# Create rate-limit directories with open permissions
os.makedirs(os.path.join(ratelimit_path, 'bans'), mode=0o777, exist_ok=True)
os.makedirs(os.path.join(ratelimit_path, 'counts'), mode=0o777, exist_ok=True)
os.chmod(ratelimit_path, 0o777)
os.chmod(os.path.join(ratelimit_path, 'bans'), 0o777)
os.chmod(os.path.join(ratelimit_path, 'counts'), 0o777)
# Set ownership for ratelimit directories
set_owner(ratelimit_path, uid, gid, recursive=True)
if rate_limit is None:
rate_limit = DEFAULT_RATE_LIMIT
if ban_duration is None:
ban_duration = DEFAULT_BAN_DURATION * 60
geoip_content = BOT_SCRIPT_TEMPLATE.format(
expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'),
expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'),
log_file=LOG_FILE,
ratelimit_dir=RATELIMIT_DIR,
bot_patterns=generate_php_bot_patterns(),
generic_patterns=generate_php_generic_patterns(),
bot_ip_ranges=generate_php_bot_ip_ranges(),
rate_limit=rate_limit,
ban_duration=ban_duration,
ban_duration_min=ban_duration // 60
)
else:
countries_array = generate_php_countries_array(geo_region)
geoip_content = GEOIP_SCRIPT_TEMPLATE.format(
region_name=region_info['name'],
region_description=region_info['description'],
expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'),
expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'),
cache_file=CACHE_FILE,
log_file=LOG_FILE,
countries_array=countries_array,
min_ranges=min_ranges
)
with open(blocking_file, 'w', encoding='utf-8') as f:
f.write(geoip_content)
set_owner(blocking_file, uid, gid)
if not silent:
print(" ✅ PHP-Blocking aktiviert")
# Step 2: Generate cache (only for GeoIP modes)
if bot_mode:
if not silent:
print(f"\n[2/3] Cache-Generierung nicht erforderlich (Bot-Only)")
range_count = 0
else:
if not silent:
print(f"\n[2/3] Generiere IP-Range-Cache ({len(region_info['countries'])} Länder)...")
success, range_count, error = generate_and_validate_cache(httpdocs, geo_region, uid, gid)
if success:
if not silent:
print(f" ✅ Cache generiert: {range_count:,} IP-Ranges")
else:
if not silent:
print(f" ⚠️ Cache-Generierung: {error}")
print(f" Fail-Open aktiv - Cache wird beim ersten Request neu generiert")
# Step 3: Register
if not silent:
print("\n[3/3] Registriere Shop...")
if bot_mode:
add_shop_to_active(shop, mode, geo_region, rate_limit, ban_duration)
else:
add_shop_to_active(shop, mode, geo_region)
# Count IP ranges for bot detection
total_ip_ranges = sum(len(ranges) for ranges in BOT_IP_RANGES.values())
if not silent:
print("\n" + "=" * 60)
print(f"{region_info['icon']} {region_info['name']} aktiviert")
print(f" Shop: {shop}")
print(f" Modus: {get_mode_description(mode)} {get_mode_icon(mode)}")
if not bot_mode:
print(f" IP-Ranges: {range_count:,}")
print(f" 🛡️ Fail-Open: Bei Cache-Fehlern wird Traffic durchgelassen")
else:
print(f" 🤖 {len(BOT_PATTERNS)} Bot-Patterns + {len(GENERIC_BOT_PATTERNS)} generische Patterns")
print(f" 🌐 {total_ip_ranges} IP-Ranges für {len(BOT_IP_RANGES)} getarnte Bots")
if rate_limit == 0:
print(f" 🚫 Rate-Limit: 0 (Bots werden SOFORT gebannt!)")
else:
print(f" 🚦 Rate-Limit: {rate_limit} req/min PRO BOT-TYP")
print(f" ⏱️ Ban-Dauer: {ban_duration // 60} min")
print(f" Alle Googlebot-Requests teilen sich EIN Limit!")
print(f" Alle Alibaba-IPs teilen sich EIN Limit!")
print(f" Gültig bis: {expiry.strftime('%Y-%m-%d %H:%M:%S CET')}")
print("=" * 60)
return True
def deactivate_blocking(shop, silent=False):
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}')
ratelimit_path = os.path.join(httpdocs, RATELIMIT_DIR)
shop_mode = get_shop_mode(shop)
shop_geo = get_shop_geo_region(shop)
region_info = get_geo_region_info(shop_geo)
if not silent:
print(f"\n🔧 Deaktiviere {region_info['icon']} {region_info['name']} für: {shop}")
print("=" * 60)
if not silent:
print("\n[1/3] PHP-Blocking entfernen...")
if os.path.isfile(backup_php):
shutil.move(backup_php, index_php)
else:
if os.path.isfile(index_php):
with open(index_php, 'r') as f:
content = f.read()
lines = [l for l in content.split('\n') if BLOCKING_FILE not in l]
with open(index_php, 'w') as f:
f.write('\n'.join(lines))
for f in [os.path.join(httpdocs, x) for x in [BLOCKING_FILE, CACHE_FILE, LOG_FILE]]:
if os.path.isfile(f):
os.remove(f)
if not silent:
print("\n[2/3] Rate-Limit Daten entfernen...")
if os.path.isdir(ratelimit_path):
shutil.rmtree(ratelimit_path)
if not silent:
print(" ✅ Rate-Limit Verzeichnis gelöscht")
elif not silent:
print(" Kein Rate-Limit Verzeichnis vorhanden")
if not silent:
print("\n[3/3] Deregistriere Shop...")
remove_shop_from_active(shop)
if not silent:
print("\n" + "=" * 60)
print(f"✅ Blocking deaktiviert für: {shop}")
print("=" * 60)
return True
def activate_all_shops():
available_shops = [s for s in get_available_shops() if s not in get_active_shops()]
if not available_shops:
print("\n⚠️ Keine Shops zum Aktivieren verfügbar")
return
print(f"\n{'=' * 60}")
print(f" Blocking für ALLE Shops aktivieren")
print(f"{'=' * 60}")
print(f"\n📋 {len(available_shops)} Shop(s):")
for shop in available_shops:
print(format_shop_with_link11(shop, prefix=""))
mode = select_mode()
bot_mode = is_bot_mode(mode)
if bot_mode:
geo_region = "none"
region_info = get_geo_region_info("none")
rate_limit, ban_duration = select_rate_limit()
else:
geo_region = select_geo_region()
region_info = get_geo_region_info(geo_region)
rate_limit, ban_duration = None, None
print(f"\n⚠️ Modus: {get_mode_description(mode)} {get_mode_icon(mode)}")
if not bot_mode:
print(f" Region: {region_info['icon']} {region_info['name']}")
else:
if rate_limit == 0:
print(f" 🚫 Rate-Limit: 0 (Bots werden SOFORT gebannt!)")
else:
print(f" Rate-Limit: {rate_limit} req/min pro Bot-Typ")
print(f" Ban-Dauer: {ban_duration // 60} min")
if input(f"\nFortfahren? (ja/nein): ").strip().lower() not in ['ja', 'j', 'yes', 'y']:
print("\n❌ Abgebrochen")
return
print(f"\n{'=' * 60}")
success_count = 0
for i, shop in enumerate(available_shops, 1):
print(f"\n[{i}/{len(available_shops)}] {shop}")
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)
if os.path.isfile(backup_php) or not os.path.isfile(index_php):
print(f" ⚠️ Übersprungen")
continue
# Ermittle Owner für diesen Shop
uid, gid = get_most_common_owner(httpdocs)
shutil.copy2(index_php, backup_php)
set_owner(backup_php, uid, gid)
with open(index_php, 'r', encoding='utf-8') as f:
content = f.read()
lines = content.split('\n')
insert_line = 0
for idx, line in enumerate(lines):
if 'declare(strict_types' in line:
insert_line = idx + 1
break
elif '<?php' in line and insert_line == 0:
insert_line = idx + 1
require_statement = f"require_once __DIR__ . '/{BLOCKING_FILE}';"
if require_statement not in content:
lines.insert(insert_line, require_statement)
with open(index_php, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
set_owner(index_php, uid, gid)
expiry = datetime.now() + timedelta(hours=72)
if bot_mode:
os.makedirs(os.path.join(ratelimit_path, 'bans'), mode=0o777, exist_ok=True)
os.makedirs(os.path.join(ratelimit_path, 'counts'), mode=0o777, exist_ok=True)
os.chmod(ratelimit_path, 0o777)
os.chmod(os.path.join(ratelimit_path, 'bans'), 0o777)
os.chmod(os.path.join(ratelimit_path, 'counts'), 0o777)
set_owner(ratelimit_path, uid, gid, recursive=True)
geoip_content = BOT_SCRIPT_TEMPLATE.format(
expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'),
expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'),
log_file=LOG_FILE,
ratelimit_dir=RATELIMIT_DIR,
bot_patterns=generate_php_bot_patterns(),
generic_patterns=generate_php_generic_patterns(),
bot_ip_ranges=generate_php_bot_ip_ranges(),
rate_limit=rate_limit,
ban_duration=ban_duration,
ban_duration_min=ban_duration // 60
)
else:
geoip_content = GEOIP_SCRIPT_TEMPLATE.format(
region_name=region_info['name'],
region_description=region_info['description'],
expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'),
expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'),
cache_file=CACHE_FILE,
log_file=LOG_FILE,
countries_array=generate_php_countries_array(geo_region),
min_ranges=MIN_RANGES.get(geo_region, 1000)
)
with open(blocking_file, 'w', encoding='utf-8') as f:
f.write(geoip_content)
set_owner(blocking_file, uid, gid)
if bot_mode:
add_shop_to_active(shop, mode, geo_region, rate_limit, ban_duration)
total_ip_ranges = sum(len(ranges) for ranges in BOT_IP_RANGES.values())
print(f" ✅ Aktiviert ({len(BOT_PATTERNS)} Patterns, {total_ip_ranges} IP-Ranges, {rate_limit} req/min)")
else:
print(f" ⏳ Cache generieren...")
cache_ok, count, _ = generate_and_validate_cache(httpdocs, geo_region, uid, gid)
add_shop_to_active(shop, mode, geo_region)
print(f" ✅ Aktiviert ({count:,} Ranges)" if cache_ok else f" ⚠️ Aktiviert (Cache-Warnung)")
success_count += 1
print(f"\n{'=' * 60}")
print(f"{success_count} Shop(s) aktiviert")
if not bot_mode:
print(f" 🛡️ Fail-Open bei Cache-Fehlern aktiv")
else:
if rate_limit == 0:
print(f" 🚫 Rate-Limit: 0 (Bots werden SOFORT gebannt!)")
else:
print(f" 🚦 Rate-Limit: {rate_limit} req/min pro Bot-Typ")
print(f"{'=' * 60}")
def activate_direct_shops_only():
available_shops = [s for s in get_available_shops() if s not in get_active_shops()]
if not available_shops:
print("\n⚠️ Keine Shops zum Aktivieren verfügbar")
return
direct_shops = get_direct_shops(available_shops)
link11_shops = get_link11_shops(available_shops)
if not direct_shops:
print("\n⚠️ Keine direkten Shops gefunden (alle sind hinter Link11)")
return
print(f"\n{'=' * 60}")
print(f" Blocking für DIREKTE Shops aktivieren (ohne Link11)")
print(f"{'=' * 60}")
print(f" {COLOR_GREEN}Grün = hinter Link11 (übersprungen){COLOR_RESET}")
print(f" {COLOR_RED}Rot = Direkt (wird aktiviert){COLOR_RESET}")
print(f"\n📋 {len(direct_shops)} direkte Shop(s) werden aktiviert:")
for shop in direct_shops:
print(f" {COLOR_RED}{shop} [Direkt]{COLOR_RESET}")
if link11_shops:
print(f"\n⏭️ {len(link11_shops)} Shop(s) hinter Link11 werden übersprungen:")
for shop in link11_shops:
print(f" {COLOR_GREEN}{shop} [Link11]{COLOR_RESET}")
mode = select_mode()
bot_mode = is_bot_mode(mode)
if bot_mode:
geo_region = "none"
region_info = get_geo_region_info("none")
rate_limit, ban_duration = select_rate_limit()
else:
geo_region = select_geo_region()
region_info = get_geo_region_info(geo_region)
rate_limit, ban_duration = None, None
print(f"\n⚠️ Modus: {get_mode_description(mode)} {get_mode_icon(mode)}")
if not bot_mode:
print(f" Region: {region_info['icon']} {region_info['name']}")
else:
if rate_limit == 0:
print(f" 🚫 Rate-Limit: 0 (Bots werden SOFORT gebannt!)")
else:
print(f" Rate-Limit: {rate_limit} req/min pro Bot-Typ")
print(f" Ban-Dauer: {ban_duration // 60} min")
if input(f"\nFortfahren? (ja/nein): ").strip().lower() not in ['ja', 'j', 'yes', 'y']:
print("\n❌ Abgebrochen")
return
print(f"\n{'=' * 60}")
success_count = 0
for i, shop in enumerate(direct_shops, 1):
print(f"\n[{i}/{len(direct_shops)}] {shop}")
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)
if os.path.isfile(backup_php) or not os.path.isfile(index_php):
print(f" ⚠️ Übersprungen")
continue
# Ermittle Owner für diesen Shop
uid, gid = get_most_common_owner(httpdocs)
shutil.copy2(index_php, backup_php)
set_owner(backup_php, uid, gid)
with open(index_php, 'r', encoding='utf-8') as f:
content = f.read()
lines = content.split('\n')
insert_line = 0
for idx, line in enumerate(lines):
if 'declare(strict_types' in line:
insert_line = idx + 1
break
elif '<?php' in line and insert_line == 0:
insert_line = idx + 1
require_statement = f"require_once __DIR__ . '/{BLOCKING_FILE}';"
if require_statement not in content:
lines.insert(insert_line, require_statement)
with open(index_php, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
set_owner(index_php, uid, gid)
expiry = datetime.now() + timedelta(hours=72)
if bot_mode:
os.makedirs(os.path.join(ratelimit_path, 'bans'), mode=0o777, exist_ok=True)
os.makedirs(os.path.join(ratelimit_path, 'counts'), mode=0o777, exist_ok=True)
os.chmod(ratelimit_path, 0o777)
os.chmod(os.path.join(ratelimit_path, 'bans'), 0o777)
os.chmod(os.path.join(ratelimit_path, 'counts'), 0o777)
set_owner(ratelimit_path, uid, gid, recursive=True)
geoip_content = BOT_SCRIPT_TEMPLATE.format(
expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'),
expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'),
log_file=LOG_FILE,
ratelimit_dir=RATELIMIT_DIR,
bot_patterns=generate_php_bot_patterns(),
generic_patterns=generate_php_generic_patterns(),
bot_ip_ranges=generate_php_bot_ip_ranges(),
rate_limit=rate_limit,
ban_duration=ban_duration,
ban_duration_min=ban_duration // 60
)
else:
geoip_content = GEOIP_SCRIPT_TEMPLATE.format(
region_name=region_info['name'],
region_description=region_info['description'],
expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'),
expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'),
cache_file=CACHE_FILE,
log_file=LOG_FILE,
countries_array=generate_php_countries_array(geo_region),
min_ranges=MIN_RANGES.get(geo_region, 1000)
)
with open(blocking_file, 'w', encoding='utf-8') as f:
f.write(geoip_content)
set_owner(blocking_file, uid, gid)
if bot_mode:
add_shop_to_active(shop, mode, geo_region, rate_limit, ban_duration)
total_ip_ranges = sum(len(ranges) for ranges in BOT_IP_RANGES.values())
print(f" ✅ Aktiviert ({len(BOT_PATTERNS)} Patterns, {total_ip_ranges} IP-Ranges, {rate_limit} req/min)")
else:
print(f" ⏳ Cache generieren...")
cache_ok, count, _ = generate_and_validate_cache(httpdocs, geo_region, uid, gid)
add_shop_to_active(shop, mode, geo_region)
print(f" ✅ Aktiviert ({count:,} Ranges)" if cache_ok else f" ⚠️ Aktiviert (Cache-Warnung)")
success_count += 1
print(f"\n{'=' * 60}")
print(f"{success_count} direkte Shop(s) aktiviert")
print(f" ⏭️ {len(link11_shops)} Link11-Shop(s) übersprungen")
if not bot_mode:
print(f" 🛡️ Fail-Open bei Cache-Fehlern aktiv")
else:
if rate_limit == 0:
print(f" 🚫 Rate-Limit: 0 (Bots werden SOFORT gebannt!)")
else:
print(f" 🚦 Rate-Limit: {rate_limit} req/min pro Bot-Typ")
print(f"{'=' * 60}")
def deactivate_all_shops():
active_shops = get_active_shops()
if not active_shops:
print("\n⚠️ Keine aktiven Shops")
return
print(f"\n{'=' * 60}")
print(f" Blocking für ALLE deaktivieren")
print(f"{'=' * 60}")
print(f"\n📋 {len(active_shops)} Shop(s):")
for shop in active_shops:
region_info = get_geo_region_info(get_shop_geo_region(shop))
mode_icon = get_mode_icon(get_shop_mode(shop))
print(f"{format_shop_with_link11(shop)} {region_info['icon']} {mode_icon}")
if input(f"\nFortfahren? (ja/nein): ").strip().lower() not in ['ja', 'j', 'yes', 'y']:
print("\n❌ Abgebrochen")
return
for i, shop in enumerate(active_shops, 1):
print(f"\n[{i}/{len(active_shops)}] {shop}")
httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs')
backup_php = os.path.join(httpdocs, f'index.php{BACKUP_SUFFIX}')
index_php = os.path.join(httpdocs, 'index.php')
ratelimit_path = os.path.join(httpdocs, RATELIMIT_DIR)
if os.path.isfile(backup_php):
shutil.move(backup_php, index_php)
for f in [os.path.join(httpdocs, x) for x in [BLOCKING_FILE, CACHE_FILE, LOG_FILE]]:
if os.path.isfile(f):
os.remove(f)
if os.path.isdir(ratelimit_path):
shutil.rmtree(ratelimit_path)
remove_shop_from_active(shop)
print(f" ✅ Deaktiviert")
print(f"\n{'=' * 60}")
print(f" ✅ Alle Shops deaktiviert")
print(f"{'=' * 60}")
def get_shop_log_stats(shop):
httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs')
log_file = os.path.join(httpdocs, LOG_FILE)
ratelimit_path = os.path.join(httpdocs, RATELIMIT_DIR)
log_entries = 0
ips = {}
bots = {}
bans = 0
if os.path.isfile(log_file):
with open(log_file, 'r') as f:
for line in f:
log_entries += 1
ip, ua = None, 'Unknown'
detected_bot = None
if 'BANNED: ' in line:
bans += 1
try:
detected_bot = line.split('BANNED: ')[1].split(' |')[0].strip()
except:
pass
elif 'BOT: ' in line:
try:
detected_bot = line.split('BOT: ')[1].split(' |')[0].strip()
except:
pass
elif 'BLOCKED (banned): ' in line:
try:
detected_bot = line.split('BLOCKED (banned): ')[1].split(' |')[0].strip()
except:
pass
if 'IP: ' in line:
try:
ip = line.split('IP: ')[1].split(' |')[0].strip()
except:
pass
if 'UA: ' in line:
try:
ua = line.split('UA: ')[1].split(' |')[0].strip()
except:
pass
if ip:
if ip not in ips:
ips[ip] = {'count': 0, 'ua': ua, 'bot': None}
ips[ip]['count'] += 1
if detected_bot and not ips[ip]['bot']:
ips[ip]['bot'] = detected_bot
if ua != 'Unknown' and ips[ip]['ua'] == 'Unknown':
ips[ip]['ua'] = ua
if detected_bot:
bots[detected_bot] = bots.get(detected_bot, 0) + 1
elif ua and ua != 'Unknown':
bot_name = detect_bot(ua, ip)
if bot_name != 'Unbekannt':
bots[bot_name] = bots.get(bot_name, 0) + 1
active_bans = 0
banned_bots = []
bans_dir = os.path.join(ratelimit_path, 'bans')
if os.path.isdir(bans_dir):
now = time.time()
for ban_file in os.listdir(bans_dir):
if ban_file.endswith('.ban'):
try:
with open(os.path.join(bans_dir, ban_file), 'r') as f:
content = f.read().strip()
parts = content.split('|', 1)
ban_until = int(parts[0])
bot_name = parts[1] if len(parts) > 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)
total_ip_ranges = sum(len(ranges) for ranges in BOT_IP_RANGES.values())
print(f"🤖 Bot-Patterns: {len(BOT_PATTERNS)} + {len(GENERIC_BOT_PATTERNS)} generische")
print(f"🌐 IP-basierte Erkennung: {total_ip_ranges} Ranges für {len(BOT_IP_RANGES)} Bot(s)")
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'], ip)
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, ip)
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():
total_ip_ranges = sum(len(ranges) for ranges in BOT_IP_RANGES.values())
print("\n" + "=" * 60)
print(" GeoIP Shop Blocker Manager v4.2.0")
print(" 🇩🇪🇦🇹🇨🇭 DACH | 🇪🇺 Eurozone+GB | 🤖 Bot-Rate-Limiting")
print(" 🛡️ Mit Cache-Validierung und Fail-Open")
print(" 🚦 Rate-Limiting nach BOT-TYP (nicht IP)")
print(f" 🌐 IP-basierte Erkennung: {total_ip_ranges} Ranges für {len(BOT_IP_RANGES)} Bot(s)")
print(" 👤 Korrekte Ownership für erstellte Dateien")
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)