diff --git a/jtl-wafi-dashboard.py b/jtl-wafi-dashboard.py index 9ef1cc2..c093f78 100644 --- a/jtl-wafi-dashboard.py +++ b/jtl-wafi-dashboard.py @@ -113,7 +113,7 @@ class ShopData: class DataStore: """In-Memory Datenspeicher - Thread-safe durch asyncio.""" - + def __init__(self): self.agents: Dict[str, AgentData] = {} self.shops: Dict[str, ShopData] = {} @@ -121,11 +121,11 @@ class DataStore: self._password_hash: Optional[str] = None self._tokens: Dict[str, str] = {} # agent_id -> token self._load_persistent_data() - + def _load_persistent_data(self): """Lädt persistente Daten (Passwort, Tokens).""" os.makedirs(DATA_DIR, exist_ok=True) - + # Config laden (Passwort) if os.path.exists(CONFIG_FILE): try: @@ -134,7 +134,7 @@ class DataStore: self._password_hash = config.get('password_hash') except: pass - + # Tokens laden if os.path.exists(TOKENS_FILE): try: @@ -142,37 +142,37 @@ class DataStore: self._tokens = json.load(f) except: pass - + def _save_config(self): """Speichert Config.""" with open(CONFIG_FILE, 'w') as f: json.dump({'password_hash': self._password_hash}, f) - + def _save_tokens(self): """Speichert Tokens.""" with open(TOKENS_FILE, 'w') as f: json.dump(self._tokens, f) - + # === Password === def get_password_hash(self) -> Optional[str]: return self._password_hash - + def set_password(self, password: str): self._password_hash = hashlib.sha256(password.encode()).hexdigest() self._save_config() - + def verify_password(self, password: str) -> bool: if not self._password_hash: return False return hashlib.sha256(password.encode()).hexdigest() == self._password_hash - + # === Sessions === def create_session(self, username: str) -> str: token = secrets.token_hex(32) expires = (utc_now() + timedelta(hours=24)).isoformat() self.sessions[token] = {'username': username, 'expires': expires} return token - + def verify_session(self, token: str) -> Optional[str]: if not token or token not in self.sessions: return None @@ -182,18 +182,18 @@ class DataStore: del self.sessions[token] return None return session['username'] - + def delete_session(self, token: str): self.sessions.pop(token, None) - + # === Agent Tokens === def get_agent_token(self, agent_id: str) -> Optional[str]: return self._tokens.get(agent_id) - + def set_agent_token(self, agent_id: str, token: str): self._tokens[agent_id] = token self._save_tokens() - + # === Agents === def get_or_create_agent(self, agent_id: str, hostname: str) -> AgentData: if agent_id not in self.agents: @@ -208,10 +208,10 @@ class DataStore: self.agents[agent_id].token = self._tokens[agent_id] self.agents[agent_id].status = 'online' return self.agents[agent_id] - + def get_agent(self, agent_id: str) -> Optional[AgentData]: return self.agents.get(agent_id) - + def get_all_agents(self) -> List[Dict]: result = [] for agent in self.agents.values(): @@ -224,7 +224,7 @@ class DataStore: status = 'offline' except: pass - + result.append({ 'id': agent.id, 'hostname': agent.hostname, @@ -239,14 +239,14 @@ class DataStore: 'shops_active': agent.shops_active }) return result - + # === Shops === def update_shop(self, agent_id: str, agent_hostname: str, shop_data: Dict) -> ShopData: domain = shop_data.get('domain') - + if domain not in self.shops: self.shops[domain] = ShopData(domain=domain, agent_id=agent_id) - + shop = self.shops[domain] shop.agent_id = agent_id shop.agent_hostname = agent_hostname @@ -260,7 +260,7 @@ class DataStore: shop.link11_ip = shop_data.get('link11_ip', '') shop.activated = shop_data.get('activated', '') shop.runtime_minutes = shop_data.get('runtime_minutes', 0) - + # Stats stats = shop_data.get('stats', {}) if stats: @@ -273,20 +273,20 @@ class DataStore: shop.unique_bots = stats.get('unique_bots', 0) shop.top_bots = stats.get('top_bots', {}) shop.top_ips = stats.get('top_ips', {}) - + # History für Graph shop.history.append({ 'timestamp': utc_now_str(), 'req_per_min': shop.req_per_min, 'active_bans': shop.active_bans }) - + return shop - + def update_shop_stats(self, domain: str, stats: Dict): 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) @@ -297,16 +297,16 @@ class DataStore: shop.unique_bots = stats.get('unique_bots', shop.unique_bots) shop.top_bots = stats.get('top_bots', shop.top_bots) shop.top_ips = stats.get('top_ips', shop.top_ips) - + timestamp = utc_now_str() - + # Gesamt-History shop.history.append({ 'timestamp': timestamp, 'req_per_min': shop.req_per_min, 'active_bans': shop.active_bans }) - + # Bot-History aktualisieren top_bots = stats.get('top_bots', {}) for bot_name, count in top_bots.items(): @@ -316,10 +316,10 @@ class DataStore: 'timestamp': timestamp, 'count': count }) - + def get_shop(self, domain: str) -> Optional[ShopData]: return self.shops.get(domain) - + def get_all_shops(self) -> List[Dict]: result = [] for shop in self.shops.values(): @@ -350,22 +350,22 @@ class DataStore: } }) return result - + def get_shop_history(self, domain: str) -> Dict: shop = self.shops.get(domain) if not shop: return {'history': [], 'bot_history': {}} - + # Bot-History in JSON-serialisierbares Format bot_history = {} for bot_name, history in shop.bot_history.items(): bot_history[bot_name] = list(history) - + return { 'history': list(shop.history), 'bot_history': bot_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.""" shops_list = [] @@ -378,30 +378,30 @@ class DataStore: 'active_bans': shop.active_bans, 'link11': shop.link11 }) - + # Sortieren if sort_by == 'active_bans': shops_list.sort(key=lambda x: x['active_bans'], reverse=True) else: shops_list.sort(key=lambda x: x['req_per_min'], reverse=True) - + if limit: return shops_list[:limit] return shops_list - + def get_stats(self) -> Dict: - agents_online = sum(1 for a in self.agents.values() + agents_online = sum(1 for a in self.agents.values() if a.approved and a.status == 'online') agents_pending = sum(1 for a in self.agents.values() if not a.approved) - + shops_active = sum(1 for s in self.shops.values() if s.status == 'active') shops_total = len(self.shops) shops_link11 = sum(1 for s in self.shops.values() if s.link11) 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()) - + return { 'agents_online': agents_online, 'agents_pending': agents_pending, @@ -423,12 +423,12 @@ store = DataStore() # ============================================================================= def generate_ssl_certificate(): os.makedirs(SSL_DIR, exist_ok=True) - + if os.path.exists(SSL_CERT) and os.path.exists(SSL_KEY): return - + print("🔐 Generiere SSL-Zertifikat...") - + try: subprocess.run([ 'openssl', 'req', '-x509', '-nodes', @@ -438,10 +438,10 @@ def generate_ssl_certificate(): '-out', SSL_CERT, '-subj', '/CN=jtl-wafi/O=JTL-WAFi/C=DE' ], check=True, capture_output=True) - + os.chmod(SSL_KEY, 0o600) os.chmod(SSL_CERT, 0o644) - + print(f"✅ SSL-Zertifikat generiert: {SSL_CERT}") except Exception as e: print(f"❌ SSL Fehler: {e}") @@ -456,7 +456,7 @@ class ConnectionManager: self.agent_connections: Dict[str, WebSocket] = {} self.browser_connections: Set[WebSocket] = set() self.agent_hostnames: Dict[str, str] = {} - + async def connect_agent(self, agent_id: str, hostname: str, websocket: WebSocket): # Alte Verbindung schließen if agent_id in self.agent_connections: @@ -464,36 +464,36 @@ class ConnectionManager: await self.agent_connections[agent_id].close() except: pass - + self.agent_connections[agent_id] = websocket self.agent_hostnames[agent_id] = hostname print(f"✅ Agent verbunden: {hostname}") - + async def disconnect_agent(self, agent_id: str): self.agent_connections.pop(agent_id, None) hostname = self.agent_hostnames.pop(agent_id, "unknown") - + # Status updaten agent = store.get_agent(agent_id) if agent: agent.status = 'offline' agent.last_seen = utc_now_str() - + print(f"❌ Agent getrennt: {hostname}") - + await self.broadcast_to_browsers({ 'type': 'agent.offline', 'data': {'agent_id': agent_id, 'hostname': hostname} }) - + async def connect_browser(self, websocket: WebSocket): self.browser_connections.add(websocket) print(f"🌐 Browser verbunden (Total: {len(self.browser_connections)})") - + async def disconnect_browser(self, websocket: WebSocket): self.browser_connections.discard(websocket) print(f"🌐 Browser getrennt (Total: {len(self.browser_connections)})") - + async def send_to_agent(self, agent_id: str, message: Dict): ws = self.agent_connections.get(agent_id) if ws: @@ -501,7 +501,7 @@ class ConnectionManager: await ws.send_json(message) except Exception as e: print(f"Send to agent error: {e}") - + async def broadcast_to_browsers(self, message: Dict): dead = set() for ws in self.browser_connections: @@ -510,11 +510,11 @@ class ConnectionManager: except: dead.add(ws) self.browser_connections -= dead - + def get_agent_for_shop(self, domain: str) -> Optional[str]: shop = store.get_shop(domain) return shop.agent_id if shop else None - + def is_agent_connected(self, agent_id: str) -> bool: return agent_id in self.agent_connections @@ -532,7 +532,7 @@ async def lifespan(app: FastAPI): app = FastAPI(title="JTL-WAFi Dashboard", version=VERSION, lifespan=lifespan) -app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, session_cookie="geoip_session", max_age=86400) +app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, session_cookie="jtl_wafi_session", max_age=86400) # ============================================================================= @@ -550,14 +550,14 @@ async def get_current_user(request: Request) -> Optional[str]: async def agent_websocket(websocket: WebSocket): await websocket.accept() agent_id = None - + try: async for message in websocket.iter_text(): try: data = json.loads(message) event_type = data.get('type') event_data = data.get('data', {}) - + if event_type == 'agent.connect': agent_id = event_data.get('agent_id') hostname = event_data.get('hostname') @@ -565,7 +565,7 @@ async def agent_websocket(websocket: WebSocket): version = event_data.get('version', '') os_info = event_data.get('os_info', {}) shops_summary = event_data.get('shops_summary', {}) - + # Agent registrieren agent = store.get_or_create_agent(agent_id, hostname) agent.hostname = hostname @@ -574,16 +574,16 @@ async def agent_websocket(websocket: WebSocket): agent.last_seen = utc_now_str() agent.shops_total = shops_summary.get('total', 0) agent.shops_active = shops_summary.get('active', 0) - + # Token prüfen stored_token = store.get_agent_token(agent_id) if stored_token and token == stored_token: agent.approved = True agent.token = stored_token agent.status = 'online' - + await manager.connect_agent(agent_id, hostname, websocket) - + # Browser informieren await manager.broadcast_to_browsers({ 'type': 'agent.online' if agent.approved else 'agent.pending', @@ -597,21 +597,21 @@ async def agent_websocket(websocket: WebSocket): 'shops_active': shops_summary.get('active', 0) } }) - + # Token senden wenn approved if agent.approved: await websocket.send_json({ 'type': 'auth.approved', 'data': {'token': agent.token} }) - + elif event_type == 'agent.heartbeat': if agent_id: agent = store.get_agent(agent_id) if agent: system = event_data.get('system', {}) shops_summary = event_data.get('shops_summary', {}) - + agent.last_seen = utc_now_str() agent.load_1m = system.get('load_1m', 0) agent.load_5m = system.get('load_5m', 0) @@ -619,7 +619,7 @@ async def agent_websocket(websocket: WebSocket): agent.uptime_seconds = system.get('uptime_seconds', 0) agent.shops_total = shops_summary.get('total', 0) agent.shops_active = shops_summary.get('active', 0) - + await manager.broadcast_to_browsers({ 'type': 'agent.update', 'data': { @@ -629,16 +629,16 @@ async def agent_websocket(websocket: WebSocket): 'shops_summary': shops_summary } }) - + elif event_type == 'shop.full_update': if agent_id: agent = store.get_agent(agent_id) hostname = agent.hostname if agent else '' shops = event_data.get('shops', []) - + for shop_data in shops: store.update_shop(agent_id, hostname, shop_data) - + await manager.broadcast_to_browsers({ 'type': 'shop.full_update', 'data': { @@ -647,42 +647,42 @@ async def agent_websocket(websocket: WebSocket): 'shops': shops } }) - + elif event_type == 'shop.stats': if agent_id: domain = event_data.get('domain') stats = event_data.get('stats', {}) - + store.update_shop_stats(domain, stats) - + await manager.broadcast_to_browsers({ 'type': 'shop.stats', 'data': {'domain': domain, 'stats': stats} }) - + elif event_type == 'log.entry': await manager.broadcast_to_browsers({ 'type': 'log.entry', 'data': event_data }) - + elif event_type == 'bot.banned': await manager.broadcast_to_browsers({ 'type': 'bot.banned', 'data': event_data }) - + elif event_type == 'command.result': await manager.broadcast_to_browsers({ 'type': 'command.result', 'data': event_data }) - + except json.JSONDecodeError: pass except Exception as e: print(f"Agent message error: {e}") - + except WebSocketDisconnect: pass except Exception as e: @@ -699,7 +699,7 @@ async def agent_websocket(websocket: WebSocket): async def dashboard_websocket(websocket: WebSocket): await websocket.accept() await manager.connect_browser(websocket) - + try: # Initial state senden await websocket.send_json({ @@ -710,13 +710,13 @@ async def dashboard_websocket(websocket: WebSocket): 'stats': store.get_stats() } }) - + async for message in websocket.iter_text(): try: data = json.loads(message) event_type = data.get('type') event_data = data.get('data', {}) - + if event_type == 'log.subscribe': domain = event_data.get('shop') agent_id = manager.get_agent_for_shop(domain) @@ -725,7 +725,7 @@ async def dashboard_websocket(websocket: WebSocket): 'type': 'log.subscribe', 'data': {'shop': domain} }) - + elif event_type == 'log.unsubscribe': domain = event_data.get('shop') agent_id = manager.get_agent_for_shop(domain) @@ -734,7 +734,7 @@ async def dashboard_websocket(websocket: WebSocket): 'type': 'log.unsubscribe', 'data': {'shop': domain} }) - + elif event_type == 'get_shop_history': domain = event_data.get('domain') data = store.get_shop_history(domain) @@ -742,7 +742,7 @@ async def dashboard_websocket(websocket: WebSocket): 'type': 'shop_history', 'data': {'domain': domain, **data} }) - + elif event_type == 'get_top_shops': sort_by = event_data.get('sort_by', 'req_per_min') limit = event_data.get('limit', 10) @@ -751,7 +751,7 @@ async def dashboard_websocket(websocket: WebSocket): 'type': 'top_shops', 'data': {'shops': shops, 'sort_by': sort_by} }) - + elif event_type == 'get_all_shops_sorted': sort_by = event_data.get('sort_by', 'req_per_min') shops = store.get_top_shops(limit=None, sort_by=sort_by) @@ -759,7 +759,7 @@ async def dashboard_websocket(websocket: WebSocket): 'type': 'all_shops_sorted', 'data': {'shops': shops, 'sort_by': sort_by} }) - + elif event_type == 'refresh': await websocket.send_json({ 'type': 'refresh', @@ -769,10 +769,10 @@ async def dashboard_websocket(websocket: WebSocket): 'stats': store.get_stats() } }) - + except Exception as e: print(f"Browser message error: {e}") - + except WebSocketDisconnect: pass except Exception as e: @@ -833,30 +833,30 @@ async def approve_agent(agent_id: str, request: Request): user = await get_current_user(request) if not user: raise HTTPException(401) - + agent = store.get_agent(agent_id) if not agent: raise HTTPException(404, "Agent nicht gefunden") - + # Token generieren token = secrets.token_hex(32) agent.approved = True agent.token = token agent.status = 'online' store.set_agent_token(agent_id, token) - + # Token an Agent senden if manager.is_agent_connected(agent_id): await manager.send_to_agent(agent_id, { 'type': 'auth.approved', 'data': {'token': token} }) - + await manager.broadcast_to_browsers({ 'type': 'agent.approved', 'data': {'agent_id': agent_id} }) - + return {"success": True} @@ -873,14 +873,14 @@ async def activate_shop( 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') - + agent_id = manager.get_agent_for_shop(domain) if not agent_id or not manager.is_agent_connected(agent_id): return JSONResponse({"success": False, "error": "Agent nicht verbunden"}) - + command_id = secrets.token_hex(8) await manager.send_to_agent(agent_id, { 'type': 'command.activate', @@ -894,7 +894,7 @@ async def activate_shop( 'bot_monitor_only': is_monitor_only if mode == 'bot' else False } }) - + return {"success": True, "command_id": command_id} @@ -903,17 +903,17 @@ async def deactivate_shop(request: Request, domain: str = Form(...)): user = await get_current_user(request) if not user: raise HTTPException(401) - + agent_id = manager.get_agent_for_shop(domain) if not agent_id or not manager.is_agent_connected(agent_id): return JSONResponse({"success": False, "error": "Agent nicht verbunden"}) - + command_id = secrets.token_hex(8) await manager.send_to_agent(agent_id, { 'type': 'command.deactivate', 'data': {'command_id': command_id, 'shop': domain} }) - + return {"success": True, "command_id": command_id} @@ -930,26 +930,26 @@ async def bulk_activate( 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') - + activated = 0 shops = store.get_all_shops() - + for shop in shops: if shop['status'] == 'active': continue - + if filter_type == 'direct' and shop['link11']: continue if filter_type == 'link11' and not shop['link11']: continue - + agent_id = shop.get('agent_id') if not agent_id or not manager.is_agent_connected(agent_id): continue - + command_id = secrets.token_hex(8) await manager.send_to_agent(agent_id, { 'type': 'command.activate', @@ -964,11 +964,11 @@ async def bulk_activate( } }) activated += 1 - + # Kleine Pause um nicht zu überlasten if activated % 5 == 0: await asyncio.sleep(0.1) - + return {"success": True, "activated": activated} @@ -977,33 +977,33 @@ async def bulk_deactivate(request: Request, filter_type: str = Form("all")): user = await get_current_user(request) if not user: raise HTTPException(401) - + deactivated = 0 shops = store.get_all_shops() - + for shop in shops: if shop['status'] != 'active': continue - + if filter_type == 'direct' and shop['link11']: continue if filter_type == 'link11' and not shop['link11']: continue - + agent_id = shop.get('agent_id') if not agent_id or not manager.is_agent_connected(agent_id): continue - + command_id = secrets.token_hex(8) await manager.send_to_agent(agent_id, { 'type': 'command.deactivate', 'data': {'command_id': command_id, 'shop': shop['domain']} }) deactivated += 1 - + if deactivated % 5 == 0: await asyncio.sleep(0.1) - + return {"success": True, "deactivated": deactivated} @@ -1017,14 +1017,14 @@ async def change_password( user = await get_current_user(request) if not user: raise HTTPException(401) - + if not store.verify_password(current): return {"success": False, "error": "Aktuelles Passwort falsch"} if new_pw != confirm: return {"success": False, "error": "Neue Passwörter stimmen nicht überein"} if len(new_pw) < 8: return {"success": False, "error": "Mindestens 8 Zeichen"} - + store.set_password(new_pw) return {"success": True} @@ -1034,7 +1034,7 @@ async def get_shop_history_api(domain: str, request: Request): user = await get_current_user(request) if not user: raise HTTPException(401) - + data = store.get_shop_history(domain) return {"domain": domain, **data} @@ -1368,27 +1368,27 @@ def main(): parser.add_argument("--no-ssl", action="store_true") parser.add_argument("--install-service", action="store_true") args = parser.parse_args() - + if args.install_service: create_systemd_service() return - + os.makedirs(DATA_DIR, exist_ok=True) - + print("=" * 60) print(f"🌍 JTL-WAFi Dashboard v{VERSION} (In-Memory)") print("=" * 60) print(f"Host: {args.host}:{args.port}") print(f"SSL: {'Nein' if args.no_ssl else 'Ja'}") print("=" * 60) - + ssl_config = {} if not args.no_ssl: generate_ssl_certificate() ssl_config = {"ssl_certfile": SSL_CERT, "ssl_keyfile": SSL_KEY} - + uvicorn.run(app, host=args.host, port=args.port, **ssl_config, log_level="info") if __name__ == "__main__": - main() \ No newline at end of file + main()