release: Mark VIII 1.1
Wireless client mode (connect to WiFi as client): - settings/wifi/client API: state, scan, connect, disconnect, route - Internet Connection topbar dialog and functional Settings > Networking card - routing toggle syncing UCI flag and daemon state - fix trailing-slash hash routes (View not available) - hidden-SSID filtering, encryption classification (Open/WPA2/WPA3/mixed) - tests for client state, scan parsing, connect/disconnect, routing
This commit is contained in:
@@ -41,6 +41,26 @@ class PayloadsProxyTest(unittest.TestCase):
|
||||
server.h_payloads_remove(type('C', (), {'args': (), 'body': {'key': 'nautilus'}})())
|
||||
self.assertTrue(any(m == 'POST' and '/api/payloads/portal/nautilus/remove' in p for m, p in calls))
|
||||
|
||||
def test_install_surfaces_daemon_error_detail(self):
|
||||
server.daemon_call = lambda m, p, body=None, token=None, timeout=15: (
|
||||
500, {'error': 'network error: Get "https://downloads.hak5.org/.../download": dial tcp: lookup downloads.hak5.org on [::1]:53: server misbehaving'})
|
||||
server.current_token = lambda: 'tok'
|
||||
status, payload = server.h_payloads_install(type('C', (), {
|
||||
'args': (), 'body': {'key': 'recon~client~recon_reporter'}})())
|
||||
self.assertEqual(status, 500)
|
||||
self.assertIn('downloads.hak5.org', payload['detail'])
|
||||
|
||||
def test_install_unwraps_raw_json_daemon_error(self):
|
||||
server.daemon_call = lambda m, p, body=None, token=None, timeout=15: (
|
||||
500, '{"error":"network error: Get \\"https://downloads.hak5.org/...\\": dial tcp: lookup downloads.hak5.org on [::1]:53: server misbehaving"}\n')
|
||||
server.current_token = lambda: 'tok'
|
||||
status, payload = server.h_payloads_install(type('C', (), {
|
||||
'args': (), 'body': {'key': 'recon~client~recon_reporter'}})())
|
||||
self.assertEqual(status, 500)
|
||||
self.assertIn('network error', payload['detail'])
|
||||
self.assertIn('downloads.hak5.org', payload['detail'])
|
||||
self.assertNotIn('{', payload['detail'])
|
||||
|
||||
def test_installed_inventory_flattens_firmware_records(self):
|
||||
old = server._payload_daemon
|
||||
server._payload_daemon = lambda method, path, body=None: (200, [{
|
||||
@@ -275,5 +295,252 @@ class SettingsTest(unittest.TestCase):
|
||||
self.assertNotIn('password', payload)
|
||||
|
||||
|
||||
class WifiClientModeTest(unittest.TestCase):
|
||||
@staticmethod
|
||||
def fake_device_run(calls, responses):
|
||||
def run(args, timeout=20, input_data=None):
|
||||
calls.append((list(args), timeout))
|
||||
key = (args[0], args[1])
|
||||
return responses.get(key, (0, '', ''))
|
||||
return run
|
||||
|
||||
def test_client_state_parses_disabled(self):
|
||||
calls = []
|
||||
run = self.fake_device_run(calls, {
|
||||
('uci', 'show'): (0,
|
||||
"wireless.wlan0cli=wifi-iface\n"
|
||||
"wireless.wlan0cli.ssid='OldNet'\n"
|
||||
"wireless.wlan0cli.disabled='1'\n"
|
||||
"wireless.wlan0cli.routed='0'\n", ''),
|
||||
('iw', 'dev'): (0, '', ''),
|
||||
('ip', '-4'): (0, '', ''),
|
||||
})
|
||||
old = server.device_run
|
||||
server.device_run = run
|
||||
try:
|
||||
status, payload = server.h_settings_wifi_client(type('C', (), {})())
|
||||
finally:
|
||||
server.device_run = old
|
||||
self.assertEqual(status, 200)
|
||||
self.assertFalse(payload['enabled'])
|
||||
self.assertFalse(payload['connected'])
|
||||
self.assertEqual(payload['ssid'], 'OldNet')
|
||||
self.assertFalse(payload['routed'])
|
||||
self.assertEqual(payload['ip'], '')
|
||||
|
||||
def test_client_state_parses_connected(self):
|
||||
calls = []
|
||||
run = self.fake_device_run(calls, {
|
||||
('uci', 'show'): (0,
|
||||
"wireless.wlan0cli=wifi-iface\n"
|
||||
"wireless.wlan0cli.ssid='All RPH Guest WIFI'\n"
|
||||
"wireless.wlan0cli.disabled='0'\n"
|
||||
"wireless.wlan0cli.routed='0'\n", ''),
|
||||
('iw', 'dev'): (0,
|
||||
"Connected to 02:18:4a:a7:6a:d9 (on wlan0cli)\n"
|
||||
"\tSSID: All RPH Guest WIFI\n"
|
||||
"\tfreq: 2462\n"
|
||||
"\tsignal: -57 dBm\n", ''),
|
||||
('ip', '-4'): (0,
|
||||
"6: wlan0cli: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500\n"
|
||||
" inet 10.10.10.5/24 brd 10.10.10.255 scope global wlan0cli\n", ''),
|
||||
})
|
||||
old = server.device_run
|
||||
server.device_run = run
|
||||
try:
|
||||
status, payload = server.h_settings_wifi_client(type('C', (), {})())
|
||||
finally:
|
||||
server.device_run = old
|
||||
self.assertEqual(status, 200)
|
||||
self.assertTrue(payload['enabled'])
|
||||
self.assertTrue(payload['connected'])
|
||||
self.assertEqual(payload['connected_ssid'], 'All RPH Guest WIFI')
|
||||
self.assertEqual(payload['ip'], '10.10.10.5')
|
||||
self.assertEqual(payload['signal'], -57)
|
||||
self.assertEqual(payload['freq'], 2462)
|
||||
|
||||
def test_scan_parses_networks_and_deduplicates(self):
|
||||
sample = (
|
||||
"BSS 02:18:4a:a7:6a:d2(on wlan0)\n"
|
||||
"\tfreq: 2462.0\n"
|
||||
"\tsignal: -63.00 dBm\n"
|
||||
"\tSSID: Riverwalk Plaza Staff\n"
|
||||
"\tRSN:\t * Version: 1\n"
|
||||
"\t\t * Group cipher: TKIP\n"
|
||||
"\t\t * Pairwise ciphers: CCMP TKIP\n"
|
||||
"\t\t * Authentication suites: PSK\n"
|
||||
"BSS 02:18:4a:a7:6a:d9(on wlan0)\n"
|
||||
"\tfreq: 2462.0\n"
|
||||
"\tsignal: -61.00 dBm\n"
|
||||
"\tSSID: All RPH Guest WIFI\n"
|
||||
"\tRSN:\t * Version: 1\n"
|
||||
"\t\t * Group cipher: CCMP\n"
|
||||
"\t\t * Pairwise ciphers: CCMP\n"
|
||||
"\t\t * Authentication suites: SAE\n"
|
||||
"BSS ea:cb:bc:8e:c5:0e(on wlan0)\n"
|
||||
"\tfreq: 2462.0\n"
|
||||
"\tsignal: -50.00 dBm\n"
|
||||
"\tSSID: OpenGuest\n"
|
||||
"BSS c6:cb:bc:8e:c5:0e(on wlan0)\n"
|
||||
"\tfreq: 2462.0\n"
|
||||
"\tsignal: -55.00 dBm\n"
|
||||
"\tSSID: \\x00\\x00\\x00\\x00\n"
|
||||
"BSS 42:18:4a:a7:6a:d2(on wlan0)\n"
|
||||
"\tfreq: 2462.0\n"
|
||||
"\tsignal: -65.00 dBm\n"
|
||||
"\tSSID: Riverwalk Plaza Staff\n"
|
||||
"\tWPA:\t * Version: 1\n"
|
||||
"\t\t * Group cipher: TKIP\n"
|
||||
"\t\t * Authentication suites: PSK\n")
|
||||
networks = server._parse_wifi_scan(sample)
|
||||
by_ssid = {n['ssid']: n for n in networks}
|
||||
self.assertIn('Riverwalk Plaza Staff', by_ssid)
|
||||
self.assertIn('All RPH Guest WIFI', by_ssid)
|
||||
self.assertIn('OpenGuest', by_ssid)
|
||||
self.assertEqual(by_ssid['Riverwalk Plaza Staff']['encryption'], 'WPA2')
|
||||
self.assertEqual(by_ssid['All RPH Guest WIFI']['encryption'], 'WPA3')
|
||||
self.assertEqual(by_ssid['OpenGuest']['encryption'], 'Open')
|
||||
self.assertEqual(by_ssid['OpenGuest']['channel'], 11)
|
||||
# hidden SSID entries are omitted
|
||||
self.assertNotIn('', by_ssid)
|
||||
# strongest BSS per SSID wins and results are signal-sorted (strongest first)
|
||||
self.assertEqual(networks[0]['ssid'], 'OpenGuest')
|
||||
self.assertEqual(networks[0]['signal'], -50)
|
||||
self.assertGreater(networks[0]['signal'], networks[1]['signal'])
|
||||
|
||||
def test_scan_reports_device_failure(self):
|
||||
old = server.device_run
|
||||
server.device_run = lambda args, timeout=20: (1, '', 'scan not supported')
|
||||
try:
|
||||
status, payload = server.h_settings_wifi_client_scan(type('C', (), {})())
|
||||
finally:
|
||||
server.device_run = old
|
||||
self.assertEqual(status, 502)
|
||||
self.assertIn('scan', payload['error'])
|
||||
|
||||
def test_connect_requires_ssid(self):
|
||||
class H:
|
||||
command = 'POST'
|
||||
old = server.device_run
|
||||
server.device_run = lambda args, timeout=20: (0, '', '')
|
||||
try:
|
||||
status, payload = server.h_settings_wifi_client_connect(
|
||||
type('C', (), {'h': H(), 'body': {'encryption': 'open'}})())
|
||||
finally:
|
||||
server.device_run = old
|
||||
self.assertEqual(status, 400)
|
||||
self.assertIn('SSID', payload['error'])
|
||||
|
||||
def test_connect_rejects_short_password(self):
|
||||
class H:
|
||||
command = 'POST'
|
||||
calls = []
|
||||
run = self.fake_device_run(calls, {})
|
||||
old = server.device_run
|
||||
server.device_run = run
|
||||
try:
|
||||
status, payload = server.h_settings_wifi_client_connect(
|
||||
type('C', (), {'h': H(), 'body': {'ssid': 'X', 'encryption': 'wpa2', 'password': 'short'}})())
|
||||
finally:
|
||||
server.device_run = old
|
||||
self.assertEqual(status, 400)
|
||||
self.assertIn('password', payload['error'])
|
||||
self.assertEqual(calls, [])
|
||||
|
||||
def test_connect_writes_uci_and_reloads(self):
|
||||
class H:
|
||||
command = 'POST'
|
||||
calls = []
|
||||
run = self.fake_device_run(calls, {})
|
||||
daemon_calls = []
|
||||
old_run = server.device_run
|
||||
old_sock = server.daemon_sock_call
|
||||
server.device_run = run
|
||||
server.daemon_sock_call = lambda m, p, body=None, timeout=10: (
|
||||
daemon_calls.append((m, p, body)) or (200, {'success': True}))
|
||||
try:
|
||||
status, payload = server.h_settings_wifi_client_connect(
|
||||
type('C', (), {'h': H(), 'body': {
|
||||
'ssid': 'All RPH Guest WIFI', 'encryption': 'wpa2wpa3',
|
||||
'password': 'Missions1', 'routed': True}})())
|
||||
finally:
|
||||
server.device_run = old_run
|
||||
server.daemon_sock_call = old_sock
|
||||
self.assertEqual(status, 200)
|
||||
sets = [args for args, _t in calls if args[:2] == ['uci', 'set']]
|
||||
expected = {
|
||||
'wireless.wlan0cli.ssid=All RPH Guest WIFI',
|
||||
'wireless.wlan0cli.encryption=sae-mixed',
|
||||
'wireless.wlan0cli.disabled=0',
|
||||
'wireless.wlan0cli.routed=1',
|
||||
'wireless.wlan0cli.key=Missions1',
|
||||
}
|
||||
got = {args[2] for args in sets}
|
||||
self.assertTrue(expected <= got)
|
||||
self.assertIn((['uci', 'commit', 'wireless'], 20), calls)
|
||||
self.assertIn((['wifi', 'reload'], 45), calls)
|
||||
self.assertEqual(daemon_calls,
|
||||
[('PUT', '/api/settings/wifi/set_client_route', {'routed': True})])
|
||||
|
||||
def test_connect_open_clears_key(self):
|
||||
class H:
|
||||
command = 'POST'
|
||||
calls = []
|
||||
run = self.fake_device_run(calls, {})
|
||||
old_run = server.device_run
|
||||
old_sock = server.daemon_sock_call
|
||||
server.device_run = run
|
||||
server.daemon_sock_call = lambda m, p, body=None, timeout=10: (200, {'success': True})
|
||||
try:
|
||||
status, payload = server.h_settings_wifi_client_connect(
|
||||
type('C', (), {'h': H(), 'body': {'ssid': 'OpenGuest', 'encryption': 'open'}})())
|
||||
finally:
|
||||
server.device_run = old_run
|
||||
server.daemon_sock_call = old_sock
|
||||
self.assertEqual(status, 200)
|
||||
self.assertIn((['uci', 'delete', 'wireless.wlan0cli.key'], 20), calls)
|
||||
self.assertNotIn((['uci', 'set', 'wireless.wlan0cli.key='], 20), calls)
|
||||
|
||||
def test_route_toggle_writes_uci_and_syncs_daemon(self):
|
||||
class H:
|
||||
command = 'POST'
|
||||
calls = []
|
||||
run = self.fake_device_run(calls, {})
|
||||
daemon_calls = []
|
||||
old_run = server.device_run
|
||||
old_sock = server.daemon_sock_call
|
||||
server.device_run = run
|
||||
server.daemon_sock_call = lambda m, p, body=None, timeout=10: (
|
||||
daemon_calls.append((m, p, body)) or (200, {'success': True}))
|
||||
try:
|
||||
status, payload = server.h_settings_wifi_client_route(
|
||||
type('C', (), {'h': H(), 'body': {'routed': True}})())
|
||||
finally:
|
||||
server.device_run = old_run
|
||||
server.daemon_sock_call = old_sock
|
||||
self.assertEqual(status, 200)
|
||||
self.assertIn((['uci', 'set', 'wireless.wlan0cli.routed=1'], 20), calls)
|
||||
self.assertIn((['uci', 'commit', 'wireless'], 20), calls)
|
||||
self.assertEqual(daemon_calls,
|
||||
[('PUT', '/api/settings/wifi/set_client_route', {'routed': True})])
|
||||
|
||||
def test_disconnect_disables_and_reloads(self):
|
||||
class H:
|
||||
command = 'POST'
|
||||
calls = []
|
||||
run = self.fake_device_run(calls, {})
|
||||
old = server.device_run
|
||||
server.device_run = run
|
||||
try:
|
||||
status, payload = server.h_settings_wifi_client_disconnect(
|
||||
type('C', (), {'h': H(), 'body': {}})())
|
||||
finally:
|
||||
server.device_run = old
|
||||
self.assertEqual(status, 200)
|
||||
self.assertIn((['uci', 'set', 'wireless.wlan0cli.disabled=1'], 20), calls)
|
||||
self.assertIn((['uci', 'commit', 'wireless'], 20), calls)
|
||||
self.assertIn((['wifi', 'reload'], 45), calls)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user