mirror of
https://github.com/smittix/intercept.git
synced 2026-07-07 17:18:11 -07:00
Major security and code quality improvements
Security: - Add input validation for all API endpoints (frequency, lat/lon, device, gain, ppm) - Add HTML escaping utility to prevent XSS attacks - Add path traversal protection for log file configuration - Add proper HTTP status codes for error responses (400, 409, 503) Performance: - Reduce SSE keepalive overhead (30s interval instead of 1s) - Add centralized SSE stream utility with optimized keepalive - Add DataStore class for thread-safe data with automatic cleanup New Features: - Add data export endpoints (/export/aircraft, /export/wifi, /export/bluetooth) - Support for both JSON and CSV export formats - Add process cleanup on application exit (atexit handlers) - Label Iridium module as demo mode with clear warnings Code Quality: - Create utils/validation.py for centralized input validation - Create utils/sse.py for SSE stream utilities - Create utils/cleanup.py for memory management - Add safe_terminate() and register_process() for process management - Improve error handling with proper logging throughout routes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+19
-5
@@ -16,6 +16,8 @@ from flask import Blueprint, jsonify, request, Response, render_template
|
||||
|
||||
import app as app_module
|
||||
from utils.logging import adsb_logger as logger
|
||||
from utils.validation import validate_device_index, validate_gain
|
||||
from utils.sse import format_sse
|
||||
|
||||
adsb_bp = Blueprint('adsb', __name__, url_prefix='/adsb')
|
||||
|
||||
@@ -218,11 +220,16 @@ def start_adsb():
|
||||
|
||||
with app_module.adsb_lock:
|
||||
if adsb_using_service:
|
||||
return jsonify({'status': 'already_running', 'message': 'ADS-B tracking already active'})
|
||||
return jsonify({'status': 'already_running', 'message': 'ADS-B tracking already active'}), 409
|
||||
|
||||
data = request.json or {}
|
||||
gain = data.get('gain', '40')
|
||||
device = data.get('device', '0')
|
||||
|
||||
# Validate inputs
|
||||
try:
|
||||
gain = int(validate_gain(data.get('gain', '40')))
|
||||
device = validate_device_index(data.get('device', '0'))
|
||||
except ValueError as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||
|
||||
# Check if dump1090 is already running externally (e.g., user started it manually)
|
||||
existing_service = check_dump1090_service()
|
||||
@@ -294,12 +301,19 @@ def stop_adsb():
|
||||
def stream_adsb():
|
||||
"""SSE stream for ADS-B aircraft."""
|
||||
def generate():
|
||||
last_keepalive = time.time()
|
||||
keepalive_interval = 30.0
|
||||
|
||||
while True:
|
||||
try:
|
||||
msg = app_module.adsb_queue.get(timeout=1)
|
||||
yield f"data: {json.dumps(msg)}\n\n"
|
||||
last_keepalive = time.time()
|
||||
yield format_sse(msg)
|
||||
except queue.Empty:
|
||||
yield f"data: {json.dumps({'type': 'keepalive'})}\n\n"
|
||||
now = time.time()
|
||||
if now - last_keepalive >= keepalive_interval:
|
||||
yield format_sse({'type': 'keepalive'})
|
||||
last_keepalive = now
|
||||
|
||||
response = Response(generate(), mimetype='text/event-stream')
|
||||
response.headers['Cache-Control'] = 'no-cache'
|
||||
|
||||
+11
-2
@@ -19,6 +19,8 @@ from flask import Blueprint, jsonify, request, Response
|
||||
|
||||
import app as app_module
|
||||
from utils.dependencies import check_tool
|
||||
from utils.logging import bluetooth_logger as logger
|
||||
from utils.sse import format_sse
|
||||
from data.oui import OUI_DATABASE, load_oui_database, get_manufacturer
|
||||
from data.patterns import AIRTAG_PREFIXES, TILE_PREFIXES, SAMSUNG_TRACKER
|
||||
|
||||
@@ -469,12 +471,19 @@ def get_bt_devices():
|
||||
def stream_bt():
|
||||
"""SSE stream for Bluetooth events."""
|
||||
def generate():
|
||||
last_keepalive = time.time()
|
||||
keepalive_interval = 30.0
|
||||
|
||||
while True:
|
||||
try:
|
||||
msg = app_module.bt_queue.get(timeout=1)
|
||||
yield f"data: {json.dumps(msg)}\n\n"
|
||||
last_keepalive = time.time()
|
||||
yield format_sse(msg)
|
||||
except queue.Empty:
|
||||
yield f"data: {json.dumps({'type': 'keepalive'})}\n\n"
|
||||
now = time.time()
|
||||
if now - last_keepalive >= keepalive_interval:
|
||||
yield format_sse({'type': 'keepalive'})
|
||||
last_keepalive = now
|
||||
|
||||
response = Response(generate(), mimetype='text/event-stream')
|
||||
response.headers['Cache-Control'] = 'no-cache'
|
||||
|
||||
+71
-17
@@ -1,4 +1,9 @@
|
||||
"""Iridium monitoring routes."""
|
||||
"""Iridium monitoring routes.
|
||||
|
||||
NOTE: This module is currently in DEMO MODE. The burst detection generates
|
||||
simulated data for demonstration purposes. Real Iridium decoding requires
|
||||
gr-iridium or iridium-toolkit which are not yet integrated.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -16,24 +21,42 @@ from flask import Blueprint, jsonify, request, Response
|
||||
|
||||
import app as app_module
|
||||
from utils.logging import iridium_logger as logger
|
||||
from utils.validation import validate_frequency, validate_device_index, validate_gain
|
||||
from utils.sse import format_sse
|
||||
|
||||
iridium_bp = Blueprint('iridium', __name__, url_prefix='/iridium')
|
||||
|
||||
# Flag indicating this is demo mode (simulated data)
|
||||
DEMO_MODE = True
|
||||
|
||||
|
||||
def monitor_iridium(process):
|
||||
"""Monitor Iridium capture and detect bursts."""
|
||||
"""
|
||||
Monitor Iridium capture and detect bursts.
|
||||
|
||||
NOTE: Currently generates SIMULATED data for demonstration.
|
||||
Real Iridium decoding is not yet implemented.
|
||||
"""
|
||||
try:
|
||||
burst_count = 0
|
||||
# Send initial demo mode warning
|
||||
app_module.satellite_queue.put({
|
||||
'type': 'info',
|
||||
'message': '⚠️ DEMO MODE: Generating simulated Iridium bursts for demonstration'
|
||||
})
|
||||
|
||||
while process.poll() is None:
|
||||
data = process.stdout.read(1024)
|
||||
if data:
|
||||
if len(data) > 0 and burst_count < 100:
|
||||
# DEMO: Generate simulated bursts (1% chance per read)
|
||||
if random.random() < 0.01:
|
||||
burst = {
|
||||
'type': 'burst',
|
||||
'demo': True, # Flag as demo data
|
||||
'time': datetime.now().strftime('%H:%M:%S.%f')[:-3],
|
||||
'frequency': f"{1616 + random.random() * 10:.3f}",
|
||||
'data': f"Frame data (simulated) - Burst #{burst_count + 1}"
|
||||
'data': f"[SIMULATED] Frame data - Burst #{burst_count + 1}"
|
||||
}
|
||||
app_module.satellite_queue.put(burst)
|
||||
app_module.iridium_bursts.append(burst)
|
||||
@@ -47,28 +70,44 @@ def monitor_iridium(process):
|
||||
@iridium_bp.route('/tools')
|
||||
def check_iridium_tools():
|
||||
"""Check for Iridium decoding tools."""
|
||||
has_tool = shutil.which('iridium-extractor') is not None or shutil.which('iridium-parser') is not None
|
||||
return jsonify({'available': has_tool})
|
||||
has_iridium = shutil.which('iridium-extractor') is not None or shutil.which('iridium-parser') is not None
|
||||
has_rtl = shutil.which('rtl_fm') is not None
|
||||
return jsonify({
|
||||
'available': has_iridium or has_rtl,
|
||||
'demo_mode': DEMO_MODE,
|
||||
'message': 'Demo mode active - generating simulated data' if DEMO_MODE else None
|
||||
})
|
||||
|
||||
|
||||
@iridium_bp.route('/start', methods=['POST'])
|
||||
def start_iridium():
|
||||
"""Start Iridium burst capture."""
|
||||
"""Start Iridium burst capture (DEMO MODE - simulated data)."""
|
||||
with app_module.satellite_lock:
|
||||
if app_module.satellite_process and app_module.satellite_process.poll() is None:
|
||||
return jsonify({'status': 'error', 'message': 'Iridium capture already running'})
|
||||
return jsonify({'status': 'error', 'message': 'Iridium capture already running'}), 409
|
||||
|
||||
data = request.json or {}
|
||||
|
||||
# Validate inputs
|
||||
try:
|
||||
freq = validate_frequency(data.get('freq', '1626.0'), min_mhz=1610.0, max_mhz=1650.0)
|
||||
gain = validate_gain(data.get('gain', '40'))
|
||||
device = validate_device_index(data.get('device', '0'))
|
||||
except ValueError as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||
|
||||
data = request.json
|
||||
freq = data.get('freq', '1626.0')
|
||||
gain = data.get('gain', '40')
|
||||
sample_rate = data.get('sampleRate', '2.048e6')
|
||||
device = data.get('device', '0')
|
||||
# Validate sample rate format
|
||||
try:
|
||||
float(sample_rate.replace('e', 'E'))
|
||||
except (ValueError, AttributeError):
|
||||
return jsonify({'status': 'error', 'message': 'Invalid sample rate format'}), 400
|
||||
|
||||
if not shutil.which('iridium-extractor') and not shutil.which('rtl_fm'):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Iridium tools not found.'
|
||||
})
|
||||
'message': 'Iridium tools not found. Requires rtl_fm or iridium-extractor.'
|
||||
}), 503
|
||||
|
||||
try:
|
||||
cmd = [
|
||||
@@ -89,9 +128,17 @@ def start_iridium():
|
||||
thread = threading.Thread(target=monitor_iridium, args=(app_module.satellite_process,), daemon=True)
|
||||
thread.start()
|
||||
|
||||
return jsonify({'status': 'started'})
|
||||
return jsonify({
|
||||
'status': 'started',
|
||||
'demo_mode': DEMO_MODE,
|
||||
'message': 'Demo mode active - data is simulated' if DEMO_MODE else None
|
||||
})
|
||||
except FileNotFoundError as e:
|
||||
logger.error(f"Tool not found: {e}")
|
||||
return jsonify({'status': 'error', 'message': f'Tool not found: {e.filename}'}), 503
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)})
|
||||
logger.error(f"Start error: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@iridium_bp.route('/stop', methods=['POST'])
|
||||
@@ -113,12 +160,19 @@ def stop_iridium():
|
||||
def stream_iridium():
|
||||
"""SSE stream for Iridium bursts."""
|
||||
def generate():
|
||||
last_keepalive = time.time()
|
||||
keepalive_interval = 30.0
|
||||
|
||||
while True:
|
||||
try:
|
||||
msg = app_module.satellite_queue.get(timeout=1)
|
||||
yield f"data: {json.dumps(msg)}\n\n"
|
||||
last_keepalive = time.time()
|
||||
yield format_sse(msg)
|
||||
except queue.Empty:
|
||||
yield f"data: {json.dumps({'type': 'keepalive'})}\n\n"
|
||||
now = time.time()
|
||||
if now - last_keepalive >= keepalive_interval:
|
||||
yield format_sse({'type': 'keepalive'})
|
||||
last_keepalive = now
|
||||
|
||||
response = Response(generate(), mimetype='text/event-stream')
|
||||
response.headers['Cache-Control'] = 'no-cache'
|
||||
|
||||
+67
-12
@@ -3,12 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import pty
|
||||
import queue
|
||||
import select
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, Generator
|
||||
|
||||
@@ -16,6 +18,9 @@ from flask import Blueprint, jsonify, request, Response
|
||||
|
||||
import app as app_module
|
||||
from utils.logging import pager_logger as logger
|
||||
from utils.validation import validate_frequency, validate_device_index, validate_gain, validate_ppm
|
||||
from utils.sse import format_sse
|
||||
from utils.process import safe_terminate, register_process
|
||||
|
||||
pager_bp = Blueprint('pager', __name__)
|
||||
|
||||
@@ -147,15 +152,35 @@ def stream_decoder(master_fd: int, process: subprocess.Popen[bytes]) -> None:
|
||||
def start_decoding() -> Response:
|
||||
with app_module.process_lock:
|
||||
if app_module.current_process:
|
||||
return jsonify({'status': 'error', 'message': 'Already running'})
|
||||
return jsonify({'status': 'error', 'message': 'Already running'}), 409
|
||||
|
||||
data = request.json or {}
|
||||
|
||||
# Validate inputs
|
||||
try:
|
||||
freq = validate_frequency(data.get('frequency', '929.6125'))
|
||||
gain = validate_gain(data.get('gain', '0'))
|
||||
ppm = validate_ppm(data.get('ppm', '0'))
|
||||
device = validate_device_index(data.get('device', '0'))
|
||||
except ValueError as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||
|
||||
data = request.json
|
||||
freq = data.get('frequency', '929.6125')
|
||||
gain = data.get('gain', '0')
|
||||
squelch = data.get('squelch', '0')
|
||||
ppm = data.get('ppm', '0')
|
||||
device = data.get('device', '0')
|
||||
protocols = data.get('protocols', ['POCSAG512', 'POCSAG1200', 'POCSAG2400', 'FLEX'])
|
||||
try:
|
||||
squelch = int(squelch)
|
||||
if not 0 <= squelch <= 1000:
|
||||
raise ValueError("Squelch must be between 0 and 1000")
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({'status': 'error', 'message': 'Invalid squelch value'}), 400
|
||||
|
||||
# Validate protocols
|
||||
valid_protocols = ['POCSAG512', 'POCSAG1200', 'POCSAG2400', 'FLEX']
|
||||
protocols = data.get('protocols', valid_protocols)
|
||||
if not isinstance(protocols, list):
|
||||
return jsonify({'status': 'error', 'message': 'Protocols must be a list'}), 400
|
||||
protocols = [p for p in protocols if p in valid_protocols]
|
||||
if not protocols:
|
||||
protocols = valid_protocols
|
||||
|
||||
# Clear queue
|
||||
while not app_module.output_queue.empty():
|
||||
@@ -301,11 +326,34 @@ def get_status() -> Response:
|
||||
@pager_bp.route('/logging', methods=['POST'])
|
||||
def toggle_logging() -> Response:
|
||||
"""Toggle message logging."""
|
||||
data = request.json
|
||||
data = request.json or {}
|
||||
if 'enabled' in data:
|
||||
app_module.logging_enabled = data['enabled']
|
||||
app_module.logging_enabled = bool(data['enabled'])
|
||||
|
||||
if 'log_file' in data and data['log_file']:
|
||||
app_module.log_file_path = data['log_file']
|
||||
# Validate path to prevent directory traversal
|
||||
try:
|
||||
requested_path = pathlib.Path(data['log_file']).resolve()
|
||||
# Only allow files in the current directory or logs subdirectory
|
||||
cwd = pathlib.Path('.').resolve()
|
||||
logs_dir = (cwd / 'logs').resolve()
|
||||
|
||||
# Check if path is within allowed directories
|
||||
is_in_cwd = str(requested_path).startswith(str(cwd))
|
||||
is_in_logs = str(requested_path).startswith(str(logs_dir))
|
||||
|
||||
if not (is_in_cwd or is_in_logs):
|
||||
return jsonify({'status': 'error', 'message': 'Invalid log file path'}), 400
|
||||
|
||||
# Ensure it's not a directory
|
||||
if requested_path.is_dir():
|
||||
return jsonify({'status': 'error', 'message': 'Log file path must be a file, not a directory'}), 400
|
||||
|
||||
app_module.log_file_path = str(requested_path)
|
||||
except (ValueError, OSError) as e:
|
||||
logger.warning(f"Invalid log file path: {e}")
|
||||
return jsonify({'status': 'error', 'message': 'Invalid log file path'}), 400
|
||||
|
||||
return jsonify({'logging': app_module.logging_enabled, 'log_file': app_module.log_file_path})
|
||||
|
||||
|
||||
@@ -314,12 +362,19 @@ def stream() -> Response:
|
||||
import json
|
||||
|
||||
def generate() -> Generator[str, None, None]:
|
||||
last_keepalive = time.time()
|
||||
keepalive_interval = 30.0 # Send keepalive every 30 seconds instead of 1 second
|
||||
|
||||
while True:
|
||||
try:
|
||||
msg = app_module.output_queue.get(timeout=1)
|
||||
yield f"data: {json.dumps(msg)}\n\n"
|
||||
last_keepalive = time.time()
|
||||
yield format_sse(msg)
|
||||
except queue.Empty:
|
||||
yield f"data: {json.dumps({'type': 'keepalive'})}\n\n"
|
||||
now = time.time()
|
||||
if now - last_keepalive >= keepalive_interval:
|
||||
yield format_sse({'type': 'keepalive'})
|
||||
last_keepalive = now
|
||||
|
||||
response = Response(generate(), mimetype='text/event-stream')
|
||||
response.headers['Cache-Control'] = 'no-cache'
|
||||
|
||||
+30
-11
@@ -6,14 +6,22 @@ import json
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from flask import Blueprint, jsonify, request, render_template, Response
|
||||
|
||||
from data.satellites import TLE_SATELLITES
|
||||
from utils.logging import satellite_logger as logger
|
||||
from utils.validation import validate_latitude, validate_longitude, validate_hours, validate_elevation
|
||||
|
||||
satellite_bp = Blueprint('satellite', __name__, url_prefix='/satellite')
|
||||
|
||||
# Maximum response size for external requests (1MB)
|
||||
MAX_RESPONSE_SIZE = 1024 * 1024
|
||||
|
||||
# Allowed hosts for TLE fetching
|
||||
ALLOWED_TLE_HOSTS = ['celestrak.org', 'celestrak.com', 'www.celestrak.org', 'www.celestrak.com']
|
||||
|
||||
# Local TLE cache (can be updated via API)
|
||||
_tle_cache = dict(TLE_SATELLITES)
|
||||
|
||||
@@ -34,13 +42,18 @@ def predict_passes():
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'skyfield library not installed. Run: pip install skyfield'
|
||||
})
|
||||
}), 503
|
||||
|
||||
data = request.json
|
||||
lat = data.get('latitude', data.get('lat', 51.5074))
|
||||
lon = data.get('longitude', data.get('lon', -0.1278))
|
||||
hours = data.get('hours', 24)
|
||||
min_el = data.get('minEl', 10)
|
||||
data = request.json or {}
|
||||
|
||||
# Validate inputs
|
||||
try:
|
||||
lat = validate_latitude(data.get('latitude', data.get('lat', 51.5074)))
|
||||
lon = validate_longitude(data.get('longitude', data.get('lon', -0.1278)))
|
||||
hours = validate_hours(data.get('hours', 24))
|
||||
min_el = validate_elevation(data.get('minEl', 10))
|
||||
except ValueError as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||
|
||||
norad_to_name = {
|
||||
25544: 'ISS',
|
||||
@@ -187,13 +200,19 @@ def get_satellite_position():
|
||||
try:
|
||||
from skyfield.api import load, wgs84, EarthSatellite
|
||||
except ImportError:
|
||||
return jsonify({'status': 'error', 'message': 'skyfield not installed'})
|
||||
return jsonify({'status': 'error', 'message': 'skyfield not installed'}), 503
|
||||
|
||||
data = request.json or {}
|
||||
|
||||
# Validate inputs
|
||||
try:
|
||||
lat = validate_latitude(data.get('latitude', data.get('lat', 51.5074)))
|
||||
lon = validate_longitude(data.get('longitude', data.get('lon', -0.1278)))
|
||||
except ValueError as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||
|
||||
data = request.json
|
||||
lat = data.get('latitude', data.get('lat', 51.5074))
|
||||
lon = data.get('longitude', data.get('lon', -0.1278))
|
||||
sat_input = data.get('satellites', [])
|
||||
include_track = data.get('includeTrack', True)
|
||||
include_track = bool(data.get('includeTrack', True))
|
||||
|
||||
norad_to_name = {
|
||||
25544: 'ISS',
|
||||
|
||||
+27
-11
@@ -6,6 +6,7 @@ import json
|
||||
import queue
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Generator
|
||||
|
||||
@@ -13,6 +14,9 @@ from flask import Blueprint, jsonify, request, Response
|
||||
|
||||
import app as app_module
|
||||
from utils.logging import sensor_logger as logger
|
||||
from utils.validation import validate_frequency, validate_device_index, validate_gain, validate_ppm
|
||||
from utils.sse import format_sse
|
||||
from utils.process import safe_terminate, register_process
|
||||
|
||||
sensor_bp = Blueprint('sensor', __name__)
|
||||
|
||||
@@ -58,13 +62,18 @@ def stream_sensor_output(process: subprocess.Popen[bytes]) -> None:
|
||||
def start_sensor() -> Response:
|
||||
with app_module.sensor_lock:
|
||||
if app_module.sensor_process:
|
||||
return jsonify({'status': 'error', 'message': 'Sensor already running'})
|
||||
return jsonify({'status': 'error', 'message': 'Sensor already running'}), 409
|
||||
|
||||
data = request.json
|
||||
freq = data.get('frequency', '433.92')
|
||||
gain = data.get('gain', '0')
|
||||
ppm = data.get('ppm', '0')
|
||||
device = data.get('device', '0')
|
||||
data = request.json or {}
|
||||
|
||||
# Validate inputs
|
||||
try:
|
||||
freq = validate_frequency(data.get('frequency', '433.92'))
|
||||
gain = validate_gain(data.get('gain', '0'))
|
||||
ppm = validate_ppm(data.get('ppm', '0'))
|
||||
device = validate_device_index(data.get('device', '0'))
|
||||
except ValueError as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||
|
||||
# Clear queue
|
||||
while not app_module.sensor_queue.empty():
|
||||
@@ -81,10 +90,10 @@ def start_sensor() -> Response:
|
||||
'-F', 'json'
|
||||
]
|
||||
|
||||
if gain and gain != '0':
|
||||
cmd.extend(['-g', str(gain)])
|
||||
if gain and gain != 0:
|
||||
cmd.extend(['-g', str(int(gain))])
|
||||
|
||||
if ppm and ppm != '0':
|
||||
if ppm and ppm != 0:
|
||||
cmd.extend(['-p', str(ppm)])
|
||||
|
||||
full_cmd = ' '.join(cmd)
|
||||
@@ -143,12 +152,19 @@ def stop_sensor() -> Response:
|
||||
@sensor_bp.route('/stream_sensor')
|
||||
def stream_sensor() -> Response:
|
||||
def generate() -> Generator[str, None, None]:
|
||||
last_keepalive = time.time()
|
||||
keepalive_interval = 30.0
|
||||
|
||||
while True:
|
||||
try:
|
||||
msg = app_module.sensor_queue.get(timeout=1)
|
||||
yield f"data: {json.dumps(msg)}\n\n"
|
||||
last_keepalive = time.time()
|
||||
yield format_sse(msg)
|
||||
except queue.Empty:
|
||||
yield f"data: {json.dumps({'type': 'keepalive'})}\n\n"
|
||||
now = time.time()
|
||||
if now - last_keepalive >= keepalive_interval:
|
||||
yield format_sse({'type': 'keepalive'})
|
||||
last_keepalive = now
|
||||
|
||||
response = Response(generate(), mimetype='text/event-stream')
|
||||
response.headers['Cache-Control'] = 'no-cache'
|
||||
|
||||
+11
-2
@@ -19,6 +19,8 @@ import app as app_module
|
||||
from utils.dependencies import check_tool
|
||||
from utils.logging import wifi_logger as logger
|
||||
from utils.process import is_valid_mac, is_valid_channel
|
||||
from utils.validation import validate_wifi_channel, validate_mac_address
|
||||
from utils.sse import format_sse
|
||||
from data.oui import get_manufacturer
|
||||
|
||||
wifi_bp = Blueprint('wifi', __name__, url_prefix='/wifi')
|
||||
@@ -758,12 +760,19 @@ def get_wifi_networks():
|
||||
def stream_wifi():
|
||||
"""SSE stream for WiFi events."""
|
||||
def generate():
|
||||
last_keepalive = time.time()
|
||||
keepalive_interval = 30.0
|
||||
|
||||
while True:
|
||||
try:
|
||||
msg = app_module.wifi_queue.get(timeout=1)
|
||||
yield f"data: {json.dumps(msg)}\n\n"
|
||||
last_keepalive = time.time()
|
||||
yield format_sse(msg)
|
||||
except queue.Empty:
|
||||
yield f"data: {json.dumps({'type': 'keepalive'})}\n\n"
|
||||
now = time.time()
|
||||
if now - last_keepalive >= keepalive_interval:
|
||||
yield format_sse({'type': 'keepalive'})
|
||||
last_keepalive = now
|
||||
|
||||
response = Response(generate(), mimetype='text/event-stream')
|
||||
response.headers['Cache-Control'] = 'no-cache'
|
||||
|
||||
Reference in New Issue
Block a user