mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Fix agent mode issues and WiFi deep scan polling
Agent fixes:
- Fix Ctrl+C hang by running cleanup in background thread
- Add force-exit on double Ctrl+C
- Improve exception handling in output reader threads to prevent
bad file descriptor errors on shutdown
- Reduce cleanup timeouts for faster shutdown
Controller/UI fixes:
- Add URL validation for agent registration (check port, protocol)
- Show helpful message when agent is unreachable during registration
- Clarify API key field label (reserved for future use)
- Add client-side URL validation with user-friendly error messages
WiFi agent mode fixes:
- Add polling fallback for deep scan when push mode is disabled
- Polls /controller/agents/{id}/wifi/data every 2 seconds
- Detect running scans when switching to an agent
- Fix scan_mode detection (agent uses params.scan_type)
This commit is contained in:
@@ -872,6 +872,146 @@ class ModeManager:
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
# =========================================================================
|
||||
@@ -914,26 +1054,34 @@ class ModeManager:
|
||||
"""Internal mode stop - terminates processes and cleans up."""
|
||||
logger.info(f"Stopping mode {mode}")
|
||||
|
||||
# Signal stop
|
||||
# Signal stop first - this unblocks any waiting threads
|
||||
if mode in self.stop_events:
|
||||
self.stop_events[mode].set()
|
||||
|
||||
# Terminate process if running
|
||||
if mode in self.processes:
|
||||
proc = self.processes[mode]
|
||||
if proc and proc.poll() is None:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
try:
|
||||
if proc and proc.poll() is None:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=2)
|
||||
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]
|
||||
|
||||
# Wait for output thread
|
||||
# Wait for output thread (short timeout since stop event is set)
|
||||
if mode in self.output_threads:
|
||||
thread = self.output_threads[mode]
|
||||
if thread and thread.is_alive():
|
||||
thread.join(timeout=2)
|
||||
thread.join(timeout=1)
|
||||
del self.output_threads[mode]
|
||||
|
||||
# Clean up
|
||||
@@ -1137,10 +1285,16 @@ class ModeManager:
|
||||
except json.JSONDecodeError:
|
||||
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:
|
||||
logger.error(f"Sensor output reader error: {e}")
|
||||
finally:
|
||||
proc.wait()
|
||||
try:
|
||||
proc.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
logger.info("Sensor output reader stopped")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -2102,15 +2256,24 @@ class ModeManager:
|
||||
|
||||
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:
|
||||
logger.error(f"Pager reader error: {e}")
|
||||
finally:
|
||||
proc.wait()
|
||||
try:
|
||||
proc.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
if 'pager_rtl' in self.processes:
|
||||
rtl_proc = self.processes['pager_rtl']
|
||||
if rtl_proc.poll() is None:
|
||||
rtl_proc.terminate()
|
||||
del self.processes['pager_rtl']
|
||||
try:
|
||||
rtl_proc = self.processes['pager_rtl']
|
||||
if rtl_proc.poll() is None:
|
||||
rtl_proc.terminate()
|
||||
del self.processes['pager_rtl']
|
||||
except Exception:
|
||||
pass
|
||||
logger.info("Pager reader stopped")
|
||||
|
||||
def _parse_pager_message(self, line: str) -> dict | None:
|
||||
@@ -2492,10 +2655,15 @@ class ModeManager:
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
except (OSError, ValueError) as e:
|
||||
logger.debug(f"ACARS reader stopped: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"ACARS reader error: {e}")
|
||||
finally:
|
||||
proc.wait()
|
||||
try:
|
||||
proc.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
logger.info("ACARS reader stopped")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -2632,15 +2800,23 @@ class ModeManager:
|
||||
|
||||
logger.debug(f"APRS: {callsign}")
|
||||
|
||||
except (OSError, ValueError) as e:
|
||||
logger.debug(f"APRS reader stopped: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"APRS reader error: {e}")
|
||||
finally:
|
||||
proc.wait()
|
||||
try:
|
||||
proc.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
if 'aprs_rtl' in self.processes:
|
||||
rtl_proc = self.processes['aprs_rtl']
|
||||
if rtl_proc.poll() is None:
|
||||
rtl_proc.terminate()
|
||||
del self.processes['aprs_rtl']
|
||||
try:
|
||||
rtl_proc = self.processes['aprs_rtl']
|
||||
if rtl_proc.poll() is None:
|
||||
rtl_proc.terminate()
|
||||
del self.processes['aprs_rtl']
|
||||
except Exception:
|
||||
pass
|
||||
logger.info("APRS reader stopped")
|
||||
|
||||
def _parse_aprs_packet(self, line: str) -> dict | None:
|
||||
@@ -2788,15 +2964,23 @@ class ModeManager:
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
except (OSError, ValueError) as e:
|
||||
logger.debug(f"RTLAMR reader stopped: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"RTLAMR reader error: {e}")
|
||||
finally:
|
||||
proc.wait()
|
||||
try:
|
||||
proc.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
if 'rtlamr_tcp' in self.processes:
|
||||
tcp_proc = self.processes['rtlamr_tcp']
|
||||
if tcp_proc.poll() is None:
|
||||
tcp_proc.terminate()
|
||||
del self.processes['rtlamr_tcp']
|
||||
try:
|
||||
tcp_proc = self.processes['rtlamr_tcp']
|
||||
if tcp_proc.poll() is None:
|
||||
tcp_proc.terminate()
|
||||
del self.processes['rtlamr_tcp']
|
||||
except Exception:
|
||||
pass
|
||||
logger.info("RTLAMR reader stopped")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -2901,10 +3085,15 @@ class ModeManager:
|
||||
|
||||
except ImportError:
|
||||
logger.warning("DSCDecoder not available (missing scipy/numpy)")
|
||||
except (OSError, ValueError) as e:
|
||||
logger.debug(f"DSC reader stopped: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"DSC reader error: {e}")
|
||||
finally:
|
||||
proc.wait()
|
||||
try:
|
||||
proc.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
logger.info("DSC reader stopped")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -3629,6 +3818,12 @@ class InterceptAgentHandler(BaseHTTPRequestHandler):
|
||||
config.push_interval = int(body['push_interval'])
|
||||
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:
|
||||
# /{mode}/start or /{mode}/stop
|
||||
parts = path.split('/')
|
||||
@@ -3794,19 +3989,53 @@ def main():
|
||||
print(" Press Ctrl+C to stop")
|
||||
print()
|
||||
|
||||
# Handle shutdown
|
||||
# Shutdown flag
|
||||
shutdown_requested = threading.Event()
|
||||
|
||||
# Handle shutdown - run cleanup in separate thread to avoid blocking
|
||||
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...")
|
||||
# Stop all running modes
|
||||
for mode in list(mode_manager.running_modes.keys()):
|
||||
mode_manager.stop_mode(mode)
|
||||
if data_push_loop:
|
||||
data_push_loop.stop()
|
||||
if push_client:
|
||||
push_client.stop()
|
||||
gps_manager.stop()
|
||||
httpd.shutdown()
|
||||
sys.exit(0)
|
||||
|
||||
def cleanup():
|
||||
# Stop all running modes first (they have subprocesses)
|
||||
for mode in list(mode_manager.running_modes.keys()):
|
||||
try:
|
||||
mode_manager.stop_mode(mode)
|
||||
except Exception as e:
|
||||
logger.debug(f"Error stopping {mode}: {e}")
|
||||
|
||||
# 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.SIGTERM, signal_handler)
|
||||
@@ -3815,9 +4044,14 @@ def main():
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
if push_client:
|
||||
push_client.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Give cleanup thread time to finish
|
||||
if shutdown_requested.is_set():
|
||||
time.sleep(0.5)
|
||||
|
||||
print("Agent stopped.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -91,6 +91,17 @@ def register_agent():
|
||||
if not base_url:
|
||||
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
|
||||
existing = get_agent_by_name(name)
|
||||
if existing:
|
||||
@@ -128,9 +139,12 @@ def register_agent():
|
||||
update_agent(agent_id, update_last_seen=True)
|
||||
|
||||
agent = get_agent(agent_id)
|
||||
message = 'Agent registered successfully'
|
||||
if capabilities is None:
|
||||
message += ' (could not connect - agent may be offline)'
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Agent registered successfully',
|
||||
'message': message,
|
||||
'agent': agent
|
||||
}), 201
|
||||
|
||||
@@ -466,6 +480,39 @@ def proxy_mode_data(agent_id: int, mode: str):
|
||||
}), 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)
|
||||
|
||||
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
|
||||
# =============================================================================
|
||||
|
||||
@@ -77,6 +77,7 @@ const WiFiMode = (function() {
|
||||
let scanMode = 'quick'; // 'quick' or 'deep'
|
||||
let eventSource = null;
|
||||
let pollTimer = null;
|
||||
let agentPollTimer = null;
|
||||
|
||||
// Data stores
|
||||
let networks = new Map(); // bssid -> network
|
||||
@@ -505,8 +506,13 @@ const WiFiMode = (function() {
|
||||
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();
|
||||
|
||||
// Also start polling for agent data (works without push enabled)
|
||||
if (isAgentMode) {
|
||||
startAgentDeepScanPolling();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[WiFiMode] Deep scan error:', error);
|
||||
showError(error.message);
|
||||
@@ -523,6 +529,9 @@ const WiFiMode = (function() {
|
||||
pollTimer = null;
|
||||
}
|
||||
|
||||
// Stop agent polling
|
||||
stopAgentDeepScanPolling();
|
||||
|
||||
// Close event stream
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
@@ -584,9 +593,15 @@ const WiFiMode = (function() {
|
||||
const status = isAgentMode && data.result ? data.result : data;
|
||||
|
||||
if (status.is_scanning || status.running) {
|
||||
setScanning(true, status.scan_mode);
|
||||
if (status.scan_mode === 'deep') {
|
||||
// Agent returns scan_type in params, local returns scan_mode
|
||||
const detectedMode = status.scan_mode || (status.params && status.params.scan_type) || 'deep';
|
||||
setScanning(true, detectedMode);
|
||||
if (detectedMode === 'deep') {
|
||||
startEventStream();
|
||||
// Also start polling for agent mode (works without push enabled)
|
||||
if (isAgentMode) {
|
||||
startAgentDeepScanPolling();
|
||||
}
|
||||
} else {
|
||||
startQuickScanPolling();
|
||||
}
|
||||
@@ -655,6 +670,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
|
||||
// ==========================================================================
|
||||
@@ -1306,6 +1391,9 @@ const WiFiMode = (function() {
|
||||
// Refresh capabilities for new agent
|
||||
checkCapabilities();
|
||||
|
||||
// Check if new agent already has a scan running
|
||||
checkScanStatus();
|
||||
|
||||
lastAgentId = currentAgentId;
|
||||
}
|
||||
|
||||
|
||||
@@ -337,6 +337,7 @@
|
||||
<div class="form-group">
|
||||
<label for="agentApiKey">API Key (optional)</label>
|
||||
<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 class="form-row">
|
||||
@@ -455,6 +456,22 @@
|
||||
const apiKey = document.getElementById('agentApiKey').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 {
|
||||
const response = await fetch('/controller/agents', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -5444,6 +5444,40 @@
|
||||
const select = document.getElementById('wifiInterfaceSelect');
|
||||
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.');
|
||||
} 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}">${label}</option>`;
|
||||
}).join('');
|
||||
showNotification('WiFi', `Found ${interfaces.length} interface(s) on agent`);
|
||||
}
|
||||
})
|
||||
.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')
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error('Failed to fetch interfaces');
|
||||
@@ -5500,6 +5534,7 @@
|
||||
}
|
||||
|
||||
const killProcesses = document.getElementById('killProcesses').checked;
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
|
||||
// Show loading state
|
||||
const btn = document.getElementById('monitorStartBtn');
|
||||
@@ -5507,7 +5542,12 @@
|
||||
btn.textContent = 'Enabling...';
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ interface: iface, action: 'start', kill_processes: killProcesses })
|
||||
@@ -5519,29 +5559,13 @@
|
||||
if (data.status === 'success') {
|
||||
monitorInterface = data.monitor_interface;
|
||||
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
|
||||
fetch('/wifi/interfaces')
|
||||
.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('');
|
||||
}
|
||||
});
|
||||
refreshWifiInterfaces();
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
alert('Error: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -5554,8 +5578,13 @@
|
||||
// Disable monitor mode
|
||||
function disableMonitorMode() {
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ interface: iface, action: 'stop' })
|
||||
@@ -5566,7 +5595,7 @@
|
||||
updateMonitorStatus(false);
|
||||
showInfo('Monitor mode disabled');
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
alert('Error: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,6 +141,10 @@ class AgentClient:
|
||||
except requests.RequestException as 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
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user