fix: harden UI actions and daemon calls for reliable control (v1.3.2)

Retry and serialize pineapd/hak5 calls, queue virtual-pager keys, and grey out buttons until the pager finishes. Deploy now installs python3-light after factory firmware. Bump version to 1.3.2.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-20 15:49:10 -05:00
co-authored by Cursor
parent cd26553d21
commit 7d48b7ad06
25 changed files with 3625 additions and 782 deletions
+136 -3
View File
@@ -80,11 +80,20 @@ class AttacksDeployTest(unittest.TestCase):
self.old_state = server.PINEAP_STATE_FILE
server.PINEAP_STATE_FILE = os.path.join(self.tmp, 'state.json')
self.old_ent = {k: getattr(server, k) for k in
('ENT_CONF', 'ENT_PIDFILE', 'ENT_EAP_USERS', 'ENT_STATE')}
('ENT_CONF', 'ENT_PIDFILE', 'ENT_EAP_USERS', 'ENT_STATE',
'ENT_DIR', 'ENT_CA_CERT', 'ENT_SERVER_CERT', 'ENT_SERVER_KEY',
'ENT_LOG', 'ENT_CAPTURES', 'ENT_DH_FILE')}
server.ENT_CONF = os.path.join(self.tmp, 'enterprise.conf')
server.ENT_PIDFILE = os.path.join(self.tmp, 'mk8.pid')
server.ENT_EAP_USERS = os.path.join(self.tmp, 'eap_users')
server.ENT_STATE = os.path.join(self.tmp, 'state.json')
server.ENT_DIR = os.path.join(self.tmp, 'ent')
server.ENT_CA_CERT = os.path.join(server.ENT_DIR, 'ca.pem')
server.ENT_SERVER_CERT = os.path.join(server.ENT_DIR, 'server.pem')
server.ENT_SERVER_KEY = os.path.join(server.ENT_DIR, 'server.key')
server.ENT_LOG = os.path.join(server.ENT_DIR, 'hostapd.log')
server.ENT_CAPTURES = os.path.join(server.ENT_DIR, 'captures.json')
server.ENT_DH_FILE = os.path.join(server.ENT_DIR, 'dh.pem')
self.old_ent_running = server._ent_running
self.old_ent_state = server._ent_state_loaded
server._ent_running = lambda: True
@@ -203,10 +212,25 @@ class AttacksDeployTest(unittest.TestCase):
self.assertIn(['iw', 'phy', 'phy1', 'interface', 'add', 'wlan1ent',
'type', 'managed'], cmds)
self.assertIn(['iw', 'dev', 'wlan1ent', 'set', 'type', 'ap'], cmds)
self.assertIn(['/usr/sbin/hostapd', '-B', '-P', server.ENT_PIDFILE,
server.ENT_CONF], cmds)
self.assertTrue(any(c[:4] == ['/usr/sbin/hostapd', '-B', '-P', server.ENT_PIDFILE]
for c in cmds))
self.assertEqual(self.f.state['pineapd.@hostapd[0].mgmtiface'], 'wlan1ent')
self.assertEqual(self.f.state['pineapd.wlan1mon.hop'], '0')
with open(server.ENT_CONF) as f:
conf = f.read()
self.assertIn('ca_cert=', conf)
self.assertIn('server_cert=', conf)
self.assertIn('private_key=', conf)
self.assertIn('ieee8021x=1', conf)
self.assertIn('eap_server=1', conf)
self.assertNotIn('eap_server_identity', conf)
self.assertNotIn('eap_server_erp', conf)
self.assertNotIn('dh_file=', conf)
self.assertIn('ieee80211w=0', conf)
with open(server.ENT_EAP_USERS) as f:
users = f.read()
self.assertIn('PEAP,TTLS', users)
self.assertIn('[2]', users)
def test_deploy_enterprise_rejects_non_5g_channel(self):
status, _ = server.h_attacks_deploy(ctx({
@@ -223,6 +247,77 @@ class AttacksDeployTest(unittest.TestCase):
cmds = [r[0] for r in self.f.runs]
self.assertIn(['iw', 'dev', 'wlan1ent', 'del'], cmds)
def test_deploy_enterprise_writes_passphrase_and_hidden(self):
status, payload = server.h_attacks_deploy(ctx({
'kind': 'enterprise', 'ssid': 'CorpAP', 'passphrase': 'labsecret',
'enctype': 'wpa2', 'hidden': True, 'channel': 36}))
self.assertEqual(status, 200)
with open(server.ENT_EAP_USERS) as f:
users = f.read()
self.assertIn('labsecret', users)
self.assertIn('PEAP,TTLS', users)
self.assertIn('[2]', users)
with open(server.ENT_CONF) as f:
conf = f.read()
self.assertIn('ignore_broadcast_ssid=1', conf)
self.assertIn('ca_cert=', conf)
def test_eap_secret_sanitizes_quotes(self):
self.assertEqual(server._eap_secret(''), 'dummy')
self.assertEqual(server._eap_secret('ab"c\ndef'), 'abcdef')
def test_hostapd_unknown_items_parse_pager_error(self):
err = ("Line 14: unknown configuration item 'eap_server_identity'\n"
"1 errors found in configuration file '/root/loot/enterprise.conf'\n"
"Failed to set up interface with /root/loot/enterprise.conf\n"
"Failed to initialize interface\n")
self.assertEqual(server._hostapd_unknown_items(err), ['eap_server_identity'])
def test_drop_hostapd_keys_removes_only_named_lines(self):
conf = ('interface=wlan1ent\n'
'eap_server=1\n'
'eap_server_identity=hostapd\n'
'dh_file=/tmp/dh.pem\n')
new, changed = server._drop_hostapd_keys(
conf, ['eap_server_identity', 'dh_file'])
self.assertTrue(changed)
self.assertIn('eap_server=1', new)
self.assertIn('interface=wlan1ent', new)
self.assertNotIn('eap_server_identity', new)
self.assertNotIn('dh_file=', new)
def test_start_ent_hostapd_strips_unknown_keys_and_retries(self):
with open(server.ENT_CONF, 'w') as f:
f.write('interface=wlan1ent\neap_server=1\neap_server_identity=hostapd\n')
orig = server.device_run
seen = []
def wrapped(args, timeout=20, input_data=None):
a = list(args)
if a and a[0] == '/usr/sbin/hostapd':
with open(server.ENT_CONF) as fh:
text = fh.read()
seen.append(text)
if 'eap_server_identity' in text:
return (1, '',
"Line 3: unknown configuration item 'eap_server_identity'\n"
"1 errors found in configuration file '%s'\n"
"Failed to initialize interface\n" % server.ENT_CONF)
return (0, '', '')
return orig(args, timeout=timeout, input_data=input_data)
server.device_run = wrapped
try:
rc, _out, _err = server._start_ent_hostapd()
finally:
server.device_run = orig
self.assertEqual(rc, 0)
self.assertGreaterEqual(len(seen), 2)
with open(server.ENT_CONF) as f:
conf = f.read()
self.assertNotIn('eap_server_identity', conf)
self.assertIn('eap_server=1', conf)
def test_deploy_validation(self):
status, _ = server.h_attacks_deploy(ctx({'kind': 'wpa', 'ssid': ''}))
self.assertEqual(status, 400)
@@ -322,5 +417,43 @@ class AttacksExportTest(unittest.TestCase):
self.assertIn('/root/loot/pcap/b.cap', hc)
class AttacksStatusTest(unittest.TestCase):
def setUp(self):
self.saved = {
'_count_table': server._count_table,
'_ent_summary': server._ent_summary,
'daemon_sock_call': server.daemon_sock_call,
'_uci_ap_summary': server._uci_ap_summary,
'_read_hop': server._read_hop,
}
def tearDown(self):
for name, fn in self.saved.items():
setattr(server, name, fn)
def test_status_exposes_enterprise_ap_for_ui(self):
server._count_table = lambda t: {
'hostap_handshake': 2, 'hostap_basic': 3, 'hostap_chalresp': 1
}.get(t, 0)
server._ent_summary = lambda detail=True: {
'enabled': True, 'live': True, 'ssid': 'CorpLab',
'iface': 'wlan1ent', 'stations': ['AA:BB:CC:DD:EE:FF'],
'auth_method': 'mschapv2', 'certs': True, 'captures': 4,
'ctrl_linked': True,
}
server.daemon_sock_call = lambda *a, **k: (200, {'pineape_disabled': False})
server._uci_ap_summary = lambda *a, **k: None
server._read_hop = lambda: '1'
status, payload = server.h_attacks_status(ctx())
self.assertEqual(status, 200)
self.assertTrue(payload['enterprise']['ap']['live'])
self.assertEqual(payload['enterprise']['ap']['ssid'], 'CorpLab')
self.assertEqual(payload['enterprise']['identities'], 3)
self.assertEqual(payload['enterprise']['mschapv2'], 1)
self.assertEqual(payload['enterprise']['creds'], 4)
self.assertEqual(payload['handshakes'], 2)
self.assertTrue(payload['enterprise']['pineape']['enabled'])
if __name__ == '__main__':
unittest.main()
+19 -13
View File
@@ -106,15 +106,19 @@ class EnvCheckTest(unittest.TestCase):
self.assertEqual(server.ENV_CHECK_STATE['pool_runtime'], 'disabled')
self.assertNotIn(['_pineap', 'SSIDPOOL', 'DISABLE'], self.runs)
def test_applies_sane_defaults_when_missing(self):
def test_warns_sane_defaults_when_missing_without_mutating(self):
self.uci_state['pineapd.@ssidpool[0].ssid'] = 'QmVlcg=='
report = server.env_check()
self.assertEqual(self.steps(report, 'sane-off UCI defaults applied')[0]['ok'], 'fixed')
self.assertEqual(
self.steps(report, 'runtime safety settings changed')[0]['ok'],
'fixed')
self.assertIn(['/etc/init.d/pineapd', 'restart'], self.runs)
for key, value in server.PINEAPD_SAFE_UCI.items():
self.assertEqual(self.uci_state[key], value)
self.steps(report, 'live PineAP UCI left unchanged')[0]['ok'], 'warn')
self.assertNotIn(['/etc/init.d/pineapd', 'restart'], self.runs)
self.assertNotIn(['/etc/init.d/pineapd', 'stop'], self.runs)
self.assertFalse(any(a[:2] == ['uci', 'set'] for a in self.runs))
self.assertFalse(any(a[:2] == ['uci', 'commit'] for a in self.runs))
self.assertFalse(any(a[:2] == ['uci', 'delete'] for a in self.runs))
self.assertEqual(self.uci_state['pineapd.@ssidpool[0].ssid'], 'QmVlcg==')
for key in server.PINEAPD_SAFE_UCI:
self.assertNotIn(key, self.uci_state)
def test_uci_pass_when_already_set(self):
self.safe_set()
@@ -213,17 +217,19 @@ class EnvCheckTest(unittest.TestCase):
report = server.env_check()
self.assertEqual(self.steps(report, 'no radio0 AP pins wlan0mon')[0]['ok'], 'pass')
def test_sta_uplink_disabled_when_enabled(self):
def test_sta_uplink_warns_when_enabled_without_mutating(self):
self.safe_set()
self.uci_state['wireless.dummy_radio0.mode'] = 'sta'
self.uci_state['wireless.dummy_radio0.ifname'] = 'wlan0'
self.uci_state['wireless.dummy_radio0.disabled'] = '0'
report = server.env_check()
self.assertEqual(self.steps(report, 'dummy_radio0 STA uplink disabled')[0]['ok'], 'fixed')
self.assertEqual(self.uci_state['wireless.dummy_radio0.disabled'], '1')
self.assertIn(['ip', 'link', 'set', 'wlan0', 'down'], self.runs)
self.assertNotIn(['wifi', 'reload'], self.runs,
'STA fix must not bounce the radios')
self.assertEqual(self.steps(report, 'dummy_radio0 STA uplink is enabled')[0]['ok'], 'warn')
self.assertEqual(self.uci_state['wireless.dummy_radio0.disabled'], '0')
self.assertNotIn(['ip', 'link', 'set', 'wlan0', 'down'], self.runs)
self.assertNotIn(['wifi', 'reload'], self.runs)
self.assertFalse(any(
a[:2] == ['uci', 'set'] and 'dummy_radio0' in a[2]
for a in self.runs if len(a) > 2))
def test_sta_uplink_pass_when_absent(self):
self.safe_set()
+14 -17
View File
@@ -73,12 +73,15 @@ class HealthCheckTest(unittest.TestCase):
self.assertIn(['ip', 'link', 'set', 'wlan0mon', 'up'], [r[0] for r in self.runs])
self.assertEqual(result['monitor_fixes'], 1)
def test_down_with_growing_sigsegv_disables_pool(self):
def test_down_restarts_pineapd_without_rewriting_uci(self):
self.ping_ok = False
self.sigsegs = 5
self.uci_state['pineapd.@ssidpool[0].ssid'] = 'QmVlcg=='
result = server.health_check()
self.assertIn('pool broadcast disabled', result['last_action'])
self.assertEqual(self.uci_state['pineapd.@ssidpool[0].disable'], '1')
self.assertEqual(result['last_action'], 'pineapd restart')
self.assertNotIn('pineapd.@ssidpool[0].disable', self.uci_state)
self.assertEqual(self.uci_state['pineapd.@ssidpool[0].ssid'], 'QmVlcg==')
self.assertFalse(any(r[0][:2] == ['uci', 'set'] for r in self.runs))
self.assertIn(['/etc/init.d/pineapd', 'restart'], [r[0] for r in self.runs])
self.assertEqual(result['fixes'], 1)
@@ -94,21 +97,15 @@ class HealthCheckTest(unittest.TestCase):
self.assertEqual(result['last_action'], 'pineapd restart')
self.assertIn(['/etc/init.d/pineapd', 'restart'], [r[0] for r in self.runs])
def test_down_stabilizes_known_crash_sources(self):
self.ping_ok = False
self.uci_state['pineapd.@ssidpool[0].disable'] = '1'
result = server.health_check()
self.assertEqual(self.uci_state['pineapd.wlan2mon.disable'], '1')
self.assertEqual(self.uci_state['pineapd.wlan1mon.bands'], '5')
self.assertIn('stabilized', result['last_action'])
def test_down_clears_refilled_pool_list(self):
def test_down_does_not_stabilize_or_clear_pool(self):
self.ping_ok = False
self.uci_state['pineapd.@ssidpool[0].disable'] = '1'
self.uci_state['pineapd.@ssidpool[0].ssid'] = 'QmVlcg=='
result = server.health_check()
self.assertNotIn('pineapd.@ssidpool[0].ssid', self.uci_state)
self.assertIn('pool-list cleared', result['last_action'])
self.assertEqual(result['last_action'], 'pineapd restart')
self.assertEqual(self.uci_state['pineapd.@ssidpool[0].ssid'], 'QmVlcg==')
self.assertNotIn('pineapd.wlan2mon.disable', self.uci_state)
self.assertNotIn('pineapd.wlan1mon.bands', self.uci_state)
def test_down_without_crash_brings_monitors_up(self):
self.ping_ok = False
@@ -118,12 +115,12 @@ class HealthCheckTest(unittest.TestCase):
self.assertEqual(result['last_action'], 'monitor interfaces brought up')
self.assertIn(['ip', 'link', 'set', 'wlan1mon', 'up'], [r[0] for r in self.runs])
def test_down_disables_pool_regardless_of_sigsegv_history(self):
def test_down_does_not_disable_pool_regardless_of_sigsegv_history(self):
self.ping_ok = False
server._health['sigsegv_last'] = 4
result = server.health_check()
self.assertIn('SSID pool broadcast disabled', result['last_action'])
self.assertEqual(self.uci_state['pineapd.@ssidpool[0].disable'], '1')
self.assertEqual(result['last_action'], 'pineapd restart')
self.assertNotIn('pineapd.@ssidpool[0].disable', self.uci_state)
def test_fix_cooldown_prevents_thrash(self):
self.ping_ok = False
+20
View File
@@ -153,6 +153,26 @@ class LoggingTest(unittest.TestCase):
self.assertEqual(server._line_count(
type('C', (), {'query': {'lines': '-10'}})(), 200), 0)
def test_system_uses_bounded_logread(self):
calls = []
def fake(args, timeout=20, input_data=None):
calls.append(list(args))
return (0, '\n'.join('line%d' % i for i in range(20)), '')
server.device_run = fake
status, payload = server.h_logging_system(type('C', (), {
'args': (), 'query': {'lines': '5'}})())
self.assertEqual(status, 200)
self.assertEqual(len(payload['lines']), 5)
self.assertTrue(calls)
self.assertEqual(calls[0][:2], ['logread', '-l'])
def test_json_or_and_pool_list_tolerate_empty(self):
self.assertIsNone(server._json_or(None))
self.assertEqual(server._parse_pool_list(None), [])
self.assertEqual(server._unique_keep_order(['a', '', 'a', 'b']), ['a', 'b'])
class SettingsTest(unittest.TestCase):
def test_hostname_get(self):
+190 -4
View File
@@ -1,5 +1,8 @@
import json
import os
import sqlite3
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'payload', 'user', 'remote_access', 'pager-webui'))
@@ -10,34 +13,217 @@ def setUpModule():
__import__('importlib').reload(server)
class EnterpriseTest(unittest.TestCase):
SCHEMA = '''
CREATE TABLE hostap_basic(id INTEGER PRIMARY KEY, scan INT, time INT, type TEXT,
identity TEXT, password TEXT, verified INT NOT NULL DEFAULT 0);
CREATE TABLE hostap_chalresp(id INTEGER PRIMARY KEY, scan INT, time INT, type TEXT,
username TEXT, challenge BLOB, response BLOB,
verified INT NOT NULL DEFAULT 0);
CREATE TABLE hostap_client(id INTEGER PRIMARY KEY, scan INT, hash INT, mac TEXT, ssid BLOB,
connected_time INT, disconnected_time INT);
'''
class EnterpriseApiTest(unittest.TestCase):
def setUp(self):
self._orig_rows = server._db_rows
self._orig_write = server._db_write
def tearDown(self):
server._db_rows = self._orig_rows
server._db_write = self._orig_write
def test_basic_rows(self):
server._db_rows = lambda db, sql: [{'time': 1, 'username': 'a', 'password': 'b'}]
status, payload = server.h_enterprise_data(type('C', (), {'args': ('basic',)})())
self.assertEqual(status, 200)
self.assertEqual(payload['table'], 'hostap_basic')
self.assertEqual(payload['rows'][0]['username'], 'a')
self.assertEqual(payload['rows'][0]['identity'], 'a')
def test_challenge_rows(self):
def test_challenge_rows_empty(self):
server._db_rows = lambda db, sql: []
status, payload = server.h_enterprise_data(type('C', (), {'args': ('challenge',)})())
self.assertEqual(status, 200)
self.assertEqual(payload['table'], 'hostap_chalresp')
self.assertEqual(payload['rows'], [])
def test_unknown_table(self):
status, payload = server.h_enterprise_data(type('C', (), {'args': ('nope',)})())
self.assertEqual(status, 400)
def test_clear(self):
def test_clear_uses_chalresp_table(self):
calls = []
server._db_write = lambda db, sql: calls.append(sql)
status, payload = server.h_enterprise_clear(type('C', (), {'body': {'table': 'challenge'}})())
self.assertEqual(status, 200)
self.assertTrue(any('hostap_challenge' in s for s in calls))
self.assertTrue(any('hostap_chalresp' in s for s in calls))
self.assertFalse(any('hostap_challenge' in s for s in calls))
def test_clear_all(self):
calls = []
server._db_write = lambda db, sql: calls.append(sql)
status, payload = server.h_enterprise_clear(type('C', (), {'body': {'table': 'all'}})())
self.assertEqual(status, 200)
joined = ' '.join(calls)
self.assertIn('hostap_basic', joined)
self.assertIn('hostap_chalresp', joined)
def test_clear_unknown_table(self):
status, payload = server.h_enterprise_clear(type('C', (), {'body': {'table': 'nope'}})())
self.assertEqual(status, 400)
class EnterpriseHashFormatTest(unittest.TestCase):
def test_blob_to_hex_bytes_and_hex_string(self):
self.assertEqual(server._blob_to_hex(b'\x11\x22\x33\x44'), '11223344')
self.assertEqual(server._blob_to_hex('AABBCCDD'), 'aabbccdd')
self.assertEqual(server._blob_to_hex("X'AABB'"), 'aabb')
self.assertEqual(server._blob_to_hex('\\xde\\xad'), 'dead')
def test_hashcat_5500_and_john(self):
chal = '1122334455667788'
resp = '00112233445566778899aabbccddeeff0011223344556677'
self.assertEqual(
server._mschap_hashcat_5500('bob', chal, resp),
'bob::::00112233445566778899aabbccddeeff0011223344556677:1122334455667788')
self.assertEqual(
server._mschap_john('bob', chal, resp),
'bob:$NETNTLM$1122334455667788$00112233445566778899aabbccddeeff0011223344556677')
def test_format_chalresp_row_hexes_blobs_and_is_json_safe(self):
row = server._format_chalresp_row({
'time': 1700000000,
'username': 'alice',
'type': 'MSCHAPV2',
'challenge': bytes.fromhex('1122334455667788'),
'response': bytes.fromhex('00112233445566778899aabbccddeeff0011223344556677'),
'verified': 0,
})
self.assertEqual(row['challenge'], '1122334455667788')
self.assertEqual(row['response'], '00112233445566778899aabbccddeeff0011223344556677')
self.assertIn('alice::::', row['hashcat'])
self.assertIn(':$NETNTLM$', row['john'])
json.dumps(row)
class EnterpriseLogParseTest(unittest.TestCase):
def test_parse_wpe_mschapv2_and_identity(self):
log = (
"mschapv2: Wed Aug 19 21:00:00 2026\n"
" username: bob\n"
" challenge: 11:22:33:44:55:66:77:88\n"
" response: 00112233445566778899aabbccddeeff0011223344556677\n"
"hashcat NETNTLM: bob::::00112233445566778899aabbccddeeff0011223344556677:1122334455667788\n"
"EAP-Identity 'alice@corp.local'\n"
"GTC: username: carol password: hunter2\n"
)
items = server._parse_ent_log(log)
kinds = [i['kind'] for i in items]
self.assertIn('mschapv2', kinds)
self.assertIn('eap-identity', kinds)
self.assertIn('gtc', kinds)
mschap = [i for i in items if i['kind'] == 'mschapv2'][0]
self.assertEqual(mschap['username'], 'bob')
self.assertIn('bob::::', mschap['hashcat'])
gtc = [i for i in items if i['kind'] == 'gtc'][0]
self.assertEqual(gtc['password'], 'hunter2')
class EnterpriseCaptureDbTest(unittest.TestCase):
def setUp(self):
fd, self.db = tempfile.mkstemp(suffix='.db')
os.close(fd)
conn = sqlite3.connect(self.db)
conn.executescript(SCHEMA)
chal = bytes.fromhex('1122334455667788')
resp = bytes.fromhex('00112233445566778899aabbccddeeff0011223344556677')
conn.execute(
"INSERT INTO hostap_basic (id, scan, time, type, identity, password, verified) "
"VALUES (1, 1, 1700000001, 'PEAP', 'bob', '', 0)")
conn.execute(
"INSERT INTO hostap_chalresp (id, scan, time, type, username, challenge, response, verified) "
"VALUES (1, 1, 1700000002, 'MSCHAPV2', 'bob', ?, ?, 0)", (chal, resp))
conn.execute(
"INSERT INTO hostap_client (id, scan, hash, mac, ssid, connected_time, disconnected_time) "
"VALUES (1, 1, 1, 'AABBCCDDEEFF', X'436F7270', 1700000003, NULL)")
conn.commit()
conn.close()
self.old_db = server.RECON_DB
server.RECON_DB = self.db
def tearDown(self):
server.RECON_DB = self.old_db
try:
os.unlink(self.db)
except OSError:
pass
def test_challenge_endpoint_returns_hashcat(self):
status, payload = server.h_enterprise_data(type('C', (), {'args': ('challenge',)})())
self.assertEqual(status, 200)
self.assertEqual(payload['table'], 'hostap_chalresp')
row = payload['rows'][0]
self.assertEqual(row['username'], 'bob')
self.assertEqual(row['challenge'], '1122334455667788')
self.assertEqual(
row['hashcat'],
'bob::::00112233445566778899aabbccddeeff0011223344556677:1122334455667788')
json.dumps(payload, default=server._json_default)
def test_radius_payload_unifies_captures(self):
status, payload = server.h_enterprise_radius(type('C', (), {'args': ()})())
self.assertEqual(status, 200)
self.assertTrue(payload['note'])
kinds = [c['kind'] for c in payload['captures']]
self.assertIn('eap-identity', kinds)
self.assertIn('mschapv2', kinds)
self.assertEqual(payload['hashcat']['mode'], 5500)
self.assertEqual(len(payload['hashcat']['lines']), 1)
self.assertEqual(payload['clients'][0]['ssid'], 'Corp')
def test_export_hashcat_download(self):
status, payload = server.h_enterprise_export(type('C', (), {'args': ('hashcat',)})())
self.assertEqual(status, 200)
self.assertIsInstance(payload, server.Download)
self.assertIn(b'bob::::', payload.data)
self.assertTrue(payload.filename.endswith('.5500'))
def test_export_john_and_json(self):
status, payload = server.h_enterprise_export(type('C', (), {'args': ('john',)})())
self.assertEqual(status, 200)
self.assertIn(b'$NETNTLM$', payload.data)
status, payload = server.h_enterprise_export(type('C', (), {'args': ('json',)})())
self.assertEqual(status, 200)
body = json.loads(payload.data.decode('utf-8'))
self.assertEqual(body['hashcat']['mode'], 5500)
class EnterpriseHarvestTest(unittest.TestCase):
def test_harvest_reads_hostapd_file_and_skips_logread(self):
calls = []
fd, log_path = tempfile.mkstemp()
os.write(fd, b"EAP-Identity 'fromfile'\n")
os.close(fd)
cap_path = log_path + '.json'
old_log, old_cap = server.ENT_LOG, server.ENT_CAPTURES
old_run = server.device_run
server.ENT_LOG = log_path
server.ENT_CAPTURES = cap_path
server.device_run = lambda args, timeout=20, input_data=None: (
calls.append(list(args)) or (0, "identity: 'syslog-user'\n", ''))
try:
items = server._harvest_ent_log()
self.assertTrue(any(item.get('username') == 'fromfile' for item in items))
self.assertFalse(any(args and args[0] == 'logread' for args in calls))
finally:
server.ENT_LOG = old_log
server.ENT_CAPTURES = old_cap
server.device_run = old_run
os.unlink(log_path)
if os.path.exists(cap_path):
os.unlink(cap_path)
if __name__ == '__main__':
unittest.main()
+5 -4
View File
@@ -53,7 +53,7 @@ class SsidPoolHandlersTest(unittest.TestCase):
server.h_ssids_post(type('C', (), {'args': (), 'body': {'action': 'add', 'ssid': 'NewNet'}})())
self.assertTrue(any(c[0] == 'PINEAPPLE_SSID_POOL_ADD' for c in calls))
def test_advertise_routes(self):
def test_advertise_enable_is_blocked(self):
calls = []
def fake(method, path, body=None, timeout=10):
@@ -61,10 +61,11 @@ class SsidPoolHandlersTest(unittest.TestCase):
return (200, {'success': True})
server.daemon_sock_call = fake
server.h_pineap_advertise(type('C', (), {'body': {'enable': True}})())
status, payload = server.h_pineap_advertise(type('C', (), {'body': {'enable': True}})())
self.assertEqual(status, 400)
self.assertEqual(calls, [])
server.h_pineap_advertise(type('C', (), {'body': {'enable': False}})())
self.assertEqual(calls, [('/api/pineap/ssidpool/enable', {'enable': True}),
('/api/pineap/ssidpool/disable', {'enable': False})])
self.assertEqual(calls, [('/api/pineap/ssidpool/disable', {'enable': False})])
def test_collect_routes(self):
calls = []
+27
View File
@@ -144,6 +144,7 @@ class PineapProxyTest(unittest.TestCase):
self.assertEqual(payload['enterprise']['enabled'], False)
self.assertEqual(payload['enterprise']['ssid'], '')
self.assertEqual(payload['pool']['collecting'], True)
self.assertTrue(payload['pool']['broadcast_blocked'])
self.assertEqual(payload['radios']['radio0']['band'], '2.4')
self.assertEqual(payload['radios']['radio1']['channel'], 'auto')
self.assertEqual(payload['pineape']['enabled'], True)
@@ -261,6 +262,32 @@ class PineapFilterTest(unittest.TestCase):
[server.HAK5CMD, 'PINEAPPLE_NETWORK_FILTER_CLEAR', 'deny'],
[server.HAK5CMD, 'PINEAPPLE_NETWORK_FILTER_MODE', 'deny']])
def test_filter_get_falls_back_to_hak5cmd_when_daemon_is_down(self):
server.daemon_sock_call = lambda method, path, body=None, timeout=10: (0, None)
def fake(args, timeout=20, input_data=None):
cmd = args[1] if len(args) > 1 else ''
if cmd.endswith('_MODE'):
return (0, 'deny\n', '')
if cmd.endswith('_LIST'):
return (0, 'AA:BB:CC:DD:EE:FF\n', '')
return (0, '', '')
server.device_run = fake
status, payload = server.h_filter_get(ctx(), 'client')
self.assertEqual(status, 200)
self.assertEqual(payload['mode'], 'deny')
self.assertEqual(payload['entries'], ['AA:BB:CC:DD:EE:FF'])
self.assertEqual(payload['source'], 'hak5cmd')
def test_filter_get_returns_empty_list_when_everything_is_down(self):
server.daemon_sock_call = lambda method, path, body=None, timeout=10: (0, None)
server.device_run = lambda args, timeout=20, input_data=None: (1, '', 'refused')
status, payload = server.h_filter_get(ctx(), 'ssid')
self.assertEqual(status, 200)
self.assertEqual(payload['entries'], [])
self.assertIn('error', payload)
if __name__ == '__main__':
unittest.main()
+148 -22
View File
@@ -163,33 +163,54 @@ class FakeSock:
class ReconHopperTest(unittest.TestCase):
def test_preflight_verifies_every_non_dfs_channel(self):
def test_preflight_probes_one_channel_per_radio(self):
calls = []
with mock.patch.object(
server, '_set_monitor_channel',
side_effect=lambda interface, channel:
calls.append((interface, channel)) or (True, '')):
self.assertEqual(
server._recon_hopper_preflight(),
(True, 'monitor channel control ready'))
with mock.patch.object(server, '_monitor_down', return_value=False):
self.assertEqual(
server._recon_hopper_preflight(),
(True, 'monitor channel control ready'))
expected = [
(interface, channel)
(interface, channels[0])
for interface, channels in server.RECON_CHANNELS.items()
for channel in channels
]
self.assertEqual(calls, expected)
self.assertEqual(server._recon_hop_state['ifaces'], ['wlan0mon', 'wlan1mon'])
def test_preflight_stops_at_first_unusable_channel(self):
def test_preflight_skips_busy_radio_and_keeps_the_other(self):
def set_channel(interface, channel):
if interface == 'wlan1mon' and channel == 44:
return False, 'wlan1mon channel 44: busy'
if interface == 'wlan0mon':
return False, 'wlan0mon channel 1: command failed: Resource busy (-16)'
return True, ''
with mock.patch.object(server, '_set_monitor_channel', side_effect=set_channel):
with mock.patch.object(server, '_monitor_down', return_value=False):
with mock.patch.object(server, '_sta_uplink_enabled', return_value=False):
with mock.patch.object(server, '_wlan0_pinned', return_value=True):
ok, detail = server._recon_hopper_preflight()
self.assertTrue(ok)
self.assertIn('wlan0mon', server._recon_hop_state['skipped'])
self.assertEqual(server._recon_hop_state['ifaces'], ['wlan1mon'])
self.assertIn('2.4 GHz hopping skipped', detail)
self.assertIn('Scanning 5 GHz only', detail)
def test_preflight_fails_when_no_monitor_is_usable(self):
with mock.patch.object(
server, '_set_monitor_channel', side_effect=set_channel):
ok, detail = server._recon_hopper_preflight()
server, '_set_monitor_channel',
return_value=(False, 'wlan0mon channel 1: No such device')):
with mock.patch.object(server, '_monitor_down', return_value=True):
ok, detail = server._recon_hopper_preflight()
self.assertFalse(ok)
self.assertIn('wlan1mon channel 44', detail)
self.assertIn('unavailable', detail.lower())
def test_busy_error_is_classified(self):
self.assertEqual(
server._iw_error_kind('wlan0mon channel 1: command failed: Resource busy (-16)'),
'busy')
self.assertEqual(server._iw_error_kind('No such device'), 'missing')
def test_set_channel_surfaces_iw_failure(self):
with mock.patch.object(
@@ -200,6 +221,67 @@ class ReconHopperTest(unittest.TestCase):
self.assertIn('wlan0mon channel 6', detail)
self.assertIn('Device or resource busy', detail)
def test_dummy_sta_not_borrowable_when_client_mode_on(self):
with mock.patch.object(server, '_wifi_client_mode_enabled', return_value=True):
with mock.patch.object(server, '_wlan0_pinned', return_value=False):
with mock.patch.object(server, '_wlan0_mgmt_enabled', return_value=False):
self.assertFalse(server._dummy_sta_borrowable())
def test_dummy_sta_borrowable_when_only_dummy_is_up(self):
with mock.patch.object(server, '_wifi_client_mode_enabled', return_value=False):
with mock.patch.object(server, '_wlan0_pinned', return_value=False):
with mock.patch.object(server, '_wlan0_mgmt_enabled', return_value=False):
with mock.patch.object(server, '_iface_associated', return_value=False):
with mock.patch.object(server, '_sta_uplink_enabled', return_value=True):
self.assertTrue(server._dummy_sta_borrowable())
def test_preflight_parks_dummy_sta_and_hops_24ghz(self):
def set_channel(interface, channel):
if interface == 'wlan0mon' and not server._recon_hop_state.get('borrowed_wlan0'):
return False, 'wlan0mon channel 1: command failed: Resource busy (-16)'
return True, ''
def borrow():
server._recon_hop_state['borrowed_wlan0'] = True
return True
with mock.patch.object(server, '_set_monitor_channel', side_effect=set_channel):
with mock.patch.object(server, '_monitor_down', return_value=False):
with mock.patch.object(server, '_dummy_sta_borrowable', return_value=True):
with mock.patch.object(server, '_borrow_dummy_sta', side_effect=borrow):
ok, detail = server._recon_hopper_preflight()
self.assertTrue(ok)
self.assertEqual(detail, 'monitor channel control ready')
self.assertEqual(server._recon_hop_state['ifaces'], ['wlan0mon', 'wlan1mon'])
self.assertTrue(server._recon_hop_state['borrowed_wlan0'])
self.assertEqual(server._recon_hop_state['skipped'], {})
def test_preflight_does_not_park_when_ap_holds_phy0(self):
def set_channel(interface, channel):
if interface == 'wlan0mon':
return False, 'wlan0mon channel 1: command failed: Resource busy (-16)'
return True, ''
with mock.patch.object(server, '_set_monitor_channel', side_effect=set_channel):
with mock.patch.object(server, '_monitor_down', return_value=False):
with mock.patch.object(server, '_dummy_sta_borrowable', return_value=False):
with mock.patch.object(server, '_borrow_dummy_sta') as borrow:
with mock.patch.object(server, '_sta_uplink_enabled', return_value=False):
with mock.patch.object(server, '_wlan0_pinned', return_value=True):
ok, detail = server._recon_hopper_preflight()
self.assertTrue(ok)
borrow.assert_not_called()
self.assertEqual(server._recon_hop_state['ifaces'], ['wlan1mon'])
self.assertIn('Open AP / Evil WPA', detail)
def test_reset_restores_parked_dummy_sta(self):
server._recon_hop_state['borrowed_wlan0'] = True
with mock.patch.object(
server, 'device_run', return_value=(0, '', '')) as run:
server._reset_recon_hop_state()
run.assert_any_call(['ip', 'link', 'set', 'wlan0', 'up'], timeout=10)
self.assertFalse(server._recon_hop_state['borrowed_wlan0'])
class DaemonSockTest(unittest.TestCase):
def setUp(self):
@@ -278,15 +360,36 @@ class DaemonSockTest(unittest.TestCase):
def test_start_reports_hopper_preflight_failure(self):
calls = []
server._recon_hopper_preflight.return_value = (
False, 'wlan1mon channel 36: Device or resource busy')
False, 'Recon radios are unavailable. wlan0mon is missing.')
server.daemon_sock_call = lambda *args, **kwargs: calls.append(args)
status, data = server.h_recon_start(
type('C', (), {'args': (), 'body': {'scan_time': 30}})())
self.assertEqual(status, 503)
self.assertEqual(data['error'], 'recon radio preflight failed')
self.assertIn('wlan1mon', data['detail'])
self.assertEqual(data['error'], 'Could not prepare recon radios')
self.assertIn('unavailable', data['detail'])
self.assertEqual(calls, [])
def test_start_returns_warning_when_a_radio_is_skipped(self):
calls = []
def fake_preflight():
server._recon_hop_state.update({
'warning': '2.4 GHz hopping skipped: Open AP is holding phy0. Scanning 5 GHz only.',
'ifaces': ['wlan1mon'],
'skipped': {'wlan0mon': '2.4 GHz hopping skipped: Open AP is holding phy0.'},
'hint': 'Stop the 2.4 GHz AP to hop 2.4 GHz.',
})
return True, server._recon_hop_state['warning']
server._recon_hopper_preflight.side_effect = fake_preflight
server.daemon_sock_call = lambda m, p, body=None: calls.append((m, p, body)) or (200, {'success': True})
status, data = server.h_recon_start(
type('C', (), {'args': (), 'body': {'scan_time': 30}})())
self.assertEqual(status, 200)
self.assertTrue(data.get('ok'))
self.assertIn('2.4 GHz hopping skipped', data.get('warning'))
self.assertEqual(data.get('hopping'), ['wlan1mon'])
self.assertEqual(calls[0][1], '/api/pineap/recon/new')
server._start_recon_hopper.assert_called_once_with(30)
class ReconScanStateTest(unittest.TestCase):
"""The webui mirrors the duration of the Pager's native timed scan."""
@@ -487,7 +590,7 @@ class ReconExamineTest(unittest.TestCase):
ctx = type('C', (), {'args': (), 'body': {'bssid': 'AA:BB:CC:DD:EE:FF'}})()
status, data = server.h_recon_examine(ctx)
self.assertEqual(status, 200)
self.assertEqual(calls[0], ('PINEAPPLE_EXAMINE_BSSID', 'AA:BB:CC:DD:EE:FF'))
self.assertEqual(calls[0], ('PINEAPPLE_EXAMINE_BSSID', 'AA:BB:CC:DD:EE:FF', '30'))
def test_examine_channel_calls_hak5(self):
calls = []
@@ -495,7 +598,24 @@ class ReconExamineTest(unittest.TestCase):
ctx = type('C', (), {'args': (), 'body': {'channel': 6}})()
status, data = server.h_recon_examine(ctx)
self.assertEqual(status, 200)
self.assertEqual(calls[0], ('PINEAPPLE_EXAMINE_CHANNEL', '6'))
self.assertEqual(calls[0], ('PINEAPPLE_EXAMINE_CHANNEL', '6', '30'))
def test_examine_channel_5ghz_sends_duration(self):
calls = []
server.hak5 = lambda *args, **kw: calls.append(args) or ''
ctx = type('C', (), {'args': (), 'body': {'channel': 140, 'seconds': 15}})()
status, data = server.h_recon_examine(ctx)
self.assertEqual(status, 200)
self.assertEqual(calls[0], ('PINEAPPLE_EXAMINE_CHANNEL', '140', '15'))
self.assertEqual(data.get('seconds'), 15)
def test_examine_compact_bssid_is_colonized(self):
calls = []
server.hak5 = lambda *args, **kw: calls.append(args) or ''
ctx = type('C', (), {'args': (), 'body': {'bssid': 'aabbccddeeff'}})()
status, data = server.h_recon_examine(ctx)
self.assertEqual(status, 200)
self.assertEqual(calls[0], ('PINEAPPLE_EXAMINE_BSSID', 'AA:BB:CC:DD:EE:FF', '30'))
def test_examine_requires_target(self):
server.hak5 = lambda *args, **kw: ''
@@ -1186,18 +1306,24 @@ class WigleTest(unittest.TestCase):
return type('C', (), {'args': args, 'body': body or {}})()
def _write(self, name, content):
with open(os.path.join(self.dir, name), 'w') as f:
f.write(content)
raw = content.encode('utf-8') if isinstance(content, str) else content
path = os.path.join(self.dir, name)
fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o644)
try:
os.write(fd, raw)
finally:
os.close(fd)
def test_file_rows_count_excludes_header(self):
self._write('a.csv', 'header\nr1\nr2\n')
self._write('b.csv', 'onlyheader\n')
payload = b'header\nr1\nr2\n'
self._write('a.csv', payload)
self._write('b.csv', b'onlyheader\n')
status, data = server.h_recon_wigle_files(self._ctx())
self.assertEqual(status, 200)
files = {f['name']: f for f in data['files']}
self.assertEqual(files['a.csv']['rows'], 2)
self.assertEqual(files['b.csv']['rows'], 0)
self.assertEqual(files['a.csv']['size'], len('header\nr1\nr2\n'))
self.assertEqual(files['a.csv']['size'], os.path.getsize(os.path.join(self.dir, 'a.csv')))
def test_file_rows_count_ignores_wigle_meta_and_header(self):
meta = 'WigleWifi-1.6,appRelease=0.0.0,model=pineapplepager,release=0.0.0\n'
+206
View File
@@ -0,0 +1,206 @@
import os
import sys
import unittest
from unittest import mock
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'payload', 'user', 'remote_access', 'pager-webui'))
import server
def setUpModule():
__import__('importlib').reload(server)
def ctx(body=None):
return type('C', (), {'body': body, 'args': (), 'query': {}})()
class DaemonRetryTest(unittest.TestCase):
def setUp(self):
self.old_sleep = server.DAEMON_SOCK_RETRY_SLEEP
self.old_retries = server.DAEMON_SOCK_RETRIES
server.DAEMON_SOCK_RETRY_SLEEP = 0
server.DAEMON_SOCK_RETRIES = 2
def tearDown(self):
server.DAEMON_SOCK_RETRY_SLEEP = self.old_sleep
server.DAEMON_SOCK_RETRIES = self.old_retries
def test_sock_retries_then_succeeds(self):
attempts = {'n': 0}
class Sock:
def __init__(self):
self.chunks = [b'HTTP/1.1 200 OK\r\n\r\n{"ok":true}', b'']
def settimeout(self, t):
pass
def connect(self, addr):
attempts['n'] += 1
if attempts['n'] < 2:
raise OSError('busy')
def sendall(self, data):
pass
def recv(self, n):
return self.chunks.pop(0) if self.chunks else b''
def close(self):
pass
with mock.patch.object(server.socket, 'socket', lambda *a, **k: Sock()):
status, data = server.daemon_sock_call('GET', '/api/pineap/get_config')
self.assertEqual(attempts['n'], 2)
self.assertEqual(status, 200)
self.assertEqual(data, {'ok': True})
def test_call_retries_on_oserror(self):
attempts = {'n': 0}
def boom(*a, **k):
attempts['n'] += 1
raise OSError('down')
with mock.patch.object(server.socket, 'socket', boom):
status, data = server.daemon_call('GET', '/api/api_ping')
self.assertEqual(attempts['n'], 3)
self.assertEqual(status, 0)
self.assertIsNone(data)
class ConfigWriteSafetyTest(unittest.TestCase):
def setUp(self):
self.old_sock = server.daemon_sock_call
def tearDown(self):
server.daemon_sock_call = self.old_sock
def test_set_config_refuses_when_get_fails(self):
calls = []
def fake(method, path, body=None, timeout=10):
calls.append((method, path))
if method == 'GET':
return 0, None
return 200, {'success': True}
server.daemon_sock_call = fake
status, payload = server.h_pineap_set_config(ctx({'loghandshake': True}))
self.assertEqual(status, 502)
self.assertIn('could not read', payload['error'])
self.assertFalse(any(c[0] == 'PUT' for c in calls))
def test_hostapd_set_refuses_when_get_fails(self):
calls = []
def fake(method, path, body=None, timeout=10):
calls.append((method, path))
if method == 'GET':
return 0, None
return 200, {'success': True}
server.daemon_sock_call = fake
status, payload = server.h_pineap_hostapd_set(ctx({'pineape_auth_pass': True}))
self.assertEqual(status, 502)
self.assertFalse(any(c[0] == 'PUT' for c in calls))
class Hak5RetryTest(unittest.TestCase):
def setUp(self):
self.old_sleep = server.HAK5_RETRY_SLEEP
self.old_run = server.device_run
server.HAK5_RETRY_SLEEP = 0
def tearDown(self):
server.HAK5_RETRY_SLEEP = self.old_sleep
server.device_run = self.old_run
def test_hak5_raises_after_retries(self):
calls = []
def fake(args, timeout=20, input_data=None):
calls.append(args)
return 1, '', 'busy'
server.device_run = fake
with self.assertRaises(RuntimeError):
server.hak5('PINEAPPLE_SSID_POOL_LIST')
self.assertEqual(len(calls), 3)
def test_hak5_succeeds_on_retry(self):
calls = []
def fake(args, timeout=20, input_data=None):
calls.append(args)
if len(calls) < 2:
return 1, '', 'busy'
return 0, 'ok\n', ''
server.device_run = fake
out = server.hak5('PINEAPPLE_SSID_POOL_LIST')
self.assertEqual(out, 'ok\n')
self.assertEqual(len(calls), 2)
def test_hak5_treats_error_text_as_failure(self):
calls = []
def fake(args, timeout=20, input_data=None):
calls.append(args)
return 0, '', 'ERROR: invalid time (expected number of seconds)'
server.device_run = fake
with self.assertRaises(RuntimeError):
server.hak5('PINEAPPLE_EXAMINE_CHANNEL', '140')
self.assertEqual(len(calls), 3)
class SsidPoolFailureTest(unittest.TestCase):
def setUp(self):
self.old_hak5 = server.hak5
def tearDown(self):
server.hak5 = self.old_hak5
def test_ssids_post_add_returns_502_on_hak5_failure(self):
server.hak5 = lambda *a, **k: (_ for _ in ()).throw(RuntimeError('busy'))
status, payload = server.h_ssids_post(ctx({'action': 'add', 'ssid': 'NewNet'}))
self.assertEqual(status, 502)
self.assertIn('ssid pool update failed', payload['error'])
def test_examine_returns_502_on_hak5_failure(self):
server.hak5 = lambda *a, **k: (_ for _ in ()).throw(RuntimeError('busy'))
status, payload = server.h_recon_examine(type('C', (), {
'args': (), 'body': {'bssid': 'AA:BB:CC:DD:EE:FF'}})())
self.assertEqual(status, 502)
self.assertEqual(payload['error'], 'examine failed')
class AdvertiseBlockTest(unittest.TestCase):
def setUp(self):
self.old_proxy = server._daemon_proxy
self.old_uci = server._uci_section
def tearDown(self):
server._daemon_proxy = self.old_proxy
server._uci_section = self.old_uci
def test_advertise_enable_always_refused(self):
server._uci_section = lambda name: {'disable': '0'}
calls = []
server._daemon_proxy = lambda method, path, body=None, timeout=15: (
calls.append((method, path, body)) or (200, {'success': True}))
status, payload = server.h_pineap_advertise(ctx({'enable': True}))
self.assertEqual(status, 400)
self.assertIn('cannot be re-enabled', payload['error'])
self.assertEqual(calls, [])
def test_advertise_disable_still_proxies(self):
calls = []
server._daemon_proxy = lambda method, path, body=None, timeout=15: (
calls.append(path) or (200, {'success': True}))
status, payload = server.h_pineap_advertise(ctx({'enable': False}))
self.assertEqual(status, 200)
self.assertEqual(calls, ['ssidpool/disable'])
if __name__ == '__main__':
unittest.main()
+159
View File
@@ -0,0 +1,159 @@
import os
import shutil
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'payload', 'user', 'remote_access', 'pager-webui'))
import server
class PagerTruthTest(unittest.TestCase):
def setUp(self):
self.uci = {
'pineapd.wlan1mon.hop': '1',
'pineapd.wlan2mon.disable': '0',
'wireless.dummy_radio0.disabled': '0',
'wireless.radio1.channel': 'auto',
'wireless.radio1.band': '5g',
'wireless.wlan1open': None,
'wireless.wlan1wpa': None,
}
self.ifaces = {}
fd, self.snap = tempfile.mkstemp(suffix='.json')
os.close(fd)
os.unlink(self.snap)
self.old_file = server.PAGER_SNAPSHOT_FILE
self.old_pineap = server.PINEAP_STATE_FILE
server.PAGER_SNAPSHOT_FILE = self.snap
server.PINEAP_STATE_FILE = self.snap + '.pineap'
self.old_run = server.device_run
server.device_run = self.fake_run
self.old_ent = server._disable_enterprise_ap
server._disable_enterprise_ap = lambda resume_hop=True: None
def tearDown(self):
server.device_run = self.old_run
server._disable_enterprise_ap = self.old_ent
server.PAGER_SNAPSHOT_FILE = self.old_file
server.PINEAP_STATE_FILE = self.old_pineap
for path in (self.snap, self.snap + '.tmp', self.snap + '.pineap'):
try:
os.unlink(path)
except OSError:
pass
def fake_run(self, args, timeout=20, input_data=None):
a = list(args)
if a[:2] == ['uci', '-q'] and a[2] == 'get':
key = a[3]
if key.startswith('wireless.') and key.count('.') == 1:
name = key.split('.', 1)[1]
if name in self.ifaces:
return 0, 'wifi-iface\n', ''
return 1, '', ''
val = self.uci.get(key)
if val is None:
return 1, '', ''
return 0, val + '\n', ''
if a[:2] == ['uci', 'show']:
sec = a[2]
name = sec.split('.', 1)[-1]
cfg = self.ifaces.get(name) or {}
body = ''.join("%s.%s='%s'\n" % (sec, k, v) for k, v in cfg.items())
return (0, body, '') if cfg or name in self.ifaces else (1, '', '')
if a[:2] == ['uci', 'set']:
expr = a[2]
if '=' not in expr:
return 0, '', ''
key, _, val = expr.partition('=')
parts = key.split('.')
if len(parts) == 2 and parts[0] == 'wireless' and val == 'wifi-iface':
self.ifaces.setdefault(parts[1], {})
return 0, '', ''
if len(parts) == 3 and parts[0] == 'wireless' and (
parts[1] in self.ifaces or parts[1] in ('wlan1open', 'wlan1wpa', 'wlan1ent')):
self.ifaces.setdefault(parts[1], {})[parts[2]] = val
return 0, '', ''
self.uci[key] = val
return 0, '', ''
if a[:2] == ['uci', 'delete']:
key = a[2]
parts = key.split('.')
if len(parts) == 2 and parts[0] == 'wireless':
self.ifaces.pop(parts[1], None)
self.uci.pop(key, None)
return 0, '', ''
if a[:2] == ['uci', 'commit']:
return 0, '', ''
if a[0] in ('wifi', '/etc/init.d/pineapd', 'kill', 'iw'):
return 0, '', ''
return 0, '', ''
def test_restore_reverts_radio1_ap_and_hop(self):
server.capture_pager_snapshot()
self.assertTrue(os.path.isfile(self.snap))
self.uci['pineapd.wlan1mon.hop'] = '0'
self.ifaces['wlan1open'] = {'ssid': 'EvilTwin', 'disabled': '0', 'device': 'radio1'}
self.uci['wireless.dummy_radio0.disabled'] = '1'
result = server.restore_pager_truth()
self.assertTrue(result['ok'])
self.assertTrue(result['restored'])
self.assertEqual(self.uci['pineapd.wlan1mon.hop'], '1')
self.assertNotIn('wlan1open', self.ifaces)
self.assertEqual(self.uci['wireless.dummy_radio0.disabled'], '0')
self.assertFalse(os.path.isfile(self.snap))
def test_restore_without_snapshot_is_safe(self):
result = server.restore_pager_truth()
self.assertTrue(result['ok'])
self.assertFalse(result['restored'])
self.assertEqual(result['reason'], 'no snapshot')
def test_mode_get_reports_snapshot(self):
server.capture_pager_snapshot()
status, payload = server.h_mode_get(None)
self.assertEqual(status, 200)
self.assertTrue(payload['snapshot'])
self.assertTrue(payload['markviii'])
self.assertEqual(payload['pager_port'], 1471)
def test_payload_refresh_falls_back_to_disk(self):
tmp = tempfile.mkdtemp()
self.addCleanup(lambda: shutil.rmtree(tmp, ignore_errors=True))
payload_dir = tmp
for part in ('user', 'games', 'snake'):
payload_dir = os.path.join(payload_dir, part)
if not os.path.isdir(payload_dir):
os.mkdir(payload_dir)
with open(os.path.join(payload_dir, 'payload.sh'), 'w') as handle:
handle.write('#!/bin/sh\n')
old_roots = server.PAYLOAD_ROOTS
old_daemon = server._payload_daemon
server.PAYLOAD_ROOTS = (tmp,)
server._payload_daemon = lambda *a, **k: (500, {'error': 'portal down'})
try:
status, data = server.h_payloads_refresh(None)
finally:
server.PAYLOAD_ROOTS = old_roots
server._payload_daemon = old_daemon
self.assertEqual(status, 200)
self.assertEqual(len(data['payloads']), 1)
self.assertEqual(data['payloads'][0]['key'], 'user~games~snake')
self.assertIn('warning', data)
def test_respawn_keeps_original_snapshot(self):
server.capture_pager_snapshot()
self.uci['pineapd.wlan1mon.hop'] = '0'
server.capture_pager_snapshot()
result = server.restore_pager_truth()
self.assertTrue(result['restored'])
self.assertEqual(self.uci['pineapd.wlan1mon.hop'], '1')
def test_uci_get_keeps_settings_default(self):
self.assertEqual(server._uci_get('missing.key', 'UTC'), 'UTC')
self.assertIsNone(server._uci_get('missing.key'))
if __name__ == '__main__':
unittest.main()
+19
View File
@@ -51,6 +51,8 @@ class StatusTest(unittest.TestCase):
def fake(args, timeout=20):
if args == ['iwinfo']:
return 0, 'wlan0 ESSID: "Pineapple"\n', ''
if args[0] == 'iw':
return 1, '', 'busy'
if args == ['iwinfo', 'wlan0', 'assoclist']:
return 0, '00:11:22:33:44:55 -64 dBm Signal: -64 dBm Rate: 12 Mbit/s\nAA:BB:CC:DD:EE:FF -40 dBm Signal: -40 dBm Rate: 24 Mbit/s\n', ''
return 0, '', ''
@@ -61,6 +63,23 @@ class StatusTest(unittest.TestCase):
self.assertEqual(clients[0]['rssi'], -64)
self.assertEqual(clients[0]['iface'], 'wlan0')
def test_assoc_clients_skips_monitor_ifaces(self):
def fake(args, timeout=20):
if args == ['iwinfo']:
return 0, 'wlan0mon ESSID: unknown\nwlan0wpa ESSID: "x"\n', ''
if args[0] == 'iw' and len(args) > 2 and args[2] == 'wlan0mon':
raise AssertionError('must not query monitor ifaces')
if args[:2] == ['iwinfo', 'wlan0mon']:
raise AssertionError('must not query monitor ifaces')
if args == ['iw', 'dev', 'wlan0wpa', 'station', 'dump']:
return 0, 'Station aa:bb:cc:dd:ee:ff (on wlan0wpa)\n\tsignal: -50 dBm\n', ''
return 1, '', ''
server.device_run = fake
clients = server.assoc_clients()
self.assertEqual(len(clients), 1)
self.assertEqual(clients[0]['mac'], 'AA:BB:CC:DD:EE:FF')
self.assertEqual(clients[0]['iface'], 'wlan0wpa')
def test_h_status_shape(self):
server.device_run = lambda args, timeout=20: (0, '', '')
server.current_token = lambda: 'tok'