diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ebf3c2..3f586d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,19 @@ every change carries a descriptive commit message. ### Fixed +- The **PLI location answer** now uses the 7-byte PLI form (TS 31.111 8.19.1): + PLMN 001/01, LAC `0001`, Cell ID `0001`. The former default was the 5-byte + *Network Rejection event* form (no Cell ID) with LAC `0000`; a card applet + that re-reads its location after a `*LOCI` status change (the Alfa MultiIMSI + applet) treated it as empty and re-requested PLI on every STATUS tick — the + PLI storm that slowed RAM Explore. The TR data is now + `13 07 00F110 0001 0001`. +- The **proactive chain is bounded on every call site**: `_handle_proactive_chain`'s + `max_rounds` defaults to `_POLL_CHAIN_MAX` (the background poll always passed it; + the manual STATUS poll and the ENVELOPE/menu/TP chains did not). A card that + re-queues a command (the PLI storm) could keep the manual poll looping forever + while it held the card lock — `/api/status-poll` never returned and every card + request waited. The manual poll also passes the bound explicitly. - The load-test loop closes properly on a raised runner error (it used to leave the run `running` forever - the PWA polled a dead run and *Clear report* stayed refused), on a card session change inside a suite iteration, and on a member that was stopped; a diff --git a/docs/api.md b/docs/api.md index 79b99b5..4193acf 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1769,13 +1769,16 @@ Lists the PROVIDE LOCAL INFORMATION qualifier codes with their names. Returns the effective PLI data dictionary as a qualifier-code map: the operator's override, else the built-in default (the qualifiers whose objects -the pinned ETSI specs define - location as the test network 001/01, the test -IMEI/IMEISV, language, access technology, search mode, battery; v3.23.1). +the pinned ETSI specs define - location as the test network 001/01 with test +LAC 0001 and Cell ID 0001 (the 7-byte PLI form per TS 31.111 8.19.1; the +5-byte form without the Cell ID belongs to the Network Rejection event +download), the test IMEI/IMEISV, language, access technology, search mode, +battery; v3.23.1, the PLI location form fixed in v3.37.0). The date/time qualifier stays empty - the host clock answers it while no override is set. ```json -{"00": "930500F1100000", "0A": "630104", "03": ""} +{"00": "130700F11000010001", "0A": "630104", "03": ""} ``` ### `POST /api/pli-dict` diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 2cab622..b92427c 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -540,7 +540,7 @@
  • 06 Access Technology · 08 IMEISV · 09 Search Mode
  • 0A Battery · 0E Multiple Access Technologies
  • -

    Значения хранятся на сервере до перезапуска. Для квалификаторов, определённых в спецификациях ETSI, заранее заданы разумные значения по умолчанию (местоположение — тестовая сеть 001/01, тестовый IMEI 490154203237518 и его IMEISV, язык «en», технология доступа E-UTRAN, автоматический поиск сети, полный заряд батареи): заданное значение переопределяет умолчание, а очистка поля возвращает умолчание; квалификатор даты/времени, пока пуст, отвечается часами хоста, а специфичные для 3GPP/3GPP2 квалификаторы (NMR, ESN, MEID, CSG/HNB, WLAN, slices, CAG, …) остаются пустыми — спецификации ETSI не определяют для них содержимое. Поле, которое сервер не может использовать (значение не hex, нечётное число цифр, код вне списка квалификаторов), указывается под кнопкой «Сохранить», а не отбрасывается молча. Когда карта выдаёт PLI, сервер вставляет значения в TERMINAL RESPONSE — в том числе в ответ тест-скрипта.

    +

    Значения хранятся на сервере до перезапуска. Для квалификаторов, определённых в спецификациях ETSI, заранее заданы разумные значения по умолчанию (местоположение — тестовая сеть 001/01 с тестовыми LAC 0001 и Cell ID 0001 — 7-байтовая форма PLI, а не короткая форма события Network Rejection; тестовый IMEI 490154203237518 и его IMEISV, язык «en», технология доступа E-UTRAN, автоматический поиск сети, полный заряд батареи): заданное значение переопределяет умолчание, а очистка поля возвращает умолчание; квалификатор даты/времени, пока пуст, отвечается часами хоста, а специфичные для 3GPP/3GPP2 квалификаторы (NMR, ESN, MEID, CSG/HNB, WLAN, slices, CAG, …) остаются пустыми — спецификации ETSI не определяют для них содержимое. Поле, которое сервер не может использовать (значение не hex, нечётное число цифр, код вне списка квалификаторов), указывается под кнопкой «Сохранить», а не отбрасывается молча. Когда карта выдаёт PLI, сервер вставляет значения в TERMINAL RESPONSE — в том числе в ответ тест-скрипта.

    8.7 Симуляция сети

    Воспроизводит шаблоны записи реального телефона при смене сетевых условий (исследование трасс в projects/UICC_NAA.md): подключение EPS, потеря сервиса / ограниченный сервис, запрет роуминга, откат на 2G, приём SMS, перенастройка cell broadcast и AUTHENTICATE. По кнопке на сценарий; карта должна быть подключена.

    diff --git a/frontend/help.html b/frontend/help.html index a9f5728..2062c6b 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -539,7 +539,7 @@
  • 06 Access Technology · 08 IMEISV · 09 Search Mode
  • 0A Battery · 0E Multiple Access Technologies
  • -

    Values persist server-side until restart. Sensible defaults are pre-settled for the qualifiers the ETSI specs define (location as the test network 001/01, the test IMEI 490154203237518 and its IMEISV, language ‘en’, E-UTRAN access technology, automatic search mode, a full battery): a value you set overrides its default, and clearing the field restores it; the date/time qualifier is answered from the host clock while empty, and the 3GPP/3GPP2-specific qualifiers (NMR, ESN, MEID, CSG/HNB, WLAN, slices, CAG, …) stay empty - the ETSI specs define no contents for them. A field the server cannot use (a value that is not hex, an odd number of digits, a code outside the qualifier list) is named under the Save button instead of being silently dropped. When the card issues PLI, the server injects the values into the TERMINAL RESPONSE - also into a test-script expectation's answer.

    +

    Values persist server-side until restart. Sensible defaults are pre-settled for the qualifiers the ETSI specs define (location as the test network 001/01 with test LAC 0001 and Cell ID 0001 — the 7-byte PLI form, not the shorter Network Rejection one, the test IMEI 490154203237518 and its IMEISV, language ‘en’, E-UTRAN access technology, automatic search mode, a full battery): a value you set overrides its default, and clearing the field restores it; the date/time qualifier is answered from the host clock while empty, and the 3GPP/3GPP2-specific qualifiers (NMR, ESN, MEID, CSG/HNB, WLAN, slices, CAG, …) stay empty - the ETSI specs define no contents for them. A field the server cannot use (a value that is not hex, an odd number of digits, a code outside the qualifier list) is named under the Save button instead of being silently dropped. When the card issues PLI, the server injects the values into the TERMINAL RESPONSE - also into a test-script expectation's answer.

    8.7 Network simulation

    Replays the write patterns a real phone performs when the network condition changes (trace study in projects/UICC_NAA.md): EPS attach, service loss / limited service, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration and AUTHENTICATE. One button per scenario; the card must be equipped.

    diff --git a/frontend/sw.js b/frontend/sw.js index d4eaa29..99a5af5 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v476'; +const CACHE = 'simple-v477'; const URLS = [ 'index.html', 'help.html', diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index fdabd13..36ec0c3 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -3592,9 +3592,12 @@ PLI_QUALIFIER_NAMES = { # specs define no contents for them ("reserved for 3GPP/3GPP2") and a # fabricated object would mislead the application more than no answer. PLI_DEFAULTS = { - # Location Information (8.19): PLMN 001/01 (the test network, TS 23.003) - # + LAC 0000 - 0x00: '1305' + '00F110' + '0000', + # Location Information (TS 31.111 8.19.1, the GERAN form a PLI answer + # uses): PLMN 001/01 (the test network, TS 23.003) + LAC 0001 + Cell ID + # 0001. Length 05 (no Cell ID) is the *Network Rejection event* form - + # a PLI answer must carry the Cell ID (length 07), otherwise the location + # reads as empty and a card applet may re-request it forever. + 0x00: '1307' + '00F110' + '0001' + '0001', # IMEI (8.20): the classic test IMEI 490154203237518, nibble-swapped # BCD with the odd 15th digit padded by F (TS 124 008 Mobile Identity) 0x01: '1408' + '94104502237315F8', @@ -6608,15 +6611,18 @@ def _parse_setup_menu_items(raw): return _attach_nai(items, nai) -def _handle_proactive_chain(scc, sw91, on_fetch=None, status_poll=True, max_rounds=None): +def _handle_proactive_chain(scc, sw91, on_fetch=None, status_poll=True, + max_rounds=_POLL_CHAIN_MAX): """Run a FETCH/TERMINAL RESPONSE chain; marks the card as busy so that terminal-initiated ENVELOPEs (Data available, Channel status, timers) wait. ``status_poll=False`` skips the trailing STATUS (used by the profile switch, where the card is mid-switch and must not be queried further). - ``max_rounds`` bounds the answers per call (the background poll passes - _POLL_CHAIN_MAX so a card that re-queues a command cannot hold the card - lock forever); ``_TEST_RUNNING`` aborts the chain before the next FETCH - - a test run owns the card and the pending command stays for the script.""" + ``max_rounds`` bounds the answers per call - the default is + _POLL_CHAIN_MAX, so a card that re-queues a command (a chatty applet) + cannot hold the card lock forever from *any* call site; a caller may pass + a different bound. ``_TEST_RUNNING`` aborts the chain before the next + FETCH - a test run owns the card and the pending command stays for the + script.""" global _PROACTIVE_BUSY _PROACTIVE_BUSY = True try: @@ -10032,7 +10038,7 @@ class PysimHandler(BaseHTTPRequestHandler): sys.stderr.write('STATUS -> %s\n' % st_sw) resp = {'sw': st_sw} if st_sw.startswith('91'): - _handle_proactive_chain(scc, st_sw) + _handle_proactive_chain(scc, st_sw, max_rounds=_POLL_CHAIN_MAX) resp['proactive'] = True self._send_json(resp) self._log_resp(resp) diff --git a/tests/test_fetch_routes.py b/tests/test_fetch_routes.py index 9f5ee0e..f6a3f27 100644 --- a/tests/test_fetch_routes.py +++ b/tests/test_fetch_routes.py @@ -146,7 +146,7 @@ class PliDictHttpTests(unittest.TestCase): try: status, resp = self._req('GET', '/api/pli-dict') self.assertEqual(status, 200, resp) - self.assertEqual(resp['00'], '130500F1100000') + self.assertEqual(resp['00'], '130700F11000010001') self.assertEqual(resp['0A'], '630104') self.assertEqual(resp['03'], '') # the host clock answers it self.assertEqual(resp['02'], '') # no default diff --git a/tests/test_ota_helpers.py b/tests/test_ota_helpers.py index 1f1a16b..8f4a023 100644 --- a/tests/test_ota_helpers.py +++ b/tests/test_ota_helpers.py @@ -1041,7 +1041,10 @@ class TestProactiveDecode(unittest.TestCase): # define (TS 102 223 8.x, tags per TS 101 220 V18.3.0 Table 7.23) import pysim_simple_server.server as srv d = srv.PLI_DEFAULTS - self.assertEqual(d[0x00], '130500F1100000') # 001/01 + LAC 0000 + # the 7-byte PLI form: 001/01 + LAC 0001 + Cell ID 0001 (length 05 + # is the Network Rejection event form, TS 31.111 8.19.1) + self.assertEqual(d[0x00], '130700F11000010001') + self.assertEqual(len(bytes.fromhex(d[0x00])), 9) self.assertEqual(d[0x01], '140894104502237315F8') # IMEI, F pad self.assertEqual(d[0x04], '2D02656E') # 'en' self.assertEqual(d[0x05], '2E020000') # Timing Advance @@ -1073,6 +1076,19 @@ class TestProactiveDecode(unittest.TestCase): srv._PLI_DATA.clear() srv._PLI_DATA.update(saved) + def test_the_pli_tr_carries_the_7_byte_location(self): + # the TR builder injects the location object after the Result; a + # 5-byte object (no Cell ID) is the Network Rejection form, not a PLI + import pysim_simple_server.server as srv + saved = srv._PLI_DATA.pop(0x00, None) + try: + tr = srv._build_tr(object(), 1, 0x26, 0x81, 0x82, 0x00) + finally: + if saved is not None: + srv._PLI_DATA[0x00] = saved + self.assertTrue(tr.startswith(bytes.fromhex('810301260002028281030100'))) + self.assertTrue(tr.endswith(bytes.fromhex('130700F11000010001'))) + def test_decode_cmd_empty_raw(self): self.assertEqual(_decode_cmd(0x26, b'', None), []) self.assertEqual(_decode_cmd(0x03, None, None), []) diff --git a/tests/test_poll.py b/tests/test_poll.py index d3c3d46..5729092 100644 --- a/tests/test_poll.py +++ b/tests/test_poll.py @@ -201,6 +201,20 @@ class TestProactiveChainOwnership(unittest.TestCase): S._handle_proactive_chain(scc, '910b', max_rounds=3) self.assertEqual(len(self._fetches(scc)), 3) + def test_every_chain_call_is_bounded_by_default(self): + # v3.37.0: the bound is the default, not only the background poll's + # choice - a chatty card must not wedge any call site (the manual + # STATUS poll and the ENVELOPE chain used to loop forever) + scc = FakeChattyScc() + S._handle_proactive_chain(scc, '910b') + self.assertEqual(len(self._fetches(scc)), S._POLL_CHAIN_MAX) + + def test_the_manual_status_poll_passes_the_bound(self): + import inspect + src = inspect.getsource(S.PysimHandler._do_POST) + branch = src.split("'/api/status-poll'", 1)[1].split('elif self.path', 1)[0] + self.assertIn('max_rounds=_POLL_CHAIN_MAX', branch) + def test_the_chain_stops_when_a_run_owns_the_card(self): scc = FakeChattyScc() S._TEST_RUNNING = True