mirror of
https://github.com/smittix/intercept.git
synced 2026-06-08 14:11:54 -07:00
fix(drone): resolve critical pipeline, frontend, and input validation issues
Data pipeline (critical): scanners/detectors now write to a separate _obs_queue; a relay thread reads observations and calls correlator.process(), which emits processed DroneContact dicts to drone_queue for SSE. Without this the SSE stream received raw unserializable dataclass objects causing JSON errors. Frontend (critical): - Add droneContactList container to drone.html so contact cards render - Add droneMap container and initialize Leaflet in drone.js init() - Define dsc-distress-pulse keyframes in drone.css (was referenced but missing) - Fix SSE reconnect: null _sse before setTimeout to prevent _connectSSE no-op loop Other fixes: - Validate rtl_sdr_index with validate_device_index(), return 400 on bad input - Move _ensure_workers() inside _drone_lock to prevent double-initialization race - Add double-call guard to RemoteIDScanner.start() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+34
-7
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import queue
|
||||
import threading
|
||||
|
||||
from flask import Blueprint, Response, jsonify, request
|
||||
@@ -13,6 +14,7 @@ from utils.drone.correlator import DroneCorrelator
|
||||
from utils.drone.remote_id import RemoteIDScanner
|
||||
from utils.drone.rf_detector import RFDetector
|
||||
from utils.sse import sse_stream_fanout
|
||||
from utils.validation import validate_device_index
|
||||
|
||||
logger = logging.getLogger("intercept.drone")
|
||||
|
||||
@@ -21,18 +23,37 @@ drone_bp = Blueprint("drone", __name__, url_prefix="/drone")
|
||||
_correlator: DroneCorrelator | None = None
|
||||
_remote_id_scanner: RemoteIDScanner | None = None
|
||||
_rf_detector: RFDetector | None = None
|
||||
_obs_queue: queue.Queue | None = None # raw observations from scanners/detectors
|
||||
_relay_thread: threading.Thread | None = None
|
||||
_drone_running = False
|
||||
_drone_lock = threading.Lock()
|
||||
|
||||
_SENTINEL = object()
|
||||
|
||||
|
||||
def _relay_observations() -> None:
|
||||
"""Read raw observations from _obs_queue and feed them into the correlator."""
|
||||
while True:
|
||||
obs = _obs_queue.get()
|
||||
if obs is _SENTINEL:
|
||||
break
|
||||
if _correlator is not None:
|
||||
_correlator.process(obs)
|
||||
|
||||
|
||||
def _ensure_workers() -> None:
|
||||
global _correlator, _remote_id_scanner, _rf_detector
|
||||
global _correlator, _remote_id_scanner, _rf_detector, _obs_queue, _relay_thread
|
||||
if _obs_queue is None:
|
||||
_obs_queue = queue.Queue(maxsize=512)
|
||||
if _correlator is None:
|
||||
_correlator = DroneCorrelator(output_queue=app_module.drone_queue)
|
||||
if _remote_id_scanner is None:
|
||||
_remote_id_scanner = RemoteIDScanner(output_queue=app_module.drone_queue)
|
||||
_remote_id_scanner = RemoteIDScanner(output_queue=_obs_queue)
|
||||
if _rf_detector is None:
|
||||
_rf_detector = RFDetector(output_queue=app_module.drone_queue)
|
||||
_rf_detector = RFDetector(output_queue=_obs_queue)
|
||||
if _relay_thread is None or not _relay_thread.is_alive():
|
||||
_relay_thread = threading.Thread(target=_relay_observations, daemon=True)
|
||||
_relay_thread.start()
|
||||
|
||||
|
||||
@drone_bp.route("/status")
|
||||
@@ -61,12 +82,16 @@ def contacts():
|
||||
@drone_bp.route("/start", methods=["POST"])
|
||||
def start():
|
||||
global _drone_running
|
||||
_ensure_workers()
|
||||
wifi_iface = request.json.get("wifi_iface") if request.json else None
|
||||
rtl_index = int((request.json or {}).get("rtl_sdr_index", 0))
|
||||
use_hackrf = bool((request.json or {}).get("use_hackrf", True))
|
||||
body = request.json or {}
|
||||
wifi_iface = body.get("wifi_iface") or None
|
||||
try:
|
||||
rtl_index = validate_device_index(body.get("rtl_sdr_index", 0))
|
||||
except ValueError as exc:
|
||||
return jsonify({"error": str(exc)}), 400
|
||||
use_hackrf = bool(body.get("use_hackrf", True))
|
||||
|
||||
with _drone_lock:
|
||||
_ensure_workers()
|
||||
if not _drone_running:
|
||||
if _remote_id_scanner:
|
||||
_remote_id_scanner.start(wifi_iface=wifi_iface)
|
||||
@@ -86,6 +111,8 @@ def stop():
|
||||
_remote_id_scanner.stop()
|
||||
if _rf_detector:
|
||||
_rf_detector.stop()
|
||||
if _obs_queue is not None:
|
||||
_obs_queue.put_nowait(_SENTINEL)
|
||||
_drone_running = False
|
||||
logger.info("Drone detection stopped")
|
||||
return jsonify({"status": "ok", "running": False})
|
||||
|
||||
@@ -69,6 +69,18 @@
|
||||
color: var(--accent-red);
|
||||
}
|
||||
|
||||
.drone-map {
|
||||
height: 280px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
margin: 0 12px 12px;
|
||||
}
|
||||
|
||||
.drone-marker-high-risk {
|
||||
animation: dsc-distress-pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes dsc-distress-pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.4; transform: scale(1.4); }
|
||||
}
|
||||
|
||||
@@ -10,10 +10,22 @@
|
||||
function init() {
|
||||
document.getElementById('droneStartBtn')?.addEventListener('click', _start);
|
||||
document.getElementById('droneStopBtn')?.addEventListener('click', _stop);
|
||||
_initMap();
|
||||
_connectSSE();
|
||||
_refreshStatus();
|
||||
}
|
||||
|
||||
function _initMap() {
|
||||
if (_map) return;
|
||||
const mapEl = document.getElementById('droneMap');
|
||||
if (!mapEl || typeof L === 'undefined') return;
|
||||
_map = L.map('droneMap', { zoomControl: true }).setView([20, 0], 2);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap',
|
||||
maxZoom: 18,
|
||||
}).addTo(_map);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
_disconnectSSE();
|
||||
if (_map) {
|
||||
@@ -34,6 +46,8 @@
|
||||
} catch (_) {}
|
||||
});
|
||||
_sse.onerror = function () {
|
||||
_sse.close();
|
||||
_sse = null;
|
||||
setTimeout(_connectSSE, 3000);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,4 +39,11 @@
|
||||
Non-compliant: <span id="droneNonCompliantCount" style="color:var(--accent-red);">0</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Detected Contacts</h3>
|
||||
<div id="droneContactList"></div>
|
||||
</div>
|
||||
|
||||
<div id="droneMap" class="drone-map"></div>
|
||||
</div>
|
||||
|
||||
@@ -98,6 +98,8 @@ class RemoteIDScanner:
|
||||
elt = elt.payload if hasattr(elt, "payload") and isinstance(elt.payload, Dot11Elt) else None
|
||||
|
||||
def start(self, wifi_iface: str | None = None) -> None:
|
||||
if self._running:
|
||||
return
|
||||
self._running = True
|
||||
if SCAPY_AVAILABLE and wifi_iface:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user