2303 lines
84 KiB
Python
2303 lines
84 KiB
Python
#!/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) |