mirror of
https://github.com/smittix/intercept.git
synced 2026-07-24 08:58:09 -07:00
Merge pull request #96 from alphafox02/fix-agent-bugs
Fix agent mode issues and WiFi deep scan polling
This commit is contained in:
+279
-41
@@ -872,6 +872,150 @@ class ModeManager:
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# WiFi Monitor Mode
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def toggle_monitor_mode(self, params: dict) -> dict:
|
||||||
|
"""Enable or disable monitor mode on a WiFi interface."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
action = params.get('action', 'start')
|
||||||
|
interface = params.get('interface', '')
|
||||||
|
kill_processes = params.get('kill_processes', False)
|
||||||
|
|
||||||
|
# Validate interface name (alphanumeric, underscore, dash only)
|
||||||
|
if not interface or not re.match(r'^[a-zA-Z][a-zA-Z0-9_-]*$', interface):
|
||||||
|
return {'status': 'error', 'message': 'Invalid interface name'}
|
||||||
|
|
||||||
|
airmon_path = self._get_tool_path('airmon-ng')
|
||||||
|
iw_path = self._get_tool_path('iw')
|
||||||
|
|
||||||
|
if action == 'start':
|
||||||
|
if airmon_path:
|
||||||
|
try:
|
||||||
|
# Get interfaces before
|
||||||
|
def get_wireless_interfaces():
|
||||||
|
interfaces = set()
|
||||||
|
try:
|
||||||
|
for iface in os.listdir('/sys/class/net'):
|
||||||
|
if os.path.exists(f'/sys/class/net/{iface}/wireless') or 'mon' in iface:
|
||||||
|
interfaces.add(iface)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return interfaces
|
||||||
|
|
||||||
|
interfaces_before = get_wireless_interfaces()
|
||||||
|
|
||||||
|
# Kill interfering processes if requested
|
||||||
|
if kill_processes:
|
||||||
|
subprocess.run([airmon_path, 'check', 'kill'],
|
||||||
|
capture_output=True, timeout=10)
|
||||||
|
|
||||||
|
# Start monitor mode
|
||||||
|
result = subprocess.run([airmon_path, 'start', interface],
|
||||||
|
capture_output=True, text=True, timeout=15)
|
||||||
|
output = result.stdout + result.stderr
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
interfaces_after = get_wireless_interfaces()
|
||||||
|
|
||||||
|
# Find the new monitor interface
|
||||||
|
new_interfaces = interfaces_after - interfaces_before
|
||||||
|
monitor_iface = None
|
||||||
|
|
||||||
|
if new_interfaces:
|
||||||
|
for iface in new_interfaces:
|
||||||
|
if 'mon' in iface:
|
||||||
|
monitor_iface = iface
|
||||||
|
break
|
||||||
|
if not monitor_iface:
|
||||||
|
monitor_iface = list(new_interfaces)[0]
|
||||||
|
|
||||||
|
# Try to parse from airmon-ng output
|
||||||
|
if not monitor_iface:
|
||||||
|
patterns = [
|
||||||
|
r'\b([a-zA-Z][a-zA-Z0-9_-]*mon)\b',
|
||||||
|
r'\[phy\d+\]([a-zA-Z][a-zA-Z0-9_-]*mon)',
|
||||||
|
r'enabled.*?\[phy\d+\]([a-zA-Z][a-zA-Z0-9_-]*)',
|
||||||
|
]
|
||||||
|
for pattern in patterns:
|
||||||
|
match = re.search(pattern, output, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
candidate = match.group(1)
|
||||||
|
if candidate and not candidate[0].isdigit():
|
||||||
|
monitor_iface = candidate
|
||||||
|
break
|
||||||
|
|
||||||
|
# Fallback: check if original interface is in monitor mode
|
||||||
|
if not monitor_iface:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['iwconfig', interface],
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
if 'Mode:Monitor' in result.stdout:
|
||||||
|
monitor_iface = interface
|
||||||
|
except (subprocess.SubprocessError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Last resort: try common naming
|
||||||
|
if not monitor_iface:
|
||||||
|
potential = interface + 'mon'
|
||||||
|
if os.path.exists(f'/sys/class/net/{potential}'):
|
||||||
|
monitor_iface = potential
|
||||||
|
|
||||||
|
if not monitor_iface or not os.path.exists(f'/sys/class/net/{monitor_iface}'):
|
||||||
|
all_wireless = list(get_wireless_interfaces())
|
||||||
|
return {
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Monitor interface not created. airmon-ng output: {output[:500]}. Available interfaces: {all_wireless}'
|
||||||
|
}
|
||||||
|
|
||||||
|
self.wifi_monitor_interface = monitor_iface
|
||||||
|
self._capabilities = None # Invalidate cache so interfaces refresh
|
||||||
|
logger.info(f"Monitor mode enabled on {monitor_iface}")
|
||||||
|
return {'status': 'success', 'monitor_interface': monitor_iface}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error enabling monitor mode: {e}")
|
||||||
|
return {'status': 'error', 'message': str(e)}
|
||||||
|
|
||||||
|
elif iw_path:
|
||||||
|
try:
|
||||||
|
subprocess.run(['ip', 'link', 'set', interface, 'down'], capture_output=True)
|
||||||
|
subprocess.run([iw_path, interface, 'set', 'monitor', 'control'], capture_output=True)
|
||||||
|
subprocess.run(['ip', 'link', 'set', interface, 'up'], capture_output=True)
|
||||||
|
self.wifi_monitor_interface = interface
|
||||||
|
self._capabilities = None # Invalidate cache
|
||||||
|
return {'status': 'success', 'monitor_interface': interface}
|
||||||
|
except Exception as e:
|
||||||
|
return {'status': 'error', 'message': str(e)}
|
||||||
|
else:
|
||||||
|
return {'status': 'error', 'message': 'No monitor mode tools available (airmon-ng or iw)'}
|
||||||
|
|
||||||
|
else: # stop
|
||||||
|
current_iface = getattr(self, 'wifi_monitor_interface', None) or interface
|
||||||
|
if airmon_path:
|
||||||
|
try:
|
||||||
|
subprocess.run([airmon_path, 'stop', current_iface],
|
||||||
|
capture_output=True, text=True, timeout=15)
|
||||||
|
self.wifi_monitor_interface = None
|
||||||
|
self._capabilities = None # Invalidate cache
|
||||||
|
return {'status': 'success', 'message': 'Monitor mode disabled'}
|
||||||
|
except Exception as e:
|
||||||
|
return {'status': 'error', 'message': str(e)}
|
||||||
|
elif iw_path:
|
||||||
|
try:
|
||||||
|
subprocess.run(['ip', 'link', 'set', current_iface, 'down'], capture_output=True)
|
||||||
|
subprocess.run([iw_path, current_iface, 'set', 'type', 'managed'], capture_output=True)
|
||||||
|
subprocess.run(['ip', 'link', 'set', current_iface, 'up'], capture_output=True)
|
||||||
|
self.wifi_monitor_interface = None
|
||||||
|
self._capabilities = None # Invalidate cache
|
||||||
|
return {'status': 'success', 'message': 'Monitor mode disabled'}
|
||||||
|
except Exception as e:
|
||||||
|
return {'status': 'error', 'message': str(e)}
|
||||||
|
|
||||||
|
return {'status': 'error', 'message': 'Unknown action'}
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Mode-specific implementations
|
# Mode-specific implementations
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -914,26 +1058,34 @@ class ModeManager:
|
|||||||
"""Internal mode stop - terminates processes and cleans up."""
|
"""Internal mode stop - terminates processes and cleans up."""
|
||||||
logger.info(f"Stopping mode {mode}")
|
logger.info(f"Stopping mode {mode}")
|
||||||
|
|
||||||
# Signal stop
|
# Signal stop first - this unblocks any waiting threads
|
||||||
if mode in self.stop_events:
|
if mode in self.stop_events:
|
||||||
self.stop_events[mode].set()
|
self.stop_events[mode].set()
|
||||||
|
|
||||||
# Terminate process if running
|
# Terminate process if running
|
||||||
if mode in self.processes:
|
if mode in self.processes:
|
||||||
proc = self.processes[mode]
|
proc = self.processes[mode]
|
||||||
if proc and proc.poll() is None:
|
try:
|
||||||
proc.terminate()
|
if proc and proc.poll() is None:
|
||||||
try:
|
proc.terminate()
|
||||||
proc.wait(timeout=3)
|
try:
|
||||||
except subprocess.TimeoutExpired:
|
proc.wait(timeout=2)
|
||||||
proc.kill()
|
except subprocess.TimeoutExpired:
|
||||||
|
proc.kill()
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except (OSError, ProcessLookupError) as e:
|
||||||
|
# Process already dead or inaccessible
|
||||||
|
logger.debug(f"Process cleanup for {mode}: {e}")
|
||||||
del self.processes[mode]
|
del self.processes[mode]
|
||||||
|
|
||||||
# Wait for output thread
|
# Wait for output thread (short timeout since stop event is set)
|
||||||
if mode in self.output_threads:
|
if mode in self.output_threads:
|
||||||
thread = self.output_threads[mode]
|
thread = self.output_threads[mode]
|
||||||
if thread and thread.is_alive():
|
if thread and thread.is_alive():
|
||||||
thread.join(timeout=2)
|
thread.join(timeout=1)
|
||||||
del self.output_threads[mode]
|
del self.output_threads[mode]
|
||||||
|
|
||||||
# Clean up
|
# Clean up
|
||||||
@@ -1137,10 +1289,16 @@ class ModeManager:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass # Not JSON, ignore
|
pass # Not JSON, ignore
|
||||||
|
|
||||||
|
except (OSError, ValueError) as e:
|
||||||
|
# Bad file descriptor or closed file - process was terminated
|
||||||
|
logger.debug(f"Sensor output reader stopped: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Sensor output reader error: {e}")
|
logger.error(f"Sensor output reader error: {e}")
|
||||||
finally:
|
finally:
|
||||||
proc.wait()
|
try:
|
||||||
|
proc.wait(timeout=1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
logger.info("Sensor output reader stopped")
|
logger.info("Sensor output reader stopped")
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
@@ -2102,15 +2260,24 @@ class ModeManager:
|
|||||||
|
|
||||||
logger.debug(f"Pager: {parsed.get('protocol')} addr={parsed.get('address')}")
|
logger.debug(f"Pager: {parsed.get('protocol')} addr={parsed.get('address')}")
|
||||||
|
|
||||||
|
except (OSError, ValueError) as e:
|
||||||
|
# Bad file descriptor or closed file - process was terminated
|
||||||
|
logger.debug(f"Pager reader stopped: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Pager reader error: {e}")
|
logger.error(f"Pager reader error: {e}")
|
||||||
finally:
|
finally:
|
||||||
proc.wait()
|
try:
|
||||||
|
proc.wait(timeout=1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
if 'pager_rtl' in self.processes:
|
if 'pager_rtl' in self.processes:
|
||||||
rtl_proc = self.processes['pager_rtl']
|
try:
|
||||||
if rtl_proc.poll() is None:
|
rtl_proc = self.processes['pager_rtl']
|
||||||
rtl_proc.terminate()
|
if rtl_proc.poll() is None:
|
||||||
del self.processes['pager_rtl']
|
rtl_proc.terminate()
|
||||||
|
del self.processes['pager_rtl']
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
logger.info("Pager reader stopped")
|
logger.info("Pager reader stopped")
|
||||||
|
|
||||||
def _parse_pager_message(self, line: str) -> dict | None:
|
def _parse_pager_message(self, line: str) -> dict | None:
|
||||||
@@ -2492,10 +2659,15 @@ class ModeManager:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
except (OSError, ValueError) as e:
|
||||||
|
logger.debug(f"ACARS reader stopped: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"ACARS reader error: {e}")
|
logger.error(f"ACARS reader error: {e}")
|
||||||
finally:
|
finally:
|
||||||
proc.wait()
|
try:
|
||||||
|
proc.wait(timeout=1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
logger.info("ACARS reader stopped")
|
logger.info("ACARS reader stopped")
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
@@ -2632,15 +2804,23 @@ class ModeManager:
|
|||||||
|
|
||||||
logger.debug(f"APRS: {callsign}")
|
logger.debug(f"APRS: {callsign}")
|
||||||
|
|
||||||
|
except (OSError, ValueError) as e:
|
||||||
|
logger.debug(f"APRS reader stopped: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"APRS reader error: {e}")
|
logger.error(f"APRS reader error: {e}")
|
||||||
finally:
|
finally:
|
||||||
proc.wait()
|
try:
|
||||||
|
proc.wait(timeout=1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
if 'aprs_rtl' in self.processes:
|
if 'aprs_rtl' in self.processes:
|
||||||
rtl_proc = self.processes['aprs_rtl']
|
try:
|
||||||
if rtl_proc.poll() is None:
|
rtl_proc = self.processes['aprs_rtl']
|
||||||
rtl_proc.terminate()
|
if rtl_proc.poll() is None:
|
||||||
del self.processes['aprs_rtl']
|
rtl_proc.terminate()
|
||||||
|
del self.processes['aprs_rtl']
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
logger.info("APRS reader stopped")
|
logger.info("APRS reader stopped")
|
||||||
|
|
||||||
def _parse_aprs_packet(self, line: str) -> dict | None:
|
def _parse_aprs_packet(self, line: str) -> dict | None:
|
||||||
@@ -2788,15 +2968,23 @@ class ModeManager:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
except (OSError, ValueError) as e:
|
||||||
|
logger.debug(f"RTLAMR reader stopped: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"RTLAMR reader error: {e}")
|
logger.error(f"RTLAMR reader error: {e}")
|
||||||
finally:
|
finally:
|
||||||
proc.wait()
|
try:
|
||||||
|
proc.wait(timeout=1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
if 'rtlamr_tcp' in self.processes:
|
if 'rtlamr_tcp' in self.processes:
|
||||||
tcp_proc = self.processes['rtlamr_tcp']
|
try:
|
||||||
if tcp_proc.poll() is None:
|
tcp_proc = self.processes['rtlamr_tcp']
|
||||||
tcp_proc.terminate()
|
if tcp_proc.poll() is None:
|
||||||
del self.processes['rtlamr_tcp']
|
tcp_proc.terminate()
|
||||||
|
del self.processes['rtlamr_tcp']
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
logger.info("RTLAMR reader stopped")
|
logger.info("RTLAMR reader stopped")
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
@@ -2901,10 +3089,15 @@ class ModeManager:
|
|||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.warning("DSCDecoder not available (missing scipy/numpy)")
|
logger.warning("DSCDecoder not available (missing scipy/numpy)")
|
||||||
|
except (OSError, ValueError) as e:
|
||||||
|
logger.debug(f"DSC reader stopped: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"DSC reader error: {e}")
|
logger.error(f"DSC reader error: {e}")
|
||||||
finally:
|
finally:
|
||||||
proc.wait()
|
try:
|
||||||
|
proc.wait(timeout=1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
logger.info("DSC reader stopped")
|
logger.info("DSC reader stopped")
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
@@ -3629,6 +3822,12 @@ class InterceptAgentHandler(BaseHTTPRequestHandler):
|
|||||||
config.push_interval = int(body['push_interval'])
|
config.push_interval = int(body['push_interval'])
|
||||||
self._send_json({'status': 'updated', 'config': config.to_dict()})
|
self._send_json({'status': 'updated', 'config': config.to_dict()})
|
||||||
|
|
||||||
|
elif path == '/wifi/monitor':
|
||||||
|
# Enable/disable monitor mode on WiFi interface
|
||||||
|
result = mode_manager.toggle_monitor_mode(body)
|
||||||
|
status = 200 if result.get('status') == 'success' else 400
|
||||||
|
self._send_json(result, status)
|
||||||
|
|
||||||
elif path.startswith('/') and path.count('/') == 2:
|
elif path.startswith('/') and path.count('/') == 2:
|
||||||
# /{mode}/start or /{mode}/stop
|
# /{mode}/start or /{mode}/stop
|
||||||
parts = path.split('/')
|
parts = path.split('/')
|
||||||
@@ -3794,19 +3993,53 @@ def main():
|
|||||||
print(" Press Ctrl+C to stop")
|
print(" Press Ctrl+C to stop")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# Handle shutdown
|
# Shutdown flag
|
||||||
|
shutdown_requested = threading.Event()
|
||||||
|
|
||||||
|
# Handle shutdown - run cleanup in separate thread to avoid blocking
|
||||||
def signal_handler(sig, frame):
|
def signal_handler(sig, frame):
|
||||||
|
if shutdown_requested.is_set():
|
||||||
|
# Already shutting down, force exit
|
||||||
|
print("\nForce exit...")
|
||||||
|
os._exit(1)
|
||||||
|
shutdown_requested.set()
|
||||||
print("\nShutting down...")
|
print("\nShutting down...")
|
||||||
# Stop all running modes
|
|
||||||
for mode in list(mode_manager.running_modes.keys()):
|
def cleanup():
|
||||||
mode_manager.stop_mode(mode)
|
# Stop all running modes first (they have subprocesses)
|
||||||
if data_push_loop:
|
for mode in list(mode_manager.running_modes.keys()):
|
||||||
data_push_loop.stop()
|
try:
|
||||||
if push_client:
|
mode_manager.stop_mode(mode)
|
||||||
push_client.stop()
|
except Exception as e:
|
||||||
gps_manager.stop()
|
logger.debug(f"Error stopping {mode}: {e}")
|
||||||
httpd.shutdown()
|
|
||||||
sys.exit(0)
|
# Stop push services
|
||||||
|
if data_push_loop:
|
||||||
|
try:
|
||||||
|
data_push_loop.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if push_client:
|
||||||
|
try:
|
||||||
|
push_client.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Stop GPS
|
||||||
|
try:
|
||||||
|
gps_manager.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Shutdown HTTP server
|
||||||
|
try:
|
||||||
|
httpd.shutdown()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Run cleanup in background thread so signal handler returns quickly
|
||||||
|
cleanup_thread = threading.Thread(target=cleanup, daemon=True)
|
||||||
|
cleanup_thread.start()
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
signal.signal(signal.SIGTERM, signal_handler)
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
@@ -3815,9 +4048,14 @@ def main():
|
|||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
finally:
|
except Exception:
|
||||||
if push_client:
|
pass
|
||||||
push_client.stop()
|
|
||||||
|
# Give cleanup thread time to finish
|
||||||
|
if shutdown_requested.is_set():
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
print("Agent stopped.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
+66
-1
@@ -91,6 +91,17 @@ def register_agent():
|
|||||||
if not base_url:
|
if not base_url:
|
||||||
return jsonify({'status': 'error', 'message': 'Base URL is required'}), 400
|
return jsonify({'status': 'error', 'message': 'Base URL is required'}), 400
|
||||||
|
|
||||||
|
# Validate URL format
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
try:
|
||||||
|
parsed = urlparse(base_url)
|
||||||
|
if parsed.scheme not in ('http', 'https'):
|
||||||
|
return jsonify({'status': 'error', 'message': 'URL must start with http:// or https://'}), 400
|
||||||
|
if not parsed.netloc:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Invalid URL format'}), 400
|
||||||
|
except Exception:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Invalid URL format'}), 400
|
||||||
|
|
||||||
# Check if agent already exists
|
# Check if agent already exists
|
||||||
existing = get_agent_by_name(name)
|
existing = get_agent_by_name(name)
|
||||||
if existing:
|
if existing:
|
||||||
@@ -128,9 +139,12 @@ def register_agent():
|
|||||||
update_agent(agent_id, update_last_seen=True)
|
update_agent(agent_id, update_last_seen=True)
|
||||||
|
|
||||||
agent = get_agent(agent_id)
|
agent = get_agent(agent_id)
|
||||||
|
message = 'Agent registered successfully'
|
||||||
|
if capabilities is None:
|
||||||
|
message += ' (could not connect - agent may be offline)'
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'message': 'Agent registered successfully',
|
'message': message,
|
||||||
'agent': agent
|
'agent': agent
|
||||||
}), 201
|
}), 201
|
||||||
|
|
||||||
@@ -466,6 +480,57 @@ def proxy_mode_data(agent_id: int, mode: str):
|
|||||||
}), 502
|
}), 502
|
||||||
|
|
||||||
|
|
||||||
|
@controller_bp.route('/agents/<int:agent_id>/wifi/monitor', methods=['POST'])
|
||||||
|
def proxy_wifi_monitor(agent_id: int):
|
||||||
|
"""Toggle monitor mode on a remote agent's WiFi interface."""
|
||||||
|
agent = get_agent(agent_id)
|
||||||
|
if not agent:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Agent not found'}), 404
|
||||||
|
|
||||||
|
data = request.json or {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = create_client_from_agent(agent)
|
||||||
|
result = client.post('/wifi/monitor', data)
|
||||||
|
|
||||||
|
# Refresh agent capabilities after monitor mode toggle so UI stays in sync
|
||||||
|
if result.get('status') == 'success':
|
||||||
|
try:
|
||||||
|
metadata = client.refresh_metadata()
|
||||||
|
if metadata.get('healthy'):
|
||||||
|
caps = metadata.get('capabilities') or {}
|
||||||
|
agent_interfaces = caps.get('interfaces', {})
|
||||||
|
if not agent_interfaces.get('sdr_devices') and caps.get('devices'):
|
||||||
|
agent_interfaces['sdr_devices'] = caps.get('devices', [])
|
||||||
|
update_agent(
|
||||||
|
agent_id,
|
||||||
|
capabilities=caps.get('modes'),
|
||||||
|
interfaces=agent_interfaces,
|
||||||
|
update_last_seen=True
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass # Non-fatal if refresh fails
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': result.get('status', 'error'),
|
||||||
|
'agent_id': agent_id,
|
||||||
|
'agent_name': agent['name'],
|
||||||
|
'monitor_interface': result.get('monitor_interface'),
|
||||||
|
'message': result.get('message')
|
||||||
|
})
|
||||||
|
|
||||||
|
except AgentConnectionError as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Cannot connect to agent: {e}'
|
||||||
|
}), 503
|
||||||
|
except AgentHTTPError as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Agent error: {e}'
|
||||||
|
}), 502
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Push Data Ingestion
|
# Push Data Ingestion
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
+106
-5
@@ -77,6 +77,7 @@ const WiFiMode = (function() {
|
|||||||
let scanMode = 'quick'; // 'quick' or 'deep'
|
let scanMode = 'quick'; // 'quick' or 'deep'
|
||||||
let eventSource = null;
|
let eventSource = null;
|
||||||
let pollTimer = null;
|
let pollTimer = null;
|
||||||
|
let agentPollTimer = null;
|
||||||
|
|
||||||
// Data stores
|
// Data stores
|
||||||
let networks = new Map(); // bssid -> network
|
let networks = new Map(); // bssid -> network
|
||||||
@@ -505,8 +506,13 @@ const WiFiMode = (function() {
|
|||||||
console.log('[WiFiMode] Agent deep scan started:', scanResult);
|
console.log('[WiFiMode] Agent deep scan started:', scanResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start SSE stream for real-time updates
|
// Start SSE stream for real-time updates (works with push-enabled agents)
|
||||||
startEventStream();
|
startEventStream();
|
||||||
|
|
||||||
|
// Also start polling for agent data (works without push enabled)
|
||||||
|
if (isAgentMode) {
|
||||||
|
startAgentDeepScanPolling();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[WiFiMode] Deep scan error:', error);
|
console.error('[WiFiMode] Deep scan error:', error);
|
||||||
showError(error.message);
|
showError(error.message);
|
||||||
@@ -523,6 +529,9 @@ const WiFiMode = (function() {
|
|||||||
pollTimer = null;
|
pollTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop agent polling
|
||||||
|
stopAgentDeepScanPolling();
|
||||||
|
|
||||||
// Close event stream
|
// Close event stream
|
||||||
if (eventSource) {
|
if (eventSource) {
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
@@ -584,9 +593,18 @@ const WiFiMode = (function() {
|
|||||||
const status = isAgentMode && data.result ? data.result : data;
|
const status = isAgentMode && data.result ? data.result : data;
|
||||||
|
|
||||||
if (status.is_scanning || status.running) {
|
if (status.is_scanning || status.running) {
|
||||||
setScanning(true, status.scan_mode);
|
// Agent returns scan_type in params, local returns scan_mode
|
||||||
if (status.scan_mode === 'deep') {
|
// Normalize: agent may return 'deepscan' or 'deep', UI expects 'deep' or 'quick'
|
||||||
|
let detectedMode = status.scan_mode || (status.params && status.params.scan_type) || 'deep';
|
||||||
|
if (detectedMode === 'deepscan') detectedMode = 'deep';
|
||||||
|
|
||||||
|
setScanning(true, detectedMode);
|
||||||
|
if (detectedMode === 'deep') {
|
||||||
startEventStream();
|
startEventStream();
|
||||||
|
// Also start polling for agent mode (works without push enabled)
|
||||||
|
if (isAgentMode) {
|
||||||
|
startAgentDeepScanPolling();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
startQuickScanPolling();
|
startQuickScanPolling();
|
||||||
}
|
}
|
||||||
@@ -655,6 +673,76 @@ const WiFiMode = (function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Agent Deep Scan Polling (fallback when push is not enabled)
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
function startAgentDeepScanPolling() {
|
||||||
|
if (agentPollTimer) return;
|
||||||
|
|
||||||
|
console.log('[WiFiMode] Starting agent deep scan polling...');
|
||||||
|
|
||||||
|
agentPollTimer = setInterval(async () => {
|
||||||
|
if (!isScanning || scanMode !== 'deep') {
|
||||||
|
clearInterval(agentPollTimer);
|
||||||
|
agentPollTimer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
|
if (!isAgentMode) {
|
||||||
|
clearInterval(agentPollTimer);
|
||||||
|
agentPollTimer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/controller/agents/${currentAgent}/wifi/data`);
|
||||||
|
if (!response.ok) return;
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.status !== 'success' || !result.data) return;
|
||||||
|
|
||||||
|
const data = result.data.data || result.data;
|
||||||
|
const agentName = result.agent_name || 'Remote';
|
||||||
|
|
||||||
|
// Process networks
|
||||||
|
if (data.networks && Array.isArray(data.networks)) {
|
||||||
|
data.networks.forEach(net => {
|
||||||
|
net._agent = agentName;
|
||||||
|
handleStreamEvent({
|
||||||
|
type: 'network_update',
|
||||||
|
network: net
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process clients
|
||||||
|
if (data.clients && Array.isArray(data.clients)) {
|
||||||
|
data.clients.forEach(client => {
|
||||||
|
client._agent = agentName;
|
||||||
|
handleStreamEvent({
|
||||||
|
type: 'client_update',
|
||||||
|
client: client
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(`[WiFiMode] Agent poll: ${data.networks?.length || 0} networks, ${data.clients?.length || 0} clients`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.debug('[WiFiMode] Agent poll error:', error);
|
||||||
|
}
|
||||||
|
}, 2000); // Poll every 2 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAgentDeepScanPolling() {
|
||||||
|
if (agentPollTimer) {
|
||||||
|
clearInterval(agentPollTimer);
|
||||||
|
agentPollTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// SSE Event Stream
|
// SSE Event Stream
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -1292,9 +1380,19 @@ const WiFiMode = (function() {
|
|||||||
|
|
||||||
console.log('[WiFiMode] Agent changed from', lastAgentId, 'to', currentAgentId);
|
console.log('[WiFiMode] Agent changed from', lastAgentId, 'to', currentAgentId);
|
||||||
|
|
||||||
// Stop any running scan
|
// Stop UI polling only - don't stop the actual scan on the agent
|
||||||
|
// The agent should continue running independently
|
||||||
if (isScanning) {
|
if (isScanning) {
|
||||||
stopScan();
|
stopAgentDeepScanPolling();
|
||||||
|
if (pollTimer) {
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
pollTimer = null;
|
||||||
|
}
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
eventSource = null;
|
||||||
|
}
|
||||||
|
setScanning(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear existing data when switching agents (unless "Show All" is enabled)
|
// Clear existing data when switching agents (unless "Show All" is enabled)
|
||||||
@@ -1306,6 +1404,9 @@ const WiFiMode = (function() {
|
|||||||
// Refresh capabilities for new agent
|
// Refresh capabilities for new agent
|
||||||
checkCapabilities();
|
checkCapabilities();
|
||||||
|
|
||||||
|
// Check if new agent already has a scan running
|
||||||
|
checkScanStatus();
|
||||||
|
|
||||||
lastAgentId = currentAgentId;
|
lastAgentId = currentAgentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -337,6 +337,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="agentApiKey">API Key (optional)</label>
|
<label for="agentApiKey">API Key (optional)</label>
|
||||||
<input type="text" id="agentApiKey" placeholder="shared-secret">
|
<input type="text" id="agentApiKey" placeholder="shared-secret">
|
||||||
|
<small style="color: #888; font-size: 11px;">Required if agent has push mode enabled with API key</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
@@ -455,6 +456,22 @@
|
|||||||
const apiKey = document.getElementById('agentApiKey').value.trim();
|
const apiKey = document.getElementById('agentApiKey').value.trim();
|
||||||
const description = document.getElementById('agentDescription').value.trim();
|
const description = document.getElementById('agentDescription').value.trim();
|
||||||
|
|
||||||
|
// Validate URL format
|
||||||
|
try {
|
||||||
|
const url = new URL(baseUrl);
|
||||||
|
if (!url.port && !baseUrl.includes(':80') && !baseUrl.includes(':443')) {
|
||||||
|
showToast('URL should include a port (e.g., http://192.168.1.50:8020)', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||||
|
showToast('URL must start with http:// or https://', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('Invalid URL format. Use: http://IP_ADDRESS:PORT', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/controller/agents', {
|
const response = await fetch('/controller/agents', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
+65
-23
@@ -5444,6 +5444,53 @@
|
|||||||
const select = document.getElementById('wifiInterfaceSelect');
|
const select = document.getElementById('wifiInterfaceSelect');
|
||||||
select.innerHTML = '<option value="">Loading interfaces...</option>';
|
select.innerHTML = '<option value="">Loading interfaces...</option>';
|
||||||
|
|
||||||
|
// Check if we're in agent mode
|
||||||
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
|
|
||||||
|
if (isAgentMode) {
|
||||||
|
// Fetch from agent via controller
|
||||||
|
fetch(`/controller/agents/${currentAgent}?refresh=true`)
|
||||||
|
.then(r => {
|
||||||
|
if (!r.ok) throw new Error('Failed to fetch agent interfaces');
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
const interfaces = data.agent?.interfaces?.wifi_interfaces || [];
|
||||||
|
if (interfaces.length === 0) {
|
||||||
|
select.innerHTML = '<option value="">No WiFi interfaces on agent</option>';
|
||||||
|
showNotification('WiFi', 'No WiFi interfaces found on remote agent.');
|
||||||
|
monitorInterface = null;
|
||||||
|
updateMonitorStatus(false);
|
||||||
|
} else {
|
||||||
|
select.innerHTML = interfaces.map(i => {
|
||||||
|
let label = i.name || i;
|
||||||
|
if (i.display_name) label = i.display_name;
|
||||||
|
else if (i.type) label += ` (${i.type})`;
|
||||||
|
if (i.monitor_capable) label += ' [Monitor OK]';
|
||||||
|
return `<option value="${i.name || i}" data-type="${i.type || 'managed'}">${label}</option>`;
|
||||||
|
}).join('');
|
||||||
|
showNotification('WiFi', `Found ${interfaces.length} interface(s) on agent`);
|
||||||
|
|
||||||
|
// Check if any interface is already in monitor mode
|
||||||
|
const monitorIface = interfaces.find(i => i.type === 'monitor');
|
||||||
|
if (monitorIface) {
|
||||||
|
monitorInterface = monitorIface.name;
|
||||||
|
updateMonitorStatus(true);
|
||||||
|
select.value = monitorIface.name;
|
||||||
|
} else {
|
||||||
|
monitorInterface = null;
|
||||||
|
updateMonitorStatus(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Failed to refresh agent interfaces:', err);
|
||||||
|
select.innerHTML = '<option value="">Error loading agent interfaces</option>';
|
||||||
|
showNotification('WiFi', 'Failed to load agent interfaces');
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fetch('/wifi/interfaces')
|
fetch('/wifi/interfaces')
|
||||||
.then(r => {
|
.then(r => {
|
||||||
if (!r.ok) throw new Error('Failed to fetch interfaces');
|
if (!r.ok) throw new Error('Failed to fetch interfaces');
|
||||||
@@ -5500,6 +5547,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const killProcesses = document.getElementById('killProcesses').checked;
|
const killProcesses = document.getElementById('killProcesses').checked;
|
||||||
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
|
|
||||||
// Show loading state
|
// Show loading state
|
||||||
const btn = document.getElementById('monitorStartBtn');
|
const btn = document.getElementById('monitorStartBtn');
|
||||||
@@ -5507,7 +5555,12 @@
|
|||||||
btn.textContent = 'Enabling...';
|
btn.textContent = 'Enabling...';
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|
||||||
fetch('/wifi/monitor', {
|
// Use agent endpoint if in agent mode
|
||||||
|
const endpoint = isAgentMode
|
||||||
|
? `/controller/agents/${currentAgent}/wifi/monitor`
|
||||||
|
: '/wifi/monitor';
|
||||||
|
|
||||||
|
fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ interface: iface, action: 'start', kill_processes: killProcesses })
|
body: JSON.stringify({ interface: iface, action: 'start', kill_processes: killProcesses })
|
||||||
@@ -5519,29 +5572,13 @@
|
|||||||
if (data.status === 'success') {
|
if (data.status === 'success') {
|
||||||
monitorInterface = data.monitor_interface;
|
monitorInterface = data.monitor_interface;
|
||||||
updateMonitorStatus(true);
|
updateMonitorStatus(true);
|
||||||
showInfo('Monitor mode enabled on ' + monitorInterface + ' - Ready to scan!');
|
const location = isAgentMode ? ' on remote agent' : '';
|
||||||
|
showInfo('Monitor mode enabled on ' + monitorInterface + location + ' - Ready to scan!');
|
||||||
|
|
||||||
// Refresh interface list and auto-select the monitor interface
|
// Refresh interface list and auto-select the monitor interface
|
||||||
fetch('/wifi/interfaces')
|
refreshWifiInterfaces();
|
||||||
.then(r => r.json())
|
|
||||||
.then(ifaceData => {
|
|
||||||
const select = document.getElementById('wifiInterfaceSelect');
|
|
||||||
if (ifaceData.interfaces.length > 0) {
|
|
||||||
select.innerHTML = ifaceData.interfaces.map(i => {
|
|
||||||
let label = i.name;
|
|
||||||
let details = [];
|
|
||||||
if (i.chipset) details.push(i.chipset);
|
|
||||||
else if (i.driver) details.push(i.driver);
|
|
||||||
if (i.mac) details.push(i.mac.substring(0, 8) + '...');
|
|
||||||
if (details.length > 0) label += ' - ' + details.join(' | ');
|
|
||||||
label += ` (${i.type})`;
|
|
||||||
if (i.monitor_capable) label += ' [Monitor OK]';
|
|
||||||
return `<option value="${i.name}" ${i.name === monitorInterface ? 'selected' : ''}>${label}</option>`;
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
alert('Error: ' + data.message);
|
alert('Error: ' + (data.message || 'Unknown error'));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
@@ -5554,8 +5591,13 @@
|
|||||||
// Disable monitor mode
|
// Disable monitor mode
|
||||||
function disableMonitorMode() {
|
function disableMonitorMode() {
|
||||||
const iface = monitorInterface || document.getElementById('wifiInterfaceSelect').value;
|
const iface = monitorInterface || document.getElementById('wifiInterfaceSelect').value;
|
||||||
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
|
|
||||||
fetch('/wifi/monitor', {
|
const endpoint = isAgentMode
|
||||||
|
? `/controller/agents/${currentAgent}/wifi/monitor`
|
||||||
|
: '/wifi/monitor';
|
||||||
|
|
||||||
|
fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ interface: iface, action: 'stop' })
|
body: JSON.stringify({ interface: iface, action: 'stop' })
|
||||||
@@ -5566,7 +5608,7 @@
|
|||||||
updateMonitorStatus(false);
|
updateMonitorStatus(false);
|
||||||
showInfo('Monitor mode disabled');
|
showInfo('Monitor mode disabled');
|
||||||
} else {
|
} else {
|
||||||
alert('Error: ' + data.message);
|
alert('Error: ' + (data.message || 'Unknown error'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ class AgentClient:
|
|||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
raise AgentHTTPError(f"Request failed: {e}")
|
raise AgentHTTPError(f"Request failed: {e}")
|
||||||
|
|
||||||
|
def post(self, path: str, data: dict | None = None) -> dict:
|
||||||
|
"""Public POST method for arbitrary endpoints."""
|
||||||
|
return self._post(path, data)
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Capability & Status
|
# Capability & Status
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user