v.0.11.4
This commit is contained in:
+53
-4
@@ -124,6 +124,7 @@ class WebServer:
|
||||
forensic_pcap: ForensicPcapRing | None = None,
|
||||
traffic_source: Any | None = None,
|
||||
metrics_provider: Callable[[], str] | None = None,
|
||||
healthcheck_provider: Callable[[], dict] | None = None,
|
||||
) -> None:
|
||||
self.config = config
|
||||
self.store = store
|
||||
@@ -140,8 +141,17 @@ class WebServer:
|
||||
self.forensic_pcap = forensic_pcap
|
||||
self.traffic_source = traffic_source
|
||||
self.metrics_provider = metrics_provider
|
||||
self.healthcheck_provider = healthcheck_provider or health_provider
|
||||
self.metrics_access = MetricsAccessControl(config) if metrics_provider is not None else None
|
||||
self.backup_manager = backup_manager or BackupManager(config.db_path, os.path.dirname(config.db_path) or ".")
|
||||
self._status_cache_lock = threading.Lock()
|
||||
self._status_cache: dict[str, Any] | None = None
|
||||
self._status_cache_at = 0.0
|
||||
self._status_cache_seconds = 30.0
|
||||
self._stats_cache_lock = threading.Lock()
|
||||
self._stats_cache: dict[str, Any] | None = None
|
||||
self._stats_cache_at = 0.0
|
||||
self._stats_cache_seconds = 60.0
|
||||
self.auth = SessionAuth(config, store)
|
||||
self._login_lock = threading.Lock()
|
||||
self._login_attempts: dict[str, deque[float]] = defaultdict(deque)
|
||||
@@ -149,10 +159,17 @@ class WebServer:
|
||||
self.thread = threading.Thread(target=self.server.serve_forever, name="web-ui", daemon=True)
|
||||
|
||||
def _status_payload(self) -> dict:
|
||||
now = time.monotonic()
|
||||
with self._status_cache_lock:
|
||||
if self._status_cache is not None and now - self._status_cache_at < self._status_cache_seconds:
|
||||
return dict(self._status_cache)
|
||||
|
||||
payload = dict(self.health_provider())
|
||||
payload["summary"] = self.store.summary()
|
||||
if self.traffic_history is not None:
|
||||
history = self.traffic_history.status()
|
||||
# main.health() already obtains this once for the service table. Reuse
|
||||
# it instead of issuing another Redis + archive status query.
|
||||
history = dict(payload.get("traffic_history") or self.traffic_history.status())
|
||||
if self.live_pipeline is not None:
|
||||
history.update(self.live_pipeline.status())
|
||||
if self.event_bus is not None:
|
||||
@@ -160,6 +177,30 @@ class WebServer:
|
||||
payload["traffic_history"] = history
|
||||
if self.analytics_cache is not None:
|
||||
payload["analytics_snapshots"] = self.analytics_cache.status()
|
||||
|
||||
with self._status_cache_lock:
|
||||
self._status_cache = dict(payload)
|
||||
self._status_cache_at = time.monotonic()
|
||||
return payload
|
||||
|
||||
def _healthcheck_payload(self) -> dict:
|
||||
return dict(self.healthcheck_provider())
|
||||
|
||||
def _stats_payload(self) -> dict:
|
||||
"""Cache aggregate SQLite statistics shared by all dashboard clients."""
|
||||
now = time.monotonic()
|
||||
with self._stats_cache_lock:
|
||||
if self._stats_cache is not None and now - self._stats_cache_at < self._stats_cache_seconds:
|
||||
return dict(self._stats_cache)
|
||||
|
||||
payload = {
|
||||
"summary": self.store.summary(),
|
||||
"analytics": self.store.analytics(),
|
||||
"ndr": self.store.ndr_summary(),
|
||||
}
|
||||
with self._stats_cache_lock:
|
||||
self._stats_cache = dict(payload)
|
||||
self._stats_cache_at = time.monotonic()
|
||||
return payload
|
||||
|
||||
def _analytics_payload(self, window_seconds: int) -> dict:
|
||||
@@ -192,6 +233,14 @@ class WebServer:
|
||||
def _login_allowed(self, client_ip: str) -> bool:
|
||||
now = time.monotonic()
|
||||
with self._login_lock:
|
||||
if len(self._login_attempts) >= 4096 and client_ip not in self._login_attempts:
|
||||
for key, values in list(self._login_attempts.items()):
|
||||
while values and values[0] < now - 300:
|
||||
values.popleft()
|
||||
if not values:
|
||||
self._login_attempts.pop(key, None)
|
||||
while len(self._login_attempts) >= 4096:
|
||||
self._login_attempts.pop(next(iter(self._login_attempts)), None)
|
||||
attempts = self._login_attempts[client_ip]
|
||||
while attempts and attempts[0] < now - 300:
|
||||
attempts.popleft()
|
||||
@@ -267,7 +316,7 @@ class WebServer:
|
||||
self._static(parsed.path)
|
||||
return
|
||||
if parsed.path == "/api/health":
|
||||
self._json(outer._status_payload())
|
||||
self._json(outer._healthcheck_payload())
|
||||
return
|
||||
if parsed.path == "/api/auth/session":
|
||||
self._auth_session()
|
||||
@@ -281,7 +330,7 @@ class WebServer:
|
||||
self._json(store.summary())
|
||||
return
|
||||
if parsed.path == "/api/stats":
|
||||
self._json({"summary": store.summary(), "analytics": store.analytics(), "ndr": store.ndr_summary()})
|
||||
self._json(outer._stats_payload())
|
||||
return
|
||||
if parsed.path == "/api/config":
|
||||
self._json(config.public_dict())
|
||||
@@ -845,7 +894,7 @@ class WebServer:
|
||||
if now - last_throughput >= 1:
|
||||
self._ws_send_json({"type": "throughput", "data": outer._current_throughput_payload(window)})
|
||||
last_throughput = now
|
||||
if now - last_status >= 5:
|
||||
if now - last_status >= 30:
|
||||
self._ws_send_json({"type": "status", "data": outer._status_payload()})
|
||||
last_status = now
|
||||
if now - last_analytics >= 10:
|
||||
|
||||
Reference in New Issue
Block a user