diff --git a/viewer.py b/viewer.py index d2fceb5..076bd0c 100644 --- a/viewer.py +++ b/viewer.py @@ -7,6 +7,7 @@ import json import os import pathlib import re +import shutil import threading import time import urllib.parse @@ -102,13 +103,19 @@ function select(i){selected=i; jsonBox.textContent=JSON.stringify(e,null,2); if(e.evidence&&e.evidence.sha256)loadPayload(e.evidence.sha256); else{payloadBytes=null;art.innerHTML='
no payload in this event';}} +let lastBytes=-1,lastKey=''; async function load(){ const p=new URLSearchParams({q:document.getElementById('q').value,event:document.getElementById('event').value, peer:document.getElementById('peer').value,payload:document.getElementById('payload').checked?'1':'0', hide:document.getElementById('hide').checked?'1':'0', size:document.getElementById('size').value,page:page}); + const key=p.toString(); const r=await fetch('/api/events?'+p);const d=await r.json(); - current=d.events;lastScanned=d.scanned;lastHidden=d.hidden;hasMore=d.has_more;render(current); + lastScanned=d.scanned;lastHidden=d.hidden;hasMore=d.has_more; + // Skip re-render when nothing changed on the live page (cheap 3s refresh). + if(page===0 && d.journal_bytes===lastBytes && key===lastKey){return;} + lastBytes=d.journal_bytes;lastKey=key; + current=d.events;render(current); document.getElementById('page').textContent='page '+(page+1); document.getElementById('prev').disabled=page<=0; document.getElementById('next').disabled=!hasMore;} @@ -117,6 +124,7 @@ document.getElementById('reload').onclick=reset; document.getElementById('prev').onclick=()=>{if(page>0){page--;load();}}; document.getElementById('next').onclick=()=>{if(hasMore){page++;load();}}; document.getElementById('q').addEventListener('keydown',e=>{if(e.key==='Enter')reset();}); +let qTimer;document.getElementById('q').addEventListener('input',()=>{clearTimeout(qTimer);qTimer=setTimeout(reset,300);}); document.getElementById('peer').addEventListener('keydown',e=>{if(e.key==='Enter')reset();}); document.getElementById('event').onchange=reset; document.getElementById('payload').onchange=reset; @@ -236,20 +244,37 @@ class Handler(http.server.BaseHTTPRequestHandler): if len(rows) >= need: break page_rows = rows[page * size:(page + 1) * size] + try: + stat = self.server.journal.stat() + journal_bytes, journal_mtime = stat.st_size, int(stat.st_mtime) + except OSError: + journal_bytes, journal_mtime = 0, 0 body = json.dumps({'events': page_rows, 'scanned': scanned, 'page': page, 'size': size, 'has_more': len(rows) >= need, 'hidden': len(hidden), - 'hidden_ips': sorted(hidden)}, ensure_ascii=True).encode() + 'hidden_ips': sorted(hidden), 'journal_bytes': journal_bytes, + 'journal_mtime': journal_mtime}, ensure_ascii=True).encode() self.send(200, body, 'application/json', (('Content-Disposition', 'inline'),)) + def send_stream(self, path, download_name=None): + # Stream large payloads without loading them into memory. + self.send_response(200) + self.send_header('Content-Type', 'application/octet-stream') + self.send_header('Content-Length', str(path.stat().st_size)) + self.send_header('X-Content-Type-Options', 'nosniff') + self.send_header('Cache-Control', 'no-store') + if download_name: + self.send_header('Content-Disposition', f'attachment; filename="{download_name}"') + self.end_headers() + if self.command != 'HEAD': + with path.open('rb') as stream: + shutil.copyfileobj(stream, self.wfile, length=1 << 20) + def artifact(self, sha, query): path = artifact_path(self.server.artifacts, sha) if path is None: return self.send(404, b'not found', 'text/plain; charset=utf-8') - data = path.read_bytes() - if self.one(query, 'inline', '0') == '1': - return self.send(200, data, 'application/octet-stream') - self.send(200, data, 'application/octet-stream', - (('Content-Disposition', f'attachment; filename="{sha}.bin"'),)) + download = None if self.one(query, 'inline', '0') == '1' else f'{sha}.bin' + self.send_stream(path, download) def main():