Compare commits
7 Commits
0217f8dc92
...
1ce23f021c
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ce23f021c | |||
| 72b0116bad | |||
| 8019326cfb | |||
| 01931b9693 | |||
| 03ec13364c | |||
| e6602c40bf | |||
| 4a6a142c3d |
+1
-1
@@ -54,7 +54,7 @@ Returns server version for compatibility checking.
|
||||
|
||||
**Example response:**
|
||||
```json
|
||||
{"version": "1.8.0"}
|
||||
{"version": "1.8.1"}
|
||||
```
|
||||
|
||||
### `GET /api/status`
|
||||
|
||||
@@ -110,18 +110,18 @@
|
||||
<p class="text-sm mb-3">Сборщик Immediate Action предлагает action indicator (<code class="font-mono text-sm">81</code>/<code class="font-mono text-sm">82</code>), структурированный сборщик проактивных команд (REFRESH, DISPLAY TEXT, PLAY TONE с авто-генерацией COMPREHENSION-TLV), или ручной hex-ввод.</p>
|
||||
|
||||
<h4 id="ber-error-action" class="font-medium mb-2 text-base">Error Action TLV (Tag 82)</h4>
|
||||
<p class="text-sm mb-2">Условное восстановление при ошибках для graceful rollback:</p>
|
||||
<p class="text-sm mb-2">Восстановление при ошибках по TS 102 226 §5.2.1.3 — одна из трёх форм:</p>
|
||||
<ul class="text-sm list-disc pl-5 mb-2">
|
||||
<li><strong>Тип действия:</strong> Немедленное (00), отложенное (01) или условное (02) выполнение</li>
|
||||
<li><strong>Условия запуска:</strong> Сопоставление SW (любая ошибка, конкретный SW, диапазон SW, конкретная команда)</li>
|
||||
<li><strong>Действие восстановления:</strong> Action Indicator (81 session indication / 82 early response) или проактивная команда (DISPLAY TEXT, PLAY TONE, REFRESH) для уведомления или отката</li>
|
||||
<li><strong>Количество попыток:</strong> Число повторных попыток до отказа (0=без повтора)</li>
|
||||
<li><strong>Проактивная команда:</strong> набор COMPREHENSION-TLV с DISPLAY TEXT или PLAY TONE (в Error Action допустимы только эти две, TS 102 226 Table 5.9)</li>
|
||||
<li><strong>Без действия:</strong> <code class="font-mono text-sm">82 00</code></li>
|
||||
<li><strong>Ссылка на запись EFRMA:</strong> <code class="font-mono text-sm">82 01 <ref></code> с номером записи <code class="font-mono text-sm">01</code>–<code class="font-mono text-sm">7F</code></li>
|
||||
<li><strong>Произвольный hex:</strong> произвольное значение TLV</li>
|
||||
</ul>
|
||||
|
||||
<h4 id="ber-script-chaining" class="font-medium mb-2 text-base">Script Chaining TLV (Tag 83)</h4>
|
||||
<p class="text-sm mb-2">Многопакетное выполнение скрипта с сохранением контекста:</p>
|
||||
<ul class="text-sm list-disc pl-5 mb-2">
|
||||
<li><strong>Флаги цепочки:</strong> Позиция First/Intermediary/Last Script (бит 0/1/2)</li>
|
||||
<li><strong>Флаги цепочки:</strong> <code class="font-mono text-sm">01</code> первый скрипт (удалять инфо о цепочке при сбросе), <code class="font-mono text-sm">11</code> первый скрипт (сохранять инфо о цепочке при сбросе, только RFM), <code class="font-mono text-sm">02</code> последующий скрипт (будут ещё), <code class="font-mono text-sm">03</code> последующий скрипт (последний)</li>
|
||||
<li><strong>Идентификатор скрипта:</strong> Корреляционный идентификатор между пакетами (1-4 байта hex, авто-инкремент подсказки)</li>
|
||||
<li><strong>Дополнительные данные:</strong> Расширенная информация цепочки (опционально hex)</li>
|
||||
<li><strong>Сохранение контекста:</strong> UICC сохраняет состояние безопасности/транзакции между пакетами</li>
|
||||
|
||||
+6
-6
@@ -110,18 +110,18 @@
|
||||
<p class="text-sm mb-3">The Immediate Action builder offers an action indicator (<code class="font-mono text-sm">81</code>/<code class="font-mono text-sm">82</code>), a structured proactive command builder (REFRESH, DISPLAY TEXT, PLAY TONE with auto-generated COMPREHENSION-TLV objects), or a freeform hex input.</p>
|
||||
|
||||
<h4 id="ber-error-action" class="font-medium mb-2 text-base">Error Action TLV (Tag 82)</h4>
|
||||
<p class="text-sm mb-2">Conditional error recovery for graceful rollback:</p>
|
||||
<p class="text-sm mb-2">Error recovery per TS 102 226 §5.2.1.3 — one of three forms:</p>
|
||||
<ul class="text-sm list-disc pl-5 mb-2">
|
||||
<li><strong>Action Type:</strong> Immediate (00), delayed (01), or conditional (02) execution</li>
|
||||
<li><strong>Trigger Conditions:</strong> SW pattern matching (any error, specific SW, SW range, specific command fails)</li>
|
||||
<li><strong>Recovery Action:</strong> Action Indicator (81 session indication / 82 early response) or Proactive Command (DISPLAY TEXT, PLAY TONE, REFRESH) to notify user or fallback</li>
|
||||
<li><strong>Retry Count:</strong> Number of retry attempts before giving up (0=no retry)</li>
|
||||
<li><strong>Proactive command:</strong> COMPREHENSION-TLV set with DISPLAY TEXT or PLAY TONE (only these two are allowed in an Error Action, TS 102 226 Table 5.9)</li>
|
||||
<li><strong>No action:</strong> <code class="font-mono text-sm">82 00</code></li>
|
||||
<li><strong>Reference to EFRMA record:</strong> <code class="font-mono text-sm">82 01 <ref></code> with record number <code class="font-mono text-sm">01</code>–<code class="font-mono text-sm">7F</code></li>
|
||||
<li><strong>Custom hex:</strong> arbitrary TLV value</li>
|
||||
</ul>
|
||||
|
||||
<h4 id="ber-script-chaining" class="font-medium mb-2 text-base">Script Chaining TLV (Tag 83)</h4>
|
||||
<p class="text-sm mb-2">Multi-packet script execution with context preservation:</p>
|
||||
<ul class="text-sm list-disc pl-5 mb-2">
|
||||
<li><strong>Chaining Flags:</strong> First/Intermediary/Last Script position (bit 0/1/2)</li>
|
||||
<li><strong>Chaining Flags:</strong> <code class="font-mono text-sm">01</code> first script (delete chaining info on reset), <code class="font-mono text-sm">11</code> first script (keep chaining info across reset, RFM only), <code class="font-mono text-sm">02</code> subsequent script (more to follow), <code class="font-mono text-sm">03</code> subsequent script (last)</li>
|
||||
<li><strong>Script ID:</strong> Correlation identifier across packets (1-4 bytes hex, auto-increment hints provided)</li>
|
||||
<li><strong>Additional Data:</strong> Extended chaining information (optional hex)</li>
|
||||
<li><strong>Context Preservation:</strong> UICC keeps security/transaction state open across chained scripts</li>
|
||||
|
||||
+642
-240
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'otaman-v8';
|
||||
const CACHE = 'otaman-v9';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
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');
|
||||
|
||||
function extractFunc(src, name) {
|
||||
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||
const m = re.exec(src);
|
||||
if (!m) throw new Error('function ' + name + ' not found');
|
||||
let i = m.index + m[0].length - 1;
|
||||
let depth = 0;
|
||||
for (; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
return src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
function findNode(tree, label) {
|
||||
if (tree.label === label) return tree;
|
||||
if (tree.children) {
|
||||
for (const c of tree.children) {
|
||||
const r = findNode(c, label);
|
||||
if (r) return r;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findNodes(tree, label) {
|
||||
const out = [];
|
||||
if (tree.label === label) out.push(tree);
|
||||
if (tree.children) {
|
||||
for (const c of tree.children) out.push(...findNodes(c, label));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const consts = ['BER_QUAL', 'BER_DEVICES', 'BER_TONES', 'GSM7_ALPHABET', 'GSM7_EXT_MAP'];
|
||||
let prefix = '';
|
||||
for (const c of consts) {
|
||||
const m = html.match(new RegExp('const\\s+' + c + '\\s*=\\s*(?:\\[|\\{)[\\s\\S]*?(?:\\n[\\]\\}];|[\\]\\}];\\n)'));
|
||||
if (!m) throw new Error(c + ' not found');
|
||||
prefix += m[0].replace(/^const /, 'var ') + '\n';
|
||||
}
|
||||
const start = html.indexOf('// ===== C-APDU Parser =====');
|
||||
const end = html.indexOf('// ===== Response Parser =====');
|
||||
eval(prefix + html.slice(start, end).replace(/^const PARSE_INS = /m, 'var PARSE_INS = '));
|
||||
|
||||
test('Compact RAM INSTALL [for install] UICC', () => {
|
||||
const tree = parseHexTree('80E60C00214F08A000000151000000C70100EA128010000000020101020200011603B0000100');
|
||||
assert.strictEqual(tree.label, 'Compact C-APDU chain');
|
||||
assert.strictEqual(tree.children.length, 1);
|
||||
const apdu = tree.children[0];
|
||||
assert.strictEqual(apdu.label, 'APDU');
|
||||
assert.ok(findNode(apdu, 'INS').desc.includes('INSTALL'));
|
||||
});
|
||||
|
||||
test('Compact RAM INSTALL [for install] SIM (CA) access domain 5A', () => {
|
||||
const tree = parseHexTree('80E60C00204F08A000000151000000C70100CA11015A000000020101020200011603B00001');
|
||||
const apdu = tree.children[0];
|
||||
assert.ok(findNode(apdu, 'INS').desc.includes('INSTALL'));
|
||||
});
|
||||
|
||||
test('Expanded AA DISPLAY TEXT UCS2 "HI" + 5s', () => {
|
||||
const tree = parseHexTree('AA1681130103012101020281820D040048004904020105');
|
||||
assert.strictEqual(tree.label, 'Expanded Script (AA)');
|
||||
assert.strictEqual(tree.children.length, 1);
|
||||
assert.strictEqual(tree.children[0].label, 'Immediate Action');
|
||||
});
|
||||
|
||||
test('Expanded AA DISPLAY TEXT GSM7 "HI"', () => {
|
||||
const tree = parseHexTree('AA0F810D0103012101020281820D02C824');
|
||||
assert.strictEqual(tree.children.length, 1);
|
||||
assert.strictEqual(tree.children[0].label, 'Immediate Action');
|
||||
});
|
||||
|
||||
test('Expanded AA REFRESH + file list', () => {
|
||||
const tree = parseHexTree('AA10810E01030101000202818212036F3B2F');
|
||||
assert.strictEqual(tree.children.length, 1);
|
||||
assert.strictEqual(tree.children[0].label, 'Immediate Action');
|
||||
});
|
||||
|
||||
test('Expanded AE80 PLAY TONE + Error Action no action', () => {
|
||||
const tree = parseHexTree('AE80810C0103200101020281820E010182000000');
|
||||
assert.strictEqual(tree.label, 'Expanded Script (AE80)');
|
||||
assert.strictEqual(tree.children.length, 2);
|
||||
assert.strictEqual(tree.children[0].label, 'Immediate Action');
|
||||
assert.strictEqual(tree.children[1].label, 'Error Action');
|
||||
});
|
||||
|
||||
test('Immediate Action CR-set tags (real-world encoding)', () => {
|
||||
const tree = parseHexTree('AA0E8101818109810301010482028182');
|
||||
assert.strictEqual(tree.children.length, 2);
|
||||
assert.strictEqual(tree.children[0].label, 'Immediate Action');
|
||||
assert.strictEqual(tree.children[1].label, 'Immediate Action');
|
||||
});
|
||||
|
||||
test('CR-set DISPLAY TEXT UCS2 "HI" + 5s', () => {
|
||||
const tree = parseHexTree('AA1581138103012101820281828D040048004984020105');
|
||||
assert.strictEqual(tree.children.length, 1);
|
||||
});
|
||||
|
||||
test('CR-set PLAY TONE (tag 8E)', () => {
|
||||
const tree = parseHexTree('AA0E810C8103012001820281828E0101');
|
||||
assert.strictEqual(tree.children.length, 1);
|
||||
});
|
||||
|
||||
test('CR-set REFRESH file list (tag 92)', () => {
|
||||
const tree = parseHexTree('AA0F810D81030101048202818292023F00');
|
||||
assert.strictEqual(tree.children.length, 1);
|
||||
});
|
||||
|
||||
test('Expanded AA Error Action = proactive DISPLAY TEXT', () => {
|
||||
const tree = parseHexTree('AA0F820D0103012101020281820D022852');
|
||||
assert.strictEqual(tree.children.length, 1);
|
||||
assert.strictEqual(tree.children[0].label, 'Error Action');
|
||||
});
|
||||
|
||||
test('Expanded AA 22 C-APDU row (GET STATUS)', () => {
|
||||
const tree = parseHexTree('AA09220780F22000024F00');
|
||||
assert.strictEqual(tree.children.length, 1);
|
||||
assert.strictEqual(tree.children[0].label, 'C-APDU');
|
||||
});
|
||||
|
||||
test('Chained SIM SELECT + UPDATE RECORD', () => {
|
||||
const tree = parseHexTree('A0A40000026F3BDC0102032B2F2D');
|
||||
assert.strictEqual(tree.label, 'Compact C-APDU chain');
|
||||
assert.strictEqual(tree.children.length, 2);
|
||||
assert.strictEqual(tree.children[0].label, 'APDU');
|
||||
});
|
||||
|
||||
test('INTERNAL AUTHENTICATE', () => {
|
||||
const tree = parseHexTree('8088000008800F3495BA2355CD');
|
||||
assert.strictEqual(tree.children.length, 1);
|
||||
const apdu = tree.children[0];
|
||||
assert.strictEqual(apdu.label, 'APDU');
|
||||
assert.ok(findNode(apdu, 'INS').desc.includes('INTERNAL AUTHENTICATE'));
|
||||
assert.ok(findNode(apdu, 'Data'));
|
||||
});
|
||||
|
||||
test('baseCompTag clears CR bit for 0x80-0x9F', () => {
|
||||
assert.strictEqual(baseCompTag('81'), '01');
|
||||
assert.strictEqual(baseCompTag('82'), '02');
|
||||
assert.strictEqual(baseCompTag('8D'), '0D');
|
||||
assert.strictEqual(baseCompTag('8E'), '0E');
|
||||
assert.strictEqual(baseCompTag('92'), '12');
|
||||
assert.strictEqual(baseCompTag('84'), '04');
|
||||
assert.strictEqual(baseCompTag('85'), '05');
|
||||
assert.strictEqual(baseCompTag('01'), '01');
|
||||
assert.strictEqual(baseCompTag('22'), '22');
|
||||
});
|
||||
|
||||
test('parseTlvList handles BER-TLVs', () => {
|
||||
const tlvs = parseTlvList('810301210182028182');
|
||||
assert.strictEqual(tlvs.length, 2);
|
||||
assert.strictEqual(tlvs[0].tag, '81');
|
||||
assert.strictEqual(tlvs[0].length, 3);
|
||||
assert.strictEqual(tlvs[0].value, '012101');
|
||||
assert.strictEqual(tlvs[1].tag, '82');
|
||||
});
|
||||
|
||||
test('gsm7Decode unpacks "HI" from C824', () => {
|
||||
const bytes = new Uint8Array([0xC8, 0x24]);
|
||||
assert.strictEqual(gsm7Decode(bytes), 'HI');
|
||||
});
|
||||
|
||||
test('decodePrivileges', () => {
|
||||
assert.ok(decodePrivileges('00').includes('None'));
|
||||
assert.ok(decodePrivileges('80').includes('Security Domain'));
|
||||
});
|
||||
@@ -0,0 +1,280 @@
|
||||
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');
|
||||
|
||||
function extractFunc(src, name) {
|
||||
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||
const m = re.exec(src);
|
||||
if (!m) throw new Error('function ' + name + ' not found');
|
||||
let i = m.index + m[0].length - 1;
|
||||
let depth = 0;
|
||||
for (; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
return src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
class StubEl {
|
||||
constructor(value = '') {
|
||||
this.value = value;
|
||||
this.checked = false;
|
||||
this.disabled = false;
|
||||
this.children = [];
|
||||
this.parentElement = null;
|
||||
this.nextElementSibling = null;
|
||||
}
|
||||
querySelector(sel) {
|
||||
return this._find(sel);
|
||||
}
|
||||
querySelectorAll(sel) {
|
||||
const out = [];
|
||||
this._findAll(sel, out);
|
||||
return out;
|
||||
}
|
||||
_find(sel) {
|
||||
for (const c of this.children) {
|
||||
if (c._matches(sel)) return c;
|
||||
const r = c._find(sel);
|
||||
if (r) return r;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
_findAll(sel, out) {
|
||||
for (const c of this.children) {
|
||||
if (c._matches(sel)) out.push(c);
|
||||
c._findAll(sel, out);
|
||||
}
|
||||
}
|
||||
_matches(sel) {
|
||||
if (sel.startsWith('.')) return this.cls === sel.slice(1);
|
||||
if (sel.startsWith('input[name="')) {
|
||||
const m = sel.match(/^input\[name="([^"]+)"\]\[value="([^"]+)"\]$/);
|
||||
if (m) return this.name === m[1] && this.value === m[2];
|
||||
const m2 = sel.match(/^input\[name="([^"]+)"\]$/);
|
||||
if (m2) return this.name === m2[1];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function el(cls, value = '') {
|
||||
const e = new StubEl(value);
|
||||
e.cls = cls;
|
||||
return e;
|
||||
}
|
||||
|
||||
function buildRow() {
|
||||
const row = new StubEl();
|
||||
row.cls = 'ber-row';
|
||||
row.dataset = { berIdx: '0' };
|
||||
const body = el('error-action-body');
|
||||
const type = el('error-action-type', 'proactive');
|
||||
type.nextElementSibling = body;
|
||||
body.parentElement = row;
|
||||
row.children.push(type, body);
|
||||
return row;
|
||||
}
|
||||
|
||||
function buildPcHtml(extraFields) {
|
||||
// Build the .ber-pc tree expected by genBerPcValue
|
||||
const pc = el('ber-pc');
|
||||
const type = el('ber-pc-type', 'display');
|
||||
const num = el('ber-pc-num', '1');
|
||||
const qual = el('ber-pc-qual', '01');
|
||||
const src = el('ber-pc-src', '81');
|
||||
const dst = el('ber-pc-dst', '82');
|
||||
const text = el('ber-pc-text', 'HI');
|
||||
const enc = el('ber-pc-enc', 'ucs2');
|
||||
const alpha = el('ber-pc-alpha', '');
|
||||
const durEnable = el('ber-pc-dur-enable');
|
||||
durEnable.checked = true;
|
||||
const durUnit = el('ber-pc-dur-unit', '01');
|
||||
const durVal = el('ber-pc-dur-val', '30');
|
||||
pc.children.push(type, num, qual, src, dst, text, enc, alpha, durEnable, durUnit, durVal);
|
||||
if (extraFields) extraFields(pc);
|
||||
return pc;
|
||||
}
|
||||
|
||||
const FNS = ['berLenStr', 'gsm7TextToSeptets', 'gsm7Encode', 'genBerPcValue', 'genErrorActionValue',
|
||||
'genScriptChainingValue'];
|
||||
let code = '';
|
||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||
const berTagsMatch = html.match(/const BER_TAGS = \{[^}]*\};/);
|
||||
const berQualMatch = html.match(/const BER_QUAL = \{[\s\S]*?\n\};/);
|
||||
const berDevMatch = html.match(/const BER_DEVICES = \[[\s\S]*?\];/);
|
||||
const berTonesMatch = html.match(/const BER_TONES = \[[\s\S]*?\];/);
|
||||
const gsm7AlphaMatch = html.match(/const GSM7_ALPHABET = \[[\s\S]*?\n\];/);
|
||||
const gsm7ExtMatch = html.match(/const GSM7_EXT_MAP = \{[\s\S]*?\n\};/);
|
||||
for (const m of [berTagsMatch, berQualMatch, berDevMatch, berTonesMatch, gsm7AlphaMatch, gsm7ExtMatch]) {
|
||||
if (!m) throw new Error('constant not found');
|
||||
code = m[0] + '\n' + code;
|
||||
}
|
||||
code += '\nvar __berConsts = {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP};';
|
||||
eval(code);
|
||||
const {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP} = __berConsts;
|
||||
|
||||
function mkRow(type) {
|
||||
const row = new StubEl();
|
||||
row.cls = 'ber-row';
|
||||
row.dataset = { berIdx: '0' };
|
||||
const body = el('error-action-body');
|
||||
const actionType = el('error-action-type', type);
|
||||
actionType.nextElementSibling = body;
|
||||
row.children.push(actionType, body);
|
||||
return row;
|
||||
}
|
||||
|
||||
test('BER_QUAL.refresh matches TS 102 223 §8.6 mapping', () => {
|
||||
const r = BER_QUAL.refresh;
|
||||
assert.strictEqual(r[0][0], '00');
|
||||
assert.strictEqual(r[0][1], 'NAA Initialization and Full File Change Notification');
|
||||
assert.strictEqual(r[4][0], '04');
|
||||
assert.strictEqual(r[4][1], 'UICC Reset');
|
||||
assert.strictEqual(r[5][1], 'NAA Application Reset (not for 2G SIM)');
|
||||
assert.strictEqual(r[6][1], 'NAA Session Reset (not for 2G SIM)');
|
||||
assert.strictEqual(r[7][1], 'Steering of Roaming');
|
||||
assert.strictEqual(r.length, 11);
|
||||
});
|
||||
|
||||
test('genBerPcValue DISPLAY TEXT GSM7 "HI"', () => {
|
||||
const pc = buildPcHtml();
|
||||
pc.children.find(c => c.cls === 'ber-pc-enc').value = 'gsm7';
|
||||
pc.children.find(c => c.cls === 'ber-pc-dur-enable').checked = false;
|
||||
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
|
||||
assert.strictEqual(val, '810D8103012101820281828D02C824');
|
||||
});
|
||||
|
||||
test('genBerPcValue DISPLAY TEXT UCS2 "HI" + 30s duration', () => {
|
||||
const pc = buildPcHtml();
|
||||
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
|
||||
// 81 13 8103 01 21 01 8202 81 82 8D 04 0048 0049 84 02 01 1E
|
||||
assert.strictEqual(val, '81138103012101820281828D04004800498402011E');
|
||||
});
|
||||
|
||||
test('genBerPcValue duration disabled omits 84', () => {
|
||||
const pc = buildPcHtml();
|
||||
pc.children.find(c => c.cls === 'ber-pc-dur-enable').checked = false;
|
||||
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
|
||||
assert.ok(!val.includes('84'));
|
||||
});
|
||||
|
||||
test('genBerPcValue PLAY TONE uses type byte 20', () => {
|
||||
const pc = buildPcHtml();
|
||||
pc.children.find(c => c.cls === 'ber-pc-type').value = 'tone';
|
||||
pc.children.find(c => c.cls === 'ber-pc-qual').value = '00';
|
||||
pc.children.push(el('ber-pc-tone', '01'));
|
||||
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
|
||||
assert.strictEqual(val, '810C8103012000820281828E0101');
|
||||
});
|
||||
|
||||
test('genBerPcValue REFRESH uses CR-set file list tag 92', () => {
|
||||
const pc = buildPcHtml();
|
||||
pc.children.find(c => c.cls === 'ber-pc-type').value = 'refresh';
|
||||
pc.children.find(c => c.cls === 'ber-pc-qual').value = '04';
|
||||
pc.children.push(el('ber-pc-flist', '3F00'));
|
||||
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
|
||||
assert.strictEqual(val, '810D81030101048202818292023F00');
|
||||
});
|
||||
|
||||
test('genErrorActionValue noaction emits 82 00', () => {
|
||||
const row = mkRow('noaction');
|
||||
const val = genErrorActionValue(row, '82');
|
||||
assert.strictEqual(val, '8200');
|
||||
});
|
||||
|
||||
test('genErrorActionValue reference emits 82 01 ref', () => {
|
||||
const row = mkRow('reference');
|
||||
row.children[1].children.push(el('error-ref', '05'));
|
||||
const val = genErrorActionValue(row, '82');
|
||||
assert.strictEqual(val, '820105');
|
||||
});
|
||||
|
||||
test('genErrorActionValue reference rejects out-of-range ref', () => {
|
||||
const row = mkRow('reference');
|
||||
row.children[1].children.push(el('error-ref', '80'));
|
||||
assert.strictEqual(genErrorActionValue(row, '82'), '');
|
||||
});
|
||||
|
||||
test('genErrorActionValue proactive wraps proactive command', () => {
|
||||
const row = mkRow('proactive');
|
||||
const pc = buildPcHtml();
|
||||
row.children[1].children.push(pc);
|
||||
const val = genErrorActionValue(row, '82');
|
||||
// DISPLAY TEXT UCS2 "HI" + 30s inside Error Action
|
||||
assert.strictEqual(val, '82138103012101820281828D04004800498402011E');
|
||||
});
|
||||
|
||||
test('genScriptChainingValue first emits 01', () => {
|
||||
const row = new StubEl();
|
||||
row.dataset = { berIdx: '0' };
|
||||
const first = el('chaining-first', 'first');
|
||||
first.name = 'chaining-0';
|
||||
first.checked = true;
|
||||
const interm = el('chaining-inter', 'intermediary');
|
||||
interm.name = 'chaining-0';
|
||||
const last = el('chaining-last', 'last');
|
||||
last.name = 'chaining-0-last';
|
||||
const keep = el('chaining-keep', 'keep');
|
||||
keep.name = 'chaining-0-keep';
|
||||
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
|
||||
const val = genScriptChainingValue(row, '83');
|
||||
assert.strictEqual(val, '830101');
|
||||
});
|
||||
|
||||
test('genScriptChainingValue first with keep emits 11', () => {
|
||||
const row = new StubEl();
|
||||
row.dataset = { berIdx: '0' };
|
||||
const first = el('chaining-first', 'first');
|
||||
first.name = 'chaining-0';
|
||||
first.checked = true;
|
||||
const interm = el('chaining-inter', 'intermediary');
|
||||
interm.name = 'chaining-0';
|
||||
const last = el('chaining-last', 'last');
|
||||
last.name = 'chaining-0-last';
|
||||
const keep = el('chaining-keep', 'keep');
|
||||
keep.name = 'chaining-0-keep';
|
||||
keep.checked = true;
|
||||
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
|
||||
const val = genScriptChainingValue(row, '83');
|
||||
assert.strictEqual(val, '830111');
|
||||
});
|
||||
|
||||
test('genScriptChainingValue intermediary last emits 03', () => {
|
||||
const row = new StubEl();
|
||||
row.dataset = { berIdx: '0' };
|
||||
const first = el('chaining-first', 'first');
|
||||
first.name = 'chaining-0';
|
||||
const interm = el('chaining-inter', 'intermediary');
|
||||
interm.name = 'chaining-0';
|
||||
interm.checked = true;
|
||||
const last = el('chaining-last', 'last');
|
||||
last.name = 'chaining-0-last';
|
||||
last.checked = true;
|
||||
const keep = el('chaining-keep', 'keep');
|
||||
keep.name = 'chaining-0-keep';
|
||||
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
|
||||
const val = genScriptChainingValue(row, '83');
|
||||
assert.strictEqual(val, '830103');
|
||||
});
|
||||
|
||||
test('genScriptChainingValue no position emits empty', () => {
|
||||
const row = new StubEl();
|
||||
row.dataset = { berIdx: '0' };
|
||||
const first = el('chaining-first', 'first');
|
||||
first.name = 'chaining-0';
|
||||
const interm = el('chaining-inter', 'intermediary');
|
||||
interm.name = 'chaining-0';
|
||||
const last = el('chaining-last', 'last');
|
||||
last.name = 'chaining-0-last';
|
||||
const keep = el('chaining-keep', 'keep');
|
||||
keep.name = 'chaining-0-keep';
|
||||
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
|
||||
assert.strictEqual(genScriptChainingValue(row, '83'), '');
|
||||
});
|
||||
@@ -0,0 +1,293 @@
|
||||
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');
|
||||
|
||||
function extractFunc(src, name) {
|
||||
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||
const m = re.exec(src);
|
||||
if (!m) throw new Error('function ' + name + ' not found');
|
||||
let i = m.index + m[0].length - 1;
|
||||
let depth = 0;
|
||||
for (; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
return src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
class StubEl {
|
||||
constructor(value = '') {
|
||||
this.value = value;
|
||||
this.checked = false;
|
||||
this.style = {};
|
||||
}
|
||||
}
|
||||
|
||||
const els = {};
|
||||
function el(id, value = '') {
|
||||
if (!els[id]) els[id] = new StubEl(value);
|
||||
return els[id];
|
||||
}
|
||||
|
||||
function reset() {
|
||||
for (const id of Object.keys(els)) delete els[id];
|
||||
}
|
||||
|
||||
const FNS = ['berLenStr', 'buildApdu', 'genRam'];
|
||||
let code = '';
|
||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||
eval(code);
|
||||
|
||||
// document.getElementById stub: global getElementById in the extracted functions
|
||||
const doc = {
|
||||
getElementById: (id) => {
|
||||
if (!els[id]) els[id] = new StubEl();
|
||||
return els[id];
|
||||
},
|
||||
};
|
||||
// genRam also references document.getElementById via global document
|
||||
global.document = doc;
|
||||
|
||||
function set(id, v) { el(id).value = v; }
|
||||
function setChecked(id, v) { el(id).checked = v; }
|
||||
|
||||
function genRamResult() {
|
||||
genRam();
|
||||
return els['ram-result'].value;
|
||||
}
|
||||
|
||||
function genRamApdu() {
|
||||
reset();
|
||||
set('ram-cmd', 'install-install');
|
||||
set('ram-aid', 'A000000151000000');
|
||||
set('ram-priv', '00');
|
||||
setChecked('ram-toolkit-enable', true);
|
||||
set('ram-tk-mode', 'ea');
|
||||
set('ram-tk-priority', '0');
|
||||
set('ram-tk-timers', '0');
|
||||
set('ram-tk-textlen', '0');
|
||||
set('ram-tk-menus', '2');
|
||||
set('ram-tk-firstpos', '1');
|
||||
set('ram-tk-firstid', '01');
|
||||
set('ram-tk-lastpos', '2');
|
||||
set('ram-tk-lastid', '02');
|
||||
set('ram-tk-channels', '0');
|
||||
set('ram-tk-msl', '16');
|
||||
set('ram-tk-tar', 'B00001');
|
||||
set('ram-tk-ad', '');
|
||||
set('ram-tk-services', '0');
|
||||
return genRamResult();
|
||||
}
|
||||
|
||||
test('UICC toolkit nested inside EA (m=2, services 0)', () => {
|
||||
const apdu = genRamApdu();
|
||||
assert.ok(apdu.includes('EA128010000000020101020200011603B0000100'), apdu);
|
||||
});
|
||||
|
||||
test('UICC m=1 emits single pair', () => {
|
||||
reset();
|
||||
set('ram-cmd', 'install-install');
|
||||
set('ram-aid', 'A000000151000000');
|
||||
set('ram-priv', '00');
|
||||
setChecked('ram-toolkit-enable', true);
|
||||
set('ram-tk-mode', 'ea');
|
||||
set('ram-tk-priority', '0');
|
||||
set('ram-tk-timers', '0');
|
||||
set('ram-tk-textlen', '0');
|
||||
set('ram-tk-menus', '1');
|
||||
set('ram-tk-firstpos', '1');
|
||||
set('ram-tk-firstid', '01');
|
||||
set('ram-tk-lastpos', '0');
|
||||
set('ram-tk-lastid', '00');
|
||||
set('ram-tk-channels', '0');
|
||||
set('ram-tk-msl', '16');
|
||||
set('ram-tk-tar', 'B00001');
|
||||
set('ram-tk-ad', '');
|
||||
set('ram-tk-services', '0');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.includes('EA10800E00000001010100011603B0000100'), apdu);
|
||||
});
|
||||
|
||||
test('UICC m=3 fills middle pair with 0000', () => {
|
||||
reset();
|
||||
set('ram-cmd', 'install-install');
|
||||
set('ram-aid', 'A000000151000000');
|
||||
set('ram-priv', '00');
|
||||
setChecked('ram-toolkit-enable', true);
|
||||
set('ram-tk-mode', 'ea');
|
||||
set('ram-tk-priority', '0');
|
||||
set('ram-tk-timers', '0');
|
||||
set('ram-tk-textlen', '0');
|
||||
set('ram-tk-menus', '3');
|
||||
set('ram-tk-firstpos', '1');
|
||||
set('ram-tk-firstid', '01');
|
||||
set('ram-tk-lastpos', '3');
|
||||
set('ram-tk-lastid', '03');
|
||||
set('ram-tk-channels', '0');
|
||||
set('ram-tk-msl', '16');
|
||||
set('ram-tk-tar', 'B00001');
|
||||
set('ram-tk-ad', '');
|
||||
set('ram-tk-services', '0');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.includes('EA1480120000000301010000030300011603B0000100'), apdu);
|
||||
});
|
||||
|
||||
test('UICC services 7 appended as final byte', () => {
|
||||
reset();
|
||||
set('ram-cmd', 'install-install');
|
||||
set('ram-aid', 'A000000151000000');
|
||||
set('ram-priv', '00');
|
||||
setChecked('ram-toolkit-enable', true);
|
||||
set('ram-tk-mode', 'ea');
|
||||
set('ram-tk-priority', '0');
|
||||
set('ram-tk-timers', '0');
|
||||
set('ram-tk-textlen', '0');
|
||||
set('ram-tk-menus', '2');
|
||||
set('ram-tk-firstpos', '1');
|
||||
set('ram-tk-firstid', '01');
|
||||
set('ram-tk-lastpos', '2');
|
||||
set('ram-tk-lastid', '02');
|
||||
set('ram-tk-channels', '0');
|
||||
set('ram-tk-msl', '16');
|
||||
set('ram-tk-tar', 'B00001');
|
||||
set('ram-tk-ad', '');
|
||||
set('ram-tk-services', '7');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.includes('EA128010000000020101020200011603B0000107'), apdu);
|
||||
});
|
||||
|
||||
test('SIM (CA) access domain FIRST, no services byte', () => {
|
||||
reset();
|
||||
set('ram-cmd', 'install-install');
|
||||
set('ram-aid', 'A000000151000000');
|
||||
set('ram-priv', '00');
|
||||
setChecked('ram-toolkit-enable', true);
|
||||
set('ram-tk-mode', 'ca');
|
||||
set('ram-tk-priority', '0');
|
||||
set('ram-tk-timers', '0');
|
||||
set('ram-tk-textlen', '0');
|
||||
set('ram-tk-menus', '2');
|
||||
set('ram-tk-firstpos', '1');
|
||||
set('ram-tk-firstid', '01');
|
||||
set('ram-tk-lastpos', '2');
|
||||
set('ram-tk-lastid', '02');
|
||||
set('ram-tk-channels', '0');
|
||||
set('ram-tk-msl', '16');
|
||||
set('ram-tk-tar', 'B00001');
|
||||
set('ram-tk-ad', '5A');
|
||||
set('ram-tk-services', '0');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.includes('CA11015A000000020101020200011603B00001'), apdu);
|
||||
});
|
||||
|
||||
test('SIM (CA) blank access domain emits length byte 00', () => {
|
||||
reset();
|
||||
set('ram-cmd', 'install-install');
|
||||
set('ram-aid', 'A000000151000000');
|
||||
set('ram-priv', '00');
|
||||
setChecked('ram-toolkit-enable', true);
|
||||
set('ram-tk-mode', 'ca');
|
||||
set('ram-tk-priority', '0');
|
||||
set('ram-tk-timers', '0');
|
||||
set('ram-tk-textlen', '0');
|
||||
set('ram-tk-menus', '2');
|
||||
set('ram-tk-firstpos', '1');
|
||||
set('ram-tk-firstid', '01');
|
||||
set('ram-tk-lastpos', '2');
|
||||
set('ram-tk-lastid', '02');
|
||||
set('ram-tk-channels', '0');
|
||||
set('ram-tk-msl', '16');
|
||||
set('ram-tk-tar', 'B00001');
|
||||
set('ram-tk-ad', '');
|
||||
set('ram-tk-services', '0');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.includes('CA1000000000020101020200011603B00001'), apdu);
|
||||
});
|
||||
|
||||
test('SIM (CA) m=3, no TAR, blank access domain', () => {
|
||||
reset();
|
||||
set('ram-cmd', 'install-install');
|
||||
set('ram-aid', 'A000000151000000');
|
||||
set('ram-priv', '00');
|
||||
setChecked('ram-toolkit-enable', true);
|
||||
set('ram-tk-mode', 'ca');
|
||||
set('ram-tk-priority', '0');
|
||||
set('ram-tk-timers', '0');
|
||||
set('ram-tk-textlen', '0');
|
||||
set('ram-tk-menus', '3');
|
||||
set('ram-tk-firstpos', '1');
|
||||
set('ram-tk-firstid', '01');
|
||||
set('ram-tk-lastpos', '3');
|
||||
set('ram-tk-lastid', '03');
|
||||
set('ram-tk-channels', '0');
|
||||
set('ram-tk-msl', '16');
|
||||
set('ram-tk-tar', '');
|
||||
set('ram-tk-ad', '');
|
||||
set('ram-tk-services', '0');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.includes('CA0F000000000301010000030300011600'), apdu);
|
||||
});
|
||||
|
||||
test('UICC m=60 uses long-form BER lengths (EA 81 87 / inner 81 84)', () => {
|
||||
reset();
|
||||
set('ram-cmd', 'install-install');
|
||||
set('ram-aid', 'A000000151000000');
|
||||
set('ram-priv', '00');
|
||||
setChecked('ram-toolkit-enable', true);
|
||||
set('ram-tk-mode', 'ea');
|
||||
set('ram-tk-priority', '0');
|
||||
set('ram-tk-timers', '0');
|
||||
set('ram-tk-textlen', '0');
|
||||
set('ram-tk-menus', '60');
|
||||
set('ram-tk-firstpos', '1');
|
||||
set('ram-tk-firstid', '01');
|
||||
set('ram-tk-lastpos', '60');
|
||||
set('ram-tk-lastid', '3C');
|
||||
set('ram-tk-channels', '0');
|
||||
set('ram-tk-msl', '16');
|
||||
set('ram-tk-tar', 'B00001');
|
||||
set('ram-tk-ad', '');
|
||||
set('ram-tk-services', '0');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.includes('EA8187808184'), apdu);
|
||||
});
|
||||
|
||||
test('LOAD P1 fixed to 80 (last block)', () => {
|
||||
reset();
|
||||
set('ram-cmd', 'load');
|
||||
set('ram-data', 'AABBCC');
|
||||
set('ram-block', '0');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.startsWith('80E88000'), apdu);
|
||||
});
|
||||
|
||||
test('DELETE: P1=00, mode in P2', () => {
|
||||
reset();
|
||||
set('ram-cmd', 'delete');
|
||||
set('ram-aid', 'AA1902BC225501');
|
||||
set('ram-del-mode', '00');
|
||||
let apdu = genRamResult();
|
||||
assert.ok(apdu.startsWith('80E40000'), apdu);
|
||||
|
||||
set('ram-del-mode', '80');
|
||||
apdu = genRamResult();
|
||||
assert.ok(apdu.startsWith('80E40080'), apdu);
|
||||
});
|
||||
|
||||
test('STORE DATA ram-enc P1 values 00/40/80/C0/E0', () => {
|
||||
for (const [enc, p1] of [['00','00'],['40','40'],['80','80'],['C0','C0'],['E0','E0']]) {
|
||||
reset();
|
||||
set('ram-cmd', 'store-data');
|
||||
set('ram-data', 'AABB');
|
||||
set('ram-enc', enc);
|
||||
set('ram-block', '0');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.startsWith('80E2' + p1 + '00'), enc + ' -> P1 ' + p1);
|
||||
}
|
||||
});
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-otaman-server"
|
||||
version = "1.8.0"
|
||||
version = "1.8.1"
|
||||
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
|
||||
@@ -18,7 +18,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '1.8.0'
|
||||
VERSION = '1.8.1'
|
||||
|
||||
|
||||
# Static file serving (the PWA lives in <repo>/frontend, served by this server
|
||||
|
||||
Reference in New Issue
Block a user