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()}