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:
2026-09-10 21:30:06 +03:00
parent c31885d40e
commit 181544b360
5 changed files with 490 additions and 8 deletions
+21 -2
View File
@@ -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 &sect;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 &sect;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&rsquo;ов Remote Application Management over HTTP по GlobalPlatform <strong>GPC v2.2 Amendment B v1.1</strong> (&sect;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 &gt; 83 &gt; 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 &sect;5.2.1) для TAR-ов, обрабатывающих расширенный формат (RAM-over-HTTP &sect;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
View File
@@ -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 &sect;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 &sect;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> (&sect;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 &gt; 83 &gt; 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 &sect;5.2.1) for TARs that process the expanded format (RAM-over-HTTP &sect;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 &mdash; insert the TAR the SD listens on (typically the OTASD TAR) there.</p>
<section class="mb-10"> <section class="mb-10">
+305 -3
View File
@@ -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">&times;</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">&times;</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
View File
@@ -1,4 +1,4 @@
const CACHE = 'otaman-v49'; const CACHE = 'otaman-v50';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+142
View File
@@ -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'));
});