Compare commits

...

10 Commits

3 changed files with 117 additions and 54 deletions
+99 -48
View File
@@ -16,7 +16,7 @@
<div class="max-w-7xl mx-auto px-6 py-4"> <div class="max-w-7xl mx-auto px-6 py-4">
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.4.1</span></h1> <h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.4.2</span></h1>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<button id="install-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" style="display:none">INSTALL PWA [for offline use]</button> <button id="install-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" style="display:none">INSTALL PWA [for offline use]</button>
<a href="https://github.com/anttro/otaman" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a> <a href="https://github.com/anttro/otaman" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a>
@@ -34,50 +34,8 @@
</div> </div>
<div id="tab-c-apdu" class="tab-content"> <div id="tab-c-apdu" class="tab-content">
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800 mb-3"> <div class="flex gap-4">
<div class="flex gap-3 items-end">
<div class="flex-1"> <div class="flex-1">
<div class="mb-2">
<select id="conv-type" onchange="updateConvFields()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">
<option value="nibswap">Nibble swap</option>
<option value="imsi">IMSI → EF.IMSI</option>
<option value="msisdn">MSISDN → BCD</option>
<option value="iccid">ICCID → hex</option>
<option value="spn">Provider Name → SPN</option>
<option value="plmn">MCC,MNC → PLMNsel</option>
<option value="plmnwact">MCC,MNC,AcT → PLMNwAcT</option>
</select>
</div>
<div class="flex gap-2 mb-2">
<input id="conv-input" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">
<button onclick="genConvert()" class="px-3 py-1.5 bg-blue-600 text-white text-xs font-medium rounded hover:bg-blue-700" data-l10n="Convert">Convert</button>
</div>
<div id="conv-plmn-fields" class="" style="display:none">
<div class="flex gap-2 mb-2">
<div class="flex-1">
<input id="conv-mcc" maxlength="3" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" placeholder="MCC">
</div>
<div class="flex-1">
<input id="conv-mnc" maxlength="3" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" placeholder="MNC">
</div>
</div>
<div id="conv-act-row" class="" style="display:none">
<select id="conv-act" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">
<option value="8000">GSM (8000)</option>
<option value="4000">UTRAN (4000)</option>
<option value="2000">E-UTRAN (2000)</option>
<option value="1000">NGRAN (1000)</option>
<option value="C000">GSM + UTRAN (C000)</option>
<option value="6000">UTRAN + E-UTRAN (6000)</option>
<option value="E000">GSM + UTRAN + E-UTRAN (E000)</option>
<option value="F000">All (F000)</option>
</select>
</div>
</div>
</div>
<textarea id="conv-result" rows="2" readonly class="flex-1 font-mono border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 bg-white dark:bg-slate-800"></textarea>
</div>
</div>
<div class="flex justify-center gap-2 mb-3"> <div class="flex justify-center gap-2 mb-3">
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-blue-600 text-white" data-sub="sim" onclick="cApduSwitchSubtab('sim')">SIM RFM</button> <button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-blue-600 text-white" data-sub="sim" onclick="cApduSwitchSubtab('sim')">SIM RFM</button>
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="usim" onclick="cApduSwitchSubtab('usim')">USIM RFM</button> <button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="usim" onclick="cApduSwitchSubtab('usim')">USIM RFM</button>
@@ -566,6 +524,48 @@
<button onclick="packToSp('ram-result')" id="ram-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Pack into Secured packet">Pack into Secured packet</button> <button onclick="packToSp('ram-result')" id="ram-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Pack into Secured packet">Pack into Secured packet</button>
</div> </div>
</div> </div>
<div class="w-1/4">
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800">
<select id="conv-type" onchange="updateConvFields()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 mb-2">
<option value="nibswap">Nibble swap</option>
<option value="imsi">IMSI → EF.IMSI</option>
<option value="msisdn">MSISDN → BCD</option>
<option value="iccid">ICCID → hex</option>
<option value="spn">Provider Name → SPN</option>
<option value="plmn">MCC,MNC → PLMNsel</option>
<option value="plmnwact">MCC,MNC,AcT → PLMNwAcT</option>
</select>
<div class="flex flex-col gap-2 mb-2">
<input id="conv-input" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">
<button onclick="genConvert()" class="px-3 py-1.5 bg-blue-600 text-white text-xs font-medium rounded hover:bg-blue-700" data-l10n="Convert">Convert</button>
</div>
<div id="conv-plmn-fields" class="" style="display:none">
<div class="flex gap-2 mb-2">
<div class="flex-1">
<input id="conv-mcc" maxlength="3" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" placeholder="MCC">
</div>
<div class="flex-1">
<input id="conv-mnc" maxlength="3" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" placeholder="MNC">
</div>
</div>
<div id="conv-act-row" class="" style="display:none">
<select id="conv-act" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">
<option value="8000">GSM (8000)</option>
<option value="4000">UTRAN (4000)</option>
<option value="2000">E-UTRAN (2000)</option>
<option value="1000">NGRAN (1000)</option>
<option value="C000">GSM + UTRAN (C000)</option>
<option value="6000">UTRAN + E-UTRAN (6000)</option>
<option value="E000">GSM + UTRAN + E-UTRAN (E000)</option>
<option value="F000">All (F000)</option>
</select>
</div>
</div>
<textarea id="conv-result" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 mt-2 bg-white dark:bg-slate-800"></textarea>
</div>
</div>
</div>
</div>
<div id="tab-sp" class="tab-content hidden"> <div id="tab-sp" class="tab-content hidden">
<div class="mb-3"> <div class="mb-3">
@@ -863,6 +863,7 @@
<button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="custom" data-l10n="Custom files">Custom files</button> <button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="custom" data-l10n="Custom files">Custom files</button>
<button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="cmd" data-l10n="pySim command line">pySim command line</button> <button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="cmd" data-l10n="pySim command line">pySim command line</button>
<button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="apdu" data-l10n="Raw APDU">Raw APDU</button> <button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="apdu" data-l10n="Raw APDU">Raw APDU</button>
<button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="proactive">Proactive UICC</button>
<button id="stk-menu-btn" onclick="stkMenuOpen()" class="pysim-subtab px-3 py-1 text-xs rounded-full bg-emerald-600 text-white hover:bg-emerald-700" style="display:none">STK: <span id="stk-menu-title"></span></button> <button id="stk-menu-btn" onclick="stkMenuOpen()" class="pysim-subtab px-3 py-1 text-xs rounded-full bg-emerald-600 text-white hover:bg-emerald-700" style="display:none">STK: <span id="stk-menu-title"></span></button>
</div> </div>
@@ -924,6 +925,10 @@
</div> </div>
<textarea id="pysim-apdu-output" rows="5" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 bg-gray-100 dark:bg-slate-800"></textarea> <textarea id="pysim-apdu-output" rows="5" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 bg-gray-100 dark:bg-slate-800"></textarea>
</div> </div>
<div id="pysim-sub-proactive" class="hidden">
<div class="mb-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="Events that the card monitors (SET UP EVENT LIST):">Events that the card monitors (SET UP EVENT LIST):</div>
<div id="pysim-event-list" class="text-xs font-mono text-gray-600 dark:text-slate-400"></div>
</div>
</div> </div>
<div id="stk-menu-panel" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden"> <div id="stk-menu-panel" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden">
@@ -935,9 +940,9 @@
</div> </div>
<div id="stk-menu-panel-content" class="flex-1 overflow-auto"></div> <div id="stk-menu-panel-content" class="flex-1 overflow-auto"></div>
<div id="stk-menu-buttons" class="flex justify-between mt-3 pt-3 border-t border-gray-200 dark:border-slate-700 hidden"> <div id="stk-menu-buttons" class="flex justify-between mt-3 pt-3 border-t border-gray-200 dark:border-slate-700 hidden">
<button onclick="stkMenuRespond('timeout')" class="px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-l10n="Timeout">Timeout</button> <button id="stk-timeout-btn" onclick="stkMenuRespond('timeout')" class="px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-l10n="Timeout">Timeout</button>
<button onclick="stkMenuRespond('cancel')" class="px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-l10n="Cancel">Cancel</button> <button id="stk-cancel-btn" onclick="stkMenuRespond('cancel')" class="px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-l10n="Cancel">Cancel</button>
<button onclick="stkMenuRespond('ok')" class="px-4 py-1 text-xs rounded-full bg-blue-600 text-white hover:bg-blue-700" data-l10n="OK">OK</button> <button id="stk-ok-btn" onclick="stkMenuRespond('ok')" class="px-4 py-1 text-xs rounded-full bg-blue-600 text-white hover:bg-blue-700" data-l10n="OK">OK</button>
</div> </div>
</div> </div>
</div> </div>
@@ -2854,6 +2859,7 @@ function stkMenuHandleResponse(data) {
if (data.type === 'display_text') { if (data.type === 'display_text') {
content.innerHTML = '<div class="text-sm whitespace-pre-wrap p-2 bg-gray-50 dark:bg-slate-700 rounded">' + esc(data.text) + '</div>'; content.innerHTML = '<div class="text-sm whitespace-pre-wrap p-2 bg-gray-50 dark:bg-slate-700 rounded">' + esc(data.text) + '</div>';
backBtn.style.display = ''; backBtn.style.display = '';
document.getElementById('stk-ok-btn').style.display = '';
btns.classList.remove('hidden'); btns.classList.remove('hidden');
} else if (data.type === 'select_item') { } else if (data.type === 'select_item') {
stkMenuStack.push(data.items); stkMenuStack.push(data.items);
@@ -2866,6 +2872,7 @@ function stkMenuHandleResponse(data) {
html += '</div>'; html += '</div>';
content.innerHTML = html; content.innerHTML = html;
backBtn.style.display = ''; backBtn.style.display = '';
document.getElementById('stk-ok-btn').style.display = 'none';
btns.classList.remove('hidden'); btns.classList.remove('hidden');
} else { } else {
content.innerHTML = '<span class="text-gray-500 text-sm">SW: ' + esc(data.sw || '?') + '</span>'; content.innerHTML = '<span class="text-gray-500 text-sm">SW: ' + esc(data.sw || '?') + '</span>';
@@ -3054,10 +3061,11 @@ function pysimSwitchSubtab(name) {
btn.classList.toggle('text-gray-700', !isActive); btn.classList.toggle('text-gray-700', !isActive);
btn.classList.toggle('dark:text-slate-300', !isActive); btn.classList.toggle('dark:text-slate-300', !isActive);
}); });
['cmd', 'apdu', 'files', 'custom'].forEach(s => { ['cmd', 'apdu', 'files', 'custom', 'proactive'].forEach(s => {
document.getElementById('pysim-sub-' + s).classList.toggle('hidden', s !== name); document.getElementById('pysim-sub-' + s).classList.toggle('hidden', s !== name);
}); });
if (name === 'custom') pysimCustomRender(); if (name === 'custom') pysimCustomRender();
if (name === 'proactive') pysimEventsRender();
} }
function pysimCmdKeydown(e) { function pysimCmdKeydown(e) {
@@ -3525,6 +3533,46 @@ function pysimFsResetEdit() {
document.getElementById('pysim-fs-cancel-btn').classList.add('hidden'); document.getElementById('pysim-fs-cancel-btn').classList.add('hidden');
} }
// ===== proactive UICC events =====
const EVENT_NAMES = {
0x00: 'MT call', 0x01: 'Call connected', 0x02: 'Call disconnected',
0x03: 'Location status', 0x04: 'User activity', 0x05: 'Idle screen available',
0x06: 'Card reader status', 0x07: 'Language selection',
0x08: 'Browser termination', 0x09: 'Data available',
0x0A: 'Channel status', 0x0B: 'Access Technology Change',
0x0C: 'Display parameters changed', 0x0D: 'Local connection',
0x0E: 'Network Search Mode Change', 0x0F: 'Browsing status',
0x10: 'Frames Information Change', 0x11: 'I-WLAN Access Status',
0x12: 'Network Rejection', 0x13: 'HCI Connectivity',
0x14: 'Change of UICC Access', 0x15: 'CSG Cell Change',
0x16: 'Contactless state request', 0x17: 'Profile Container',
0x18: 'LTE D2D Discovery Monitoring', 0x19: 'LTE D2D Communication Monitoring',
0x1A: 'LTE D2D Announcement Response', 0x1B: 'LTE D2D Revocation',
0x1C: 'LTE D2D Application Port', 0x1D: 'LTE D2D Security Recovery',
0x1E: 'Off-net Emergency Call', 0x1F: 'ECall Over IMS',
0x20: 'EARFCN Update', 0x21: 'SCEF Channel Status',
};
async function pysimEventsRender() {
const el = document.getElementById('pysim-event-list');
el.innerHTML = '<span class="text-gray-400">' + t('Loading...') + '</span>';
try {
const events = await pysimFetch('/api/events');
if (!events || !events.length) {
el.innerHTML = '<span class="text-gray-400">' + t('No events configured.') + '</span>';
return;
}
let html = '';
events.forEach(e => {
const name = EVENT_NAMES[e] || ('Event 0x' + e.toString(16).padStart(2, '0').toUpperCase());
html += '<div class="flex items-center gap-2 py-0.5"><span class="w-6 text-right text-gray-400">' + e.toString(16).padStart(2, '0').toUpperCase() + '</span> ' + esc(name) + '</div>';
});
el.innerHTML = html;
} catch (e) {
el.innerHTML = '<span class="text-red-500">Error: ' + esc(e.message) + '</span>';
}
}
// ===== custom files ===== // ===== custom files =====
let pysimCustomFiles = []; let pysimCustomFiles = [];
@@ -3747,6 +3795,9 @@ const LANG_RU = {
'Verifying against pySim reference...': 'Сверка с эталоном pySim...', 'Verifying against pySim reference...': 'Сверка с эталоном pySim...',
'Verify vs pySim': 'Проверить в pySim', 'Verify vs pySim': 'Проверить в pySim',
'Send to Card': 'Отправить на карту', 'Send to Card': 'Отправить на карту',
'Proactive UICC': 'Проактивный UICC',
'Events that the card monitors (SET UP EVENT LIST):': 'События, отслеживаемые картой (SET UP EVENT LIST):',
'No events configured.': 'Нет настроенных событий.',
}; };
let currentLang = 'en'; let currentLang = 'en';
+4 -4
View File
@@ -695,6 +695,10 @@ video {
width: 33.333333%; width: 33.333333%;
} }
.w-1\/4 {
width: 25%;
}
.w-10 { .w-10 {
width: 2.5rem; width: 2.5rem;
} }
@@ -789,10 +793,6 @@ video {
align-items: flex-start; align-items: flex-start;
} }
.items-end {
align-items: flex-end;
}
.items-center { .items-center {
align-items: center; align-items: center;
} }
+12
View File
@@ -0,0 +1,12 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
const opens = (html.match(/<div\b/g) || []).length;
const closes = (html.match(/<\/div>/g) || []).length;
test('HTML <div> tags are balanced', () => {
assert.strictEqual(opens, closes, `Unbalanced divs: ${opens} opens vs ${closes} closes`);
});