diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 4217da0..2eeafa9 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -507,11 +507,11 @@
  • События звонка — MT call (transaction identifier, номер вызывающего + TON → Address, subaddress, IMS URI, media type; источник network), Call connected и Call disconnected (transaction identifier, источник near/far, media type и причина — отсутствует, нулевая форма radio link timeout или свои байты), а также необязательная причина IMS-разрыва (SIP / Q.850 + cause)
  • Browser termination (причина user / error), (I-)WLAN access status (нет покрытия / покрытие / соединение) и CSG cell selection (статус + механизм выбора, access technology, CSG ID, HNB name, PLMN ID; источник network)
  • Channel status (канал + состояние звена + инфо, тег B8, с необязательными bearer description B5 и local address BE) и Data available (тот же channel status + длина данных канала, тег B7), Display parameters changed (строки/столбцы + флаги sizing/fonts/effects, тег C0), Network search mode change (manual/automatic, тег E5), Browsing status (код ошибки браузера, тег E4) и Frames information change (фрейм по умолчанию + инфо по фреймам, тег E7)
  • -
  • IMS registration (IMPU URI + IMS status code) и Incoming IMS data (IARI) — источник network; CAG cell selection (статус + механизм, access technology, CAG information list + HRNN list) и Slices status change (allowed / rejected / served / partially, access technology, списки allowed / served / rejected S-NSSAI, с маппингом и без) — источник network
  • +
  • IMS registration (IMPU URI + IMS status code) и Incoming IMS data (IARI) — источник network; CAG cell selection (статус + механизм, access technology, CAG information list + HRNN list) и Slices status change (allowed / rejected / served / partially, access technology, списки allowed / served / rejected S-NSSAI, с маппингом и без, partial NSSAI) — источник network
  • Data Connection Status Change (0x1D) — статус (successful / rejected / dropped-deactivated), тип соединения (PDP / PDN / PDU), необязательная причина (E/5G)SM, transaction identifier и location status, а также необязательные date-time, access technology, APN, тип PDP/PDN/PDU и location information (MCC/MNC + LAC + Cell ID) — TS 131 111 §7.5.25
  • Poll Interval Negotiation (0x1C) — предлагает интервал опроса в виде Duration (единица + значение); после отправки показывается ответ UICC (принято / отклонено / изменено + длительность), а значение изменено становится интервалом фонового опроса (TS 102 223 §7.5.22). Форма появляется, когда карта подписана на это событие.
  • -

    Отправка события использует ENVELOPE(Event Download) по TS 102 223 / TS 131 111. Форма скрывает параметры, появившиеся после действующего релиза CAT — релиза пресета подключённой карты (совпадение по ICCID) или Rel-12, если он не указан, — а примечание под формой называет источник и то, что было скрыто (релиз задаётся на вкладке «Карты»).

    +

    Отправка события использует ENVELOPE(Event Download) по TS 102 223 / TS 131 111. Форма скрывает параметры, появившиеся после действующего релиза CAT — релиза пресета подключённой карты (совпадение по ICCID) или Rel-12, если он не указан, — а примечание под формой называет источник и то, что было скрыто (релиз задаётся на вкладке «Карты»). Информация о срезах отправляется в одном ENVELOPE (не более 252 байт внутренних данных) — доставка цепочкой из нескольких ENVELOPE не реализована, поэтому данные должны помещаться в один конверт; конверт с partial NSSAI автоматически помечается как Last Envelope.

    8.3 Журнал проактивных команд

    Хронологический список извлечённых проактивных команд. Каждая строка показывает время, код типа, имя и декодированный квалификатор (для команд, у которых он есть). Раскрытие строки показывает декодированную команду: текст и пункты DISPLAY TEXT / SELECT ITEM / SET UP MENU, параметры BIP-каналов, действия TIMER MANAGEMENT, названия квалификаторов PROVIDE LOCAL INFORMATION (все стандартные, включая ESN 07, MEID 0B и Supported RATs 1A) и SEND SHORT MESSAGE — разбирается SMS TPDU (тип, TP-MR, TP-DA, TP-PID, TP-DCS, срок действия, TP-UDL), а пользовательские данные показываются как текст для текстовых кодировок (GSM 7-бит, UCS2, 8-бит) или как secured-пакет (TS 31.115) при TP-PID = 7F (SIM data download); заголовки конкатенации UDH декодируются. Для команд с данными ответа показывается строка Ответ: с байтами TERMINAL RESPONSE (без служебных TLV); ответы PROVIDE LOCAL INFORMATION декодируются через словарь данных PLI.

    diff --git a/frontend/help.html b/frontend/help.html index 987afb6..eb3591d 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -506,11 +506,11 @@
  • Call events — MT call (transaction identifier, calling party number + TON → Address, subaddress, IMS URI, media type; source network), Call connected and Call disconnected (transaction identifier, near/far-end source, media type, and a cause — absent, the zero-length radio-link-timeout form, or custom bytes), plus the optional IMS disconnection cause (SIP / Q.850 + cause)
  • Browser termination (user / error cause), (I-)WLAN access status (no coverage / coverage / connection on-going) and CSG cell selection (status + selection mechanism, access technology, CSG ID, HNB name, PLMN ID; source network)
  • Channel status (channel + link state + channel info, tag B8, with an optional bearer description B5 and local address BE) and Data available (the same channel status plus the channel data length, tag B7), Display parameters changed (rows/columns + sizing/fonts/effects flags, tag C0), Network search mode change (manual/automatic, tag E5), Browsing status (browser error code, tag E4) and Frames information change (default frame + per-frame info, tag E7)
  • -
  • IMS registration (IMPU URIs + IMS status code) and Incoming IMS data (IARI) — source network; CAG cell selection (status + mechanism, access technology, CAG information list + HRNN list) and Slices status change (allowed / rejected / served / partially status, access technology, allowed / served / rejected S-NSSAI lists, with and without mapping) — source network
  • +
  • IMS registration (IMPU URIs + IMS status code) and Incoming IMS data (IARI) — source network; CAG cell selection (status + mechanism, access technology, CAG information list + HRNN list) and Slices status change (allowed / rejected / served / partially status, access technology, allowed / served / rejected S-NSSAI lists, with and without mapping, partial NSSAI) — source network
  • Data Connection Status Change (0x1D) — status (successful / rejected / dropped-deactivated), connection type (PDP / PDN / PDU), optional (E/5G)SM cause, transaction identifier and location status, plus optional date-time, access technology, APN, PDP/PDN/PDU type and location information (MCC/MNC + LAC + Cell ID) — TS 131 111 §7.5.25
  • Poll Interval Negotiation (0x1C) — proposes a poll interval as a Duration (unit + value); after sending, the UICC's answer is shown (accepted / rejected / modified + duration), and a modified duration becomes the background poll interval (TS 102 223 §7.5.22). The form appears when the card subscribed to this event.
  • -

    Sending an event uses ENVELOPE(Event Download) per TS 102 223 / TS 131 111. The form hides the parameters introduced after the effective CAT release — the equipped card preset's CAT release (matched by ICCID), or Rel-12 when unspecified — and a note under the form names the source and what was hidden (the release is set in the Cards tab).

    +

    Sending an event uses ENVELOPE(Event Download) per TS 102 223 / TS 131 111. The form hides the parameters introduced after the effective CAT release — the equipped card preset's CAT release (matched by ICCID), or Rel-12 when unspecified — and a note under the form names the source and what was hidden (the release is set in the Cards tab). Slice information is sent in a single ENVELOPE (max 252 bytes of inner data) — chained (multi-envelope) delivery is not implemented, so the data must fit one envelope; an envelope carrying the partial NSSAI is marked as the Last Envelope automatically.

    8.3 Proactive command log

    Chronological list of fetched proactive commands. Each row shows the elapsed time, type code, name, and a decoded qualifier (for commands that have one). Expanding a row shows the decoded command: DISPLAY TEXT / SELECT ITEM / SET UP MENU text and items, BIP channel parameters, TIMER MANAGEMENT actions, PROVIDE LOCAL INFORMATION qualifier names (all standard qualifiers, including ESN 07, MEID 0B and Supported RATs 1A), and SEND SHORT MESSAGE — the SMS TPDU is parsed (type, TP-MR, TP-DA, TP-PID, TP-DCS, validity period, TP-UDL) with the user data shown as text for text codings (GSM 7-bit, UCS2, 8-bit) or as a secured packet (TS 31.115) when TP-PID = 7F (SIM data download); UDH concatenation headers are decoded. Commands with response data show a Response: line with the TERMINAL RESPONSE bytes (boilerplate TLVs stripped); PROVIDE LOCAL INFORMATION responses are decoded using the PLI data dictionary decoders.

    diff --git a/frontend/index.html b/frontend/index.html index 3df769e..c604979 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.13.4'; +const SIMPLE_VERSION = '3.14.0'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -9136,10 +9136,11 @@ const CAT_RELEASE = { '56/D6 CSG ID': 9, '57/D7 HNB name': 9, '31/B1 IMS URI': 10, - '77/F7 IARI': 10, + '76/F6 IARI': 10, '77/F7 IMPU list': 11, '78/F8 IMS status code': 11, '7E/FE Media Type': 12, + '73/F3 URI truncated': 12, '09/89 PLMN ID': 4, '3F/BF Access technology': 4, 'B4 Supported Radio Access Technologies': 13, @@ -9159,6 +9160,8 @@ const CAT_RELEASE = { '77/F7 Allowed slices with S-NSSAI mapping': 18, 'D7 Rejected slices with S-NSSAI mapping': 18, 'B1 Rejected slices information': 18, + '79/F9 Partial NSSAI': 18, + '70/F0 Last Envelope': 18, }, pli: { '00': 4, '01': 4, '02': 4, '03': 4, '04': 4, '06': 4, '07': 4, '08': 6, @@ -13897,8 +13900,12 @@ const EVENT_FORMS = { d += '88' + (h.length / 2).toString(16).padStart(2, '0') + h; } if (v.ims_uri) { - const b = new TextEncoder().encode(v.ims_uri).slice(0, 100); + let b = new TextEncoder().encode(v.ims_uri); + const truncated = b.length > 100; + b = b.slice(0, 100); d += 'B1' + b.length.toString(16).padStart(2, '0') + bytesToHex(Array.from(b)); + // 8.135: the object is included when the URI was truncated + if (truncated) d += 'F300'; } if (v.media !== '' && v.media !== undefined) d += 'FE01' + ((parseInt(v.media) || 0) & 0xFF).toString(16).padStart(2, '0'); @@ -13973,13 +13980,13 @@ const EVENT_FORMS = { { id: 'status', label: 'Status', type: 'select', opts: [{v:0,l:'Card reader removed'},{v:1,l:'Card reader inserted'}] } ], - build(v) { return '' + (v.status|0).toString(16).padStart(2,'0'); } + build(v) { return 'A001' + ((parseInt(v.status) || 0) & 0xFF).toString(16).padStart(2,'0'); } }, 0x07: { title: 'Language selection', fields: [ { id: 'lang', label: 'Language code', type: 'text', placeholder: 'ISO 639, e.g. en, ru', pattern: '[a-zA-Z]{2,3}' } ], - build(v) { const b = new TextEncoder().encode(v.lang||'en'); return b.length.toString(16).padStart(2,'0') + bytesToHex(Array.from(b)); } + build(v) { const b = new TextEncoder().encode(v.lang||'en'); return 'AD' + b.length.toString(16).padStart(2,'0') + bytesToHex(Array.from(b)); } }, 0x08: { title: 'Browser termination', fields: [ @@ -14240,7 +14247,7 @@ const EVENT_FORMS = { build(v) { if (!v.iari) return ''; const b = new TextEncoder().encode(v.iari).slice(0, 100); - return ('F7' + b.length.toString(16).padStart(2, '0') + bytesToHex(Array.from(b))).toUpperCase(); + return ('F6' + b.length.toString(16).padStart(2, '0') + bytesToHex(Array.from(b))).toUpperCase(); } }, 0x1C: { title: 'Poll interval negotiation', @@ -14357,6 +14364,7 @@ const EVENT_FORMS = { { id: 'allowed', label: 'Allowed slices (optional, hex)', type: 'text', pattern: '[0-9A-Fa-f]{8,80}', placeholder: '4 bytes per S-NSSAI (SST + SD)' }, { id: 'rejected_map', label: 'Rejected slices with S-NSSAI mapping (optional, hex)', type: 'text', pattern: '[0-9A-Fa-f]{2,80}', placeholder: 'TS 24.501 elements + rejection cause', rel: 18 }, { id: 'rejected', label: 'Rejected slices (optional, hex)', type: 'text', pattern: '[0-9A-Fa-f]{2,80}', placeholder: 'TS 24.501 rejected S-NSSAI elements', rel: 18 }, + { id: 'partial', label: 'Partial NSSAI (optional, hex)', type: 'text', pattern: '[0-9A-Fa-f]{2,80}', placeholder: 'TS 24.501 partial NSSAI elements', rel: 18 }, ], build(v) { let d = ''; @@ -14377,6 +14385,11 @@ const EVENT_FORMS = { list(v.allowed, 'F8'); list(v.rejected_map, 'D7'); list(v.rejected, 'B1'); + list(v.partial, 'F9'); + // 7.5.27.2: a single envelope carrying the partial NSSAI is the + // last one - the chaining marker goes with it (multi-envelope + // delivery is not implemented; the data must fit one envelope) + if ((v.partial || '').replace(/[^0-9a-fA-F]/g, '')) d += 'F000'; return d.toUpperCase(); } }, @@ -14799,6 +14812,24 @@ function eventFieldsForRelease(cfg, rel) { return { fields: fields, hidden: hidden }; } +// The single-ENVELOPE budget: the inner data (event list + device identities + +// the event objects) is wrapped as D6 81 + inner, and the APDU Lc must +// stay within one byte (255) - so the inner data is capped at 252 bytes +// (8 header bytes + at most 244 bytes of event data). Chained +// (multi-envelope) delivery is not implemented: the slice/NSSAI data must fit +// one envelope (see the help). +const EVENT_INNER_MAX = 252; + +// The ENVELOPE inner size of an event-data hex string (pure). +function eventEnvelopeSize(eventDataHex) { + return 8 + Math.floor((eventDataHex || '').length / 2); +} + +// Whether the event data fits one ENVELOPE (pure). +function eventFitsOneEnvelope(eventDataHex) { + return eventEnvelopeSize(eventDataHex) <= EVENT_INNER_MAX; +} + function pysimEventSendForm(eventType) { const modal = document.getElementById('event-send-modal'); const title = document.getElementById('event-send-title'); @@ -14925,6 +14956,13 @@ async function pysimEventSend() { err.classList.remove('hidden'); return; } + if (eventData && !eventFitsOneEnvelope(eventData)) { + err.textContent = t('The event data does not fit one ENVELOPE') + ' (' + + eventEnvelopeSize(eventData) + ' > ' + EVENT_INNER_MAX + ' B). ' + + t('Multi-envelope (chained) delivery is not implemented - reduce the slice lists.'); + err.classList.remove('hidden'); + return; + } } btn.disabled = true; @@ -20738,6 +20776,8 @@ const LANG_RU = { 'Event sent.': 'Событие отправлено.', 'Event data sent.': 'Данные события отправлены.', 'Sending...': 'Отправка...', + 'The event data does not fit one ENVELOPE': 'Данные события не помещаются в один ENVELOPE', + 'Multi-envelope (chained) delivery is not implemented - reduce the slice lists.': 'Доставка в несколько ENVELOPE (цепочкой) не реализована — сократите списки срезов.', 'Yes': 'Да', 'No': 'Нет', 'Help': 'Справка', diff --git a/frontend/sw.js b/frontend/sw.js index 0e0e25d..8e4241c 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v374'; +const CACHE = 'simple-v375'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/event_forms.test.js b/frontend/tests/event_forms.test.js index f308d19..0b87745 100644 --- a/frontend/tests/event_forms.test.js +++ b/frontend/tests/event_forms.test.js @@ -34,6 +34,7 @@ eval(extractFunc(html, 'cmdQualifierShort')); eval(extractBlock('const EVENT_NAMES = {', 'const REJECTION_CAUSES = [').replace(/^const /gm, 'var ')); eval(extractBlock('const REJECTION_CAUSES = [', 'const EVENT_FORMS = {').replace(/^const /gm, 'var ')); eval(extractBlock('const EVENT_FORMS = {', 'const PLI_QUALIFIERS = [').replace(/^const /gm, 'var ')); +eval(extractBlock('const EVENT_INNER_MAX = ', 'function pysimEventSendForm').replace(/^const /gm, 'var ')); eval(extractFunc(html, 'eventFieldsForRelease')); eval(extractFunc(html, 'encPlmn')); eval(extractFunc(html, 'bytesToHex')); @@ -172,6 +173,11 @@ test('the call events build their 31.111 object sets', () => { // call disconnected: radio link timeout = zero-length cause, custom = bytes assert.strictEqual(EVENT_FORMS[0x02].build({ ti: '00', cause_mode: 'rlt', media: '' }), '1C01009A00'); assert.strictEqual(EVENT_FORMS[0x02].build({ ti: '00', cause_mode: 'custom', cause: '10', media: '' }), '1C01009A0110'); + // a URI longer than the 100-byte limit is cut and tagged (8.135) + const long = EVENT_FORMS[0x00].build({ ti: '00', ims_uri: 'x'.repeat(120) }); + assert.ok(long.includes('B164' + '78'.repeat(100)), 'the URI is cut to 100 bytes'); + assert.ok(long.endsWith('F300'), long.slice(-8)); + assert.ok(!EVENT_FORMS[0x00].build({ ti: '00', ims_uri: 'sip:a@b' }).includes('F300')); }); test('the browser, WLAN and CSG events build their object sets', () => { @@ -199,6 +205,9 @@ test('the data/display/search/frames events build their object sets', () => { assert.strictEqual(EVENT_FORMS[0x0F].build({ status: '12' }), 'E40112'); assert.strictEqual(EVENT_FORMS[0x0F].build({ status: '' }), 'E40100'); assert.strictEqual(EVENT_FORMS[0x10].build({ frame: '01', list: '0410' }), 'E703010410'); + // Card reader status (8.33) and Language (8.45) carry their object tags + assert.strictEqual(EVENT_FORMS[0x06].build({ status: '1' }), 'A00101'); + assert.strictEqual(EVENT_FORMS[0x07].build({ lang: 'en' }), 'AD02656E'); }); test('the IMS, CAG and slices events build their object sets', () => { @@ -207,7 +216,7 @@ test('the IMS, CAG and slices events build their object sets', () => { assert.strictEqual(EVENT_FORMS[0x17].build({ impu: 'sip:a@b', code: '200' }), 'F70980077369703A614062F803323030'); assert.strictEqual(EVENT_FORMS[0x18].src, '83'); - assert.strictEqual(EVENT_FORMS[0x18].build({ iari: 'a' }), 'F70161'); + assert.strictEqual(EVENT_FORMS[0x18].build({ iari: 'a' }), 'F60161'); // CAG cell selection: tech + status/mechanism + information list assert.strictEqual(EVENT_FORMS[0x1E].src, '83'); assert.strictEqual(EVENT_FORMS[0x1E].build({ status: '1', mech: 'manual', tech: '8', list: '00000001' }), @@ -216,6 +225,9 @@ test('the IMS, CAG and slices events build their object sets', () => { assert.strictEqual(EVENT_FORMS[0x1F].src, '83'); assert.strictEqual(EVENT_FORMS[0x1F].build({ status: '1', tech: '8', served: '01020304' }), 'BF0108D50101D6050101020304'); + // partial NSSAI: the F9 object + the automatic Last Envelope marker + assert.strictEqual(EVENT_FORMS[0x1F].build({ status: '1', partial: '0102' }), 'D50101F9020102F000'); + assert.ok(!EVENT_FORMS[0x1F].build({ status: '1', served: '01020304' }).includes('F000')); }); test('the location-status event can carry the location information', () => { @@ -265,10 +277,22 @@ test('event fields introduced after the effective release are hidden', () => { assert.deepStrictEqual(r.hidden.map(f => f.id).sort(), ['code', 'impu']); // slices: the mapping/rejected objects came in Rel-18 r = eventFieldsForRelease(EVENT_FORMS[0x1F], 16); - assert.deepStrictEqual(r.hidden.map(f => f.id).sort(), ['allowed_map', 'rejected', 'rejected_map']); + assert.deepStrictEqual(r.hidden.map(f => f.id).sort(), ['allowed_map', 'partial', 'rejected', 'rejected_map']); assert.strictEqual(eventFieldsForRelease(EVENT_FORMS[0x1F], 18).hidden.length, 0); // a field without a known release is never hidden r = eventFieldsForRelease(EVENT_FORMS[0x12], 4); assert.strictEqual(r.hidden.length, 0); assert.strictEqual(r.fields.length, EVENT_FORMS[0x12].fields.length); }); + +test('the single-envelope budget is enforced by the size helper', () => { + // inner = 8 header bytes + the event data; D6 81 + inner must stay + // within the 1-byte APDU Lc, so the inner data is capped at 252 bytes + assert.strictEqual(EVENT_INNER_MAX, 252); + assert.strictEqual(eventEnvelopeSize(''), 8); + assert.strictEqual(eventEnvelopeSize('AABB'), 10); + assert.strictEqual(eventEnvelopeSize('AB'.repeat(244)), 252); + assert.ok(eventFitsOneEnvelope('AB'.repeat(244))); + assert.ok(!eventFitsOneEnvelope('AB'.repeat(245))); + assert.ok(eventFitsOneEnvelope(null)); +}); diff --git a/pyproject.toml b/pyproject.toml index f84dc67..6ba132d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.13.4" +version = "3.14.0" 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/catrelease.py b/pysim_simple_server/catrelease.py index 587f1d9..478a9f1 100644 --- a/pysim_simple_server/catrelease.py +++ b/pysim_simple_server/catrelease.py @@ -73,10 +73,11 @@ CAT_RELEASE = { '56/D6 CSG ID': 9, '57/D7 HNB name': 9, '31/B1 IMS URI': 10, - '77/F7 IARI': 10, + '76/F6 IARI': 10, '77/F7 IMPU list': 11, '78/F8 IMS status code': 11, '7E/FE Media Type': 12, + '73/F3 URI truncated': 12, '09/89 PLMN ID': 4, '3F/BF Access technology': 4, 'B4 Supported Radio Access Technologies': 13, @@ -96,6 +97,8 @@ CAT_RELEASE = { '77/F7 Allowed slices with S-NSSAI mapping': 18, 'D7 Rejected slices with S-NSSAI mapping': 18, 'B1 Rejected slices information': 18, + '79/F9 Partial NSSAI': 18, + '70/F0 Last Envelope': 18, }, 'pli': { '00': 4, '01': 4, '02': 4, '03': 4, '04': 4, '06': 4, '07': 4, '08': 6, diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index ac08996..076abc1 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -32,7 +32,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.13.4' +VERSION = '3.14.0' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE @@ -4595,11 +4595,24 @@ def _verify_adm(scc, app, adm_hex): 'error': 'Security status not satisfied' if sw == '6982' else 'Error'} +# The single-ENVELOPE budget for event data: the inner data (event list + +# device identities + the event objects) is wrapped as D6 81 + inner, +# and the APDU Lc must stay within one byte (255), so the inner data is +# capped at 252 bytes (244 bytes of event data + the 8-byte header). +# Chained (multi-envelope) delivery is not implemented - the slice/S-NSSAI +# data must fit one envelope (see the help). +_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): """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.""" + 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) src_byte = 0x82 if src: try: @@ -7205,6 +7218,10 @@ class PysimHandler(BaseHTTPRequestHandler): resp['net_state'] = self.server.net_state self._send_json(resp) self._log_resp(resp) + except ValueError as e: + # the single-envelope budget (no chained delivery) + self._send_json({'error': str(e)}, 400) + self._log_resp({'error': str(e)}) except Exception as e: sys.stderr.write('Event send error: %s\n' % e) _handle_card_disconnect(stale=_is_transport_fatal(e)) diff --git a/tests/test_ota_helpers.py b/tests/test_ota_helpers.py index b35f987..ac75da5 100644 --- a/tests/test_ota_helpers.py +++ b/tests/test_ota_helpers.py @@ -934,6 +934,20 @@ class TestEventDownload(unittest.TestCase): d6 = bytes([0xD6, 0x81, len(inner)]) + inner self.assertEqual(apdu, '80c20000%02x%s' % (len(d6), d6.hex())) + def test_event_data_beyond_one_envelope_is_refused(self): + # the tool sends one envelope per event: the inner data is capped at + # 252 bytes (the D6 long-form length and the APDU Lc stay in one byte); + # chained (multi-envelope) delivery is not implemented + with self.assertRaises(ValueError) as ctx: + self._send(0x1F, bytes(245)) + self.assertIn('one ENVELOPE', str(ctx.exception)) + # 244 bytes of event data still fit (inner = 252, the maximum) + apdu, sw = self._send(0x1F, bytes(244)) + self.assertEqual(sw, '9000') + inner = bytes([0x99, 0x01, 0x1F, 0x82, 0x02, 0x82, 0x81]) + bytes(244) + d6 = bytes([0xD6, 0x81, len(inner)]) + inner + self.assertEqual(apdu, '80c20000%02x%s' % (len(d6), d6.hex())) + class TestTimerManagement(unittest.TestCase): """Terminal side of TIMER MANAGEMENT (TS 102 223 6.6.21, 6.8.13/14, 7.4).