show url requests

This commit is contained in:
Thomas Ciesla
2026-01-09 15:11:30 +01:00
parent 13fd0daf6d
commit a435ba5c8c
2 changed files with 71 additions and 11 deletions

View File

@@ -1331,6 +1331,10 @@ class LiveStatsTracker:
self.blocked_paths: Dict[str, List[float]] = {} # Path -> [blocked timestamps] self.blocked_paths: Dict[str, List[float]] = {} # Path -> [blocked timestamps]
self.ip_404s: Dict[str, List[float]] = {} # IP -> [404 timestamps] self.ip_404s: Dict[str, List[float]] = {} # IP -> [404 timestamps]
# IP Request Details (für IP-Detail Popup)
self.ip_request_details: Dict[str, List[Dict]] = {} # IP -> [{ts, path, ua}, ...]
self.max_request_details = 100 # Max Anzahl Details pro IP
# Human/Bot Counters # Human/Bot Counters
self.human_requests: List[float] = [] self.human_requests: List[float] = []
self.bot_requests: List[float] = [] self.bot_requests: List[float] = []
@@ -1353,7 +1357,13 @@ class LiveStatsTracker:
self.human_requests = [t for t in self.human_requests if t > cutoff] self.human_requests = [t for t in self.human_requests if t > cutoff]
self.bot_requests = [t for t in self.bot_requests if t > cutoff] self.bot_requests = [t for t in self.bot_requests if t > cutoff]
def record_request(self, ip: str, path: str, is_bot: bool, is_blocked: bool = False, is_404: bool = False): # Request Details cleanup
for ip in list(self.ip_request_details.keys()):
self.ip_request_details[ip] = [r for r in self.ip_request_details[ip] if r['ts'] > cutoff]
if not self.ip_request_details[ip]:
del self.ip_request_details[ip]
def record_request(self, ip: str, path: str, is_bot: bool, is_blocked: bool = False, is_404: bool = False, user_agent: str = ''):
"""Zeichnet einen Request auf.""" """Zeichnet einen Request auf."""
now = time.time() now = time.time()
@@ -1389,6 +1399,30 @@ class LiveStatsTracker:
if ip not in self.ip_info: if ip not in self.ip_info:
self.ip_info[ip] = get_ip_info(ip) self.ip_info[ip] = get_ip_info(ip)
# Request Details speichern (für IP-Detail Popup)
if ip not in self.ip_request_details:
self.ip_request_details[ip] = []
self.ip_request_details[ip].append({
'ts': now,
'path': path,
'ua': user_agent,
'blocked': is_blocked
})
# Limitieren auf max_request_details
if len(self.ip_request_details[ip]) > self.max_request_details:
self.ip_request_details[ip] = self.ip_request_details[ip][-self.max_request_details:]
def get_ip_requests(self, ip: str, limit: int = 0) -> List[Dict]:
"""Gibt die Requests einer IP zurück. limit=0 für alle."""
self.cleanup_old_data()
if ip not in self.ip_request_details:
return []
# Neueste zuerst
all_requests = list(reversed(self.ip_request_details[ip]))
if limit > 0:
return all_requests[:limit]
return all_requests
def get_top_ips(self, limit: int = 10) -> List[Dict[str, Any]]: def get_top_ips(self, limit: int = 10) -> List[Dict[str, Any]]:
"""Gibt Top IPs mit Zusatzinfos zurück.""" """Gibt Top IPs mit Zusatzinfos zurück."""
self.cleanup_old_data() self.cleanup_old_data()
@@ -3858,6 +3892,13 @@ class JTLWAFiAgent:
if 'Path: ' in line: if 'Path: ' in line:
path = line.split('Path: ')[1].split(' |')[0].strip() path = line.split('Path: ')[1].split(' |')[0].strip()
# User-Agent extrahieren
user_agent = ''
if 'UA: ' in line:
user_agent = line.split('UA: ')[1].split(' |')[0].strip()
elif 'User-Agent: ' in line:
user_agent = line.split('User-Agent: ')[1].split(' |')[0].strip()
# Bot/Human erkennen # Bot/Human erkennen
is_bot = any(x in line for x in ['BOT: ', 'BOT:', 'BLOCKED_BOT:', 'MONITOR_BOT:', 'BANNED_BOT:']) is_bot = any(x in line for x in ['BOT: ', 'BOT:', 'BLOCKED_BOT:', 'MONITOR_BOT:', 'BANNED_BOT:'])
@@ -3868,7 +3909,7 @@ class JTLWAFiAgent:
is_404 = '404' in line is_404 = '404' in line
if ip: if ip:
tracker.record_request(ip, path, is_bot, is_blocked, is_404) tracker.record_request(ip, path, is_bot, is_blocked, is_404, user_agent)
except Exception as e: except Exception as e:
logger.debug(f"LiveStats record error: {e}") logger.debug(f"LiveStats record error: {e}")
@@ -4490,9 +4531,10 @@ class JTLWAFiAgent:
}) })
async def _handle_whois_command(self, data: Dict[str, Any]): async def _handle_whois_command(self, data: Dict[str, Any]):
"""Führt WHOIS-Lookup für eine IP durch und sendet Ergebnis.""" """Führt WHOIS-Lookup für eine IP durch und sendet Ergebnis mit Request-Historie."""
command_id = data.get('command_id', 'unknown') command_id = data.get('command_id', 'unknown')
ip = data.get('ip') ip = data.get('ip')
shop = data.get('shop') # Optional: für Request-Historie
if not ip: if not ip:
await self._send_event('whois_result', { await self._send_event('whois_result', {
@@ -4506,7 +4548,23 @@ class JTLWAFiAgent:
# WHOIS-Lookup durchführen (wird gecacht) # WHOIS-Lookup durchführen (wird gecacht)
result = whois_lookup(ip) result = whois_lookup(ip)
# Ergebnis senden (ohne raw für kleinere Payload) # Request-Historie für diese IP sammeln (aus allen Shops wenn kein Shop angegeben)
requests = []
if shop:
tracker = get_shop_stats_tracker(shop)
requests = tracker.get_ip_requests(ip) # Alle Requests
else:
# Aus allen aktiven Shops sammeln
for s in list(_shop_stats_trackers.keys()):
tracker = get_shop_stats_tracker(s)
shop_requests = tracker.get_ip_requests(ip) # Alle Requests
for r in shop_requests:
r['shop'] = s
requests.extend(shop_requests)
# Nach Zeit sortieren (neueste zuerst)
requests.sort(key=lambda x: x['ts'], reverse=True)
# Ergebnis senden
await self._send_event('whois_result', { await self._send_event('whois_result', {
'command_id': command_id, 'command_id': command_id,
'success': True, 'success': True,
@@ -4516,10 +4574,11 @@ class JTLWAFiAgent:
'asn': result.get('asn', ''), 'asn': result.get('asn', ''),
'country': result.get('country', ''), 'country': result.get('country', ''),
'abuse': result.get('abuse', ''), 'abuse': result.get('abuse', ''),
'range': result.get('range', '') 'range': result.get('range', ''),
'requests': requests
}) })
logger.info(f"WHOIS für {ip}: {result.get('org', 'Unknown')} ({result.get('asn', 'N/A')})") logger.info(f"WHOIS für {ip}: {result.get('org', 'Unknown')} ({result.get('asn', 'N/A')}) - {len(requests)} Requests")
except Exception as e: except Exception as e:
logger.error(f"WHOIS Fehler für {ip}: {e}") logger.error(f"WHOIS Fehler für {ip}: {e}")

View File

@@ -1778,11 +1778,12 @@ def get_dashboard_html() -> str:
function renderDetailTopIps(ips){if(!ips||!ips.length)return '<div style="color:var(--text-secondary);padding:8px">Keine Daten</div>';return ips.map(ip=>'<div class="ip-list-item"><div class="ip-info"><span class="ip-addr" style="cursor:pointer" onclick="showIpDetail(\\''+ip.ip+'\\')">'+ip.ip+'</span><span class="ip-meta">'+getCountryName(ip.country)+' | '+(ip.org||ip.asn||'-')+'</span></div><span class="ip-count">'+ip.count+'x</span><div class="ip-actions"><button class="btn btn-danger" onclick="detailQuickBan(\\''+ip.ip+'\\')">🚫</button><button class="btn btn-success" onclick="detailQuickWhitelist(\\''+ip.ip+'\\')">✓</button></div></div>').join('');} function renderDetailTopIps(ips){if(!ips||!ips.length)return '<div style="color:var(--text-secondary);padding:8px">Keine Daten</div>';return ips.map(ip=>'<div class="ip-list-item"><div class="ip-info"><span class="ip-addr" style="cursor:pointer" onclick="showIpDetail(\\''+ip.ip+'\\')">'+ip.ip+'</span><span class="ip-meta">'+getCountryName(ip.country)+' | '+(ip.org||ip.asn||'-')+'</span></div><span class="ip-count">'+ip.count+'x</span><div class="ip-actions"><button class="btn btn-danger" onclick="detailQuickBan(\\''+ip.ip+'\\')">🚫</button><button class="btn btn-success" onclick="detailQuickWhitelist(\\''+ip.ip+'\\')">✓</button></div></div>').join('');}
function detailQuickBan(ip){if(!currentDetailShop)return;const duration=prompt('Ban-Dauer auswählen:\\n\\n900 = 15 Minuten\\n3600 = 1 Stunde\\n21600 = 6 Stunden\\n86400 = 24 Stunden\\n604800 = 7 Tage\\n-1 = Permanent','3600');if(duration===null)return;ws.send(JSON.stringify({type:'command.ban',data:{shop:currentDetailShop,ip:ip,duration:parseInt(duration),reason:'Manual ban from dashboard'}}));toast('IP '+ip+' wird gebannt...','success');} function detailQuickBan(ip){if(!currentDetailShop)return;const duration=prompt('Ban-Dauer auswählen:\\n\\n900 = 15 Minuten\\n3600 = 1 Stunde\\n21600 = 6 Stunden\\n86400 = 24 Stunden\\n604800 = 7 Tage\\n-1 = Permanent','3600');if(duration===null)return;ws.send(JSON.stringify({type:'command.ban',data:{shop:currentDetailShop,ip:ip,duration:parseInt(duration),reason:'Manual ban from dashboard'}}));toast('IP '+ip+' wird gebannt...','success');}
function detailQuickWhitelist(ip){if(!currentDetailShop)return;const desc=prompt('Beschreibung (optional):','');if(desc===null)return;ws.send(JSON.stringify({type:'command.whitelist',data:{shop:currentDetailShop,ip:ip,description:desc}}));toast('IP '+ip+' wird gewhitelisted...','success');} function detailQuickWhitelist(ip){if(!currentDetailShop)return;const desc=prompt('Beschreibung (optional):','');if(desc===null)return;ws.send(JSON.stringify({type:'command.whitelist',data:{shop:currentDetailShop,ip:ip,description:desc}}));toast('IP '+ip+' wird gewhitelisted...','success');}
let currentWhoisIp=null; let currentWhoisIp=null,currentWhoisData=null,showAllRequests=false;
function showIpDetail(ip){currentWhoisIp=ip;document.getElementById('ipDetailContent').innerHTML='<div style="text-align:center;padding:20px;color:var(--text-secondary)">Lade WHOIS-Daten für '+ip+'...</div>';document.getElementById('ipDetailModal').classList.add('open');ws.send(JSON.stringify({type:'command.whois',data:{ip:ip,command_id:'whois_'+Date.now()}}));} function showIpDetail(ip){currentWhoisIp=ip;currentWhoisData=null;showAllRequests=false;document.getElementById('ipDetailContent').innerHTML='<div style="text-align:center;padding:20px;color:var(--text-secondary)">Lade WHOIS-Daten für '+ip+'...</div>';document.getElementById('ipDetailModal').classList.add('open');const shop=currentDetailShop||currentLogsShop||'';ws.send(JSON.stringify({type:'command.whois',data:{ip:ip,shop:shop,command_id:'whois_'+Date.now()}}));}
function renderWhoisResult(data){if(!data.success){document.getElementById('ipDetailContent').innerHTML='<div style="padding:16px;color:var(--danger)">Fehler: '+(data.error||'Unbekannter Fehler')+'</div>';return;}const ip=data.ip||currentWhoisIp;const html='<div style="padding:4px 0"><table style="width:100%;border-collapse:collapse"><tr><td style="padding:8px 0;color:var(--text-secondary);width:120px">IP-Adresse</td><td style="padding:8px 0;font-weight:bold;font-family:monospace">'+ip+'</td></tr><tr><td style="padding:8px 0;color:var(--text-secondary)">Organisation</td><td style="padding:8px 0">'+(data.org||'-')+'</td></tr><tr><td style="padding:8px 0;color:var(--text-secondary)">Netname</td><td style="padding:8px 0">'+(data.netname||'-')+'</td></tr><tr><td style="padding:8px 0;color:var(--text-secondary)">ASN</td><td style="padding:8px 0">'+(data.asn||'-')+'</td></tr><tr><td style="padding:8px 0;color:var(--text-secondary)">Land</td><td style="padding:8px 0">'+(data.country?getCountryName(data.country)+' ('+data.country+')':'-')+'</td></tr><tr><td style="padding:8px 0;color:var(--text-secondary)">IP-Range</td><td style="padding:8px 0;font-family:monospace;font-size:12px">'+(data.range||'-')+'</td></tr><tr><td style="padding:8px 0;color:var(--text-secondary)">Abuse-Kontakt</td><td style="padding:8px 0">'+(data.abuse?'<a href="mailto:'+data.abuse+'" style="color:var(--accent)">'+data.abuse+'</a>':'-')+'</td></tr></table></div><div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--border);display:flex;gap:8px;flex-wrap:wrap">'+(currentDetailShop?'<button class="btn btn-danger" onclick="ipDetailBan(\\''+ip+'\\')">🚫 Bannen</button><button class="btn btn-success" onclick="ipDetailWhitelist(\\''+ip+'\\')">✓ Whitelist</button>':'')+'<a class="btn btn-secondary" href="https://www.whois.com/whois/'+ip+'" target="_blank">🔗 WHOIS.com</a></div>';document.getElementById('ipDetailContent').innerHTML=html;} function renderWhoisResult(data){if(!data.success){document.getElementById('ipDetailContent').innerHTML='<div style="padding:16px;color:var(--danger)">Fehler: '+(data.error||'Unbekannter Fehler')+'</div>';return;}currentWhoisData=data;renderWhoisContent();}
function ipDetailBan(ip){if(!currentDetailShop){toast('Kein Shop ausgewählt','warning');return;}const duration=prompt('Ban-Dauer auswählen:\\n\\n900 = 15 Minuten\\n3600 = 1 Stunde\\n21600 = 6 Stunden\\n86400 = 24 Stunden\\n604800 = 7 Tage\\n-1 = Permanent','3600');if(duration===null)return;ws.send(JSON.stringify({type:'command.ban',data:{shop:currentDetailShop,ip:ip,duration:parseInt(duration),reason:'Manual ban from IP detail'}}));toast('IP '+ip+' wird gebannt...','success');closeModal('ipDetailModal');} function renderWhoisContent(){const data=currentWhoisData;if(!data)return;const ip=data.ip||currentWhoisIp;const allReqs=data.requests||[];const initialLimit=50;const showLimit=showAllRequests?allReqs.length:Math.min(initialLimit,allReqs.length);const reqs=allReqs.slice(0,showLimit);let reqsHtml='';if(allReqs.length>0){reqsHtml='<div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--border)"><div style="font-weight:600;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center"><span>📋 Requests ('+showLimit+'/'+allReqs.length+')</span>'+(allReqs.length>initialLimit&&!showAllRequests?'<button class="btn btn-secondary" style="padding:4px 12px;font-size:11px" onclick="showAllRequests=true;renderWhoisContent()">Alle '+allReqs.length+' anzeigen</button>':'')+'</div><div style="max-height:'+(showAllRequests?'500px':'300px')+';overflow-y:auto;font-size:12px" id="ipReqsList">';reqs.forEach(r=>{const t=new Date(r.ts*1000).toLocaleTimeString('de-DE');const blocked=r.blocked?'<span style="color:var(--danger)">[BLOCKED]</span> ':'';reqsHtml+='<div style="padding:6px 0;border-bottom:1px solid var(--border)"><div style="display:flex;justify-content:space-between;align-items:center"><span style="color:var(--text-secondary)">'+t+'</span>'+blocked+'</div><div style="font-family:monospace;word-break:break-all;margin:4px 0;color:var(--accent)">'+r.path+'</div>'+(r.ua?'<div style="color:var(--text-secondary);font-size:11px;word-break:break-all">'+r.ua+'</div>':'')+'</div>';});reqsHtml+='</div></div>';}else{reqsHtml='<div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--border);color:var(--text-secondary)">📋 Keine aktuellen Requests gefunden</div>';}const html='<div style="padding:4px 0"><table style="width:100%;border-collapse:collapse"><tr><td style="padding:8px 0;color:var(--text-secondary);width:120px">IP-Adresse</td><td style="padding:8px 0;font-weight:bold;font-family:monospace">'+ip+'</td></tr><tr><td style="padding:8px 0;color:var(--text-secondary)">Organisation</td><td style="padding:8px 0">'+(data.org||'-')+'</td></tr><tr><td style="padding:8px 0;color:var(--text-secondary)">Netname</td><td style="padding:8px 0">'+(data.netname||'-')+'</td></tr><tr><td style="padding:8px 0;color:var(--text-secondary)">ASN</td><td style="padding:8px 0">'+(data.asn||'-')+'</td></tr><tr><td style="padding:8px 0;color:var(--text-secondary)">Land</td><td style="padding:8px 0">'+(data.country?getCountryName(data.country)+' ('+data.country+')':'-')+'</td></tr><tr><td style="padding:8px 0;color:var(--text-secondary)">IP-Range</td><td style="padding:8px 0;font-family:monospace;font-size:12px">'+(data.range||'-')+'</td></tr><tr><td style="padding:8px 0;color:var(--text-secondary)">Abuse-Kontakt</td><td style="padding:8px 0">'+(data.abuse?'<a href="mailto:'+data.abuse+'" style="color:var(--accent)">'+data.abuse+'</a>':'-')+'</td></tr></table></div><div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--border);display:flex;gap:8px;flex-wrap:wrap">'+(currentDetailShop||currentLogsShop?'<button class="btn btn-danger" onclick="ipDetailBan(\\''+ip+'\\')">🚫 Bannen</button><button class="btn btn-success" onclick="ipDetailWhitelist(\\''+ip+'\\')">✓ Whitelist</button>':'')+'<a class="btn btn-secondary" href="https://www.whois.com/whois/'+ip+'" target="_blank">🔗 WHOIS.com</a></div>'+reqsHtml;document.getElementById('ipDetailContent').innerHTML=html;}
function ipDetailWhitelist(ip){if(!currentDetailShop){toast('Kein Shop ausgewählt','warning');return;}const desc=prompt('Beschreibung (optional):','');if(desc===null)return;ws.send(JSON.stringify({type:'command.whitelist',data:{shop:currentDetailShop,ip:ip,description:desc}}));toast('IP '+ip+' wird gewhitelisted...','success');closeModal('ipDetailModal');} function ipDetailBan(ip){const shop=currentDetailShop||currentLogsShop;if(!shop){toast('Kein Shop ausgewählt','warning');return;}const duration=prompt('Ban-Dauer auswählen:\\n\\n900 = 15 Minuten\\n3600 = 1 Stunde\\n21600 = 6 Stunden\\n86400 = 24 Stunden\\n604800 = 7 Tage\\n-1 = Permanent','3600');if(duration===null)return;ws.send(JSON.stringify({type:'command.ban',data:{shop:shop,ip:ip,duration:parseInt(duration),reason:'Manual ban from IP detail'}}));toast('IP '+ip+' wird gebannt...','success');closeModal('ipDetailModal');}
function ipDetailWhitelist(ip){const shop=currentDetailShop||currentLogsShop;if(!shop){toast('Kein Shop ausgewählt','warning');return;}const desc=prompt('Beschreibung (optional):','');if(desc===null)return;ws.send(JSON.stringify({type:'command.whitelist',data:{shop:shop,ip:ip,description:desc}}));toast('IP '+ip+' wird gewhitelisted...','success');closeModal('ipDetailModal');}
function quickBan(ip){if(!currentLogsShop)return;const duration=prompt('Ban-Dauer auswählen:\\n\\n900 = 15 Minuten\\n3600 = 1 Stunde\\n21600 = 6 Stunden\\n86400 = 24 Stunden\\n604800 = 7 Tage\\n-1 = Permanent','3600');if(duration===null)return;ws.send(JSON.stringify({type:'command.ban',data:{shop:currentLogsShop,ip:ip,duration:parseInt(duration),reason:'Manual ban from dashboard'}}));toast('IP '+ip+' wird gebannt...','success');} function quickBan(ip){if(!currentLogsShop)return;const duration=prompt('Ban-Dauer auswählen:\\n\\n900 = 15 Minuten\\n3600 = 1 Stunde\\n21600 = 6 Stunden\\n86400 = 24 Stunden\\n604800 = 7 Tage\\n-1 = Permanent','3600');if(duration===null)return;ws.send(JSON.stringify({type:'command.ban',data:{shop:currentLogsShop,ip:ip,duration:parseInt(duration),reason:'Manual ban from dashboard'}}));toast('IP '+ip+' wird gebannt...','success');}
function quickWhitelist(ip){if(!currentLogsShop)return;const desc=prompt('Beschreibung (optional):','');if(desc===null)return;ws.send(JSON.stringify({type:'command.whitelist',data:{shop:currentLogsShop,ip:ip,description:desc}}));toast('IP '+ip+' wird gewhitelisted...','success');} function quickWhitelist(ip){if(!currentLogsShop)return;const desc=prompt('Beschreibung (optional):','');if(desc===null)return;ws.send(JSON.stringify({type:'command.whitelist',data:{shop:currentLogsShop,ip:ip,description:desc}}));toast('IP '+ip+' wird gewhitelisted...','success');}
function addLogEntry(line){const c=document.getElementById('logsContent');if(c.querySelector('div[style*="color:#666"]'))c.innerHTML='';const e=document.createElement('div');e.className='log-entry'+(line.includes('BANNED')?' banned':'');const ipRegex=/\\b(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\b/g;e.innerHTML=line.replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(ipRegex,'<span class="log-ip" style="cursor:pointer" onclick="showIpDetail(\\'$1\\')">$1</span>');c.insertBefore(e,c.firstChild);while(c.children.length>100)c.removeChild(c.lastChild);} function addLogEntry(line){const c=document.getElementById('logsContent');if(c.querySelector('div[style*="color:#666"]'))c.innerHTML='';const e=document.createElement('div');e.className='log-entry'+(line.includes('BANNED')?' banned':'');const ipRegex=/\\b(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\b/g;e.innerHTML=line.replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(ipRegex,'<span class="log-ip" style="cursor:pointer" onclick="showIpDetail(\\'$1\\')">$1</span>');c.insertBefore(e,c.firstChild);while(c.children.length>100)c.removeChild(c.lastChild);}