""" INTERCEPT - Signal Intelligence Platform Flask application and shared state. """ from __future__ import annotations import sys import site # Ensure user site-packages is available (may be disabled when running as root/sudo) if not site.ENABLE_USER_SITE: user_site = site.getusersitepackages() if user_site and user_site not in sys.path: sys.path.insert(0, user_site) import os import queue import threading import platform import subprocess from typing import Any from flask import Flask, render_template, jsonify, send_file, Response, request from utils.dependencies import check_tool, check_all_dependencies, TOOL_DEPENDENCIES from utils.process import cleanup_stale_processes from utils.sdr import SDRFactory # Create Flask app app = Flask(__name__) # ============================================ # GLOBAL PROCESS MANAGEMENT # ============================================ # Pager decoder current_process = None output_queue = queue.Queue() process_lock = threading.Lock() # RTL_433 sensor sensor_process = None sensor_queue = queue.Queue() sensor_lock = threading.Lock() # WiFi wifi_process = None wifi_queue = queue.Queue() wifi_lock = threading.Lock() # Bluetooth bt_process = None bt_queue = queue.Queue() bt_lock = threading.Lock() # ADS-B aircraft adsb_process = None adsb_queue = queue.Queue() adsb_lock = threading.Lock() # Satellite/Iridium satellite_process = None satellite_queue = queue.Queue() satellite_lock = threading.Lock() # ============================================ # GLOBAL STATE DICTIONARIES # ============================================ # Logging settings logging_enabled = False log_file_path = 'pager_messages.log' # WiFi state wifi_monitor_interface = None wifi_networks = {} # BSSID -> network info wifi_clients = {} # Client MAC -> client info wifi_handshakes = [] # Captured handshakes # Bluetooth state bt_interface = None bt_devices = {} # MAC -> device info bt_beacons = {} # MAC -> beacon info (AirTags, Tiles, iBeacons) bt_services = {} # MAC -> list of services # Aircraft (ADS-B) state adsb_aircraft = {} # ICAO hex -> aircraft info # Satellite state iridium_bursts = [] # List of detected Iridium bursts satellite_passes = [] # Predicted satellite passes # ============================================ # MAIN ROUTES # ============================================ @app.route('/') def index() -> str: tools = { 'rtl_fm': check_tool('rtl_fm'), 'multimon': check_tool('multimon-ng'), 'rtl_433': check_tool('rtl_433') } devices = [d.to_dict() for d in SDRFactory.detect_devices()] return render_template('index.html', tools=tools, devices=devices) @app.route('/favicon.svg') def favicon() -> Response: return send_file('favicon.svg', mimetype='image/svg+xml') @app.route('/devices') def get_devices() -> Response: """Get all detected SDR devices with hardware type info.""" devices = SDRFactory.detect_devices() return jsonify([d.to_dict() for d in devices]) @app.route('/dependencies') def get_dependencies() -> Response: """Get status of all tool dependencies.""" results = check_all_dependencies() # Determine OS for install instructions system = platform.system().lower() if system == 'darwin': install_method = 'brew' elif system == 'linux': install_method = 'apt' else: install_method = 'manual' return jsonify({ 'os': system, 'install_method': install_method, 'modes': results }) @app.route('/export/aircraft', methods=['GET']) def export_aircraft() -> Response: """Export aircraft data as JSON or CSV.""" import csv import io format_type = request.args.get('format', 'json').lower() if format_type == 'csv': output = io.StringIO() writer = csv.writer(output) writer.writerow(['icao', 'callsign', 'altitude', 'speed', 'heading', 'lat', 'lon', 'squawk', 'last_seen']) for icao, ac in adsb_aircraft.items(): writer.writerow([ icao, ac.get('callsign', ''), ac.get('altitude', ''), ac.get('speed', ''), ac.get('heading', ''), ac.get('lat', ''), ac.get('lon', ''), ac.get('squawk', ''), ac.get('lastSeen', '') ]) response = Response(output.getvalue(), mimetype='text/csv') response.headers['Content-Disposition'] = 'attachment; filename=aircraft.csv' return response else: return jsonify({ 'timestamp': __import__('datetime').datetime.utcnow().isoformat(), 'aircraft': list(adsb_aircraft.values()) }) @app.route('/export/wifi', methods=['GET']) def export_wifi() -> Response: """Export WiFi networks as JSON or CSV.""" import csv import io format_type = request.args.get('format', 'json').lower() if format_type == 'csv': output = io.StringIO() writer = csv.writer(output) writer.writerow(['bssid', 'ssid', 'channel', 'signal', 'encryption', 'clients']) for bssid, net in wifi_networks.items(): writer.writerow([ bssid, net.get('ssid', ''), net.get('channel', ''), net.get('signal', ''), net.get('encryption', ''), net.get('clients', 0) ]) response = Response(output.getvalue(), mimetype='text/csv') response.headers['Content-Disposition'] = 'attachment; filename=wifi_networks.csv' return response else: return jsonify({ 'timestamp': __import__('datetime').datetime.utcnow().isoformat(), 'networks': list(wifi_networks.values()), 'clients': list(wifi_clients.values()) }) @app.route('/export/bluetooth', methods=['GET']) def export_bluetooth() -> Response: """Export Bluetooth devices as JSON or CSV.""" import csv import io format_type = request.args.get('format', 'json').lower() if format_type == 'csv': output = io.StringIO() writer = csv.writer(output) writer.writerow(['mac', 'name', 'rssi', 'type', 'manufacturer', 'last_seen']) for mac, dev in bt_devices.items(): writer.writerow([ mac, dev.get('name', ''), dev.get('rssi', ''), dev.get('type', ''), dev.get('manufacturer', ''), dev.get('lastSeen', '') ]) response = Response(output.getvalue(), mimetype='text/csv') response.headers['Content-Disposition'] = 'attachment; filename=bluetooth_devices.csv' return response else: return jsonify({ 'timestamp': __import__('datetime').datetime.utcnow().isoformat(), 'devices': list(bt_devices.values()), 'beacons': list(bt_beacons.values()) }) @app.route('/killall', methods=['POST']) def kill_all() -> Response: """Kill all decoder and WiFi processes.""" global current_process, sensor_process, wifi_process, adsb_process # Import adsb module to reset its state from routes import adsb as adsb_module killed = [] processes_to_kill = [ 'rtl_fm', 'multimon-ng', 'rtl_433', 'airodump-ng', 'aireplay-ng', 'airmon-ng', 'dump1090' ] for proc in processes_to_kill: try: result = subprocess.run(['pkill', '-f', proc], capture_output=True) if result.returncode == 0: killed.append(proc) except (subprocess.SubprocessError, OSError): pass with process_lock: current_process = None with sensor_lock: sensor_process = None with wifi_lock: wifi_process = None # Reset ADS-B state with adsb_lock: adsb_process = None adsb_module.adsb_using_service = False return jsonify({'status': 'killed', 'processes': killed}) def main() -> None: """Main entry point.""" import argparse import config parser = argparse.ArgumentParser( description='INTERCEPT - Signal Intelligence Platform', epilog='Environment variables: INTERCEPT_HOST, INTERCEPT_PORT, INTERCEPT_DEBUG, INTERCEPT_LOG_LEVEL' ) parser.add_argument( '-p', '--port', type=int, default=config.PORT, help=f'Port to run server on (default: {config.PORT})' ) parser.add_argument( '-H', '--host', default=config.HOST, help=f'Host to bind to (default: {config.HOST})' ) parser.add_argument( '-d', '--debug', action='store_true', default=config.DEBUG, help='Enable debug mode' ) parser.add_argument( '--check-deps', action='store_true', help='Check dependencies and exit' ) args = parser.parse_args() # Check dependencies only if args.check_deps: results = check_all_dependencies() print("Dependency Status:") print("-" * 40) for mode, info in results.items(): status = "✓" if info['ready'] else "✗" print(f"\n{status} {info['name']}:") for tool, tool_info in info['tools'].items(): tool_status = "✓" if tool_info['installed'] else "✗" req = " (required)" if tool_info['required'] else "" print(f" {tool_status} {tool}{req}") sys.exit(0) print("=" * 50) print(" INTERCEPT // Signal Intelligence") print(" Pager / 433MHz / Aircraft / Satellite / WiFi / BT") print("=" * 50) print() # Clean up any stale processes from previous runs cleanup_stale_processes() # Register blueprints from routes import register_blueprints register_blueprints(app) print(f"Open http://localhost:{args.port} in your browser") print() print("Press Ctrl+C to stop") print() # Avoid loading a global ~/.env when running the script directly. app.run( host=args.host, port=args.port, debug=args.debug, threaded=True, load_dotenv=False, )