diff --git a/frontend/index.html b/frontend/index.html index 035bb08..4748722 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1877,7 +1877,7 @@ // ===== Version ===== // Single source of truth for the PWA version: shown in the header and used // by the server version check in pysimConnect(). -const SIMPLE_VERSION = '3.18.3'; +const SIMPLE_VERSION = '3.18.4'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== diff --git a/frontend/sw.js b/frontend/sw.js index d19eff1..4fe3d11 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v385'; +const CACHE = 'simple-v386'; const URLS = [ 'index.html', 'help.html', diff --git a/pyproject.toml b/pyproject.toml index 2e85344..05c0fbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.18.3" +version = "3.18.4" description = "HTTP REST server wrapping pysim for the SIMple PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 96f0a27..fb09474 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -34,7 +34,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.18.3' +VERSION = '3.18.4' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE @@ -4652,11 +4652,14 @@ _EVENT_INNER_MAX = 252 _EVENT_DATA_MAX = _EVENT_INNER_MAX - 8 -def _send_event_download(scc, event_type, event_data=None, drain=True, src=None): +def _send_event_download(scc, event_type, event_data=None, drain=True, src=None, + log=True): """Send ENVELOPE(Event Download) for the given event type. Builds: CLA C2 0000 Lc D6 [len] (99 01 [type] 82 02 81 [extra]). `src` is the source device identity (TS 102 223 8.7: '82' terminal, - '83' network); the default is the terminal.""" + '83' network); the default is the terminal. `log=False` suppresses the + generic ENVELOPE lines - the test runner logs its own step-annotated line + with the event name, the source and the response SW.""" if event_data and len(event_data) > _EVENT_DATA_MAX: raise ValueError('event data does not fit one ENVELOPE (max %d bytes; ' 'chained delivery is not implemented)' % _EVENT_DATA_MAX) @@ -4679,12 +4682,14 @@ def _send_event_download(scc, event_type, event_data=None, drain=True, src=None) # read as a long-form indicator) d6_tlv = bytes([0xD6, 0x81, len(inner)]) + bytes(inner) env_hex = '%sc20000%02x%s' % (scc.cat_cla, len(d6_tlv), d6_tlv.hex()) - sys.stderr.write('ENVELOPE(Event Download): type=0x%02x data=%s\n' % (event_type, event_data.hex() if event_data else '(none)')) + if log: + sys.stderr.write('ENVELOPE(Event Download): type=0x%02x data=%s\n' % (event_type, event_data.hex() if event_data else '(none)')) data, sw = scc._tp.send_apdu(env_hex) if sw.startswith('61'): get_len = int(sw[2:], 16) if len(sw) == 4 else 0x100 data, sw = scc._tp.send_apdu('00c00000%02x' % get_len) - sys.stderr.write('ENVELOPE SW: %s\n' % sw) + if log: + sys.stderr.write('ENVELOPE SW: %s\n' % sw) if drain and sw.startswith('91'): _handle_proactive_chain(scc, sw) sw = '9000' @@ -5989,7 +5994,7 @@ def _test_run_action(server, step, ctx): if kind == 'envelope': data, sw = _send_event_download(scc, p['event'], bytes.fromhex(p['data']) if p['data'] else None, - drain=False, src=p.get('src')) + drain=False, src=p.get('src'), log=False) sent = 'ENVELOPE(Event Download) type=0x%02X' % p['event'] _test_log('%s src=%s data=%s -> SW=%s%s' % (sent, p.get('src') or '82', p['data'] or '(none)', sw or '(none)', @@ -5998,7 +6003,7 @@ def _test_run_action(server, step, ctx): # The semantic event download: the builder owns the byte layout. data_hex = events.build(p['event'], p.get('fields') or {}) data, sw = _send_event_download(scc, p['event'], bytes.fromhex(data_hex), - drain=False, src=p.get('src')) + drain=False, src=p.get('src'), log=False) sent = 'ENVELOPE(Event Download) type=0x%02X (%s) data=%s' % ( p['event'], events.event_name(p['event']), data_hex) _test_log('%s src=%s -> SW=%s%s' diff --git a/tests/test_ota_helpers.py b/tests/test_ota_helpers.py index 002305a..4c625b0 100644 --- a/tests/test_ota_helpers.py +++ b/tests/test_ota_helpers.py @@ -297,6 +297,9 @@ class TestSmsConcatenation(unittest.TestCase): _ota_reference('16', '01', '15', '15', 'b00000', '0000000001', apdu, K, K) def test_send_secured_packet_sends_the_segments_in_order(self): + import contextlib + import io + import pysim_simple_server.server as srv apdu = '80E80000F0' + '00' * 240 + '00' sp_hex, _ = _build_secured_packet('16', '01', '15', '15', 'b00000', @@ -307,13 +310,19 @@ class TestSmsConcatenation(unittest.TestCase): sent.append(tpdu_hex.upper()) return '', '9000' + buf = io.StringIO() with mock.patch.object(srv, '_send_envelope', side_effect=fake_envelope): - result = _send_secured_packet(object(), sp_hex, oa_number='12345') + with contextlib.redirect_stderr(buf): + result = _send_secured_packet(object(), sp_hex, oa_number='12345') self.assertTrue(result['success'], result) self.assertEqual(result['bytes'], len(sp_hex) // 2) self.assertEqual(result['segments'], len(sent)) self.assertTrue(2 <= result['segments'] <= SCP80_MAX_SEGMENTS) total = result['segments'] + # every segment's answer is logged (the send-ota and test-run logs) + log = buf.getvalue() + for num in range(1, total + 1): + self.assertIn('OTA SEND: ENVELOPE %d/%d -> 9000' % (num, total), log) for num, tpdu in enumerate(sent, start=1): concat = '000301%02X%02X' % (total, num) # IEI 00, IEDL 3, ref 01 udhl = '07' if num == 1 else '05' diff --git a/tests/test_testscript.py b/tests/test_testscript.py index 30fd6be..4f75a35 100644 --- a/tests/test_testscript.py +++ b/tests/test_testscript.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """Tests for the test-script engine (pure parts) and the server-side runner.""" +import contextlib +import io import sys import time import unittest @@ -522,26 +524,39 @@ class TestRunnerActions(RunnerTestCase): def test_event_action_sends_the_built_object(self): scc = FakeScc() scc.push('80C2', '', '9000') - run = self.run_script(FakeServer(scc), [ - {'type': 'action', 'kind': 'event', - 'params': {'event': 'location status', - 'fields': {'status': '0', 'mcc': '250', 'mnc': '01', - 'lac': '00FF', 'cell': '0001'}}}, - ]) + buf = io.StringIO() + with contextlib.redirect_stderr(buf): + run = self.run_script(FakeServer(scc), [ + {'type': 'action', 'kind': 'event', + 'params': {'event': 'location status', + 'fields': {'status': '0', 'mcc': '250', 'mnc': '01', + 'lac': '00FF', 'cell': '0001'}}}, + ]) self.assertEqual(run['status'], 'ok', run['steps']) apdu = scc.sent[0].upper() # event list + device identities (terminal default) + the built object self.assertIn('99010382028281' + '9B0100930752F01000FF0001', apdu) + # the runner logs one step-annotated ENVELOPE line, not the generic + # sender pair as well (its line carries the name, src and SW) + log = buf.getvalue() + self.assertIn('TEST-RUN step 1: ENVELOPE(Event Download) type=0x03 ' + '(location_status) data=9B0100930752F01000FF0001 src=82 -> SW=9000', + log) + self.assertNotIn('ENVELOPE(Event Download): type=', log) + self.assertNotIn('ENVELOPE SW:', log) # the src override names the network as the source scc2 = FakeScc() scc2.push('80C2', '', '9000') - run = self.run_script(FakeServer(scc2), [ - {'type': 'action', 'kind': 'event', - 'params': {'event': '0x0B', 'fields': {'tech': '8'}, 'src': '83'}}, - ]) + buf2 = io.StringIO() + with contextlib.redirect_stderr(buf2): + run = self.run_script(FakeServer(scc2), [ + {'type': 'action', 'kind': 'event', + 'params': {'event': '0x0B', 'fields': {'tech': '8'}, 'src': '83'}}, + ]) self.assertEqual(run['status'], 'ok', run['steps']) apdu = scc2.sent[0].upper() self.assertIn('99010B82028381' + 'BF0108', apdu) + self.assertIn('src=83 -> SW=9000', buf2.getvalue()) # the raw envelope action takes the same source override scc3 = FakeScc() scc3.push('80C2', '', '9000') @@ -733,8 +748,6 @@ class TestRunnerActions(RunnerTestCase): # the semantic lines a verification needs: the plaintext C-APDU, the # built secured packet, the FETCH/CMD/TR pair of an expectation and # the menu selection's ENVELOPE with the resolved item - import contextlib - import io scc = FakeScc() scc.push('80F2', '', '9102') scc.push('8012', self.SETUP_MENU_CMD, '9000') @@ -770,11 +783,11 @@ class TestRunnerActions(RunnerTestCase): self.assertIn('TEST-RUN step 3: TR=', log) self.assertIn('TEST-RUN step 4: MENU-SELECT ENVELOPE=', log) self.assertIn("item=1 ('One')", log) + # the report's label carries the plaintext APDU too (log/report parity) + self.assertIn('C-APDU=80E2900000', run['steps'][0]['sent']) def test_the_run_log_carries_the_inline_por(self): # the decoded PoR and its R-APDU are logged with the raw packet - import contextlib - import io scc = FakeScc() preset = {'kic': '15', 'kid': '15', 'kicKey': 'AA' * 16, 'kidKey': 'BB' * 16, 'counter': '0000000A', 'tars': preset_tars()} @@ -799,8 +812,6 @@ class TestRunnerActions(RunnerTestCase): self.assertIn('TEST-RUN step 1: R-APDU SW=9000 data=AABB', log) def test_expect_decodes_the_submit_por(self): - import contextlib - import io scc = FakeScc() preset = {'kic': '15', 'kid': '15', 'kicKey': 'AA' * 16, 'kidKey': 'BB' * 16, 'counter': '0000000A', 'tars': preset_tars()}