jtl-wafi-dashboard.py aktualisiert

This commit is contained in:
2025-12-22 14:11:54 +01:00
parent fd9dc5d79c
commit 0a62fabacd

View File

@@ -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()
main()