Compare commits
3 Commits
32509b102c
...
e607637782
| Author | SHA1 | Date | |
|---|---|---|---|
| e607637782 | |||
| 072e8fe08b | |||
| feeefa3018 |
+1
-1
@@ -54,7 +54,7 @@ Returns server version for compatibility checking.
|
|||||||
|
|
||||||
**Example response:**
|
**Example response:**
|
||||||
```json
|
```json
|
||||||
{"version": "1.9.1"}
|
{"version": "1.9.2"}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `GET /api/status`
|
### `GET /api/status`
|
||||||
|
|||||||
+114
-15
@@ -18,7 +18,7 @@
|
|||||||
<div class="max-w-7xl mx-auto px-6 py-2">
|
<div class="max-w-7xl mx-auto px-6 py-2">
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<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.9.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.9.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>
|
||||||
@@ -62,6 +62,9 @@
|
|||||||
<option value="deactivate-file">DEACTIVATE FILE</option>
|
<option value="deactivate-file">DEACTIVATE FILE</option>
|
||||||
<option value="verify">VERIFY PIN</option>
|
<option value="verify">VERIFY PIN</option>
|
||||||
<option value="change">CHANGE PIN</option>
|
<option value="change">CHANGE PIN</option>
|
||||||
|
<option value="disable">DISABLE PIN</option>
|
||||||
|
<option value="enable">ENABLE PIN</option>
|
||||||
|
<option value="unblock">UNBLOCK PIN</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -121,6 +124,17 @@
|
|||||||
<input id="sim-offset" onchange="updateP1P2Display('sim')" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="0000" maxlength="4" value="0000">
|
<input id="sim-offset" onchange="updateP1P2Display('sim')" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="0000" maxlength="4" value="0000">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="sim-act-row" class="mb-3">
|
||||||
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Target">Target</label>
|
||||||
|
<select id="sim-act-target" onchange="updateActFileInput('sim')" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
|
||||||
|
<option value="current" data-l10n="Current EF (case 1)">Current EF (case 1)</option>
|
||||||
|
<option value="fid" data-l10n="By FID">By FID</option>
|
||||||
|
<option value="mfpath" data-l10n="Path from MF">Path from MF</option>
|
||||||
|
<option value="dfpath" data-l10n="Path from current DF">Path from current DF</option>
|
||||||
|
</select>
|
||||||
|
<input id="sim-act-file" class="mt-1.5 font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="6FC5 / 7FFF6FC5" style="display:none">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="sim-le-row" class="mb-3">
|
<div id="sim-le-row" class="mb-3">
|
||||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Le (hex)</label>
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Le (hex)</label>
|
||||||
<input id="sim-le" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="00" maxlength="2" value="00">
|
<input id="sim-le" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="00" maxlength="2" value="00">
|
||||||
@@ -198,6 +212,9 @@
|
|||||||
<option value="deactivate-file">DEACTIVATE FILE</option>
|
<option value="deactivate-file">DEACTIVATE FILE</option>
|
||||||
<option value="verify">VERIFY PIN</option>
|
<option value="verify">VERIFY PIN</option>
|
||||||
<option value="change">CHANGE PIN</option>
|
<option value="change">CHANGE PIN</option>
|
||||||
|
<option value="disable">DISABLE PIN</option>
|
||||||
|
<option value="enable">ENABLE PIN</option>
|
||||||
|
<option value="unblock">UNBLOCK PIN</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -257,6 +274,17 @@
|
|||||||
<input id="usim-offset" onchange="updateP1P2Display('usim')" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="0000" maxlength="4" value="0000">
|
<input id="usim-offset" onchange="updateP1P2Display('usim')" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="0000" maxlength="4" value="0000">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="usim-act-row" class="mb-3">
|
||||||
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Target">Target</label>
|
||||||
|
<select id="usim-act-target" onchange="updateActFileInput('usim')" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
|
||||||
|
<option value="current" data-l10n="Current EF (case 1)">Current EF (case 1)</option>
|
||||||
|
<option value="fid" data-l10n="By FID">By FID</option>
|
||||||
|
<option value="mfpath" data-l10n="Path from MF">Path from MF</option>
|
||||||
|
<option value="dfpath" data-l10n="Path from current DF">Path from current DF</option>
|
||||||
|
</select>
|
||||||
|
<input id="usim-act-file" class="mt-1.5 font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="6FC5 / 7FFF6FC5" style="display:none">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="usim-le-row" class="mb-3">
|
<div id="usim-le-row" class="mb-3">
|
||||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Le (hex)</label>
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Le (hex)</label>
|
||||||
<input id="usim-le" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="00" maxlength="2" value="00">
|
<input id="usim-le" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="00" maxlength="2" value="00">
|
||||||
@@ -1104,7 +1132,7 @@ const BINARY_OPS = new Set(['update-binary', 'read-binary', 'erase-binary']);
|
|||||||
const READ_OPS = new Set(['read-record', 'read-binary']);
|
const READ_OPS = new Set(['read-record', 'read-binary']);
|
||||||
const DATA_OPS = new Set(['update-record', 'update-binary']);
|
const DATA_OPS = new Set(['update-record', 'update-binary']);
|
||||||
const RECSIZE_OPS = new Set(['update-record']);
|
const RECSIZE_OPS = new Set(['update-record']);
|
||||||
const PIN_OPS = new Set(['verify', 'change']);
|
const PIN_OPS = new Set(['verify', 'change', 'disable', 'enable', 'unblock']);
|
||||||
|
|
||||||
function updateSimUsimFields(mode) {
|
function updateSimUsimFields(mode) {
|
||||||
if (!mode) return;
|
if (!mode) return;
|
||||||
@@ -1115,9 +1143,15 @@ function updateSimUsimFields(mode) {
|
|||||||
document.getElementById(mode + '-data-row').style.display = DATA_OPS.has(cmd) ? '' : 'none';
|
document.getElementById(mode + '-data-row').style.display = DATA_OPS.has(cmd) ? '' : 'none';
|
||||||
document.getElementById(mode + '-recsize-row').style.display = RECSIZE_OPS.has(cmd) ? '' : 'none';
|
document.getElementById(mode + '-recsize-row').style.display = RECSIZE_OPS.has(cmd) ? '' : 'none';
|
||||||
document.getElementById(mode + '-pin-row').style.display = PIN_OPS.has(cmd) ? '' : 'none';
|
document.getElementById(mode + '-pin-row').style.display = PIN_OPS.has(cmd) ? '' : 'none';
|
||||||
document.getElementById(mode + '-pin-val-row').style.display = cmd === 'verify' ? '' : 'none';
|
const pinValCmds = cmd === 'verify' || cmd === 'disable' || cmd === 'enable';
|
||||||
document.getElementById(mode + '-pin-old-row').style.display = cmd === 'change' ? '' : 'none';
|
document.getElementById(mode + '-pin-val-row').style.display = pinValCmds ? '' : 'none';
|
||||||
document.getElementById(mode + '-pin-new-row').style.display = cmd === 'change' ? '' : 'none';
|
const oldNewCmds = cmd === 'change' || cmd === 'unblock';
|
||||||
|
document.getElementById(mode + '-pin-old-row').style.display = oldNewCmds ? '' : 'none';
|
||||||
|
document.getElementById(mode + '-pin-new-row').style.display = oldNewCmds ? '' : 'none';
|
||||||
|
if (oldNewCmds) {
|
||||||
|
document.querySelector('#' + mode + '-pin-old-row label').textContent = cmd === 'unblock' ? 'Unblock PIN' : 'Old PIN';
|
||||||
|
}
|
||||||
|
document.getElementById(mode + '-act-row').style.display = (cmd === 'activate-file' || cmd === 'deactivate-file') ? '' : 'none';
|
||||||
if (cmd === 'select') {
|
if (cmd === 'select') {
|
||||||
document.getElementById(mode + '-select-checkbox-row').style.display = 'none';
|
document.getElementById(mode + '-select-checkbox-row').style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
@@ -1135,6 +1169,11 @@ function updateSimSelectMethod(mode) {
|
|||||||
document.getElementById(mode + '-sel-chain-row').style.display = method === 'chain' ? '' : 'none';
|
document.getElementById(mode + '-sel-chain-row').style.display = method === 'chain' ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateActFileInput(mode) {
|
||||||
|
const target = document.getElementById(mode + '-act-target').value;
|
||||||
|
document.getElementById(mode + '-act-file').style.display = target === 'current' ? 'none' : '';
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSimOverride(mode) {
|
function toggleSimOverride(mode) {
|
||||||
const checked = document.getElementById(mode + '-override').checked;
|
const checked = document.getElementById(mode + '-override').checked;
|
||||||
document.getElementById(mode + '-p1').disabled = !checked;
|
document.getElementById(mode + '-p1').disabled = !checked;
|
||||||
@@ -1178,8 +1217,14 @@ function updateP1P2Display(mode) {
|
|||||||
} else if (cmd === 'read-binary' || cmd === 'update-binary' || cmd === 'erase-binary') {
|
} else if (cmd === 'read-binary' || cmd === 'update-binary' || cmd === 'erase-binary') {
|
||||||
p1 = (offset >> 8) & 0xFF; p2 = offset & 0xFF;
|
p1 = (offset >> 8) & 0xFF; p2 = offset & 0xFF;
|
||||||
} else if (cmd === 'activate-file' || cmd === 'deactivate-file') {
|
} else if (cmd === 'activate-file' || cmd === 'deactivate-file') {
|
||||||
|
const target = document.getElementById(mode + '-act-target').value;
|
||||||
|
if (target === 'current') {
|
||||||
p1 = 0x00; p2 = 0x00;
|
p1 = 0x00; p2 = 0x00;
|
||||||
} else if (cmd === 'verify' || cmd === 'change') {
|
} else {
|
||||||
|
p1 = target === 'fid' ? 0x00 : (target === 'mfpath' ? 0x08 : 0x09);
|
||||||
|
p2 = 0x00;
|
||||||
|
}
|
||||||
|
} else if (cmd === 'verify' || cmd === 'change' || cmd === 'disable' || cmd === 'enable' || cmd === 'unblock') {
|
||||||
p1 = 0x00; p2 = parseInt(pin, 16);
|
p1 = 0x00; p2 = parseInt(pin, 16);
|
||||||
} else {
|
} else {
|
||||||
p1 = 0x00; p2 = 0x00;
|
p1 = 0x00; p2 = 0x00;
|
||||||
@@ -1417,6 +1462,9 @@ const OPS = {
|
|||||||
'deactivate-file': {ins: 0x04},
|
'deactivate-file': {ins: 0x04},
|
||||||
'verify': {ins: 0x20},
|
'verify': {ins: 0x20},
|
||||||
'change': {ins: 0x24},
|
'change': {ins: 0x24},
|
||||||
|
'disable': {ins: 0x26},
|
||||||
|
'enable': {ins: 0x28},
|
||||||
|
'unblock': {ins: 0x2C},
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSelValue(mode) {
|
function getSelValue(mode) {
|
||||||
@@ -1513,13 +1561,23 @@ function genSimUsim(mode) {
|
|||||||
} else if (cmd === 'erase-binary') {
|
} else if (cmd === 'erase-binary') {
|
||||||
p1 = (offset >> 8) & 0xFF; p2 = offset & 0xFF; p3 = 0x00;
|
p1 = (offset >> 8) & 0xFF; p2 = offset & 0xFF; p3 = 0x00;
|
||||||
} else if (cmd === 'activate-file' || cmd === 'deactivate-file') {
|
} else if (cmd === 'activate-file' || cmd === 'deactivate-file') {
|
||||||
|
const target = document.getElementById(mode + '-act-target').value;
|
||||||
|
if (target === 'current') {
|
||||||
p1 = 0x00; p2 = 0x00; p3 = null;
|
p1 = 0x00; p2 = 0x00; p3 = null;
|
||||||
} else if (cmd === 'verify') {
|
} else {
|
||||||
|
const fileHex = (document.getElementById(mode + '-act-file').value || '').replace(/[^0-9a-fA-F]/g, '');
|
||||||
|
if (!fileHex || fileHex.length % 2 !== 0) { resultEl.value = 'Error: specify FID or path (even hex length)'; return; }
|
||||||
|
p1 = target === 'fid' ? 0x00 : (target === 'mfpath' ? 0x08 : 0x09);
|
||||||
|
p2 = 0x00;
|
||||||
|
p3 = fileHex.length / 2;
|
||||||
|
data = fileHex;
|
||||||
|
}
|
||||||
|
} else if (cmd === 'verify' || cmd === 'disable' || cmd === 'enable') {
|
||||||
const pinVal = document.getElementById(mode + '-pin-val').value || '';
|
const pinVal = document.getElementById(mode + '-pin-val').value || '';
|
||||||
if (!pinVal) { resultEl.value = 'Error: specify PIN value'; return; }
|
if (!pinVal) { resultEl.value = 'Error: specify PIN value'; return; }
|
||||||
const pinHex = Array.from(pinVal).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('').padEnd(16, 'F').slice(0, 16);
|
const pinHex = Array.from(pinVal).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('').padEnd(16, 'F').slice(0, 16);
|
||||||
p1 = 0x00; p2 = parseInt(pin, 16); p3 = 0x08; data = pinHex;
|
p1 = 0x00; p2 = parseInt(pin, 16); p3 = 0x08; data = pinHex;
|
||||||
} else if (cmd === 'change') {
|
} else if (cmd === 'change' || cmd === 'unblock') {
|
||||||
const oldPin = document.getElementById(mode + '-pin-old').value || '';
|
const oldPin = document.getElementById(mode + '-pin-old').value || '';
|
||||||
const newPin = document.getElementById(mode + '-pin-new').value || '';
|
const newPin = document.getElementById(mode + '-pin-new').value || '';
|
||||||
if (!oldPin || !newPin) { resultEl.value = 'Error: specify old and new PIN'; return; }
|
if (!oldPin || !newPin) { resultEl.value = 'Error: specify old and new PIN'; return; }
|
||||||
@@ -2544,16 +2602,42 @@ const PARSE_INS = {
|
|||||||
'04': {name:'DEACTIVATE FILE', data:'none'},
|
'04': {name:'DEACTIVATE FILE', data:'none'},
|
||||||
'20': {name:'VERIFY', data:'lc'},
|
'20': {name:'VERIFY', data:'lc'},
|
||||||
'24': {name:'CHANGE', data:'lc'},
|
'24': {name:'CHANGE', data:'lc'},
|
||||||
|
'26': {name:'DISABLE PIN', data:'lc'},
|
||||||
|
'28': {name:'ENABLE PIN', data:'lc'},
|
||||||
|
'2C': {name:'UNBLOCK PIN', data:'lc'},
|
||||||
'E6': {name:'INSTALL', data:'lc'},
|
'E6': {name:'INSTALL', data:'lc'},
|
||||||
'E8': {name:'LOAD', data:'lc'},
|
'E8': {name:'LOAD', data:'lc'},
|
||||||
|
'D4': {name:'RESIZE FILE', data:'lc'},
|
||||||
|
'E0': {name:'CREATE FILE', data:'lc'},
|
||||||
'E4': {name:'DELETE', data:'lc'},
|
'E4': {name:'DELETE', data:'lc'},
|
||||||
'F2': {name:'GET STATUS', data:'lc'},
|
'F2': {name:'GET STATUS', data:'lc'},
|
||||||
'CA': {name:'GET DATA', data:'none'},
|
'CA': {name:'GET DATA', data:'none'},
|
||||||
|
'DA': {name:'PUT DATA', data:'lc'},
|
||||||
|
'CB': {name:'RETRIEVE DATA', data:'le'},
|
||||||
|
'DB': {name:'SET DATA', data:'lc'},
|
||||||
|
'A2': {name:'SEARCH RECORD', data:'lc'},
|
||||||
|
'32': {name:'INCREASE', data:'lc'},
|
||||||
|
'84': {name:'GET CHALLENGE', data:'le'},
|
||||||
|
'88': {name:'AUTHENTICATE', data:'lc'},
|
||||||
|
'89': {name:'AUTHENTICATE (odd INS)', data:'lc'},
|
||||||
'E2': {name:'STORE DATA', data:'lc'},
|
'E2': {name:'STORE DATA', data:'lc'},
|
||||||
'F0': {name:'SET STATUS', data:'lc'},
|
'F0': {name:'SET STATUS', data:'lc'},
|
||||||
'82': {name:'EXTERNAL AUTHENTICATE', data:'lc'},
|
'82': {name:'EXTERNAL AUTHENTICATE', data:'lc'},
|
||||||
'88': {name:'INTERNAL AUTHENTICATE', data:'lc'},
|
|
||||||
'C0': {name:'GET RESPONSE', data:'le'},
|
'C0': {name:'GET RESPONSE', data:'le'},
|
||||||
|
'10': {name:'TERMINAL PROFILE', data:'lc'},
|
||||||
|
'C2': {name:'ENVELOPE', data:'lc'},
|
||||||
|
'12': {name:'FETCH', data:'le'},
|
||||||
|
'14': {name:'TERMINAL RESPONSE', data:'lc'},
|
||||||
|
'76': {name:'SUSPEND UICC', data:'lc'},
|
||||||
|
'AA': {name:'TERMINAL CAPABILITY', data:'lc'},
|
||||||
|
'73': {name:'MANAGE SECURE CHANNEL', data:'lc'},
|
||||||
|
'75': {name:'TRANSACT DATA', data:'lc'},
|
||||||
|
'78': {name:'GET IDENTITY', data:'le'},
|
||||||
|
'7A': {name:'EXCHANGE CAPABILITIES', data:'lc'},
|
||||||
|
'7C': {name:'MANAGE LSI', data:'lc'},
|
||||||
|
'50': {name:'INITIALIZE UPDATE', data:'lc'},
|
||||||
|
'D8': {name:'PUT KEY', data:'lc'},
|
||||||
|
'70': {name:'MANAGE CHANNEL', data:'none'},
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseBerLen(hex, i) {
|
function parseBerLen(hex, i) {
|
||||||
@@ -2905,13 +2989,17 @@ function describeP1P2(ins, p1, p2) {
|
|||||||
let d1 = '', d2 = '';
|
let d1 = '', d2 = '';
|
||||||
if (ins === 'A4') {
|
if (ins === 'A4') {
|
||||||
d1 = {'00':'by FID','01':'child DF','03':'parent DF','04':'by DF name (AID)','08':'path from MF','09':'path from current DF'}[p1] || '';
|
d1 = {'00':'by FID','01':'child DF','03':'parent DF','04':'by DF name (AID)','08':'path from MF','09':'path from current DF'}[p1] || '';
|
||||||
const resp = (v2 >> 2) & 0x03;
|
const resp = (v2 >> 2) & 0x07;
|
||||||
if (resp === 0x01) d2 = 'return FCP template';
|
if (resp === 0x01) d2 = 'return FCP template';
|
||||||
else if (resp === 0x03) d2 = 'no response data';
|
else if (resp === 0x03) d2 = 'no response data';
|
||||||
if (v1 === 0x04) {
|
if (v1 === 0x04) {
|
||||||
const occName = {'00':'first or only occurrence','01':'last occurrence','10':'next occurrence','11':'previous occurrence'}[(v2 & 0x03).toString(2).padStart(2,'0')];
|
const occName = {'00':'first or only occurrence','01':'last occurrence','10':'next occurrence','11':'previous occurrence'}[(v2 & 0x03).toString(2).padStart(2,'0')];
|
||||||
if (occName) d2 += (d2 ? ', ' : '') + occName;
|
if (occName) d2 += (d2 ? ', ' : '') + occName;
|
||||||
}
|
}
|
||||||
|
} else if (ins === 'B0' || ins === 'D6') {
|
||||||
|
if (v1 & 0x80) d1 = 'SFI mode, SFI ' + (v1 & 0x1F);
|
||||||
|
else d1 = 'offset high';
|
||||||
|
d2 = 'offset low';
|
||||||
} else if (ins === 'B2' || ins === 'DC') {
|
} else if (ins === 'B2' || ins === 'DC') {
|
||||||
const modeKey = (v2 & 0x07).toString(16).padStart(2, '0');
|
const modeKey = (v2 & 0x07).toString(16).padStart(2, '0');
|
||||||
d2 = {'02':'next record','03':'previous record','04':'absolute/current'}[modeKey] || '';
|
d2 = {'02':'next record','03':'previous record','04':'absolute/current'}[modeKey] || '';
|
||||||
@@ -3262,7 +3350,7 @@ const SW_MAP = {
|
|||||||
'62F3': 'Warning: Response data available',
|
'62F3': 'Warning: Response data available',
|
||||||
'63F1': 'Warning: More data expected',
|
'63F1': 'Warning: More data expected',
|
||||||
'63F2': 'Warning: More data expected and proactive command pending',
|
'63F2': 'Warning: More data expected and proactive command pending',
|
||||||
'63CX': 'Verification failed, X retries remaining (SW2 & 0x0F = tries)',
|
'63CX': 'Verification failed, X retries remaining (SW2 & 0x0F = tries; X=0: no counter provided / blocked for VERIFY)',
|
||||||
'6400': 'No information given, state of NV memory unchanged',
|
'6400': 'No information given, state of NV memory unchanged',
|
||||||
'6500': 'No information given, state of NV memory changed',
|
'6500': 'No information given, state of NV memory changed',
|
||||||
'6581': 'Memory problem',
|
'6581': 'Memory problem',
|
||||||
@@ -3285,7 +3373,10 @@ const SW_MAP = {
|
|||||||
'6A84': 'Not enough memory space',
|
'6A84': 'Not enough memory space',
|
||||||
'6A86': 'Incorrect parameters P1-P2',
|
'6A86': 'Incorrect parameters P1-P2',
|
||||||
'6A87': 'Lc inconsistent with P1-P2',
|
'6A87': 'Lc inconsistent with P1-P2',
|
||||||
|
'6A85': 'Nc inconsistent with TLV data object',
|
||||||
'6A88': 'Referenced data not found',
|
'6A88': 'Referenced data not found',
|
||||||
|
'6A89': 'File already exists',
|
||||||
|
'6A8A': 'DF name already exists',
|
||||||
'6B00': 'Wrong parameter(s) P1-P2',
|
'6B00': 'Wrong parameter(s) P1-P2',
|
||||||
'6D00': 'Instruction code not supported or invalid',
|
'6D00': 'Instruction code not supported or invalid',
|
||||||
'6E00': 'Class not supported',
|
'6E00': 'Class not supported',
|
||||||
@@ -3296,7 +3387,7 @@ const SW_MAP = {
|
|||||||
'9864': 'Minimum UICC suspension time is too long',
|
'9864': 'Minimum UICC suspension time is too long',
|
||||||
},
|
},
|
||||||
gp: {
|
gp: {
|
||||||
'6310': 'More data available — send GET STATUS [next] (P2=42)',
|
'6310': 'More data available — issue GET STATUS [get next occurrence]',
|
||||||
'6581': 'Memory failure',
|
'6581': 'Memory failure',
|
||||||
'6A80': 'Incorrect values in command data',
|
'6A80': 'Incorrect values in command data',
|
||||||
'6A81': 'Function not supported (card Life Cycle State is CARD_LOCKED)',
|
'6A81': 'Function not supported (card Life Cycle State is CARD_LOCKED)',
|
||||||
@@ -3313,7 +3404,8 @@ const SW_MAP = {
|
|||||||
'6283': 'Warning: Card Life Cycle State is CARD_LOCKED',
|
'6283': 'Warning: Card Life Cycle State is CARD_LOCKED',
|
||||||
},
|
},
|
||||||
uicc: {
|
uicc: {
|
||||||
'9100': 'Normal ending with proactive UICC command pending (SW2 = length of response data)',
|
'91XX': 'Normal ending with proactive UICC command pending (SW2 = length of response data)',
|
||||||
|
'9100': 'Normal ending with proactive UICC command pending (no response data)',
|
||||||
'92XX': 'Normal ending with ongoing data transfer session info',
|
'92XX': 'Normal ending with ongoing data transfer session info',
|
||||||
'9300': 'SIM Application Toolkit is busy',
|
'9300': 'SIM Application Toolkit is busy',
|
||||||
'62F1': 'More data available (ENVELOPE, RETRIEVE DATA, SET DATA)',
|
'62F1': 'More data available (ENVELOPE, RETRIEVE DATA, SET DATA)',
|
||||||
@@ -3381,10 +3473,11 @@ const SW_CMD_NAMES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LIFECYCLE_MAP = {
|
const LIFECYCLE_MAP = {
|
||||||
'01': 'LOADED (ELF)',
|
'01': 'OP_READY (card) / LOADED (ELF)',
|
||||||
'03': 'INSTALLED',
|
'03': 'INSTALLED',
|
||||||
'07': 'SELECTABLE',
|
'07': 'SELECTABLE',
|
||||||
'0F': 'PERSONALIZED (SD) / SECURED (card)',
|
'0F': 'SECURED (card)',
|
||||||
|
'1F': 'PERSONALIZED (SD)',
|
||||||
'7F': 'CARD_LOCKED',
|
'7F': 'CARD_LOCKED',
|
||||||
'FF': 'TERMINATED',
|
'FF': 'TERMINATED',
|
||||||
'83': 'LOCKED (SD)',
|
'83': 'LOCKED (SD)',
|
||||||
@@ -5681,6 +5774,12 @@ const LANG_RU = {
|
|||||||
'ELF AID (hex)': 'ELF AID (hex)',
|
'ELF AID (hex)': 'ELF AID (hex)',
|
||||||
'Module AID (hex)': 'AID модуля (hex)',
|
'Module AID (hex)': 'AID модуля (hex)',
|
||||||
'Security level (P1)': 'Уровень безопасности (P1)',
|
'Security level (P1)': 'Уровень безопасности (P1)',
|
||||||
|
'Target': 'Цель',
|
||||||
|
'Current EF (case 1)': 'Текущий EF (case 1)',
|
||||||
|
'By FID': 'По FID',
|
||||||
|
'Path from MF': 'Путь от MF',
|
||||||
|
'Path from current DF': 'Путь от текущего DF',
|
||||||
|
'Unblock PIN': 'Разблокировка PIN',
|
||||||
'Byte 1': 'Байт 1',
|
'Byte 1': 'Байт 1',
|
||||||
'Byte 2': 'Байт 2',
|
'Byte 2': 'Байт 2',
|
||||||
'SIM/UICC Toolkit parameters': 'Параметры SIM/UICC Toolkit',
|
'SIM/UICC Toolkit parameters': 'Параметры SIM/UICC Toolkit',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'otaman-v12';
|
const CACHE = 'otaman-v13';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ test('INTERNAL AUTHENTICATE', () => {
|
|||||||
assert.strictEqual(tree.children.length, 1);
|
assert.strictEqual(tree.children.length, 1);
|
||||||
const apdu = tree.children[0];
|
const apdu = tree.children[0];
|
||||||
assert.strictEqual(apdu.label, 'APDU');
|
assert.strictEqual(apdu.label, 'APDU');
|
||||||
assert.ok(findNode(apdu, 'INS').desc.includes('INTERNAL AUTHENTICATE'));
|
assert.ok(findNode(apdu, 'INS').desc.includes('AUTHENTICATE'));
|
||||||
assert.ok(findNode(apdu, 'Data'));
|
assert.ok(findNode(apdu, 'Data'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -328,3 +328,35 @@ test('CLA 84-87 labeled GlobalPlatform secure messaging', () => {
|
|||||||
const apdu = tree.children[0];
|
const apdu = tree.children[0];
|
||||||
assert.strictEqual(findNode(apdu, 'CLA').desc, 'GlobalPlatform (secure messaging)');
|
assert.strictEqual(findNode(apdu, 'CLA').desc, 'GlobalPlatform (secure messaging)');
|
||||||
});
|
});
|
||||||
|
test('New PARSE_INS labels decode (FETCH le, RESIZE D4, RETRIEVE CB, AUTH 89)', () => {
|
||||||
|
const fetchTree = parseHexTree('8012000020');
|
||||||
|
assert.ok(findNode(fetchTree.children[0], 'INS').desc.includes('FETCH'));
|
||||||
|
assert.ok(findNode(fetchTree.children[0], 'Le'));
|
||||||
|
|
||||||
|
const resizeTree = parseHexTree('80D4000004' + '00000FA0');
|
||||||
|
assert.ok(findNode(resizeTree.children[0], 'INS').desc.includes('RESIZE FILE'));
|
||||||
|
assert.strictEqual(findNode(resizeTree.children[0], 'Data').desc, '00000FA0');
|
||||||
|
|
||||||
|
const retrTree = parseHexTree('80CB000000');
|
||||||
|
assert.ok(findNode(retrTree.children[0], 'INS').desc.includes('RETRIEVE DATA'));
|
||||||
|
|
||||||
|
const authTree = parseHexTree('0089000008' + '0011223344556677');
|
||||||
|
assert.ok(findNode(authTree.children[0], 'INS').desc.includes('odd INS'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('READ BINARY SFI mode and offset descriptors', () => {
|
||||||
|
const sfi = parseHexTree('00B0830602');
|
||||||
|
const p1sfi = findNode(sfi.children[0], 'P1').desc;
|
||||||
|
assert.ok(p1sfi.includes('SFI') && p1sfi.includes('3'), p1sfi);
|
||||||
|
assert.strictEqual(findNode(sfi.children[0], 'P2').desc, 'offset low');
|
||||||
|
|
||||||
|
const off = parseHexTree('00B0010202');
|
||||||
|
assert.strictEqual(findNode(off.children[0], 'P1').desc, 'offset high');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SELECT P2 response field per ETSI b5-b3', () => {
|
||||||
|
const fcp = parseHexTree('00A40804047FFF6FC500');
|
||||||
|
assert.ok(findNode(fcp.children[0], 'P2').desc.includes('FCP'));
|
||||||
|
const none = parseHexTree('00A4000C026F3B');
|
||||||
|
assert.ok(findNode(none.children[0], 'P2').desc.includes('no response data'));
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
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 extractBlock(startMarker, endMarker) {
|
||||||
|
const start = html.indexOf(startMarker);
|
||||||
|
const end = html.indexOf(endMarker, start);
|
||||||
|
if (start < 0 || end < 0) throw new Error('block not found');
|
||||||
|
return html.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response parser maps + lookupSw live between SW_MAP and updateRespCmd.
|
||||||
|
// Rewrite top-level const -> var so the maps leak out of sloppy-mode eval.
|
||||||
|
eval(extractBlock('const SW_MAP = {', 'function updateRespCmd').replace(/^const /gm, 'var '));
|
||||||
|
|
||||||
|
test('lookupSw wildcard 91XX resolves proactive pending (any length)', () => {
|
||||||
|
const desc = lookupSw('91', '14', 'uicc');
|
||||||
|
assert.ok(desc.includes('proactive'), desc);
|
||||||
|
assert.ok(!desc.includes('no response data'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lookupSw 63CX extracts retry counter', () => {
|
||||||
|
const desc = lookupSw('63', 'C3', 'uicc');
|
||||||
|
assert.ok(desc.includes('3'), desc);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SW_MAP.generic covers ISO 6A85/6A89/6A8A', () => {
|
||||||
|
assert.ok(SW_MAP.generic['6A85']);
|
||||||
|
assert.ok(SW_MAP.generic['6A89']);
|
||||||
|
assert.ok(SW_MAP.generic['6A8A']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gp 6310 label does not reference removed P2=42 option', () => {
|
||||||
|
assert.ok(!SW_MAP.gp['6310'].includes('42'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('LIFECYCLE_MAP per GPC v2.3 life cycles', () => {
|
||||||
|
assert.strictEqual(LIFECYCLE_MAP['01'], 'OP_READY (card) / LOADED (ELF)');
|
||||||
|
assert.strictEqual(LIFECYCLE_MAP['03'], 'INSTALLED');
|
||||||
|
assert.strictEqual(LIFECYCLE_MAP['07'], 'SELECTABLE');
|
||||||
|
assert.strictEqual(LIFECYCLE_MAP['0F'], 'SECURED (card)');
|
||||||
|
assert.strictEqual(LIFECYCLE_MAP['1F'], 'PERSONALIZED (SD)');
|
||||||
|
assert.strictEqual(LIFECYCLE_MAP['7F'], 'CARD_LOCKED');
|
||||||
|
assert.strictEqual(LIFECYCLE_MAP['FF'], 'TERMINATED');
|
||||||
|
assert.strictEqual(LIFECYCLE_MAP['83'], 'LOCKED (SD)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PRIVILEGE_NAMES matches GPC v2.3 Tables 11-7/11-8/11-9', () => {
|
||||||
|
assert.strictEqual(PRIVILEGE_NAMES[0][7], 'Mandated DAP Verification');
|
||||||
|
assert.strictEqual(PRIVILEGE_NAMES[1][2], 'Token Verification');
|
||||||
|
assert.strictEqual(PRIVILEGE_NAMES[1][7], 'Global Service');
|
||||||
|
assert.strictEqual(PRIVILEGE_NAMES[2][0], 'Receipt Generation');
|
||||||
|
assert.strictEqual(PRIVILEGE_NAMES[2][3], 'Contactless Self-Activation');
|
||||||
|
});
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
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 = {};
|
||||||
|
this.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const els = {};
|
||||||
|
const doc = {
|
||||||
|
getElementById: (id) => {
|
||||||
|
if (!els[id]) els[id] = new StubEl();
|
||||||
|
return els[id];
|
||||||
|
},
|
||||||
|
querySelector: () => new StubEl(),
|
||||||
|
};
|
||||||
|
global.document = doc;
|
||||||
|
|
||||||
|
const FNS = ['buildApdu', 'buildOpNoCla', 'buildSelect', 'getSelValue', 'buildSelectApdu',
|
||||||
|
'updateSimUsimFields', 'updateP1P2Display', 'genSimUsim', 'OPS'];
|
||||||
|
let code = '';
|
||||||
|
for (const f of FNS) {
|
||||||
|
if (f === 'OPS') {
|
||||||
|
const m = html.match(/const OPS = \{[\s\S]*?\n\};/);
|
||||||
|
code += m[0].replace(/^const /, 'var ') + '\n';
|
||||||
|
} else {
|
||||||
|
code += extractFunc(html, f) + '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
code += '\nvar SIM_PRESETS = {};';
|
||||||
|
eval(code);
|
||||||
|
|
||||||
|
function set(id, v) { els[id] = Object.assign(new StubEl(), { value: v }); }
|
||||||
|
function setChecked(id, v) { els[id] = Object.assign(new StubEl(), { checked: v }); }
|
||||||
|
|
||||||
|
function setup(mode, opts) {
|
||||||
|
for (const k of Object.keys(els)) delete els[k];
|
||||||
|
set(mode + '-cmd', opts.cmd);
|
||||||
|
setChecked(mode + '-select', !!opts.doSelect);
|
||||||
|
set(mode + '-sel-method', opts.selMethod || 'fid');
|
||||||
|
set(mode + '-sel-fid', opts.fid || '');
|
||||||
|
set(mode + '-sel-path', opts.path || '');
|
||||||
|
set(mode + '-sel-dfname', opts.dfname || '');
|
||||||
|
set(mode + '-sel-chain', opts.chain || '');
|
||||||
|
set(mode + '-record', opts.record || '1');
|
||||||
|
set(mode + '-rec-mode', opts.recMode || '04');
|
||||||
|
set(mode + '-offset', opts.offset || '0000');
|
||||||
|
set(mode + '-le', opts.le || '00');
|
||||||
|
set(mode + '-data', opts.data || '');
|
||||||
|
set(mode + '-recsize', opts.recsize || '0');
|
||||||
|
set(mode + '-pin', opts.pin || '01');
|
||||||
|
set(mode + '-pin-val', opts.pinVal || '');
|
||||||
|
set(mode + '-pin-old', opts.pinOld || '');
|
||||||
|
set(mode + '-pin-new', opts.pinNew || '');
|
||||||
|
set(mode + '-act-target', opts.actTarget || 'current');
|
||||||
|
set(mode + '-act-file', opts.actFile || '');
|
||||||
|
set(mode + '-override', '');
|
||||||
|
els[mode + '-result'] = new StubEl();
|
||||||
|
els[mode + '-pack-btn'] = new StubEl();
|
||||||
|
genSimUsim(mode);
|
||||||
|
return els[mode + '-result'].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('VERIFY PIN FF-pads to 8 bytes (Lc=08)', () => {
|
||||||
|
const apdu = setup('usim', { cmd: 'verify', pin: '01', pinVal: '1234' });
|
||||||
|
assert.strictEqual(apdu, '002000010831323334FFFFFFFF');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CHANGE PIN emits two 8-byte fields (Lc=10)', () => {
|
||||||
|
const apdu = setup('usim', { cmd: 'change', pin: '01', pinOld: '1234', pinNew: '5678' });
|
||||||
|
assert.strictEqual(apdu, '002400011031323334FFFFFFFF35363738FFFFFFFF');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DISABLE PIN single padded PIN (INS 26)', () => {
|
||||||
|
const apdu = setup('usim', { cmd: 'disable', pin: '02', pinVal: '9999' });
|
||||||
|
assert.strictEqual(apdu, '002600020839393939FFFFFFFF');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ENABLE PIN single padded PIN (INS 28)', () => {
|
||||||
|
const apdu = setup('usim', { cmd: 'enable', pin: '01', pinVal: '1111' });
|
||||||
|
assert.strictEqual(apdu, '002800010831313131FFFFFFFF');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('UNBLOCK PIN two padded PINs (INS 2C)', () => {
|
||||||
|
const apdu = setup('usim', { cmd: 'unblock', pin: '02', pinOld: '12345678', pinNew: '4321' });
|
||||||
|
assert.strictEqual(apdu, '002C000210313233343536373834333231FFFFFFFF');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ACTIVATE FILE case-1 (no data, no Le)', () => {
|
||||||
|
const apdu = setup('usim', { cmd: 'activate-file' });
|
||||||
|
assert.strictEqual(apdu, '00440000');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DEACTIVATE FILE case-1', () => {
|
||||||
|
const apdu = setup('sim', { cmd: 'deactivate-file' });
|
||||||
|
assert.strictEqual(apdu, 'A0040000');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ACTIVATE FILE by FID (P1=00 + Lc/FID)', () => {
|
||||||
|
const apdu = setup('usim', { cmd: 'activate-file', actTarget: 'fid', actFile: '6F3B' });
|
||||||
|
assert.strictEqual(apdu, '00440000026F3B');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ACTIVATE FILE by path from MF (P1=08)', () => {
|
||||||
|
const apdu = setup('usim', { cmd: 'activate-file', actTarget: 'mfpath', actFile: '7FFF6FC5' });
|
||||||
|
assert.strictEqual(apdu, '00440800047FFF6FC5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DEACTIVATE FILE by path from current DF (P1=09, SIM CLA)', () => {
|
||||||
|
const apdu = setup('sim', { cmd: 'deactivate-file', actTarget: 'dfpath', actFile: '6F3B' });
|
||||||
|
assert.strictEqual(apdu, 'A0040900026F3B');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('READ RECORD next mode forces P1=00', () => {
|
||||||
|
const apdu = setup('usim', { cmd: 'read-record', recMode: '02', le: '20' });
|
||||||
|
assert.strictEqual(apdu, '00B2000220');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SELECT by FID USIM: P2=0C, no Le', () => {
|
||||||
|
const apdu = setup('usim', { cmd: 'select', selMethod: 'fid', fid: '6FC5' });
|
||||||
|
assert.strictEqual(apdu, '00A4000C026FC5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SELECT by path USIM: P2=04, Le=00', () => {
|
||||||
|
const apdu = setup('usim', { cmd: 'select', selMethod: 'path', path: '7FFF6FC5' });
|
||||||
|
assert.strictEqual(apdu, '00A40804047FFF6FC500');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SELECT by FID SIM (CLA A0): no Le anywhere', () => {
|
||||||
|
const apdu = setup('sim', { cmd: 'select', selMethod: 'fid', fid: '6FC5' });
|
||||||
|
assert.strictEqual(apdu, 'A0A40000026FC5');
|
||||||
|
});
|
||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-otaman-server"
|
name = "pysim-otaman-server"
|
||||||
version = "1.9.1"
|
version = "1.9.2"
|
||||||
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# 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
|
from osmocom.tlv import BER_TLV_IE
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.9.1'
|
VERSION = '1.9.2'
|
||||||
|
|
||||||
|
|
||||||
# Static file serving (the PWA lives in <repo>/frontend, served by this server
|
# Static file serving (the PWA lives in <repo>/frontend, served by this server
|
||||||
|
|||||||
Reference in New Issue
Block a user