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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user