diff --git a/frontend/index.html b/frontend/index.html index 5a3c464..760204a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1665,7 +1665,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.6.9'; +const SIMPLE_VERSION = '3.6.10'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -8721,11 +8721,9 @@ async function ramExplore(sp) { async function paginate(p1, collector, parser, label) { // Compact listing format (GP 11.4.2.2: P2.b2=0) with the chained GET // RESPONSE - see ramGetStatusApdu(). The card's SCP80 layer runs both - // commands, so the PoR carries the listing. ELF queries (P1=20/10) use - // SPI2=0x21 (PoR via SMS-SUBMIT) because the listing won't fit in the - // ENVELOPE response. - const isElf = (p1 === '20' || p1 === '10'); - const spi2 = isElf ? '21' : '01'; + // commands, so the PoR carries the listing; ramListingSpi2() picks the + // PoR transport per query. + const spi2 = ramListingSpi2(p1); let p2 = '00'; let guard = 0; while (guard++ < 32) { @@ -8736,13 +8734,19 @@ async function ramExplore(sp) { // page itself is incomplete: the card consumed it, and a retry with // the same counter is rejected (cntr_low). if (res.success && spPorAccepted(res.por)) cntr = ramIncrementCntr(cntr); - if (!res.success || !res.por || res.por.response_status !== 'por_ok') { + if (!res.success || !res.por || !spPorAccepted(res.por)) { const errorMsg = res.por ? res.por.response_status : (res.error || t('no data')); errors.push(label + ': ' + errorMsg); break; } const data = res.por.decoded ? res.por.decoded.last_response_data : ''; const sw = (res.por.decoded ? res.por.decoded.last_status_word : '').toUpperCase(); + if (!data && res.por.response_status === 'actual_response_sms_submit') { + // Accepted, but the actual-response SMS was not captured: there + // is nothing to parse for this page (not a card error). + errors.push(label + ': ' + t('response via SMS not captured')); + break; + } // Defensive: a remote 61XX means the data is still pending (the // chained GET RESPONSE normally prevents this). if (sw && sw.startsWith('61')) { @@ -8858,7 +8862,7 @@ async function ramDeleteFromExplorer(aid, withCascade) { const resultEl = document.getElementById('ram-result'); const stepsEl = document.getElementById('ram-steps'); const sw = res.por && res.por.decoded ? res.por.decoded.last_status_word : ''; - if (!res.success || !res.por || res.por.response_status !== 'por_ok' || + if (!res.success || !res.por || !spPorAccepted(res.por) || (sw && !ramRemoteSwOk(sw))) { resultEl.textContent = t('Failed') + ': ' + (res.por ? res.por.response_status : (res.error || res.sw)) + @@ -8867,7 +8871,7 @@ async function ramDeleteFromExplorer(aid, withCascade) { return; } stepsEl.classList.remove('hidden'); - stepsEl.textContent = label + ' ' + aid + ' -> ' + sw; + stepsEl.textContent = label + ' ' + aid + (sw ? ' -> ' + sw : ''); resultEl.textContent = t('OK'); resultEl.classList.remove('hidden', 'text-red-600'); resultEl.classList.add('text-green-600'); // Continue from the counter the delete consumed: replaying the old one @@ -9502,6 +9506,16 @@ function ramGetStatusApdu(p1, p2) { return '80F2' + p1 + p2 + '024F0000' + 'C0000000'; } +// The ISD (80) and memory responses fit the ENVELOPE; the Apps (40) and ELF +// (20/10) listings can exceed it, and the card then delivers the actual +// response as an SMS-SUBMIT - those queries must request the PoR in submit +// mode (SPI2=0x21). With SPI2=0x01 the card answers the envelope PoR with +// actual_response_sms_submit and the data never arrives (live 2026-09-28: +// "Partial - Apps: actual_response_sms_submit"). +function ramListingSpi2(p1) { + return (p1 === '40' || p1 === '20' || p1 === '10') ? '21' : '01'; +} + function downloadJson(name, obj) { const blob = new Blob([JSON.stringify(obj, null, 2)], {type: 'application/json'}); const a = document.createElement('a'); @@ -16447,6 +16461,7 @@ const LANG_RU = { 'Memory': 'Память', 'no data': 'нет данных', '(no data)': '(нет данных)', + 'response via SMS not captured': 'ответ по SMS не перехвачен', 'free NV': 'свободно NV', 'Failed': 'Ошибка', 'cascade': 'каскадно', diff --git a/frontend/sw.js b/frontend/sw.js index c524c83..89819fa 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v282'; +const CACHE = 'simple-v283'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js index 73f1d96..820fe13 100644 --- a/frontend/tests/ram.test.js +++ b/frontend/tests/ram.test.js @@ -23,7 +23,7 @@ function extractFunc(src, name) { // Extract chain builder functions and dependencies const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu', - 'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', + 'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', '_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry', 'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml']; @@ -440,6 +440,14 @@ test('ramRemoteSwOk mirrors the server success set', () => { } }); +test('ramListingSpi2 requests the SMS-submit PoR for the listing queries', () => { + // Apps (40) and ELF (20/10) listings can exceed the ENVELOPE response; + // with SPI2=0x01 the card answers actual_response_sms_submit and the data + // never arrives (the live "Partial - Apps" bug). + for (const p1 of ['40', '20', '10']) assert.strictEqual(ramListingSpi2(p1), '21', p1); + for (const p1 of ['80', '00', 'FF']) assert.strictEqual(ramListingSpi2(p1), '01', p1); +}); + function stubDeleteEnv(sendResult) { // ramShowProgress/ramHideProgress are the real extracted helpers const els = fakeRamDocument(['ram-result', 'ram-steps', 'ram-progress', 'ram-progress-text']); diff --git a/frontend/tests/ram_counter_flow.test.js b/frontend/tests/ram_counter_flow.test.js index c4e806f..2153b47 100644 --- a/frontend/tests/ram_counter_flow.test.js +++ b/frontend/tests/ram_counter_flow.test.js @@ -98,6 +98,17 @@ test('a refused DELETE still advances the counter but does not re-explore', asyn assert.strictEqual(calls.explored, null); }); +test('a delete accepted via actual_response_sms_submit advances and re-explores', async () => { + // The card consumed the packet and will deliver the remote result as an + // SMS-SUBMIT; the counter must advance and the re-explore must run (the + // remote SW is unknown, so no SW is shown). + const { els, calls } = fakeEnv({ response_status: 'actual_response_sms_submit' }); + await ramDeleteFromExplorer('F0414C46416101', false); + assert.strictEqual(cards[0].cntr, '0000000006'); + assert.strictEqual(calls.explored, '0000000006'); + assert.strictEqual(els['ram-result'].textContent, 'OK'); +}); + test('a send failure leaves the preset untouched', async () => { const { calls } = fakeEnv({ response_status: 'por_ok' }, false); await ramDeleteFromExplorer('F0414C46416101', false); diff --git a/pyproject.toml b/pyproject.toml index a63a439..ed757bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.6.9" +version = "3.6.10" 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 ac0a7ec..3729fd2 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.6.9' +VERSION = '3.6.10' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE