From 70c1a2d2f2c07602d205a6ac6fa81536a63c4189 Mon Sep 17 00:00:00 2001 From: thomasciesla Date: Tue, 30 Dec 2025 11:19:18 +0100 Subject: [PATCH] jtl-wafi-dashboard.py aktualisiert --- jtl-wafi-dashboard.py | 287 +++++++++++++++++++++++++++++------------- 1 file changed, 201 insertions(+), 86 deletions(-) diff --git a/jtl-wafi-dashboard.py b/jtl-wafi-dashboard.py index 74eb5c5..c439227 100644 --- a/jtl-wafi-dashboard.py +++ b/jtl-wafi-dashboard.py @@ -1,13 +1,16 @@ #!/usr/bin/env python3 """ -JTL-WAFi Dashboard v2.2.0 - WebSocket Real-Time Dashboard +JTL-WAFi Dashboard v2.5.0 - WebSocket Real-Time Dashboard -ÄNDERUNG: Keine SQLite mehr für Echtzeit-Daten! +ÄNDERUNG v2.5: Country Rate-Limiting ersetzt GeoIP-Blocking! +- Bot Rate-Limiting: Bots nach Rate-Limit bannen +- Country Rate-Limiting: Länder nach Rate-Limit bannen +- Unlimitierte Länder: Definierte Länder werden nicht limitiert +- Monitor-Only Modus: Nur überwachen ohne zu blocken - Alle Agent/Shop-Daten im Memory - DB nur für: Passwort, Tokens, Sessions -- Kein Locking, keine Timeouts -v2.2.0: In-Memory Storage für maximale Performance +v2.5.0: Country Rate-Limiting Feature """ import os @@ -31,7 +34,7 @@ import uvicorn # ============================================================================= # VERSION & CONFIG # ============================================================================= -VERSION = "2.4.0" +VERSION = "2.5.0" DATA_DIR = "/var/lib/jtl-wafi" SSL_DIR = "/var/lib/jtl-wafi/ssl" @@ -83,32 +86,48 @@ class AgentData: @dataclass class ShopData: + """Shop-Daten v2.5 mit Country-Rate-Limiting Support.""" domain: str agent_id: str agent_hostname: str = "" status: str = "inactive" - mode: str = "" - geo_region: str = "" - rate_limit: int = 0 - ban_duration: int = 0 - bot_monitor_only: bool = False link11: bool = False link11_ip: str = "" activated: str = "" runtime_minutes: float = 0.0 - # Stats + + # v2.5: Neue Konfiguration + bot_mode: bool = False + bot_rate_limit: int = 0 + bot_ban_duration: int = 0 + country_mode: bool = False + country_rate_limit: int = 0 + country_ban_duration: int = 0 + unlimited_countries: List[str] = field(default_factory=list) + monitor_only: bool = False + + # Stats (v2.5 erweitert) log_entries: int = 0 - total_bans: int = 0 - active_bans: int = 0 + bot_bans: int = 0 + country_bans: int = 0 + active_bot_bans: int = 0 + active_country_bans: int = 0 banned_bots: List[str] = field(default_factory=list) + banned_countries: List[str] = field(default_factory=list) req_per_min: float = 0.0 unique_ips: int = 0 unique_bots: int = 0 + unique_countries: int = 0 top_bots: Dict[str, int] = field(default_factory=dict) top_ips: Dict[str, int] = field(default_factory=dict) - # History für Graph - jetzt pro Bot + top_countries: Dict[str, int] = field(default_factory=dict) + human_requests: int = 0 + bot_requests: int = 0 + + # History für Graph history: deque = field(default_factory=lambda: deque(maxlen=HISTORY_MAX_POINTS)) - bot_history: Dict[str, deque] = field(default_factory=dict) # bot_name -> deque of {timestamp, count} + bot_history: Dict[str, deque] = field(default_factory=dict) + country_history: Dict[str, deque] = field(default_factory=dict) class DataStore: @@ -242,6 +261,7 @@ class DataStore: # === Shops === def update_shop(self, agent_id: str, agent_hostname: str, shop_data: Dict) -> ShopData: + """Aktualisiert Shop-Daten vom Agent (v2.5).""" domain = shop_data.get('domain') if domain not in self.shops: @@ -251,52 +271,75 @@ class DataStore: shop.agent_id = agent_id shop.agent_hostname = agent_hostname shop.status = shop_data.get('status', 'inactive') - shop.mode = shop_data.get('mode', '') - shop.geo_region = shop_data.get('geo_region', '') - shop.rate_limit = shop_data.get('rate_limit', 0) - shop.ban_duration = shop_data.get('ban_duration', 0) - shop.bot_monitor_only = shop_data.get('bot_monitor_only', False) shop.link11 = bool(shop_data.get('link11')) shop.link11_ip = shop_data.get('link11_ip', '') shop.activated = shop_data.get('activated', '') shop.runtime_minutes = shop_data.get('runtime_minutes', 0) - # Stats + # v2.5 Konfiguration + shop.bot_mode = shop_data.get('bot_mode', False) + shop.bot_rate_limit = shop_data.get('bot_rate_limit') or 0 + shop.bot_ban_duration = shop_data.get('bot_ban_duration') or 0 + shop.country_mode = shop_data.get('country_mode', False) + shop.country_rate_limit = shop_data.get('country_rate_limit') or 0 + shop.country_ban_duration = shop_data.get('country_ban_duration') or 0 + shop.unlimited_countries = shop_data.get('unlimited_countries', []) + shop.monitor_only = shop_data.get('monitor_only', False) + + # Stats (v2.5) stats = shop_data.get('stats', {}) if stats: shop.log_entries = stats.get('log_entries', 0) - shop.total_bans = stats.get('total_bans', 0) - shop.active_bans = stats.get('active_bans', 0) + shop.bot_bans = stats.get('bot_bans', 0) + shop.country_bans = stats.get('country_bans', 0) + shop.active_bot_bans = stats.get('active_bot_bans', 0) + shop.active_country_bans = stats.get('active_country_bans', 0) shop.banned_bots = stats.get('banned_bots', []) + shop.banned_countries = stats.get('banned_countries', []) shop.req_per_min = stats.get('req_per_min', 0) shop.unique_ips = stats.get('unique_ips', 0) shop.unique_bots = stats.get('unique_bots', 0) + shop.unique_countries = stats.get('unique_countries', 0) shop.top_bots = stats.get('top_bots', {}) shop.top_ips = stats.get('top_ips', {}) + shop.top_countries = stats.get('top_countries', {}) + shop.human_requests = stats.get('human_requests', 0) + shop.bot_requests = stats.get('bot_requests', 0) # History für Graph shop.history.append({ 'timestamp': utc_now_str(), 'req_per_min': shop.req_per_min, - 'active_bans': shop.active_bans + 'active_bot_bans': shop.active_bot_bans, + 'active_country_bans': shop.active_country_bans, + 'human_requests': shop.human_requests, + 'bot_requests': shop.bot_requests }) return shop def update_shop_stats(self, domain: str, stats: Dict): + """Aktualisiert Shop-Statistiken (v2.5).""" if domain not in self.shops: return shop = self.shops[domain] shop.log_entries = stats.get('log_entries', shop.log_entries) - shop.total_bans = stats.get('total_bans', shop.total_bans) - shop.active_bans = stats.get('active_bans', shop.active_bans) + shop.bot_bans = stats.get('bot_bans', shop.bot_bans) + shop.country_bans = stats.get('country_bans', shop.country_bans) + shop.active_bot_bans = stats.get('active_bot_bans', shop.active_bot_bans) + shop.active_country_bans = stats.get('active_country_bans', shop.active_country_bans) shop.banned_bots = stats.get('banned_bots', shop.banned_bots) + shop.banned_countries = stats.get('banned_countries', shop.banned_countries) shop.req_per_min = stats.get('req_per_min', shop.req_per_min) shop.unique_ips = stats.get('unique_ips', shop.unique_ips) shop.unique_bots = stats.get('unique_bots', shop.unique_bots) + shop.unique_countries = stats.get('unique_countries', shop.unique_countries) shop.top_bots = stats.get('top_bots', shop.top_bots) shop.top_ips = stats.get('top_ips', shop.top_ips) + shop.top_countries = stats.get('top_countries', shop.top_countries) + shop.human_requests = stats.get('human_requests', shop.human_requests) + shop.bot_requests = stats.get('bot_requests', shop.bot_requests) timestamp = utc_now_str() @@ -304,7 +347,10 @@ class DataStore: shop.history.append({ 'timestamp': timestamp, 'req_per_min': shop.req_per_min, - 'active_bans': shop.active_bans + 'active_bot_bans': shop.active_bot_bans, + 'active_country_bans': shop.active_country_bans, + 'human_requests': shop.human_requests, + 'bot_requests': shop.bot_requests }) # Bot-History aktualisieren @@ -317,10 +363,21 @@ class DataStore: 'count': count }) + # Country-History aktualisieren + top_countries = stats.get('top_countries', {}) + for country, count in top_countries.items(): + if country not in shop.country_history: + shop.country_history[country] = deque(maxlen=HISTORY_MAX_POINTS) + shop.country_history[country].append({ + 'timestamp': timestamp, + 'count': count + }) + def get_shop(self, domain: str) -> Optional[ShopData]: return self.shops.get(domain) def get_all_shops(self) -> List[Dict]: + """Gibt alle Shops mit v2.5 Struktur zurück.""" result = [] for shop in self.shops.values(): result.append({ @@ -328,46 +385,65 @@ class DataStore: 'agent_id': shop.agent_id, 'agent_hostname': shop.agent_hostname, 'status': shop.status, - 'mode': shop.mode, - 'geo_region': shop.geo_region, - 'rate_limit': shop.rate_limit, - 'ban_duration': shop.ban_duration, - 'bot_monitor_only': shop.bot_monitor_only, 'link11': shop.link11, 'link11_ip': shop.link11_ip, 'activated': shop.activated, 'runtime_minutes': shop.runtime_minutes, + # v2.5 Konfiguration + 'bot_mode': shop.bot_mode, + 'bot_rate_limit': shop.bot_rate_limit, + 'bot_ban_duration': shop.bot_ban_duration, + 'country_mode': shop.country_mode, + 'country_rate_limit': shop.country_rate_limit, + 'country_ban_duration': shop.country_ban_duration, + 'unlimited_countries': shop.unlimited_countries, + 'monitor_only': shop.monitor_only, + # Stats 'stats': { 'log_entries': shop.log_entries, - 'total_bans': shop.total_bans, - 'active_bans': shop.active_bans, + 'bot_bans': shop.bot_bans, + 'country_bans': shop.country_bans, + 'active_bot_bans': shop.active_bot_bans, + 'active_country_bans': shop.active_country_bans, 'banned_bots': shop.banned_bots, + 'banned_countries': shop.banned_countries, 'req_per_min': shop.req_per_min, 'unique_ips': shop.unique_ips, 'unique_bots': shop.unique_bots, + 'unique_countries': shop.unique_countries, 'top_bots': shop.top_bots, - 'top_ips': shop.top_ips + 'top_ips': shop.top_ips, + 'top_countries': shop.top_countries, + 'human_requests': shop.human_requests, + 'bot_requests': shop.bot_requests } }) return result def get_shop_history(self, domain: str) -> Dict: + """Gibt Shop-History inkl. Country-History zurück.""" shop = self.shops.get(domain) if not shop: - return {'history': [], 'bot_history': {}} + return {'history': [], 'bot_history': {}, 'country_history': {}} # Bot-History in JSON-serialisierbares Format bot_history = {} for bot_name, history in shop.bot_history.items(): bot_history[bot_name] = list(history) + # Country-History in JSON-serialisierbares Format + country_history = {} + for country, history in shop.country_history.items(): + country_history[country] = list(history) + return { 'history': list(shop.history), - 'bot_history': bot_history + 'bot_history': bot_history, + 'country_history': country_history } def get_top_shops(self, limit: int = 10, sort_by: str = 'req_per_min') -> List[Dict]: - """Gibt Top Shops sortiert nach req_per_min oder active_bans zurück.""" + """Gibt Top Shops sortiert nach verschiedenen Kriterien zurück.""" shops_list = [] for shop in self.shops.values(): shops_list.append({ @@ -375,13 +451,17 @@ class DataStore: 'agent_hostname': shop.agent_hostname, 'status': shop.status, 'req_per_min': shop.req_per_min, - 'active_bans': shop.active_bans, - 'link11': shop.link11 + 'active_bot_bans': shop.active_bot_bans, + 'active_country_bans': shop.active_country_bans, + 'link11': shop.link11, + 'bot_mode': shop.bot_mode, + 'country_mode': shop.country_mode, + 'monitor_only': shop.monitor_only }) # Sortieren if sort_by == 'active_bans': - shops_list.sort(key=lambda x: x['active_bans'], reverse=True) + shops_list.sort(key=lambda x: x['active_bot_bans'] + x['active_country_bans'], reverse=True) else: shops_list.sort(key=lambda x: x['req_per_min'], reverse=True) @@ -400,7 +480,8 @@ class DataStore: shops_direct = shops_total - shops_link11 req_per_min = sum(s.req_per_min for s in self.shops.values()) - active_bans = sum(s.active_bans for s in self.shops.values()) + active_bot_bans = sum(s.active_bot_bans for s in self.shops.values()) + active_country_bans = sum(s.active_country_bans for s in self.shops.values()) return { 'agents_online': agents_online, @@ -410,7 +491,9 @@ class DataStore: 'shops_link11': shops_link11, 'shops_direct': shops_direct, 'req_per_min': round(req_per_min, 1), - 'active_bans': active_bans + 'active_bot_bans': active_bot_bans, + 'active_country_bans': active_country_bans, + 'active_bans': active_bot_bans + active_country_bans } @@ -864,18 +947,29 @@ async def approve_agent(agent_id: str, request: Request): async def activate_shop( request: Request, domain: str = Form(...), - mode: str = Form(...), - geo_region: str = Form("dach"), - rate_limit: int = Form(30), - ban_duration: int = Form(300), - bot_monitor_only: str = Form("false") + bot_mode: str = Form("true"), + bot_rate_limit: int = Form(30), + bot_ban_duration: int = Form(300), + country_mode: str = Form("false"), + country_rate_limit: int = Form(100), + country_ban_duration: int = Form(600), + unlimited_countries: str = Form(""), + monitor_only: str = Form("false") ): + """Aktiviert Blocking für einen Shop (v2.5).""" user = await get_current_user(request) if not user: raise HTTPException(401) - # String "true"/"false" zu Boolean konvertieren - is_monitor_only = bot_monitor_only.lower() in ('true', '1', 'yes', 'on') + # String zu Boolean konvertieren + is_bot_mode = bot_mode.lower() in ('true', '1', 'yes', 'on') + is_country_mode = country_mode.lower() in ('true', '1', 'yes', 'on') + is_monitor_only = monitor_only.lower() in ('true', '1', 'yes', 'on') + + # unlimited_countries als Liste parsen + countries_list = [] + if unlimited_countries: + countries_list = [c.strip().lower() for c in unlimited_countries.split(',') if c.strip()] agent_id = manager.get_agent_for_shop(domain) if not agent_id or not manager.is_agent_connected(agent_id): @@ -887,11 +981,14 @@ async def activate_shop( 'data': { 'command_id': command_id, 'shop': domain, - 'mode': mode, - 'geo_region': geo_region if mode == 'geoip' else None, - 'rate_limit': rate_limit if mode == 'bot' and not is_monitor_only else None, - 'ban_duration': ban_duration if mode == 'bot' and not is_monitor_only else None, - 'bot_monitor_only': is_monitor_only if mode == 'bot' else False + 'bot_mode': is_bot_mode and not is_monitor_only, + 'bot_rate_limit': bot_rate_limit if is_bot_mode else None, + 'bot_ban_duration': bot_ban_duration if is_bot_mode else None, + 'country_mode': is_country_mode and not is_monitor_only, + 'country_rate_limit': country_rate_limit if is_country_mode else None, + 'country_ban_duration': country_ban_duration if is_country_mode else None, + 'unlimited_countries': countries_list if is_country_mode else [], + 'monitor_only': is_monitor_only } }) @@ -920,19 +1017,30 @@ async def deactivate_shop(request: Request, domain: str = Form(...)): @app.post("/api/shops/bulk-activate") async def bulk_activate( request: Request, - mode: str = Form(...), - geo_region: str = Form("dach"), - rate_limit: int = Form(30), - ban_duration: int = Form(300), - bot_monitor_only: str = Form("false"), + bot_mode: str = Form("true"), + bot_rate_limit: int = Form(30), + bot_ban_duration: int = Form(300), + country_mode: str = Form("false"), + country_rate_limit: int = Form(100), + country_ban_duration: int = Form(600), + unlimited_countries: str = Form(""), + monitor_only: str = Form("false"), filter_type: str = Form("all") ): + """Bulk-Aktivierung für mehrere Shops (v2.5).""" user = await get_current_user(request) if not user: raise HTTPException(401) - # String "true"/"false"/"on" zu Boolean konvertieren - is_monitor_only = bot_monitor_only.lower() in ('true', '1', 'yes', 'on') + # String zu Boolean konvertieren + is_bot_mode = bot_mode.lower() in ('true', '1', 'yes', 'on') + is_country_mode = country_mode.lower() in ('true', '1', 'yes', 'on') + is_monitor_only = monitor_only.lower() in ('true', '1', 'yes', 'on') + + # unlimited_countries als Liste parsen + countries_list = [] + if unlimited_countries: + countries_list = [c.strip().lower() for c in unlimited_countries.split(',') if c.strip()] activated = 0 shops = store.get_all_shops() @@ -956,11 +1064,14 @@ async def bulk_activate( 'data': { 'command_id': command_id, 'shop': shop['domain'], - 'mode': mode, - 'geo_region': geo_region if mode == 'geoip' else None, - 'rate_limit': rate_limit if mode == 'bot' and not is_monitor_only else None, - 'ban_duration': ban_duration if mode == 'bot' and not is_monitor_only else None, - 'bot_monitor_only': is_monitor_only if mode == 'bot' else False + 'bot_mode': is_bot_mode and not is_monitor_only, + 'bot_rate_limit': bot_rate_limit if is_bot_mode else None, + 'bot_ban_duration': bot_ban_duration if is_bot_mode else None, + 'country_mode': is_country_mode and not is_monitor_only, + 'country_rate_limit': country_rate_limit if is_country_mode else None, + 'country_ban_duration': country_ban_duration if is_country_mode else None, + 'unlimited_countries': countries_list if is_country_mode else [], + 'monitor_only': is_monitor_only } }) activated += 1 @@ -1120,7 +1231,7 @@ def get_dashboard_html() -> str: - JTL-WAFi Dashboard v2.3 + JTL-WAFi Dashboard v2.5