jtl-wafi-dashboard.py aktualisiert

This commit is contained in:
2025-12-19 22:08:27 +01:00
parent 7ed0388b05
commit 7295133b51

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
GeoIP Dashboard v2.2.0 - WebSocket Real-Time Dashboard
JTL-WAFi Dashboard v2.2.0 - WebSocket Real-Time Dashboard
ÄNDERUNG: Keine SQLite mehr für Echtzeit-Daten!
- Alle Agent/Shop-Daten im Memory
@@ -33,12 +33,12 @@ import uvicorn
# =============================================================================
VERSION = "2.3.0"
DATA_DIR = "/var/lib/geoip-dashboard"
SSL_DIR = "/var/lib/geoip-dashboard/ssl"
SSL_CERT = "/var/lib/geoip-dashboard/ssl/server.crt"
SSL_KEY = "/var/lib/geoip-dashboard/ssl/server.key"
CONFIG_FILE = "/var/lib/geoip-dashboard/config.json"
TOKENS_FILE = "/var/lib/geoip-dashboard/tokens.json"
DATA_DIR = "/var/lib/jtl-wafi"
SSL_DIR = "/var/lib/jtl-wafi/ssl"
SSL_CERT = "/var/lib/jtl-wafi/ssl/server.crt"
SSL_KEY = "/var/lib/jtl-wafi/ssl/server.key"
CONFIG_FILE = "/var/lib/jtl-wafi/config.json"
TOKENS_FILE = "/var/lib/jtl-wafi/tokens.json"
AGENT_TIMEOUT = 120
HISTORY_MAX_POINTS = 1000 # Max Datenpunkte pro Shop
@@ -433,7 +433,7 @@ def generate_ssl_certificate():
'-newkey', 'rsa:2048',
'-keyout', SSL_KEY,
'-out', SSL_CERT,
'-subj', '/CN=geoip-dashboard/O=GeoIP/C=DE'
'-subj', '/CN=jtl-wafi/O=JTL-WAFi/C=DE'
], check=True, capture_output=True)
os.chmod(SSL_KEY, 0o600)
@@ -528,7 +528,7 @@ async def lifespan(app: FastAPI):
yield
app = FastAPI(title="GeoIP Dashboard", version=VERSION, lifespan=lifespan)
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)
@@ -1036,7 +1036,7 @@ def get_setup_html(error: str = None) -> str:
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoIP Dashboard - Setup</title>
<title>JTL-WAFi Dashboard - Setup</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; color: #e0e0e0; }}
@@ -1075,7 +1075,7 @@ def get_login_html(error: str = None) -> str:
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoIP Dashboard - Login</title>
<title>JTL-WAFi Dashboard - Login</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; color: #e0e0e0; }}
@@ -1090,7 +1090,7 @@ def get_login_html(error: str = None) -> str:
</head>
<body>
<div class="container">
<h1>🌍 GeoIP Dashboard</h1>
<h1>🌍 JTL-WAFi Dashboard</h1>
{error_html}
<form method="POST" action="/login">
<input type="password" name="password" required placeholder="Passwort" autofocus>
@@ -1107,7 +1107,7 @@ def get_dashboard_html() -> str:
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoIP Dashboard v2.3</title>
<title>JTL-WAFi Dashboard v2.3</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
@@ -1227,7 +1227,7 @@ def get_dashboard_html() -> str:
</head>
<body>
<header>
<div class="logo">🌍 GeoIP <span>Dashboard</span> <small style="font-size:11px;opacity:0.5">v2.3</small></div>
<div class="logo">🌍 JTL-<span>WAFi</span> <small style="font-size:11px;opacity:0.5">v2.3</small></div>
<div class="header-right">
<div class="clock" id="clock">--:--:--</div>
<div class="connection-status"><div class="status-dot" id="wsStatus"></div><span id="wsStatusText">Verbinde...</span></div>
@@ -1269,8 +1269,8 @@ def get_dashboard_html() -> str:
</div>
</main>
<div class="logs-panel" id="logsPanel"><div class="logs-header"><span>📜 Live Logs: <span id="logsShop">-</span></span><button class="btn btn-secondary" onclick="closeLogs()">✕</button></div><div class="logs-content" id="logsContent"></div></div>
<div class="modal-overlay" id="activateModal"><div class="modal"><h3 class="modal-title">🚀 Shop aktivieren</h3><form id="activateForm"><input type="hidden" name="domain" id="activateDomain"><div class="form-group"><label>Domain</label><input type="text" id="activateDomainDisplay" readonly></div><div class="form-group"><label>Modus</label><select name="mode" id="activateMode" onchange="toggleModeOptions()"><option value="bot">🤖 Bot Rate-Limiting</option><option value="geoip">🌍 GeoIP-Blocking</option></select></div><div class="form-group" id="geoRegionGroup" style="display:none"><label>Region</label><select name="geo_region"><option value="dach">🇩🇪🇦🇹🇨🇭 DACH</option><option value="eurozone">🇪🇺 Eurozone</option></select></div><div class="form-group" id="rateLimitGroup"><label>Rate-Limit (Req/min)</label><input type="number" name="rate_limit" value="30" min="1"></div><div class="form-group" id="banDurationGroup"><label>Ban-Dauer (Sekunden)</label><input type="number" name="ban_duration" value="300" min="60"><small>300=5min</small></div><div class="modal-actions"><button type="button" class="btn btn-secondary" onclick="closeModal('activateModal')">Abbrechen</button><button type="submit" class="btn btn-primary">Aktivieren</button></div></form></div></div>
<div class="modal-overlay" id="bulkActivateModal"><div class="modal"><h3 class="modal-title">⚡ Massenaktivierung</h3><form id="bulkActivateForm"><div class="form-group"><label>Modus</label><select name="mode" id="bulkActivateMode" onchange="toggleBulkModeOptions()"><option value="bot">🤖 Bot Rate-Limiting</option><option value="geoip">🌍 GeoIP-Blocking</option></select></div><div class="form-group" id="bulkGeoRegionGroup" style="display:none"><label>Region</label><select name="geo_region"><option value="dach">🇩🇪🇦🇹🇨🇭 DACH</option><option value="eurozone">🇪🇺 Eurozone</option></select></div><div class="form-group" id="bulkRateLimitGroup"><label>Rate-Limit (Req/min)</label><input type="number" name="rate_limit" value="30" min="1"></div><div class="form-group" id="bulkBanDurationGroup"><label>Ban-Dauer (Sek)</label><input type="number" name="ban_duration" value="300" min="60"><small>300=5min, 3600=1h</small></div><div class="form-group"><label>Filter</label><select name="filter_type"><option value="all">Alle inaktiven</option><option value="direct">Nur Direkte ⚡</option><option value="link11">Nur Link11 🛡️</option></select></div><div class="modal-actions"><button type="button" class="btn btn-secondary" onclick="closeModal('bulkActivateModal')">Abbrechen</button><button type="submit" class="btn btn-success">▶️ Aktivieren</button></div></form></div></div>
<div class="modal-overlay" id="activateModal"><div class="modal"><h3 class="modal-title">🚀 Shop aktivieren</h3><form id="activateForm"><input type="hidden" name="domain" id="activateDomain"><div class="form-group"><label>Domain</label><input type="text" id="activateDomainDisplay" readonly></div><div class="form-group"><label>Modus</label><select name="mode" id="activateMode" onchange="toggleModeOptions()"><option value="bot">🤖 Bot Rate-Limiting</option><option value="geoip">🛡️ JTL-WAFi-Blocking</option></select></div><div class="form-group" id="geoRegionGroup" style="display:none"><label>Region</label><select name="geo_region"><option value="dach">🇩🇪🇦🇹🇨🇭 DACH</option><option value="eurozone">🇪🇺 Eurozone</option></select></div><div class="form-group" id="rateLimitGroup"><label>Rate-Limit (Req/min)</label><input type="number" name="rate_limit" value="30" min="1"></div><div class="form-group" id="banDurationGroup"><label>Ban-Dauer (Sekunden)</label><input type="number" name="ban_duration" value="300" min="60"><small>300=5min</small></div><div class="modal-actions"><button type="button" class="btn btn-secondary" onclick="closeModal('activateModal')">Abbrechen</button><button type="submit" class="btn btn-primary">Aktivieren</button></div></form></div></div>
<div class="modal-overlay" id="bulkActivateModal"><div class="modal"><h3 class="modal-title">⚡ Massenaktivierung</h3><form id="bulkActivateForm"><div class="form-group"><label>Modus</label><select name="mode" id="bulkActivateMode" onchange="toggleBulkModeOptions()"><option value="bot">🤖 Bot Rate-Limiting</option><option value="geoip">🛡️ JTL-WAFi-Blocking</option></select></div><div class="form-group" id="bulkGeoRegionGroup" style="display:none"><label>Region</label><select name="geo_region"><option value="dach">🇩🇪🇦🇹🇨🇭 DACH</option><option value="eurozone">🇪🇺 Eurozone</option></select></div><div class="form-group" id="bulkRateLimitGroup"><label>Rate-Limit (Req/min)</label><input type="number" name="rate_limit" value="30" min="1"></div><div class="form-group" id="bulkBanDurationGroup"><label>Ban-Dauer (Sek)</label><input type="number" name="ban_duration" value="300" min="60"><small>300=5min, 3600=1h</small></div><div class="form-group"><label>Filter</label><select name="filter_type"><option value="all">Alle inaktiven</option><option value="direct">Nur Direkte ⚡</option><option value="link11">Nur Link11 🛡️</option></select></div><div class="modal-actions"><button type="button" class="btn btn-secondary" onclick="closeModal('bulkActivateModal')">Abbrechen</button><button type="submit" class="btn btn-success">▶️ Aktivieren</button></div></form></div></div>
<div class="modal-overlay" id="bulkDeactivateModal"><div class="modal"><h3 class="modal-title">⏹️ Massendeaktivierung</h3><form id="bulkDeactivateForm"><div class="form-group"><label>Filter</label><select name="filter_type"><option value="all">Alle aktiven</option><option value="direct">Nur Direkte ⚡</option><option value="link11">Nur Link11 🛡️</option></select></div><div class="modal-actions"><button type="button" class="btn btn-secondary" onclick="closeModal('bulkDeactivateModal')">Abbrechen</button><button type="submit" class="btn btn-danger">⏹️ Deaktivieren</button></div></form></div></div>
<div class="modal-overlay" id="passwordModal"><div class="modal"><h3 class="modal-title">🔑 Passwort ändern</h3><form id="passwordForm"><div class="form-group"><label>Aktuelles Passwort</label><input type="password" name="current" required></div><div class="form-group"><label>Neues Passwort</label><input type="password" name="new_pw" required minlength="8"></div><div class="form-group"><label>Bestätigen</label><input type="password" name="confirm" required></div><div class="modal-actions"><button type="button" class="btn btn-secondary" onclick="closeModal('passwordModal')">Abbrechen</button><button type="submit" class="btn btn-primary">Speichern</button></div></form></div></div>
<div class="modal-overlay" id="allShopsModal"><div class="modal xlarge"><h3 class="modal-title"><span>📊 Alle Shops</span><button class="btn btn-secondary" onclick="closeModal('allShopsModal')">✕</button></h3><div style="margin-bottom:16px"><button class="btn" id="sortByReq" onclick="sortAllShops('req_per_min')">Nach Requests</button> <button class="btn btn-secondary" id="sortByBans" onclick="sortAllShops('active_bans')">Nach Bans</button></div><table><thead><tr><th>#</th><th>Domain</th><th>Server</th><th>Status</th><th>Req/min</th><th>Bans</th><th>Typ</th></tr></thead><tbody id="allShopsTable"></tbody></table></div></div>
@@ -1305,7 +1305,7 @@ def get_dashboard_html() -> str:
function openAllShopsModal(){document.getElementById('allShopsModal').classList.add('open');sortAllShops('req_per_min');}
function sortAllShops(by){currentSortBy=by;document.getElementById('sortByReq').className='btn'+(by==='req_per_min'?' btn-primary':' btn-secondary');document.getElementById('sortByBans').className='btn'+(by==='active_bans'?' btn-primary':' btn-secondary');if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'get_all_shops_sorted',data:{sort_by:by}}));}
function renderAllShopsTable(l,by){document.getElementById('allShopsTable').innerHTML=l.map((s,i)=>'<tr onclick="openDetailModal(\\''+s.domain+'\\')" style="cursor:pointer"><td>'+(i+1)+'</td><td><strong>'+s.domain+'</strong></td><td>'+(s.agent_hostname||'-')+'</td><td><span class="status-badge status-'+(s.status||'inactive')+'">'+(s.status==='active'?'':'')+'</span></td><td style="'+(by==='req_per_min'?'color:var(--accent);font-weight:600':'')+'">'+(s.req_per_min||0).toFixed(1)+'</td><td style="'+(by==='active_bans'?'color:var(--warning);font-weight:600':'')+'">'+(s.active_bans||0)+'</td><td>'+(s.link11?'🛡️':'')+'</td></tr>').join('');}
function openDetailModal(d){const s=shops[d];if(!s)return;document.getElementById('detailDomain').textContent=d;document.getElementById('detailStatus').textContent=s.status==='active'?'✅ Aktiv':'⭕ Inaktiv';document.getElementById('detailMode').textContent=s.mode==='bot'?'🤖 Bot':s.mode==='geoip'?'🌍 GeoIP':'-';document.getElementById('detailRegion').textContent=s.mode==='geoip'&&s.geo_region?s.geo_region.toUpperCase():(s.mode==='bot'?'NONE':'-');document.getElementById('detailRateLimit').textContent=s.rate_limit?s.rate_limit+'/min':'-';document.getElementById('detailBanDuration').textContent=s.ban_duration?(s.ban_duration>=60?Math.round(s.ban_duration/60)+' min':s.ban_duration+'s'):'-';document.getElementById('detailRuntime').textContent=formatRuntime(s.runtime_minutes);const st=s.stats||{};document.getElementById('detailReqMin').textContent=(st.req_per_min||0).toFixed(1);document.getElementById('detailActiveBans').textContent=st.active_bans||0;document.getElementById('detailTotalBans').textContent=st.total_bans||0;document.getElementById('detailTopBots').innerHTML=Object.entries(st.top_bots||{}).sort((a,b)=>b[1]-a[1]).map(([n,c])=>'<div class="bot-item"><span>'+n+'</span><span>'+c+'</span></div>').join('')||'<div style="color:var(--text-secondary);padding:8px">Keine Daten</div>';document.getElementById('detailBannedBots').innerHTML=(st.banned_bots||[]).map(n=>'<div class="bot-item"><span>🚫 '+n+'</span></div>').join('')||'<div style="color:var(--text-secondary);padding:8px">Keine Bans</div>';document.getElementById('chartLegend').innerHTML='';const cv=document.getElementById('requestChart');cv.getContext('2d').clearRect(0,0,cv.width,cv.height);if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'get_shop_history',data:{domain:d}}));document.getElementById('detailModal').classList.add('open');}
function openDetailModal(d){const s=shops[d];if(!s)return;document.getElementById('detailDomain').textContent=d;document.getElementById('detailStatus').textContent=s.status==='active'?'✅ Aktiv':'⭕ Inaktiv';document.getElementById('detailMode').textContent=s.mode==='bot'?'🤖 Bot':s.mode==='geoip'?'🛡️ JTL-WAFi':'-';document.getElementById('detailRegion').textContent=s.mode==='geoip'&&s.geo_region?s.geo_region.toUpperCase():(s.mode==='bot'?'NONE':'-');document.getElementById('detailRateLimit').textContent=s.rate_limit?s.rate_limit+'/min':'-';document.getElementById('detailBanDuration').textContent=s.ban_duration?(s.ban_duration>=60?Math.round(s.ban_duration/60)+' min':s.ban_duration+'s'):'-';document.getElementById('detailRuntime').textContent=formatRuntime(s.runtime_minutes);const st=s.stats||{};document.getElementById('detailReqMin').textContent=(st.req_per_min||0).toFixed(1);document.getElementById('detailActiveBans').textContent=st.active_bans||0;document.getElementById('detailTotalBans').textContent=st.total_bans||0;document.getElementById('detailTopBots').innerHTML=Object.entries(st.top_bots||{}).sort((a,b)=>b[1]-a[1]).map(([n,c])=>'<div class="bot-item"><span>'+n+'</span><span>'+c+'</span></div>').join('')||'<div style="color:var(--text-secondary);padding:8px">Keine Daten</div>';document.getElementById('detailBannedBots').innerHTML=(st.banned_bots||[]).map(n=>'<div class="bot-item"><span>🚫 '+n+'</span></div>').join('')||'<div style="color:var(--text-secondary);padding:8px">Keine Bans</div>';document.getElementById('chartLegend').innerHTML='';const cv=document.getElementById('requestChart');cv.getContext('2d').clearRect(0,0,cv.width,cv.height);if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'get_shop_history',data:{domain:d}}));document.getElementById('detailModal').classList.add('open');}
function updateBotChart(data){const cv=document.getElementById('requestChart'),ctx=cv.getContext('2d'),ct=cv.parentElement,w=ct.clientWidth-32,h=230;cv.width=w;cv.height=h;ctx.clearRect(0,0,w,h);const bh=data.bot_history||{},bn=Object.keys(bh).slice(0,10);if(bn.length===0){ctx.fillStyle='#a0a0b0';ctx.font='14px sans-serif';ctx.fillText('Noch keine Bot-Daten',w/2-60,h/2);return;}let ts=new Set();bn.forEach(b=>bh[b].forEach(p=>ts.add(p.timestamp)));ts=[...ts].sort();if(ts.length<2){ctx.fillStyle='#a0a0b0';ctx.font='14px sans-serif';ctx.fillText('Warte auf Daten...',w/2-50,h/2);return;}let mx=1;bn.forEach(b=>bh[b].forEach(p=>{if(p.count>mx)mx=p.count;}));const pd={t:20,r:20,b:40,l:50},cW=w-pd.l-pd.r,cH=h-pd.t-pd.b;ctx.strokeStyle='rgba(255,255,255,0.1)';ctx.lineWidth=1;for(let i=0;i<=4;i++){const y=pd.t+(cH/4)*i;ctx.beginPath();ctx.moveTo(pd.l,y);ctx.lineTo(w-pd.r,y);ctx.stroke();ctx.fillStyle='#a0a0b0';ctx.font='10px sans-serif';ctx.fillText(Math.round(mx-(mx/4)*i),5,y+4);}const step=Math.max(1,Math.floor(ts.length/6));ctx.fillStyle='#a0a0b0';ctx.font='10px sans-serif';for(let i=0;i<ts.length;i+=step){const t=ts[i].split(' ')[1]?.substring(0,5)||ts[i],x=pd.l+(cW/(ts.length-1))*i;ctx.fillText(t,x-15,h-10);}const lg=document.getElementById('chartLegend');lg.innerHTML='';bn.forEach((bot,idx)=>{const c=BOT_COLORS[idx%BOT_COLORS.length],pts=bh[bot];ctx.strokeStyle=c;ctx.lineWidth=2;ctx.beginPath();pts.forEach((p,i)=>{const ti=ts.indexOf(p.timestamp),x=pd.l+(cW/(ts.length-1))*ti,y=pd.t+cH-(p.count/mx)*cH;i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});ctx.stroke();lg.innerHTML+='<div class="legend-item"><div class="legend-color" style="background:'+c+'"></div><span>'+bot+'</span></div>';});}
function openLogs(d){currentLogsShop=d;document.getElementById('logsShop').textContent=d;document.getElementById('logsContent').innerHTML='<div style="color:#666">Warte auf Logs...</div>';document.getElementById('logsPanel').classList.add('open');if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'log.subscribe',data:{shop:d}}));}
function closeLogs(){if(currentLogsShop&&ws&&ws.readyState===1)ws.send(JSON.stringify({type:'log.unsubscribe',data:{shop:currentLogsShop}}));currentLogsShop=null;document.getElementById('logsPanel').classList.remove('open');}
@@ -1321,12 +1321,12 @@ def get_dashboard_html() -> str:
def create_systemd_service():
service = """[Unit]
Description=GeoIP Dashboard v2.3 (WebSocket)
Description=JTL-WAFi Dashboard v2.3 (WebSocket)
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/geoip-dashboard/geoip_dashboard.py
ExecStart=/usr/bin/python3 /opt/jtl-wafi/dashboard.py
Restart=always
RestartSec=10
User=root
@@ -1335,15 +1335,15 @@ Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
"""
with open("/etc/systemd/system/geoip-dashboard.service", 'w') as f:
with open("/etc/systemd/system/jtl-wafi.service", 'w') as f:
f.write(service)
print("✅ Service erstellt")
print(" systemctl daemon-reload && systemctl enable --now geoip-dashboard")
print(" systemctl daemon-reload && systemctl enable --now jtl-wafi")
def main():
import argparse
parser = argparse.ArgumentParser(description=f"GeoIP Dashboard v{VERSION}")
parser = argparse.ArgumentParser(description=f"JTL-WAFi Dashboard v{VERSION}")
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--port", type=int, default=8000)
parser.add_argument("--no-ssl", action="store_true")
@@ -1357,7 +1357,7 @@ def main():
os.makedirs(DATA_DIR, exist_ok=True)
print("=" * 60)
print(f"🌍 GeoIP Dashboard v{VERSION} (In-Memory)")
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'}")
@@ -1372,4 +1372,4 @@ def main():
if __name__ == "__main__":
main()
main()