scp81 passthru, proactive decoders, ADM badge, file manager layout (v2.2.1)
SCP81: - New listener mode "passthru": no local listener - the card's BIP channels connect straight to a configured external platform (host/port required), which terminates TLS and runs the dialog. The status API reports mode/target (_SCP81_MODE/_SCP81_TARGET) and clears them on stop. PWA mode select, hint, Start validation; TLS PSK stays the default. Proactive command decoding (Phone tab log): - SEND SHORT MESSAGE (0x13) is now decoded: alpha, address (TON/NPI + number), 3GPP-SMS TPDU (type, TP-MR, TP-DA, TP-PID 0x7F flagged as SIM data download, TP-DCS, TP-VP, TP-UDL), UDH concatenation IEs, and the TP-UD as text (GSM-7 with a septet unpacker, UCS2, 8-bit) or as a TS 31.115 secured packet for PID 0x7F; malformed TPDUs fall back to the raw hex line. - PROVIDE LOCAL INFORMATION qualifier names completed per TS 102 223 V18.3.0: ESN (07), MEID (0B), Supported RATs (1A); 05 relabelled "Reserved for GSM (Timing Advance)". Fixed in the server dict and both frontend tables. Header: - Compact ADM badge next to the card indicator: "ADM ✓" green when pySim's adm_verified is set, "ADM ✗" red otherwise, hidden without a card session or when the server is down; updated ahead of the card state-key early return so it never disturbs the connect/reset flow. File manager: - Sort pills (FID/Name), Probe all files button and progress line are pinned above the tree instead of scrolling with it; the tree box cap grows from 420px to 65vh. SW cache otaman-v165; help EN/RU updated; tests 234 Python + 361 frontend.
This commit is contained in:
@@ -152,6 +152,32 @@ class BipTerminalTest(unittest.TestCase):
|
||||
self.assertIn('close', kinds)
|
||||
peer.stop()
|
||||
|
||||
def test_passthru_mode_roundtrip_via_bip_control(self):
|
||||
# SCP81 passthru: the control API enables BIP with the external
|
||||
# platform as the target and starts no local listener; the card's
|
||||
# channel talks straight to that platform.
|
||||
peer = PeerServer(greeting=b'PLATFORM')
|
||||
peer.start()
|
||||
try:
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'passthru',
|
||||
'host': '127.0.0.1', 'port': peer.port})
|
||||
self.assertTrue(resp['ok'], resp)
|
||||
self.assertEqual(resp['listener']['mode'], 'passthru')
|
||||
self.assertEqual(server._BIP.target, ('127.0.0.1', peer.port))
|
||||
cid, err = server._BIP.open('10.9.9.9', 10174, 512)
|
||||
self.assertIsNone(err)
|
||||
self.assertTrue(server._BIP.send(cid, b'CARDHELLO'))
|
||||
data = b''
|
||||
for _ in range(20):
|
||||
data = server._BIP.receive(cid, 100)
|
||||
if data:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
self.assertEqual(data, b'PLATFORM')
|
||||
finally:
|
||||
server._scp81_bip_control({'action': 'stop'})
|
||||
peer.stop()
|
||||
|
||||
def test_disabled_terminal_refuses_open(self):
|
||||
bip = httpota.BipTerminal()
|
||||
cid, err = bip.open('127.0.0.1', 1, 512)
|
||||
|
||||
@@ -277,6 +277,17 @@ class TestProactiveDecode(unittest.TestCase):
|
||||
srv._PROACTIVE_SESSION_START = 1234.0
|
||||
srv._PLI_DATA[0x00] = '93055210011000'
|
||||
|
||||
@staticmethod
|
||||
def _cmd_raw(cmd_type, qualifier, extras=b''):
|
||||
"""A D0-wrapped proactive command (header TLVs + extras)."""
|
||||
body = (bytes([0x81, 0x03, 0x01, cmd_type, qualifier])
|
||||
+ bytes([0x82, 0x02, 0x83, 0x81]) + extras)
|
||||
return bytes([0xD0, len(body)]) + body
|
||||
|
||||
@staticmethod
|
||||
def _decoded(cmd_type, raw, qualifier=None):
|
||||
return {d['label']: d['value'] for d in _decode_cmd(cmd_type, raw, qualifier)}
|
||||
|
||||
def test_decode_cmd_poll_interval(self):
|
||||
r = _decode_cmd(0x03, bytes.fromhex('d00d8103010300820283818402011e'), None)
|
||||
self.assertEqual(r, [{'label': 'Interval', 'value': '30 s'}])
|
||||
@@ -286,13 +297,68 @@ class TestProactiveDecode(unittest.TestCase):
|
||||
self.assertEqual(r, [{'label': 'Events', 'value': 'Call connected'}])
|
||||
|
||||
def test_decode_cmd_send_short_message(self):
|
||||
r = _decode_cmd(0x13, bytes.fromhex('d0158103011300820283818b0b916106152670f900a35f020101'), None)
|
||||
self.assertEqual(r, [{'label': 'SMS TPDU', 'value': '916106152670f900a35f02'}])
|
||||
# SEND SHORT MESSAGE with an SMS-SUBMIT TPDU carrying GSM-7 text.
|
||||
tpdu = bytes.fromhex('010006912143F5000005E8329BFD06')
|
||||
raw = self._cmd_raw(0x13, 0, bytes([0x8B, len(tpdu)]) + tpdu)
|
||||
r = self._decoded(0x13, raw)
|
||||
self.assertEqual(r['Type'], 'SMS-SUBMIT')
|
||||
self.assertEqual(r['TP-MR'], '0')
|
||||
self.assertEqual(r['TP-DA'], '12345')
|
||||
self.assertEqual(r['TP-PID'], '0x00')
|
||||
self.assertEqual(r['TP-DCS'], '0x00')
|
||||
self.assertEqual(r['TP-UDL'], '5')
|
||||
self.assertEqual(r['Text'], 'hello')
|
||||
self.assertEqual(r['SMS TPDU'], tpdu.hex().upper())
|
||||
|
||||
def test_decode_cmd_send_short_message_udh_8bit(self):
|
||||
# UDHI + concatenation IE (16-bit ref) + 8-bit text data.
|
||||
udh = bytes.fromhex('0608040001020341 42'.replace(' ', ''))
|
||||
tpdu = (bytes.fromhex('4100' '06912143F5' '00' '04' '09') + udh)
|
||||
raw = self._cmd_raw(0x13, 0, bytes([0x8B, len(tpdu)]) + tpdu)
|
||||
r = self._decoded(0x13, raw)
|
||||
self.assertEqual(r['Concat (16-bit ref)'], '1, part 2/3')
|
||||
self.assertEqual(r['Text'], 'AB')
|
||||
|
||||
def test_decode_cmd_send_short_message_ucs2(self):
|
||||
text = 'Тест'.encode('utf-16-be')
|
||||
tpdu = (bytes.fromhex('0100' '06912143F5' '00' '08' '%02X' % len(text))
|
||||
+ text)
|
||||
raw = self._cmd_raw(0x13, 0, bytes([0x8B, len(tpdu)]) + tpdu)
|
||||
r = self._decoded(0x13, raw)
|
||||
self.assertEqual(r['TP-DCS'], '0x08')
|
||||
self.assertEqual(r['Text'], 'Тест')
|
||||
|
||||
def test_decode_cmd_send_short_message_secured_packet(self):
|
||||
# PID 0x7F = SIM data download: the UD is a secured packet (TS 31.115).
|
||||
tpdu = bytes.fromhex('0100' '06912143F5' '7F' 'F6' '03' 'AABBCC')
|
||||
raw = self._cmd_raw(0x13, 0, bytes([0x8B, len(tpdu)]) + tpdu)
|
||||
r = self._decoded(0x13, raw)
|
||||
self.assertEqual(r['TP-PID'], '0x7F (SIM data download)')
|
||||
self.assertEqual(r['Secured packet (TS 31.115)'], '3 bytes: AABBCC')
|
||||
|
||||
def test_decode_cmd_send_short_message_malformed_falls_back(self):
|
||||
# A malformed/garbage TPDU must not raise: the raw hex line remains.
|
||||
raw = bytes.fromhex('d0158103011300820283818b0b916106152670f900a35f020101')
|
||||
r = self._decoded(0x13, raw)
|
||||
self.assertEqual(r['SMS TPDU'], '916106152670F900A35F02')
|
||||
|
||||
def test_decode_cmd_pli_qualifier_name(self):
|
||||
r = _decode_cmd(0x26, b'\xd0', 0x00)
|
||||
self.assertTrue(r[0]['value'].startswith('Location Information (MCC, MNC, LAC/TAC, Cell ID)'))
|
||||
|
||||
def test_decode_cmd_pli_all_standard_qualifiers_named(self):
|
||||
# TS 102 223 V18.3.0 (PLI qualifier coding): names must exist even
|
||||
# without a special data decoder, e.g. ESN (07) and MEID (0B).
|
||||
cases = {
|
||||
0x07: 'ESN',
|
||||
0x0B: 'MEID',
|
||||
0x1A: 'Supported Radio Access Technologies',
|
||||
0x05: 'Reserved for GSM',
|
||||
}
|
||||
for qualifier, name in cases.items():
|
||||
r = _decode_cmd(0x26, b'\xd0', qualifier)
|
||||
self.assertIn(name, r[0]['value'], 'qualifier 0x%02X' % qualifier)
|
||||
|
||||
def test_decode_cmd_timer_management_start(self):
|
||||
# TS 102 223 6.6.21/8.37/8.38: start timer 3 for 14:07:32
|
||||
raw = bytes.fromhex('d011810301270082028182a40103a503417023')
|
||||
|
||||
@@ -682,6 +682,38 @@ class BipControlTest(unittest.TestCase):
|
||||
self.assertFalse(resp['ok'])
|
||||
self.assertIn('unsupported mode', resp['error'])
|
||||
|
||||
def test_passthru_mode_targets_the_external_server(self):
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'passthru',
|
||||
'host': '10.11.12.13', 'port': 10174})
|
||||
self.assertTrue(resp['ok'], resp)
|
||||
self.assertIsNone(server._SCP81_LISTENER) # no local listener
|
||||
self.assertEqual(resp['listener']['mode'], 'passthru')
|
||||
self.assertEqual(resp['listener']['host'], '10.11.12.13')
|
||||
self.assertEqual(resp['listener']['port'], 10174)
|
||||
self.assertEqual(resp['listener']['target'], '10.11.12.13:10174')
|
||||
self.assertTrue(resp['bip']['enabled'])
|
||||
self.assertEqual(server._BIP.target, ('10.11.12.13', 10174))
|
||||
# the status endpoint sees the passthru mode while it runs ...
|
||||
self.assertEqual(server._scp81_listener_status()['mode'], 'passthru')
|
||||
# ... and stopping clears it (no stale listener in the status)
|
||||
server._scp81_bip_control({'action': 'stop'})
|
||||
self.assertIsNone(server._scp81_listener_status())
|
||||
self.assertFalse(server._BIP.enabled)
|
||||
|
||||
def test_passthru_mode_requires_an_explicit_target(self):
|
||||
# No defaults for a remote platform: the target must be configured.
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'passthru'})
|
||||
self.assertFalse(resp['ok'])
|
||||
self.assertIn('host and port', resp['error'])
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'passthru',
|
||||
'host': '10.0.0.1'})
|
||||
self.assertFalse(resp['ok'])
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'passthru',
|
||||
'port': 1234})
|
||||
self.assertFalse(resp['ok'])
|
||||
self.assertIsNone(server._SCP81_LISTENER)
|
||||
self.assertFalse(server._BIP.enabled)
|
||||
|
||||
def test_start_accepts_explicit_script_list(self):
|
||||
server._SCP81_PSKS = {}
|
||||
server._SCP81_PSK_LEGACY = None
|
||||
|
||||
Reference in New Issue
Block a user