github
@@ -2134,11 +2134,11 @@ const BER_QUAL = {
display: [
['00','Normal priority'],
['01','High priority'],
- ['80','Clear after delay']
+ ['80','Wait for user to clear message']
],
tone: [
- ['00','Monotonous tone'],
- ['01','Alternating tone']
+ ['00','Vibrate alert: up to terminal'],
+ ['01','Vibrate alert with tone']
]
};
const BER_TONES = [
@@ -2473,7 +2473,7 @@ function genBerPcValue(row, tag) {
const enc = pc.querySelector('.ber-pc-enc').value;
if (text) {
if (enc === 'ucs2') {
- const buf = [];
+ const buf = [0x08];
for (const c of text) { const cd=c.charCodeAt(0); buf.push((cd>>8)&0xFF,cd&0xFF); }
value += '8D' + berLenStr(buf.length) + buf.map(b=>b.toString(16).padStart(2,'0').toUpperCase()).join('');
} else {
@@ -2481,9 +2481,9 @@ function genBerPcValue(row, tag) {
const septets = gsm7TextToSeptets(text);
const packed = gsm7Encode(septets);
const inner = Array.from(packed).map(b=>b.toString(16).padStart(2,'0').toUpperCase());
- value += '8D' + berLenStr(inner.length) + inner.join('');
+ value += '8D' + berLenStr(inner.length + 1) + '00' + inner.join('');
} catch(e) {
- value += '8D' + berLenStr(text.length) + text.split('').map(c=>c.charCodeAt(0).toString(16).padStart(2,'0').toUpperCase()).join('');
+ value += '8D' + berLenStr(text.length + 1) + '04' + text.split('').map(c=>c.charCodeAt(0).toString(16).padStart(2,'0').toUpperCase()).join('');
}
}
}
@@ -2552,6 +2552,7 @@ const PARSE_INS = {
'F0': {name:'SET STATUS', data:'lc'},
'82': {name:'EXTERNAL AUTHENTICATE', data:'lc'},
'88': {name:'INTERNAL AUTHENTICATE', data:'lc'},
+ 'C0': {name:'GET RESPONSE', data:'le'},
};
function parseBerLen(hex, i) {
@@ -2638,6 +2639,31 @@ function gsm7Decode(octets) {
function decodeTextData(hex) {
if (!hex || hex.length < 2) return '?';
+ const dcs = parseInt(hex.substr(0, 2), 16);
+ const rest = hex.substr(2);
+ if (dcs === 0x08 && rest.length >= 4 && rest.length % 4 === 0) {
+ let ucs2 = '';
+ for (let i = 0; i < rest.length; i += 4) {
+ const cp = parseInt(rest.substr(i, 4), 16);
+ if (cp < 0x20 || cp > 0x10FFFF) return '?';
+ ucs2 += String.fromCodePoint(cp);
+ }
+ if (ucs2.length > 0) return ucs2;
+ }
+ if (dcs === 0x00 || dcs === 0x04) {
+ try {
+ const bytes = new Uint8Array(rest.length / 2);
+ for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(rest.substr(i * 2, 2), 16);
+ if (dcs === 0x00) {
+ const gsm = gsm7Decode(bytes);
+ if (gsm.length > 0 && !gsm.includes('\uFFFD')) return gsm;
+ } else {
+ let s = '';
+ for (const b of bytes) { if (b < 0x20 || b > 0x7E) return '?'; s += String.fromCharCode(b); }
+ if (s.length > 0) return s;
+ }
+ } catch (e) {}
+ }
if (hex.length >= 4 && hex.length % 4 === 0) {
let allPrintable = true;
let ucs2 = '';
@@ -2745,11 +2771,18 @@ function describeProactiveCommand(tlvs) {
let desc = '';
if (base === '01') {
const cmdNum = parseInt(tlv.value.substr(0, 2), 16);
+ const typeByte = tlv.value.substr(2, 2);
const qual = tlv.value.substr(4, 2);
+ const typeKey = { '01': 'refresh', '21': 'display', '20': 'tone' }[typeByte];
+ const cmdName = { refresh: 'REFRESH', display: 'DISPLAY TEXT', tone: 'PLAY TONE' }[typeKey];
let typeName = 'Command #' + cmdNum;
- for (const [k, arr] of Object.entries(BER_QUAL)) {
- const found = arr.find(([v]) => v === qual);
- if (found) { typeName += ', ' + k.toUpperCase() + ' qual ' + qual + ' ' + found[1]; break; }
+ if (typeKey && cmdName) {
+ const arr = BER_QUAL[typeKey];
+ const found = arr ? arr.find(([v]) => v === qual) : null;
+ if (found) typeName += ', ' + cmdName + ' qual ' + qual + ' ' + found[1];
+ else typeName += ', ' + cmdName + ' qual ' + qual;
+ } else {
+ typeName += ', type ' + typeByte + ' qual ' + qual;
}
desc = typeName;
} else if (base === '02') {
@@ -2841,6 +2874,10 @@ function parseChainingFlags(hex) {
function parseActionRow(hex, tag) {
if (!hex || hex === '00') return [{label:'No action', hex:hex||'', desc:tag === '82' ? 'No action (82 00)' : ''}];
if (hex.length === 2) {
+ const val = parseInt(hex, 16);
+ if (val >= 0x01 && val <= 0x7F) {
+ return [{label:'Reference to EFRMA record', hex, desc:'Record 0x' + hex}];
+ }
const names = {'81':'Proactive session indication','82':'Early response'};
return [{label:'Action indicator', hex, desc:names[hex] || 'Value ' + hex}];
}
@@ -2861,7 +2898,7 @@ function parseOneApdu(hex) {
const p3 = hex.substr(8, 2);
const insInfo = PARSE_INS[ins];
const children = [
- {label:'CLA', hex:cla, desc:cla === '80' ? 'GP/RAM' : cla === 'A0' ? 'SIM' : 'UICC'},
+ {label:'CLA', hex:cla, desc:cla === '80' ? 'GP/RAM' : cla === 'A0' ? 'SIM' : parseInt(cla,16) >= 0x84 && parseInt(cla,16) <= 0x87 ? 'GlobalPlatform (secure messaging)' : 'UICC'},
{label:'INS', hex:ins, desc:insInfo ? insInfo.name : 'Unknown'},
{label:'P1', hex:p1, desc:''},
{label:'P2', hex:p2, desc:''},
diff --git a/frontend/sw.js b/frontend/sw.js
index eac5e3c..eaf7ea7 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'otaman-v9';
+const CACHE = 'otaman-v10';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/ber.test.js b/frontend/tests/ber.test.js
index bdadd12..e6972ab 100644
--- a/frontend/tests/ber.test.js
+++ b/frontend/tests/ber.test.js
@@ -148,14 +148,14 @@ test('genBerPcValue DISPLAY TEXT GSM7 "HI"', () => {
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');
+ assert.strictEqual(val, '810E8103012101820281828D0300C824');
});
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');
+ assert.strictEqual(val, '81148103012101820281828D0508004800498402011E');
});
test('genBerPcValue duration disabled omits 84', () => {
@@ -208,7 +208,7 @@ test('genErrorActionValue proactive wraps proactive command', () => {
row.children[1].children.push(pc);
const val = genErrorActionValue(row, '82');
// DISPLAY TEXT UCS2 "HI" + 30s inside Error Action
- assert.strictEqual(val, '82138103012101820281828D04004800498402011E');
+ assert.strictEqual(val, '82148103012101820281828D0508004800498402011E');
});
test('genScriptChainingValue first emits 01', () => {
diff --git a/pyproject.toml b/pyproject.toml
index 6413756..1391e6f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-otaman-server"
-version = "1.8.1"
+version = "1.9.0"
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.
diff --git a/pysim_otaman_server/server.py b/pysim_otaman_server/server.py
index 8ed9015..82f2776 100644
--- a/pysim_otaman_server/server.py
+++ b/pysim_otaman_server/server.py
@@ -18,7 +18,7 @@ from osmocom.construct import GsmOrUcs2Adapter
from osmocom.tlv import BER_TLV_IE
-VERSION = '1.8.1'
+VERSION = '1.9.0'
# Static file serving (the PWA lives in
/frontend, served by this server