frontend: add HTTP OTA constructor (GP RAM-over-HTTP v1.1)
New 'HTTP OTA' sub-tab in the C-APDU constructor builds Remote Application
Management over HTTP payloads (GPC v2.2 Amendment B v1.1 §4.7):
- Trigger (Push SMS): 81 > 83 > 84/[85]/[86]/89, with optional Command
Scripting template ('AA') wrap per TS 102 226 §5.2.1
- Store (SD admin params): STORE DATA TLV mode (80 E2 90 00), tag 85/A5
- Connection parameters: OPEN CHANNEL comprehension-TLV row editor + presets
(Device Identities 02, Alpha 80, Bearer 01)
- Security parameters (Table 4-6): LV PSK Identity + LV KVN/KID
- Retry policy (Table 4-7): retry counter + TS 102 223 timer TLV (25 03)
- HTTP POST (Tables 4-8/9/10): 8A host / 8B agent / 8C URI, text→octets
- 'Pack into Secured packet' reuses packToSp for SPI/counter filling
Pure builder functions covered by 12 new tests reproducing the byte layout
from the spec tables. i18n entries, help.html/help-ru.html §2.7 added.
SW cache bumped otaman-v49 → v50.
This commit is contained in:
+21
-2
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
<section class="mb-10">
|
<section class="mb-10">
|
||||||
<h2 id="c-apdu" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">2. Вкладка C-APDU</h2>
|
<h2 id="c-apdu" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">2. Вкладка C-APDU</h2>
|
||||||
<p class="mb-3">Построение командных APDU (C-APDU). Пять подвкладок охватывают разные поколения карт и наборы команд: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong> и <strong>C-APDU Parser</strong>.</p>
|
<p class="mb-3">Построение командных APDU (C-APDU). Шесть подвкладок охватывают разные поколения карт и наборы команд: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong> и <strong>C-APDU Parser</strong>.</p>
|
||||||
|
|
||||||
<h3 id="sim-rfm" class="text-lg font-medium mb-2">2.1 SIM RFM</h3>
|
<h3 id="sim-rfm" class="text-lg font-medium mb-2">2.1 SIM RFM</h3>
|
||||||
<p class="mb-2">CLA = <code class="font-mono text-sm">A0</code> (GSM 11.11 / TS 151 011, ISO 7816-4). Удалённое управление файлами классических SIM-карт.</p>
|
<p class="mb-2">CLA = <code class="font-mono text-sm">A0</code> (GSM 11.11 / TS 151 011, ISO 7816-4). Удалённое управление файлами классических SIM-карт.</p>
|
||||||
@@ -204,7 +204,26 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 id="c-apdu-parser" class="text-lg font-medium mb-2">2.6 C-APDU Parser</h3>
|
<h3 id="c-apdu-parser" class="text-lg font-medium mb-2">2.6 C-APDU Parser</h3>
|
||||||
<p class="text-sm mb-3">Вставка raw APDU hex и отображение сворачиваемого дерева. Автоматически определяет контейнер: <strong>Expanded Script</strong> (начало <code class="font-mono text-sm">AA</code> или <code class="font-mono text-sm">AE80</code>, декодируется по ETSI TS 102 226 §5.2.1) или <strong>Compact C-APDU chain</strong> (последовательность C-APDU ISO 7816). Каждый узел показывает метку, hex и краткое описание; родительские узлы раскрываются в подэлементы.
|
<p class="text-sm mb-3">Вставка raw APDU hex и отображение сворачиваемого дерева. Автоматически определяет контейнер: <strong>Expanded Script</strong> (начало <code class="font-mono text-sm">AA</code> или <code class="font-mono text-sm">AE80</code>, декодируется по ETSI TS 102 226 §5.2.1) или <strong>Compact C-APDU chain</strong> (последовательность C-APDU ISO 7816). Каждый узел показывает метку, hex и краткое описание; родительские узлы раскрываются в подэлементы.</p>
|
||||||
|
|
||||||
|
<h3 id="http-ota" class="text-lg font-medium mb-2">2.7 HTTP OTA</h3>
|
||||||
|
<p class="text-sm mb-3">Сборка payload’ов Remote Application Management over HTTP по GlobalPlatform <strong>GPC v2.2 Amendment B v1.1</strong> (§4.7). Два режима:</p>
|
||||||
|
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||||
|
<li><strong>Trigger (Push SMS)</strong> — параметры запуска административной сессии (<code class="font-mono text-sm">81 > 83 > 84/[85]/[86]/89</code>, таблица 4-3). Сообщение, которое просит Security Domain карты исходящим запросом начать HTTP-сессию.</li>
|
||||||
|
<li><strong>Store (SD admin params)</strong> — записывает те же параметры как данные карты (параметры Security Domain) командой <strong>STORE DATA в TLV-режиме</strong> (<code class="font-mono text-sm">80 E2 90 00</code>, P1=90 = последний блок + BER-TLV по GP v2.2 Amendment B v1.1.3), обёрнутые в тег <code class="font-mono text-sm">85</code> (или <code class="font-mono text-sm">A5</code>) по таблице 4-4.</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-sm mb-2">Разделы соответствуют таблицам спецификации:</p>
|
||||||
|
<table class="w-full text-sm mb-3 border-collapse">
|
||||||
|
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Раздел</th><th class="text-left py-1 px-2">Tag</th><th class="text-left py-1 px-2">Содержимое</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Параметры соединения</td><td class="py-1 px-2 font-mono">84</td><td class="py-1 px-2">Любые COMPREHENSION-TLV для открытия TCP-соединения (OPEN CHANNEL по TS 102 223): Device Identities <code class="font-mono text-sm">02</code>, Alpha <code class="font-mono text-sm">80</code>, Bearer <code class="font-mono text-sm">01</code>, вендорские TLV. Редактор строк + пресеты, редактируемый hex.</td></tr>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Параметры безопасности</td><td class="py-1 px-2 font-mono">85</td><td class="py-1 px-2">Таблица 4-6: LV PSK Identity (текст), LV Key version/KID. Идентифицирует ключ PSK TLS (RFC 4279).</td></tr>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Политика повтора</td><td class="py-1 px-2 font-mono">86</td><td class="py-1 px-2">Таблица 4-7: счётчик повторов (2 байта, напр. <code class="font-mono text-sm">B000</code>), задержка повторной попытки как timer TLV из TS 102 223 (<code class="font-mono text-sm">25 03 HH MM SS</code>), опциональный вендорский TLV отчёта о сбое.</td></tr>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">HTTP POST</td><td class="py-1 px-2 font-mono">89</td><td class="py-1 px-2">Таблицы 4-8/9/10: Host-заголовок (<code class="font-mono text-sm">8A</code>), X-Admin-From agent ID (<code class="font-mono text-sm">8B</code>), URI (<code class="font-mono text-sm">8C</code>) — текст преобразуется в октеты.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="text-sm mb-2"><strong>Command Scripting template</strong> (флажок) оборачивает всю команду <code class="font-mono text-sm">81</code> в формат Expanded Remote Application data с определённой длиной (<code class="font-mono text-sm">AA</code>, ETSI TS 102 226 §5.2.1) для TAR-ов, обрабатывающих расширенный формат (RAM-over-HTTP §4.7).</p>
|
||||||
|
<p class="text-sm mb-2"><strong>Pack into Secured packet</strong> отправляет готовый payload на вкладку SCP80 для заполнения SPI/счётчика — там укажите TAR, который слушает SD (обычно TAR OTASD).</p>
|
||||||
|
|
||||||
|
|
||||||
<section class="mb-10">
|
<section class="mb-10">
|
||||||
|
|||||||
+21
-2
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
<section class="mb-10">
|
<section class="mb-10">
|
||||||
<h2 id="c-apdu" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">2. C-APDU tab</h2>
|
<h2 id="c-apdu" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">2. C-APDU tab</h2>
|
||||||
<p class="mb-3">Builds command APDUs (C-APDUs). Five sub-tabs cover different card generations and command sets: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, and <strong>C-APDU Parser</strong>.</p>
|
<p class="mb-3">Builds command APDUs (C-APDUs). Six sub-tabs cover different card generations and command sets: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong>, and <strong>C-APDU Parser</strong>.</p>
|
||||||
|
|
||||||
<h3 id="sim-rfm" class="text-lg font-medium mb-2">2.1 SIM RFM</h3>
|
<h3 id="sim-rfm" class="text-lg font-medium mb-2">2.1 SIM RFM</h3>
|
||||||
<p class="mb-2">CLA = <code class="font-mono text-sm">A0</code> (GSM 11.11 / TS 151 011, ISO 7816-4). Remote File Management for classic SIM cards.</p>
|
<p class="mb-2">CLA = <code class="font-mono text-sm">A0</code> (GSM 11.11 / TS 151 011, ISO 7816-4). Remote File Management for classic SIM cards.</p>
|
||||||
@@ -204,7 +204,26 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 id="c-apdu-parser" class="text-lg font-medium mb-2">2.6 C-APDU Parser</h3>
|
<h3 id="c-apdu-parser" class="text-lg font-medium mb-2">2.6 C-APDU Parser</h3>
|
||||||
<p class="text-sm mb-3">Pastes raw APDU hex and renders a collapsible tree. It auto-detects the container: an <strong>Expanded Script</strong> (leading <code class="font-mono text-sm">AA</code> or <code class="font-mono text-sm">AE80</code>, decoded per ETSI TS 102 226 §5.2.1) or a <strong>Compact C-APDU chain</strong> (a sequence of ISO 7816 C-APDUs). Each node shows its label, hex and a short description; parent nodes expand to reveal their sub-elements.
|
<p class="text-sm mb-3">Pastes raw APDU hex and renders a collapsible tree. It auto-detects the container: an <strong>Expanded Script</strong> (leading <code class="font-mono text-sm">AA</code> or <code class="font-mono text-sm">AE80</code>, decoded per ETSI TS 102 226 §5.2.1) or a <strong>Compact C-APDU chain</strong> (a sequence of ISO 7816 C-APDUs). Each node shows its label, hex and a short description; parent nodes expand to reveal their sub-elements.</p>
|
||||||
|
|
||||||
|
<h3 id="http-ota" class="text-lg font-medium mb-2">2.7 HTTP OTA</h3>
|
||||||
|
<p class="text-sm mb-3">Builds the Remote Application Management over HTTP payloads defined in GlobalPlatform <strong>GPC v2.2 Amendment B v1.1</strong> (§4.7). Two modes:</p>
|
||||||
|
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||||
|
<li><strong>Trigger (Push SMS)</strong> — administration session triggering parameters (<code class="font-mono text-sm">81 > 83 > 84/[85]/[86]/89</code>, Table 4-3). This is the message that asks the card's Security Domain to dial out and start an HTTP session.</li>
|
||||||
|
<li><strong>Store (SD admin params)</strong> — writes the same parameters as card (Security Domain) data via <strong>STORE DATA in TLV mode</strong> (<code class="font-mono text-sm">80 E2 90 00</code>, P1=90 = last block + BER-TLV per GP v2.2 Amendment B v1.1.3), wrapped in tag <code class="font-mono text-sm">85</code> (or <code class="font-mono text-sm">A5</code>) per Table 4-4.</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-sm mb-2">Sections mirror the spec tables:</p>
|
||||||
|
<table class="w-full text-sm mb-3 border-collapse">
|
||||||
|
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Section</th><th class="text-left py-1 px-2">Tag</th><th class="text-left py-1 px-2">Contents</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Connection parameters</td><td class="py-1 px-2 font-mono">84</td><td class="py-1 px-2">Any COMPREHENSION-TLV needed to open the TCP connection (OPEN CHANNEL per TS 102 223): Device Identities <code class="font-mono text-sm">02</code>, Alpha <code class="font-mono text-sm">80</code>, Bearer <code class="font-mono text-sm">01</code>, vendor TLVs. Row editor + presets, editable hex.</td></tr>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Security parameters</td><td class="py-1 px-2 font-mono">85</td><td class="py-1 px-2">Table 4-6: LV PSK Identity (text), LV Key version/KID. Identifies the PSK TLS key (RFC 4279).</td></tr>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Retry policy</td><td class="py-1 px-2 font-mono">86</td><td class="py-1 px-2">Table 4-7: retry counter (2 bytes, e.g. <code class="font-mono text-sm">B000</code>), retry waiting delay as the TS 102 223 timer TLV (<code class="font-mono text-sm">25 03 HH MM SS</code>), optional vendor-specific report-failure TLV.</td></tr>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">HTTP POST</td><td class="py-1 px-2 font-mono">89</td><td class="py-1 px-2">Tables 4-8/9/10: Host header (<code class="font-mono text-sm">8A</code>), X-Admin-From agent ID (<code class="font-mono text-sm">8B</code>), URI (<code class="font-mono text-sm">8C</code>) — text converted to octets.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="text-sm mb-2"><strong>Command Scripting template</strong> (checkbox) wraps the whole <code class="font-mono text-sm">81</code> triggering command in the definite-length Expanded Remote Application data format (<code class="font-mono text-sm">AA</code>, ETSI TS 102 226 §5.2.1) for TARs that process the expanded format (RAM-over-HTTP §4.7).</p>
|
||||||
|
<p class="text-sm mb-2"><strong>Pack into Secured packet</strong> sends the built payload to the SCP80 tab for SPI/counter filling — insert the TAR the SD listens on (typically the OTASD TAR) there.</p>
|
||||||
|
|
||||||
|
|
||||||
<section class="mb-10">
|
<section class="mb-10">
|
||||||
|
|||||||
+305
-3
@@ -43,7 +43,8 @@
|
|||||||
<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>
|
||||||
<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="ber" onclick="cApduSwitchSubtab('ber')">Expanded Script</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="ber" onclick="cApduSwitchSubtab('ber')">Expanded Script</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="ram" onclick="cApduSwitchSubtab('ram')">RAM/GP</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="ram" onclick="cApduSwitchSubtab('ram')">RAM/GP</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="parse" onclick="cApduSwitchSubtab('parse')" data-l10n="C-APDU Parser">C-APDU Parser</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="httpota" onclick="cApduSwitchSubtab('httpota')">HTTP OTA</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="parse" onclick="cApduSwitchSubtab('parse')" data-l10n="C-APDU Parser">C-APDU Parser</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="c-apdu-sub-sim">
|
<div id="c-apdu-sub-sim">
|
||||||
<div id="chain-sim-rows"></div>
|
<div id="chain-sim-rows"></div>
|
||||||
@@ -142,6 +143,119 @@
|
|||||||
<button onclick="parseHex()" class="mb-3 px-5 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700" data-l10n="Decode">Decode</button>
|
<button onclick="parseHex()" class="mb-3 px-5 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700" data-l10n="Decode">Decode</button>
|
||||||
<div id="parse-output"></div>
|
<div id="parse-output"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="c-apdu-sub-httpota" class="hidden">
|
||||||
|
<div class="flex gap-2 mb-3">
|
||||||
|
<button id="hota-mode-trigger" onclick="hotaSetMode('trigger')" class="px-4 py-1.5 text-sm rounded-full bg-blue-600 text-white" data-l10n="Trigger (Push SMS)">Trigger (Push SMS)</button>
|
||||||
|
<button id="hota-mode-store" onclick="hotaSetMode('store')" class="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-l10n="Store (SD admin params)">Store (SD admin params)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="hota-store-tag-row" class="mb-3 hidden">
|
||||||
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="SD params tag">SD params tag</label>
|
||||||
|
<select id="hota-store-tag" onchange="hotaUpdatePreview()" 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="85">'85' — GP v2.2 Amendment B v1.1 (Table 4-4)</option>
|
||||||
|
<option value="A5">'A5' — GP v2.2 Amendment B v1.1.3</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800 mb-3">
|
||||||
|
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
|
||||||
|
<input type="checkbox" id="hota-include-conn" checked onchange="hotaUpdatePreview()" class="rounded">
|
||||||
|
<span data-l10n="Connection parameters ('84')">Connection parameters ('84')</span>
|
||||||
|
</label>
|
||||||
|
<div id="hota-conn-tlvs" class="hidden"></div>
|
||||||
|
<div class="flex flex-wrap gap-1 mt-2">
|
||||||
|
<button onclick="hotaAddTlv()" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ TLV">+ TLV</button>
|
||||||
|
<button onclick="hotaAddPreset('devid')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ Device Identities">+ Device Identities</button>
|
||||||
|
<button onclick="hotaAddPreset('alpha')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ Alpha (empty)">+ Alpha (empty)</button>
|
||||||
|
<button onclick="hotaAddPreset('bearer')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ Bearer (GPRS)">+ Bearer (GPRS)</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800 mb-3">
|
||||||
|
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
|
||||||
|
<input type="checkbox" id="hota-include-sec" onchange="hotaUpdatePreview()" class="rounded">
|
||||||
|
<span data-l10n="Security parameters ('85')">Security parameters ('85')</span>
|
||||||
|
</label>
|
||||||
|
<div id="hota-sec-fields" class="hidden space-y-2">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="PSK Identity (text)">PSK Identity (text)</label>
|
||||||
|
<input id="hota-psk-text" 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="Test123">
|
||||||
|
</div>
|
||||||
|
<div class="w-16"><label class="block text-xs text-gray-600 dark:text-slate-400">KVN</label>
|
||||||
|
<input id="hota-kvn" maxlength="2" value="01" 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">
|
||||||
|
</div>
|
||||||
|
<div class="w-16"><label class="block text-xs text-gray-600 dark:text-slate-400">KID</label>
|
||||||
|
<input id="hota-kid" maxlength="2" value="01" 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">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800 mb-3">
|
||||||
|
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
|
||||||
|
<input type="checkbox" id="hota-include-retry" onchange="hotaUpdatePreview()" class="rounded">
|
||||||
|
<span data-l10n="Retry policy ('86')">Retry policy ('86')</span>
|
||||||
|
</label>
|
||||||
|
<div id="hota-retry-fields" class="hidden space-y-2">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Retry counter (B0,00=unlimited)">Retry counter (B0,00=unlimited)</label>
|
||||||
|
<input id="hota-retry-counter" maxlength="4" 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="B000">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="w-16"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Hours">Hours</label>
|
||||||
|
<input id="hota-delay-h" type="number" min="0" max="255" 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="0">
|
||||||
|
</div>
|
||||||
|
<div class="w-16"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Minutes">Minutes</label>
|
||||||
|
<input id="hota-delay-m" type="number" min="0" max="59" 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="0">
|
||||||
|
</div>
|
||||||
|
<div class="w-16"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Seconds">Seconds</label>
|
||||||
|
<input id="hota-delay-s" type="number" min="0" max="59" 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="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 items-end">
|
||||||
|
<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Report failure (vendor-specific TLV)">Report failure (vendor-specific TLV)</label>
|
||||||
|
<input id="hota-retry-report" maxlength="40" 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="0A08...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800 mb-3">
|
||||||
|
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
|
||||||
|
<input type="checkbox" id="hota-include-http" onchange="hotaUpdatePreview()" class="rounded">
|
||||||
|
<span data-l10n="HTTP POST ('89', 8A=host,8B=agent,8C=URI)">HTTP POST ('89', 8A=host,8B=agent,8C=URI)</span>
|
||||||
|
</label>
|
||||||
|
<div id="hota-http-fields" class="hidden space-y-2">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Host (text→hex)">Host (text→hex)</label>
|
||||||
|
<input id="hota-host" 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="megafon.ru">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="HTTP agent ID (text, X-Admin-From)">HTTP agent ID (text, X-Admin-From)</label>
|
||||||
|
<input id="hota-agent" 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="v1.0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="URI (text→hex)">URI (text→hex)</label>
|
||||||
|
<input id="hota-uri" 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="http://megafon.ru/sdp">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800 mb-3 hidden" id="hota-expanded-row">
|
||||||
|
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300">
|
||||||
|
<input type="checkbox" id="hota-expanded" onchange="hotaUpdatePreview()" class="rounded">
|
||||||
|
<span data-l10n="Wrapped in Command Scripting template ('AA')">Wrapped in Command Scripting template ('AA')</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea id="hota-preview" rows="5" readonly oninput="hotaPreviewEdited()" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800 mb-3" placeholder="RAM-over-HTTP payload will appear here..."></textarea>
|
||||||
|
<button onclick="packToSp('hota-preview')" id="hota-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 class="w-1/4">
|
<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">
|
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800">
|
||||||
@@ -874,12 +988,178 @@ function cApduSwitchSubtab(name) {
|
|||||||
btn.classList.toggle('text-gray-700', !active);
|
btn.classList.toggle('text-gray-700', !active);
|
||||||
btn.classList.toggle('dark:text-slate-300', !active);
|
btn.classList.toggle('dark:text-slate-300', !active);
|
||||||
});
|
});
|
||||||
['sim', 'usim', 'ber', 'ram', 'parse'].forEach(s => {
|
['sim', 'usim', 'ber', 'ram', 'parse', 'httpota'].forEach(s => {
|
||||||
const el = document.getElementById('c-apdu-sub-' + s);
|
const el = document.getElementById('c-apdu-sub-' + s);
|
||||||
if (el) el.classList.toggle('hidden', s !== name);
|
if (el) el.classList.toggle('hidden', s !== name);
|
||||||
});
|
});
|
||||||
setHelpAnchor({sim:'sim-rfm', usim:'usim-rfm', ber:'ber-tlv', ram:'ram-gp', parse:'c-apdu'}[name] || 'c-apdu');
|
setHelpAnchor({sim:'sim-rfm', usim:'usim-rfm', ber:'ber-tlv', ram:'ram-gp', parse:'c-apdu', httpota:'http-ota'}[name] || 'c-apdu');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== HTTP OTA constructor =====
|
||||||
|
let _hotaMode = 'trigger';
|
||||||
|
function _hotaHex(s) { return (s || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); }
|
||||||
|
function _hotaAsciiHex(s) { return (s || '').split('').map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('').toUpperCase(); }
|
||||||
|
function _hotaBerLen(n) {
|
||||||
|
if (n < 128) return n.toString(16).padStart(2, '0').toUpperCase();
|
||||||
|
if (n < 256) return '81' + n.toString(16).padStart(2, '0').toUpperCase();
|
||||||
|
return '82' + n.toString(16).padStart(4, '0').toUpperCase();
|
||||||
|
}
|
||||||
|
function _hotaTlv(tag8, val8) { const v = _hotaHex(val8); return tag8.toUpperCase() + _hotaBerLen(v.length / 2) + v; }
|
||||||
|
|
||||||
|
function hotaTlv(tag, value) { return _hotaTlv(tag, value); }
|
||||||
|
function hotaBuildConn(tlvs) {
|
||||||
|
let content = '';
|
||||||
|
for (const t of tlvs) {
|
||||||
|
const v = _hotaHex(t.value);
|
||||||
|
if (!t.tag) continue;
|
||||||
|
content += t.tag.toUpperCase() + _hotaBerLen(v.length / 2) + v;
|
||||||
|
}
|
||||||
|
return '84' + _hotaBerLen(content.length / 2) + content;
|
||||||
|
}
|
||||||
|
function hotaBuildSec(s) {
|
||||||
|
const psk = _hotaAsciiHex(s.pskIdentity);
|
||||||
|
const kvn = _hotaHex(s.kvn).padStart(2, '0');
|
||||||
|
const kid = _hotaHex(s.kid).padStart(2, '0');
|
||||||
|
const data = (psk.length / 2).toString(16).padStart(2, '0').toUpperCase() + psk + '02' + kvn + kid;
|
||||||
|
return '85' + _hotaBerLen(data.length / 2) + data;
|
||||||
|
}
|
||||||
|
function hotaBuildRetry(r) {
|
||||||
|
const counter = _hotaHex(r.counter);
|
||||||
|
const hh = (parseInt(r.delayH) || 0).toString(16).padStart(2, '0').toUpperCase();
|
||||||
|
const mm = (parseInt(r.delayM) || 0).toString(16).padStart(2, '0').toUpperCase();
|
||||||
|
const ss = (parseInt(r.delayS) || 0).toString(16).padStart(2, '0').toUpperCase();
|
||||||
|
const timerTlv = '2503' + hh + mm + ss;
|
||||||
|
const report = _hotaHex(r.reportFailure);
|
||||||
|
const content = counter + timerTlv + (report && report.length % 2 === 0 ? report : '');
|
||||||
|
return '86' + _hotaBerLen(content.length / 2) + content;
|
||||||
|
}
|
||||||
|
function hotaBuildHttpPost(h) {
|
||||||
|
const host = _hotaAsciiHex(h.host);
|
||||||
|
const agent = _hotaAsciiHex(h.agent);
|
||||||
|
const uri = _hotaAsciiHex(h.uri);
|
||||||
|
const content = _hotaTlv('8A', host) + _hotaTlv('8B', agent) + _hotaTlv('8C', uri);
|
||||||
|
return '89' + _hotaBerLen(content.length / 2) + content;
|
||||||
|
}
|
||||||
|
function hotaBuildTrigger(conn, sec, retry, httpPost, expanded) {
|
||||||
|
let content = conn;
|
||||||
|
if (sec) content += sec;
|
||||||
|
if (retry) content += retry;
|
||||||
|
if (httpPost) content += httpPost;
|
||||||
|
const sd = '83' + _hotaBerLen(content.length / 2) + content;
|
||||||
|
const trig = '81' + _hotaBerLen(sd.length / 2) + sd;
|
||||||
|
if (expanded) return 'AA' + _hotaBerLen(trig.length / 2) + trig;
|
||||||
|
return trig;
|
||||||
|
}
|
||||||
|
function hotaBuildStore(conn, sec, retry, httpPost, storeTag) {
|
||||||
|
const inner = _hotaTlv(storeTag, conn + (sec || '') + (retry || '') + (httpPost || ''));
|
||||||
|
return '80E29000' + _hotaBerLen(inner.length / 2) + inner;
|
||||||
|
}
|
||||||
|
function hotaBuild(mode, conn, sec, retry, httpPost, storeTag, expanded) {
|
||||||
|
return mode === 'store'
|
||||||
|
? hotaBuildStore(conn, sec, retry, httpPost, storeTag)
|
||||||
|
: hotaBuildTrigger(conn, sec, retry, httpPost, expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hotaSetMode(mode) {
|
||||||
|
_hotaMode = mode;
|
||||||
|
document.getElementById('hota-mode-trigger').className = 'px-4 py-1.5 text-sm rounded-full ' + (mode === 'trigger' ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600');
|
||||||
|
document.getElementById('hota-mode-store').className = 'px-4 py-1.5 text-sm rounded-full ' + (mode === 'store' ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600');
|
||||||
|
document.getElementById('hota-store-tag-row').classList.toggle('hidden', mode !== 'store');
|
||||||
|
document.getElementById('hota-expanded-row').classList.toggle('hidden', mode !== 'trigger');
|
||||||
|
hotaUpdatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hotaToggleConn() {
|
||||||
|
const on = document.getElementById('hota-include-conn').checked;
|
||||||
|
document.getElementById('hota-conn-tlvs').classList.toggle('hidden', !on);
|
||||||
|
hotaUpdatePreview();
|
||||||
|
}
|
||||||
|
document.addEventListener('change', function(e) {
|
||||||
|
if (e.target && e.target.id === 'hota-include-conn') hotaToggleConn();
|
||||||
|
else if (e.target && e.target.id === 'hota-include-sec') document.getElementById('hota-sec-fields').classList.toggle('hidden', !e.target.checked);
|
||||||
|
else if (e.target && e.target.id === 'hota-include-retry') document.getElementById('hota-retry-fields').classList.toggle('hidden', !e.target.checked);
|
||||||
|
else if (e.target && e.target.id === 'hota-include-http') document.getElementById('hota-http-fields').classList.toggle('hidden', !e.target.checked);
|
||||||
|
});
|
||||||
|
hotaToggleConn();
|
||||||
|
|
||||||
|
function hotaAddTlv() {
|
||||||
|
const container = document.getElementById('hota-conn-tlvs');
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'flex gap-1 items-center mb-1';
|
||||||
|
row.innerHTML =
|
||||||
|
'<input maxlength="2" oninput="hotaUpdatePreview()" class="font-mono w-14 border border-gray-300 dark:border-slate-600 text-sm rounded px-1.5 py-1 dark:bg-slate-800" placeholder="Tag">' +
|
||||||
|
'<input oninput="hotaUpdatePreview()" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-1.5 py-1 dark:bg-slate-800" placeholder="Value">' +
|
||||||
|
'<button onclick="this.parentElement.remove();hotaUpdatePreview()" class="text-gray-400 hover:text-red-500 px-1" title="Delete">×</button>';
|
||||||
|
container.appendChild(row);
|
||||||
|
hotaUpdatePreview();
|
||||||
|
}
|
||||||
|
function hotaAddPreset(name) {
|
||||||
|
const container = document.getElementById('hota-conn-tlvs');
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'flex gap-1 items-center mb-1';
|
||||||
|
const presets = {
|
||||||
|
devid: ['02', '8182'],
|
||||||
|
alpha: ['80', ''],
|
||||||
|
bearer: ['01', '014001'],
|
||||||
|
};
|
||||||
|
const [tag, value] = presets[name] || ['', ''];
|
||||||
|
row.innerHTML =
|
||||||
|
'<input maxlength="2" value="' + tag + '" oninput="hotaUpdatePreview()" class="font-mono w-14 border border-gray-300 dark:border-slate-600 text-sm rounded px-1.5 py-1 dark:bg-slate-800" placeholder="Tag">' +
|
||||||
|
'<input value="' + value + '" oninput="hotaUpdatePreview()" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-1.5 py-1 dark:bg-slate-800" placeholder="Value">' +
|
||||||
|
'<button onclick="this.parentElement.remove();hotaUpdatePreview()" class="text-gray-400 hover:text-red-500 px-1" title="Delete">×</button>';
|
||||||
|
container.appendChild(row);
|
||||||
|
hotaUpdatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hotaReadConnTlvs() {
|
||||||
|
const tlvs = [];
|
||||||
|
document.querySelectorAll('#hota-conn-tlvs .flex').forEach(row => {
|
||||||
|
const inputs = row.querySelectorAll('input');
|
||||||
|
if (inputs.length >= 2) tlvs.push({ tag: _hotaHex(inputs[0].value), value: inputs[1].value });
|
||||||
|
});
|
||||||
|
return tlvs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hotaUpdatePreview() {
|
||||||
|
const connOn = document.getElementById('hota-include-conn').checked;
|
||||||
|
const secOn = document.getElementById('hota-include-sec').checked;
|
||||||
|
const retryOn = document.getElementById('hota-include-retry').checked;
|
||||||
|
const httpOn = document.getElementById('hota-include-http').checked;
|
||||||
|
const expanded = document.getElementById('hota-expanded').checked;
|
||||||
|
const storeTag = document.getElementById('hota-store-tag').value;
|
||||||
|
const conn = connOn ? hotaBuildConn(hotaReadConnTlvs()) : '';
|
||||||
|
const sec = secOn ? hotaBuildSec({
|
||||||
|
pskIdentity: document.getElementById('hota-psk-text').value,
|
||||||
|
kvn: document.getElementById('hota-kvn').value,
|
||||||
|
kid: document.getElementById('hota-kid').value,
|
||||||
|
}) : '';
|
||||||
|
const retry = retryOn ? hotaBuildRetry({
|
||||||
|
counter: document.getElementById('hota-retry-counter').value,
|
||||||
|
delayH: document.getElementById('hota-delay-h').value,
|
||||||
|
delayM: document.getElementById('hota-delay-m').value,
|
||||||
|
delayS: document.getElementById('hota-delay-s').value,
|
||||||
|
reportFailure: document.getElementById('hota-retry-report').value,
|
||||||
|
}) : '';
|
||||||
|
const http = httpOn ? hotaBuildHttpPost({
|
||||||
|
host: document.getElementById('hota-host').value,
|
||||||
|
agent: document.getElementById('hota-agent').value,
|
||||||
|
uri: document.getElementById('hota-uri').value,
|
||||||
|
}) : '';
|
||||||
|
const hex = hotaBuild(_hotaMode, conn, sec, retry, http, storeTag, expanded);
|
||||||
|
const preview = document.getElementById('hota-preview');
|
||||||
|
preview.value = hex;
|
||||||
|
document.getElementById('hota-pack-btn').disabled = !hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hotaPreviewEdited() {
|
||||||
|
const hex = document.getElementById('hota-preview').value;
|
||||||
|
document.getElementById('hota-pack-btn').disabled = !hex;
|
||||||
|
}
|
||||||
|
document.querySelectorAll('#c-apdu-sub-httpota input, #c-apdu-sub-httpota select').forEach(el => {
|
||||||
|
if (el.type === 'checkbox') return;
|
||||||
|
el.addEventListener('input', hotaUpdatePreview);
|
||||||
|
});
|
||||||
|
|
||||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
|
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
|
||||||
});
|
});
|
||||||
@@ -7530,6 +7810,28 @@ const LANG_RU = {
|
|||||||
'errors': 'ошибок',
|
'errors': 'ошибок',
|
||||||
'expected': 'ожидалось',
|
'expected': 'ожидалось',
|
||||||
'actual': 'фактически',
|
'actual': 'фактически',
|
||||||
|
'HTTP OTA': 'HTTP OTA',
|
||||||
|
'Trigger (Push SMS)': 'Триггер (Push SMS)',
|
||||||
|
'Store (SD admin params)': 'Store (параметры SD)',
|
||||||
|
'SD params tag': 'Tag параметров SD',
|
||||||
|
"Connection parameters ('84')": "Параметры соединения ('84')",
|
||||||
|
'+ TLV': '+ TLV',
|
||||||
|
'+ Device Identities': '+ Device Identities',
|
||||||
|
'+ Alpha (empty)': '+ Alpha (пустой)',
|
||||||
|
'+ Bearer (GPRS)': '+ Bearer (GPRS)',
|
||||||
|
"Security parameters ('85')": "Параметры безопасности ('85')",
|
||||||
|
'PSK Identity (text)': 'PSK Identity (текст)',
|
||||||
|
"Retry policy ('86')": "Политика повтора ('86')",
|
||||||
|
'Retry counter (B0,00=unlimited)': 'Счётчик повторов (B0,00=бесконечно)',
|
||||||
|
'Hours': 'Часы',
|
||||||
|
'Minutes': 'Минуты',
|
||||||
|
'Seconds': 'Секунды',
|
||||||
|
"Report failure (vendor-specific TLV)": "Отчёт о сбое (специфичный TLV)",
|
||||||
|
"HTTP POST ('89', 8A=host,8B=agent,8C=URI)": "HTTP POST ('89', 8A=host,8B=agent,8C=URI)",
|
||||||
|
'Host (text→hex)': 'Хост (текст→hex)',
|
||||||
|
"HTTP agent ID (text, X-Admin-From)": "ID HTTP агента (текст, X-Admin-From)",
|
||||||
|
'URI (text→hex)': 'URI (текст→hex)',
|
||||||
|
"Wrapped in Command Scripting template ('AA')": "Обёртка в Command Scripting template ('AA')",
|
||||||
};
|
};
|
||||||
let currentLang = 'en';
|
let currentLang = 'en';
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'otaman-v49';
|
const CACHE = 'otaman-v50';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FNS = ['_hotaHex', '_hotaAsciiHex', '_hotaBerLen', '_hotaTlv',
|
||||||
|
'hotaTlv', 'hotaBuildConn', 'hotaBuildSec', 'hotaBuildRetry',
|
||||||
|
'hotaBuildHttpPost', 'hotaBuildTrigger', 'hotaBuildStore', 'hotaBuild'];
|
||||||
|
let code = '';
|
||||||
|
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||||
|
|
||||||
|
eval(code);
|
||||||
|
|
||||||
|
test('_hotaHex strips non-hex and uppercases', () => {
|
||||||
|
assert.strictEqual(_hotaHex(' 81 82 '), '8182');
|
||||||
|
assert.strictEqual(_hotaHex('b0-00'), 'B000');
|
||||||
|
assert.strictEqual(_hotaHex(''), '');
|
||||||
|
assert.strictEqual(_hotaHex(undefined), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_hotaAsciiHex encodes text as ASCII bytes', () => {
|
||||||
|
assert.strictEqual(_hotaAsciiHex('megafon.ru'), '6D656761666F6E2E7275');
|
||||||
|
assert.strictEqual(_hotaAsciiHex('v1.0'), '76312E30');
|
||||||
|
assert.strictEqual(_hotaAsciiHex('/sd'), '2F7364');
|
||||||
|
assert.strictEqual(_hotaAsciiHex(''), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_hotaBerLen encodes definite lengths', () => {
|
||||||
|
assert.strictEqual(_hotaBerLen(0), '00');
|
||||||
|
assert.strictEqual(_hotaBerLen(11), '0B');
|
||||||
|
assert.strictEqual(_hotaBerLen(127), '7F');
|
||||||
|
assert.strictEqual(_hotaBerLen(128), '8180');
|
||||||
|
assert.strictEqual(_hotaBerLen(255), '81FF');
|
||||||
|
assert.strictEqual(_hotaBerLen(256), '820100');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hotaTlv wraps tag + definite length + value', () => {
|
||||||
|
assert.strictEqual(hotaTlv('02', '8182'), '02028182');
|
||||||
|
assert.strictEqual(hotaTlv('05', ''), '0500');
|
||||||
|
assert.strictEqual(hotaTlv('8A', '6D656761666F6E2E7275'), '8A0A6D656761666F6E2E7275');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hotaBuildConn produces a full 84 TLV', () => {
|
||||||
|
assert.strictEqual(hotaBuildConn([{ tag: '02', value: '8182' }]), '840402028182');
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuildConn([
|
||||||
|
{ tag: '01', value: '014001' },
|
||||||
|
{ tag: '02', value: '81 82' },
|
||||||
|
{ tag: '05', value: '' },
|
||||||
|
]),
|
||||||
|
'840B0103014001020281820500');
|
||||||
|
assert.strictEqual(hotaBuildConn([{ tag: '02', value: '8182' }, { tag: '', value: 'FF' }]), '840402028182');
|
||||||
|
assert.strictEqual(hotaBuildConn([]), '8400');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hotaBuildSec produces a full 85 TLV per Table 4-6', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuildSec({ pskIdentity: 'Test123', kvn: '01', kid: '01' }),
|
||||||
|
'850B0754657374313233020101');
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuildSec({ pskIdentity: '', kvn: '01', kid: '01' }),
|
||||||
|
'850400020101');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hotaBuildRetry uses TS 102 223 timer TLV (25 03) and wraps in 86', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuildRetry({ counter: 'B000', delayH: 1, delayM: 2, delayS: 3, reportFailure: '' }),
|
||||||
|
'8607B0002503010203');
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuildRetry({ counter: 'b0 00', delayH: '1', delayM: '2', delayS: '3', reportFailure: '0A080102030405060708' }),
|
||||||
|
'8611B00025030102030A080102030405060708');
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuildRetry({ counter: '0000', delayH: 0, delayM: 0, delayS: 0, reportFailure: '0A0' }),
|
||||||
|
'860700002503000000');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hotaBuildHttpPost wraps 8A/8B/8C in a full 89 TLV', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuildHttpPost({ host: '', agent: '', uri: '' }),
|
||||||
|
'89068A008B008C00');
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuildHttpPost({ host: 'megafon.ru', agent: 'v1.0', uri: '/sd' }),
|
||||||
|
'89178A0A6D656761666F6E2E72758B0476312E308C032F7364');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hotaBuildTrigger wraps 81 > 83 > (84/85/86/89)', () => {
|
||||||
|
const conn = '840402028182';
|
||||||
|
const sec = '850400020101';
|
||||||
|
const retry = '8607B0002503010203';
|
||||||
|
const httpPost = '89068A008B008C00';
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuildTrigger(conn, sec, retry, httpPost, false),
|
||||||
|
'811F831D8404020281828504000201018607B000250301020389068A008B008C00');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hotaBuildTrigger expanded wraps the 81 command in Command Scripting template AA', () => {
|
||||||
|
const conn = '840402028182';
|
||||||
|
const plain = hotaBuildTrigger(conn, '', '', '', false);
|
||||||
|
const expanded = hotaBuildTrigger(conn, '', '', '', true);
|
||||||
|
const byteLen = plain.length / 2;
|
||||||
|
assert.strictEqual(expanded, 'AA' + _hotaBerLen(byteLen) + plain);
|
||||||
|
assert.ok(expanded.startsWith('AA'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hotaBuildStore emits STORE DATA TLV-mode APDU (80 E2 90 00)', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuildStore('840402028182', '', '', '', '85'),
|
||||||
|
'80E29000088506840402028182');
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuildStore('840402028182', '', '', '', 'A5'),
|
||||||
|
'80E2900008A506840402028182');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hotaBuild dispatches on mode', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuild('trigger', '840402028182', '', '', '', '85', false),
|
||||||
|
hotaBuildTrigger('840402028182', '', '', '', false));
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuild('store', '840402028182', '', '', '', '85', false),
|
||||||
|
hotaBuildStore('840402028182', '', '', '', '85'));
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuild('store', '840402028182', '', '8607B0002503010203', '', '85', false),
|
||||||
|
hotaBuildStore('840402028182', '', '8607B0002503010203', '', '85'));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user