scp81: rename passthru to redirect, add a true passthru mode (v2.2.13)
The former 'passthru' mode is now 'redirect': it pins the configured target and every BIP channel is connected there (the card's requested address is only logged; host/port required). The name 'passthru' now means the new mode: no listener and no target - each channel dials the destination the card requests in OPEN CHANNEL (Other address + Transport level port, TCP client, remote, 0x02 only). The specs define no default port (TS 102 223 8.59), so an incomplete or non-TCP request fails the channel with result 3A and an open-fail log reason. BipTerminal gains a mode (enable(host, port, mode=...), open(..., proto=...)), reports it in status(), and the control API/status expose redirect (target) and passthru (per-channel targets). The PWA mode selector shows four modes with per-mode notes and disables Host/Port in passthru; the status line shows each channel's actual target. Docs, help (EN/RU), READMEs and the AGENTS notes updated; SW cache otaman-v179. Breaking API change: mode:'passthru' has the new semantics - use mode:'redirect' for the previous behavior (no alias).
This commit is contained in:
@@ -590,10 +590,11 @@ Values persist on the server until restart. Apply → hex updates; Save → POST
|
||||
|
||||
The **SCP81** tab drives HTTP OTA (GP RAM over HTTP, GPC v2.2 Amendment B) and has two pills: **Listener** and **Scripts**.
|
||||
|
||||
The **Listener** starts/stops the target the card's BIP channel is redirected to, in one of three modes:
|
||||
The **Listener** starts/stops the target the card's BIP channel is redirected to, in one of four modes:
|
||||
|
||||
- **PSK TLS server** (default) — a PSK TLS listener on **Host:Port** that answers with the TLS 1.2 PSK cipher suites of the spec and speaks the GP HTTP administration dialog (`X-Admin-*` headers, `200` with a command string or `204 No Content`). PSK keys come from the card presets (**Cards** tab): the key is picked by the identity the card sends in the handshake, and Start is refused when no preset has both parts. Keys are never stored or logged; an unrecognised identity is logged as `tls-psk-unknown`.
|
||||
- **Pass-through (external server)** — no local listener: every BIP channel is connected to the configured external platform (Host and Port required), which terminates TLS and runs the administration dialog; the address the card requests is only logged.
|
||||
- **Redirect to external server** — no local listener: every BIP channel is connected to the configured target (Host and Port required), which terminates TLS and runs the administration dialog; the address the card requests is only logged.
|
||||
- **Pass-through (card destination)** — no listener and no target: the terminal connects each BIP channel to the destination the card requests in OPEN CHANNEL (`Other address` + transport port, TCP client remote only; no spec default port, so an incomplete request fails the channel). TLS is terminated by that platform, so the server's network is used (lab only).
|
||||
- **Capture (dump)** — accepts the card's TCP channel and logs whatever it sends (e.g. the TLS ClientHello) without answering.
|
||||
|
||||
**Script** selects the command list served over the session: **None** (leave the server's configured script) or one of the scripts created in the **Scripts** pill; **Restart script** re-queues the selected script with `force`, starting over from the first APDU.
|
||||
|
||||
+3
-2
@@ -560,10 +560,11 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
||||
|
||||
Вкладка **SCP81** управляет HTTP OTA (GP RAM over HTTP, GPC v2.2 Amendment B) и содержит две подвкладки: **Listener** и **Scripts**.
|
||||
|
||||
**Listener** запускает и останавливает цель, на которую перенаправляется BIP-канал карты, в одном из трёх режимов:
|
||||
**Listener** запускает и останавливает цель, на которую перенаправляется BIP-канал карты, в одном из четырёх режимов:
|
||||
|
||||
- **PSK TLS server** (по умолчанию) — PSK TLS-слушатель на **Host:Port**, отвечающий PSK-наборами TLS 1.2 из спецификации и ведущий HTTP-диалог административной сессии GP (заголовки `X-Admin-*`, `200` со строкой команд или `204 No Content`). PSK-ключи берутся из пресетов карт (вкладка **Cards**): ключ выбирается по идентификатору, который карта присылает в рукопожатии; если ни у одного пресета нет пары, Start не запускается. Ключи не сохраняются и не записываются в журнал; незнакомый идентификатор помечается записью `tls-psk-unknown`.
|
||||
- **Pass-through (external server)** — без локального слушателя: каждый BIP-канал подключается к указанной внешней платформе (нужны Host и Port), которая завершает TLS и ведёт административный диалог; запрошенный картой адрес только журналируется.
|
||||
- **Перенаправление на внешний сервер** — без локального слушателя: каждый BIP-канал подключается к указанной цели (нужны Host и Port), которая завершает TLS и ведёт административный диалог; запрошенный картой адрес только журналируется.
|
||||
- **Проброс (адрес карты)** — без слушателя и без цели: терминал подключает каждый BIP-канал к адресу, который карта запрашивает в OPEN CHANNEL (`Other address` + порт transport level, только TCP-клиент, удалённое соединение; порт по умолчанию спецификациями не задан, поэтому неполный запрос отклоняет канал). TLS завершается на той платформе, и используется сеть сервера (только для лаборатории).
|
||||
- **Capture (dump)** — принимает TCP-канал карты и записывает всё, что она отправляет (например, TLS ClientHello), не отвечая.
|
||||
|
||||
**Script** выбирает список команд, отдаваемый в сессии: **None** (оставить настроенный на сервере скрипт) или один из скриптов, созданных на подвкладке **Scripts**; **Restart script** заново ставит выбранный скрипт в очередь с `force`, начиная с первого APDU.
|
||||
|
||||
+28
-10
@@ -507,14 +507,29 @@ without answering:
|
||||
{"action": "start", "mode": "dump", "host": "127.0.0.1", "port": 8443}
|
||||
```
|
||||
|
||||
Pass-through mode (`mode: "passthru"`) starts **no local listener**: every BIP
|
||||
channel the card opens is connected to the configured external platform
|
||||
(`host`/`port` are required — no defaults), which terminates TLS and runs the
|
||||
administration dialog; the address the card requests is only logged. The
|
||||
status API reports `mode: "passthru"` with the target while it runs.
|
||||
Redirect mode (`mode: "redirect"`) starts **no local listener**: every BIP
|
||||
channel the card opens is connected to the configured target (`host`/`port`
|
||||
are required — no defaults), which terminates TLS and runs the administration
|
||||
dialog; the address the card requests is only logged. The status API reports
|
||||
`mode: "redirect"` with the target while it runs. (This is the behavior that
|
||||
was called `passthru` before 2.2.13 — the name is now taken by the mode
|
||||
below.)
|
||||
|
||||
```json
|
||||
{"action": "start", "mode": "passthru", "host": "203.0.113.10", "port": 10174}
|
||||
{"action": "start", "mode": "redirect", "host": "203.0.113.10", "port": 10174}
|
||||
```
|
||||
|
||||
Pass-through mode (`mode: "passthru"`) starts **no listener and has no
|
||||
target**: every BIP channel dials the destination the card requests in OPEN
|
||||
CHANNEL — the `Other address` (`3E`/`BE`) plus the `Transport level`
|
||||
(`3C`/`BC`) port, TCP client remote (`02`) only. The specs define no default
|
||||
port, so an incomplete or non-TCP request fails the channel with result `3A`
|
||||
and an `open-fail` log reason; `host`/`port` in the request are ignored. The
|
||||
status API reports `{"mode": "passthru"}` and the per-channel targets appear
|
||||
in `bip.channels`.
|
||||
|
||||
```json
|
||||
{"action": "start", "mode": "passthru"}
|
||||
```
|
||||
|
||||
TLS mode runs the Phase B PSK TLS server (GPC v2.2 Amendment B): the PSK
|
||||
@@ -545,16 +560,19 @@ Stop either mode with `{"action": "stop"}` (also disables the BIP terminal).
|
||||
### `GET /api/scp81/status`
|
||||
|
||||
```json
|
||||
{"bip": {"enabled": true, "target": "127.0.0.1:8443", "channels": [], "seq": 12},
|
||||
{"bip": {"enabled": true, "mode": "redirect", "target": "127.0.0.1:8443", "channels": [], "seq": 12},
|
||||
"listener": {"mode": "tls", "host": "127.0.0.1", "port": 8443,
|
||||
"psk_identities": ["89012345678901234567"], "psk_wildcard": false,
|
||||
"identity_seen": "89012345678901234567", "identity_matched": true}}
|
||||
```
|
||||
|
||||
Listener modes: `tls` (local PSK TLS server), `dump` (capture-only TCP
|
||||
listener) and `passthru` (no local listener; the BIP channels go straight to
|
||||
`host:port`, e.g. an external HTTP OTA platform — reported as
|
||||
`{"mode": "passthru", "host": ..., "port": ..., "target": "host:port"}`).
|
||||
listener), `redirect` (no local listener; the BIP channels go straight to the
|
||||
configured `host:port`, e.g. an external HTTP OTA platform — reported as
|
||||
`{"mode": "redirect", "host": ..., "port": ..., "target": "host:port"}`) and
|
||||
`passthru` (no listener and no target; each channel dials the destination the
|
||||
card requests in OPEN CHANNEL — reported as `{"mode": "passthru"}`, with the
|
||||
actual peer in `bip.channels[].target`).
|
||||
|
||||
`psk_identities` lists the identities the listener accepts (keys are never
|
||||
exposed); `psk_wildcard` marks the legacy single-key mode. `identity_seen` /
|
||||
|
||||
@@ -323,7 +323,8 @@
|
||||
<p class="mb-2">Кнопки <strong>Start</strong> / <strong>Остановить</strong> управляют целью; <strong>Перезапустить скрипт</strong> ставит выбранный скрипт в очередь заново с <code class="font-mono text-sm">force</code>, начиная прогон с первого APDU. Режимы:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>PSK TLS сервер</strong> (по умолчанию) — PSK TLS-слушатель на <strong>Хост</strong>:<strong>Порт</strong>, отвечающий PSK-наборами TLS 1.2 из спецификации и ведущий HTTP-диалог административной сессии GP (заголовки <code class="font-mono text-sm">X-Admin-*</code>, <code class="font-mono text-sm">200</code> со строкой команд или <code class="font-mono text-sm">204 No Content</code>). PSK-ключи берутся из предустановок карт (вкладка <strong>«Карты»</strong>): ключ выбирается по идентификатору, который карта присылает в рукопожатии; если ни у одной предустановки нет пары, <strong>Start</strong> не запускается. Ключ не сохраняется и не записывается в журнал; незнакомый идентификатор помечается в журнале как <code class="font-mono text-sm">tls-psk-unknown</code>.</li>
|
||||
<li><strong>Проброс (внешний сервер)</strong> — без локального слушателя: каждый BIP-канал подключается к указанной внешней платформе (нужны <strong>Хост</strong> и <strong>Порт</strong>), которая завершает TLS и ведёт административный диалог; запрошенный картой адрес только журналируется.</li>
|
||||
<li><strong>Перенаправление на внешний сервер</strong> — без локального слушателя: каждый BIP-канал подключается к указанной цели (нужны <strong>Хост</strong> и <strong>Порт</strong>), которая завершает TLS и ведёт административный диалог; запрошенный картой адрес только журналируется.</li>
|
||||
<li><strong>Проброс (адрес карты)</strong> — без слушателя и без цели: терминал подключает каждый BIP-канал к адресу, который карта запрашивает в OPEN CHANNEL (Other address + порт Transport level, только TCP-клиент, удалённое соединение; спецификации не задают порт по умолчанию, поэтому неполный запрос отклоняет канал). TLS завершается на той платформе, и используется сеть сервера (только для лаборатории).</li>
|
||||
<li><strong>Захват (dump)</strong> — принимает TCP-канал карты и записывает всё, что она отправляет (например, TLS ClientHello), не отвечая.</li>
|
||||
</ul>
|
||||
<p class="mb-2"><strong>Сценарий</strong> выбирает список команд, отдаваемый в сессии — <strong>Нет</strong> (оставить настроенный на сервере скрипт) или один из скриптов подвкладки <strong>«Скрипты»</strong>; выбранный список передаётся серверу при старте слушателя.</p>
|
||||
|
||||
+2
-1
@@ -323,7 +323,8 @@
|
||||
<p class="mb-2"><strong>Start</strong> / <strong>Stop</strong> control the target; <strong>Restart script</strong> queues the selected script again with <code class="font-mono text-sm">force</code>, restarting its run from the first APDU. Modes:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>PSK TLS server</strong> (default) — a PSK TLS listener on <strong>Host</strong>:<strong>Port</strong> that answers with the TLS 1.2 PSK cipher suites of the spec and speaks the GP HTTP administration dialog (<code class="font-mono text-sm">X-Admin-*</code> headers, <code class="font-mono text-sm">200</code> with a command string or <code class="font-mono text-sm">204 No Content</code>). PSK keys come from the card presets (<strong>Cards</strong> tab): the listener picks the key by the identity the card sends in the handshake, and <strong>Start</strong> is refused when no preset has both parts. The key is never stored or logged; <code class="font-mono text-sm">tls-psk-unknown</code> marks an unrecognised identity.</li>
|
||||
<li><strong>Pass-through (external server)</strong> — no local listener: every BIP channel is connected to the configured external platform (<strong>Host</strong> and <strong>Port</strong> required), which terminates TLS and runs the administration dialog; the address the card requests is only logged.</li>
|
||||
<li><strong>Redirect to external server</strong> — no local listener: every BIP channel is connected to the configured target (<strong>Host</strong> and <strong>Port</strong> required), which terminates TLS and runs the administration dialog; the address the card requests is only logged.</li>
|
||||
<li><strong>Pass-through (card destination)</strong> — no listener and no target: the terminal connects each BIP channel to the destination the card requests in OPEN CHANNEL (Other address + Transport level port, TCP client remote only; the specs define no default port, so an incomplete request fails the channel). TLS is terminated by that platform, and the server's network is used (lab only).</li>
|
||||
<li><strong>Capture (dump)</strong> — accepts the card's TCP channel and logs whatever it sends (e.g. the TLS ClientHello) without answering.</li>
|
||||
</ul>
|
||||
<p class="mb-2"><strong>Script</strong> selects the command list served over the session — <strong>None</strong> (leave the server's configured script) or one of the scripts from the <strong>Scripts</strong> pill; the chosen list is sent to the server when the listener starts.</p>
|
||||
|
||||
+32
-18
@@ -18,7 +18,7 @@
|
||||
<div class="max-w-7xl mx-auto px-6 py-2">
|
||||
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.2.12</span></h1>
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.2.13</span></h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<span id="state-indicator" class="flex items-center select-none" style="cursor:default" title="Connecting...">
|
||||
<span id="state-indicator-dot" class="text-xs text-gray-400" title="Connecting...">●</span>
|
||||
@@ -969,7 +969,8 @@
|
||||
<label class="block mb-1 text-xs font-medium text-gray-600 dark:text-slate-400" data-l10n="Mode">Mode</label>
|
||||
<select id="scp81-mode" onchange="scp81ModeChanged()" class="border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">
|
||||
<option value="tls" data-l10n="PSK TLS server" selected>PSK TLS server</option>
|
||||
<option value="passthru" data-l10n="Pass-through (external server)">Pass-through (external server)</option>
|
||||
<option value="redirect" data-l10n="Redirect to external server">Redirect to external server</option>
|
||||
<option value="passthru" data-l10n="Pass-through (card destination)">Pass-through (card destination)</option>
|
||||
<option value="dump" data-l10n="Capture (dump)">Capture (dump)</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -991,7 +992,8 @@
|
||||
</div>
|
||||
<div id="scp81-msg" class="text-xs mt-2 hidden"></div>
|
||||
<div id="scp81-psk-note" class="mt-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="PSK keys come from the card presets (Cards tab): the listener picks the key by the identity the card sends in the TLS handshake.">PSK keys come from the card presets (Cards tab): the listener picks the key by the identity the card sends in the TLS handshake.</div>
|
||||
<div id="scp81-passthru-note" class="hidden mt-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="Pass-through: every BIP channel the card opens is connected to this Host:Port (the external HTTP OTA platform); TLS is terminated there, the card's requested address is only logged.">Pass-through: every BIP channel the card opens is connected to this Host:Port (the external HTTP OTA platform); TLS is terminated there, the card's requested address is only logged.</div>
|
||||
<div id="scp81-redirect-note" class="hidden mt-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="Redirect: every BIP channel the card opens is connected to this Host:Port (the external HTTP OTA platform); TLS is terminated there, and the address the card requests is only logged.">Redirect: every BIP channel the card opens is connected to this Host:Port (the external HTTP OTA platform); TLS is terminated there, and the address the card requests is only logged.</div>
|
||||
<div id="scp81-passthru-note" class="hidden mt-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="Pass-through: the terminal connects each BIP channel to the destination the card requests in OPEN CHANNEL (Other address + transport port, TCP client only); Host and Port above are not used, TLS is terminated by that platform, and the server's network is used (lab only).">Pass-through: the terminal connects each BIP channel to the destination the card requests in OPEN CHANNEL (Other address + transport port, TCP client only); Host and Port above are not used, TLS is terminated by that platform, and the server's network is used (lab only).</div>
|
||||
</div>
|
||||
<div class="border border-gray-200 dark:border-slate-700 rounded p-3 mb-3">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
@@ -7764,7 +7766,7 @@ async function pysimProactiveLogRender() {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== SCP81: HTTP OTA listener (dump capture / PSK TLS server) =====
|
||||
// ===== SCP81: HTTP OTA listener (PSK TLS / redirect / passthru / capture) =====
|
||||
let _scp81Timer = null;
|
||||
let _scp81LastStatus = null;
|
||||
|
||||
@@ -7774,8 +7776,18 @@ function scp81ModeChanged() {
|
||||
if (row) row.style.display = (mode === 'tls') ? '' : 'none';
|
||||
const pskNote = document.getElementById('scp81-psk-note');
|
||||
if (pskNote) pskNote.classList.toggle('hidden', mode !== 'tls');
|
||||
const directNote = document.getElementById('scp81-passthru-note');
|
||||
if (directNote) directNote.classList.toggle('hidden', mode !== 'passthru');
|
||||
const redirectNote = document.getElementById('scp81-redirect-note');
|
||||
if (redirectNote) redirectNote.classList.toggle('hidden', mode !== 'redirect');
|
||||
const passthruNote = document.getElementById('scp81-passthru-note');
|
||||
if (passthruNote) passthruNote.classList.toggle('hidden', mode !== 'passthru');
|
||||
// passthru dials the destination from the card's OPEN CHANNEL: the
|
||||
// configured target fields do not apply.
|
||||
for (const id of ['scp81-host', 'scp81-port']) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) continue;
|
||||
el.disabled = (mode === 'passthru');
|
||||
el.classList.toggle('opacity-40', mode === 'passthru');
|
||||
}
|
||||
}
|
||||
|
||||
function scp81SwitchSubtab(name) {
|
||||
@@ -8109,7 +8121,7 @@ async function scp81StatusRefresh() {
|
||||
el.textContent = t('not running');
|
||||
return st;
|
||||
}
|
||||
let s = l.mode + ' ' + l.host + ':' + l.port;
|
||||
let s = l.mode + (l.host && l.port ? ' ' + l.host + ':' + l.port : '');
|
||||
if (l.mode === 'tls') {
|
||||
const ids = l.psk_identities || [];
|
||||
s += ' | PSK: ' + ids.length;
|
||||
@@ -8121,7 +8133,7 @@ async function scp81StatusRefresh() {
|
||||
: t('unknown identity')) + ']';
|
||||
}
|
||||
}
|
||||
const ch = (bip.channels || []).map(c => 'ch' + c.id + ' in:' + c.bytes_in + ' out:' + c.bytes_out).join(', ');
|
||||
const ch = (bip.channels || []).map(c => 'ch' + c.id + (c.target ? ' → ' + c.target : '') + ' in:' + c.bytes_in + ' out:' + c.bytes_out).join(', ');
|
||||
if (ch) s += ' | ' + ch;
|
||||
el.textContent = s;
|
||||
return st;
|
||||
@@ -8152,15 +8164,15 @@ async function scp81Start() {
|
||||
const mode = document.getElementById('scp81-mode').value;
|
||||
const hostVal = document.getElementById('scp81-host').value.trim();
|
||||
const portVal = document.getElementById('scp81-port').value.trim();
|
||||
if (mode === 'passthru' && (!hostVal || !portVal)) {
|
||||
scp81Msg(t('Pass-through requires the target host and port'), 'text-red-500');
|
||||
if (mode === 'redirect' && (!hostVal || !portVal)) {
|
||||
scp81Msg(t('Redirect requires the target host and port'), 'text-red-500');
|
||||
return;
|
||||
}
|
||||
const body = {
|
||||
action: 'start', mode: mode,
|
||||
host: hostVal || '127.0.0.1',
|
||||
port: parseInt(portVal || '8443', 10),
|
||||
};
|
||||
const body = { action: 'start', mode: mode };
|
||||
if (mode !== 'passthru') {
|
||||
body.host = hostVal || '127.0.0.1';
|
||||
body.port = parseInt(portVal || '8443', 10);
|
||||
}
|
||||
if (mode === 'tls') {
|
||||
const map = cardsPskMap();
|
||||
if (!map.length) {
|
||||
@@ -11433,9 +11445,11 @@ const LANG_RU = {
|
||||
'LOAD blocks': 'Блоки LOAD',
|
||||
'clamped from': 'ограничено с',
|
||||
'auto-fit': 'авто',
|
||||
'Pass-through (external server)': 'Проброс (внешний сервер)',
|
||||
'Pass-through: every BIP channel the card opens is connected to this Host:Port (the external HTTP OTA platform); TLS is terminated there, the card\'s requested address is only logged.': 'Проброс: каждый открываемый картой BIP-канал подключается к этому Host:Port (внешняя платформа HTTP OTA); TLS завершается там, запрошенный картой адрес только журналируется.',
|
||||
'Pass-through requires the target host and port': 'Для режима проброса нужны host и port внешнего сервера',
|
||||
'Redirect to external server': 'Перенаправление на внешний сервер',
|
||||
'Pass-through (card destination)': 'Проброс (адрес карты)',
|
||||
'Redirect: every BIP channel the card opens is connected to this Host:Port (the external HTTP OTA platform); TLS is terminated there, and the address the card requests is only logged.': 'Перенаправление: каждый открываемый картой BIP-канал подключается к этому Host:Port (внешняя платформа HTTP OTA); TLS завершается там, а запрошенный картой адрес только журналируется.',
|
||||
'Pass-through: the terminal connects each BIP channel to the destination the card requests in OPEN CHANNEL (Other address + transport port, TCP client only); Host and Port above are not used, TLS is terminated by that platform, and the server\'s network is used (lab only).': 'Проброс: терминал подключает каждый BIP-канал к адресу, который карта запрашивает в OPEN CHANNEL (Other address + порт Transport level, только TCP-клиент); поля Host и Port выше не используются, TLS завершается на той платформе, и используется сеть сервера (только для лаборатории).',
|
||||
'Redirect requires the target host and port': 'Для перенаправления нужны host и port внешнего сервера',
|
||||
'ADM verified': 'ADM подтверждён',
|
||||
'ADM not verified': 'ADM не подтверждён',
|
||||
'TERMINAL PROFILE': 'TERMINAL PROFILE',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'otaman-v178';
|
||||
const CACHE = 'otaman-v179';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -168,3 +168,14 @@ test('every help anchor used by the UI exists in help.html', () => {
|
||||
assert.ok(html.includes("'scp81-listener' : 'scp81-scripts'"));
|
||||
assert.ok(html.includes("? 'scp81-scripts' : 'scp81-listener'"));
|
||||
});
|
||||
|
||||
test('SCP81 listener exposes its four modes with the matching notes', () => {
|
||||
for (const v of ['tls', 'redirect', 'passthru', 'dump']) {
|
||||
assert.ok(html.includes('value="' + v + '"'), v);
|
||||
}
|
||||
assert.ok(html.includes('id="scp81-redirect-note"'));
|
||||
assert.ok(html.includes('id="scp81-passthru-note"'));
|
||||
// redirect needs the configured target; passthru uses the card's one
|
||||
assert.ok(html.includes("mode === 'redirect' && (!hostVal || !portVal)"));
|
||||
assert.ok(html.includes("el.disabled = (mode === 'passthru')"));
|
||||
});
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-otaman-server"
|
||||
version = "2.2.12"
|
||||
version = "2.2.13"
|
||||
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.
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""HTTP OTA (SCP81 / GP RAM over HTTP) emulation.
|
||||
|
||||
Phase A: terminal-side BIP emulation (OPEN/SEND/RECEIVE/CLOSE CHANNEL) plus a
|
||||
raw TCP capture listener. The card's BIP channel is always redirected to the
|
||||
locally configured target (the future PSK TLS platform); the address the card
|
||||
requested is only logged.
|
||||
raw TCP capture listener. In the default redirect mode the card's BIP channel
|
||||
is always redirected to the locally configured target (the future PSK TLS
|
||||
platform) and the address the card requested is only logged; in passthru mode
|
||||
the channel dials the destination the card requests in OPEN CHANNEL (TCP,
|
||||
UICC in client mode, remote connection).
|
||||
|
||||
Reference behavior (TS 102 223 8.52-8.56, GP v2.2 Amendment B) is taken from
|
||||
the captured real-terminal traces in samples/HTTP_OTA/traces:
|
||||
@@ -154,6 +156,7 @@ class BipTerminal:
|
||||
|
||||
def __init__(self):
|
||||
self.enabled = False
|
||||
self.mode = 'redirect'
|
||||
self.target = None
|
||||
self.channels = {}
|
||||
self.next_id = 1
|
||||
@@ -213,10 +216,17 @@ class BipTerminal:
|
||||
name='bip-monitor', daemon=True)
|
||||
self._monitor.start()
|
||||
|
||||
def enable(self, host, port):
|
||||
self.target = (host, int(port))
|
||||
def enable(self, host=None, port=None, mode='redirect'):
|
||||
"""Enable the BIP terminal.
|
||||
|
||||
'redirect' (default) pins one target: every channel goes there whatever
|
||||
address the card requests. 'passthru' has no target at all: every
|
||||
channel dials the destination the card requested in OPEN CHANNEL."""
|
||||
self.mode = mode if mode in ('redirect', 'passthru') else 'redirect'
|
||||
self.target = (host, int(port)) if host and port not in (None, '') else None
|
||||
self.enabled = True
|
||||
self.log('enabled', target='%s:%d' % self.target)
|
||||
self.log('enabled', mode=self.mode,
|
||||
target='%s:%d' % self.target if self.target else None)
|
||||
self._start_monitor()
|
||||
|
||||
def disable(self):
|
||||
@@ -271,12 +281,40 @@ class BipTerminal:
|
||||
return cid
|
||||
return None
|
||||
|
||||
def open(self, requested_host, requested_port, buffer_size):
|
||||
"""Open a channel to the redirect target. Returns (channel_id, error)."""
|
||||
if not self.enabled or not self.target:
|
||||
def open(self, requested_host, requested_port, buffer_size, proto=None):
|
||||
"""Open a channel.
|
||||
|
||||
Redirect modes connect to the pinned target; passthru dials the
|
||||
destination the card sent in OPEN CHANNEL (Other address + Transport
|
||||
level port). Returns (channel_id, error)."""
|
||||
if not self.enabled:
|
||||
return None, 'bip disabled'
|
||||
target = self.target
|
||||
requested = '%s:%s' % (requested_host, requested_port)
|
||||
if self.mode == 'passthru':
|
||||
# Use the card's request as-is: TCP, UICC in client mode, remote
|
||||
# connection (TS 102 223 6.4.27.2 / 8.59). The specs define no
|
||||
# default port, so an incomplete or non-TCP request fails.
|
||||
host = (requested_host or '').strip()
|
||||
try:
|
||||
port = int(requested_port)
|
||||
except (TypeError, ValueError):
|
||||
port = 0
|
||||
if proto != 0x02:
|
||||
reason = 'card did not request TCP client transport (passthru)'
|
||||
elif not host or host == '-':
|
||||
reason = 'card did not request a destination address (passthru)'
|
||||
elif not 0 < port <= 0xFFFF:
|
||||
reason = 'card did not request a valid port (passthru)'
|
||||
else:
|
||||
reason = None
|
||||
if reason:
|
||||
self.log('open-fail', requested=requested, reason=reason)
|
||||
return None, reason
|
||||
target = (host, port)
|
||||
else:
|
||||
if not self.target:
|
||||
return None, 'bip disabled'
|
||||
target = self.target
|
||||
cid = self._alloc_id()
|
||||
if cid is None:
|
||||
self.log('open-fail', requested=requested, reason='no free channel')
|
||||
@@ -363,6 +401,7 @@ class BipTerminal:
|
||||
})
|
||||
return {
|
||||
'enabled': self.enabled,
|
||||
'mode': self.mode,
|
||||
'target': '%s:%d' % self.target if self.target else None,
|
||||
'channels': channels,
|
||||
'seq': self.seq,
|
||||
|
||||
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '2.2.12'
|
||||
VERSION = '2.2.13'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -851,9 +851,12 @@ _PLI_DATA = {q: '' for q in PLI_QUALIFIER_NAMES}
|
||||
|
||||
_BIP = httpota.BipTerminal()
|
||||
_SCP81_LISTENER = None
|
||||
# Active listener mode and target: ('dump'|'tls'|'passthru', host, port).
|
||||
# passthru has no listener object - the BIP channels connect straight to the
|
||||
# external platform - so the mode/target are tracked here for the status API.
|
||||
# Active listener mode: 'dump' | 'tls' | 'redirect' | 'passthru'.
|
||||
# 'redirect' pins one target and has no listener object - the BIP channels
|
||||
# connect straight to the configured external platform (TLS terminated
|
||||
# there). 'passthru' has neither listener nor target: each channel dials the
|
||||
# destination the card requests in OPEN CHANNEL. Mode/target are tracked here
|
||||
# for the status API.
|
||||
_SCP81_MODE = None
|
||||
_SCP81_TARGET = None
|
||||
# PSK table of the TLS listener: identity -> key (memory only, never logged or
|
||||
@@ -1409,7 +1412,7 @@ def _handle_bip_command(scc, cmd_num, cmd_type, cmd_qual, raw, dev_src, dev_dst)
|
||||
# destination TLVs - see the AGENTS.md HTTP OTA notes).
|
||||
_BIP.log('open-relaxed', address=addr, port=port,
|
||||
note='destination/transport not fully specified')
|
||||
cid, err = _BIP.open(addr or '-', port or 0, buffer_size)
|
||||
cid, err = _BIP.open(addr or '-', port or 0, buffer_size, proto=proto)
|
||||
if cid is None:
|
||||
return _bip_tr(cmd_num, cmd_type, cmd_qual, dev_src, dev_dst, 0x3A, 0x00, extra)
|
||||
if cmd_qual and (cmd_qual & 0x04):
|
||||
@@ -1461,10 +1464,15 @@ def _handle_bip_command(scc, cmd_num, cmd_type, cmd_qual, raw, dev_src, dev_dst)
|
||||
|
||||
def _scp81_listener_status():
|
||||
if not _SCP81_LISTENER:
|
||||
if _SCP81_MODE == 'passthru' and _SCP81_TARGET:
|
||||
return {'mode': 'passthru', 'host': _SCP81_TARGET[0],
|
||||
if _SCP81_MODE == 'redirect' and _SCP81_TARGET:
|
||||
return {'mode': 'redirect', 'host': _SCP81_TARGET[0],
|
||||
'port': _SCP81_TARGET[1],
|
||||
'target': '%s:%d' % _SCP81_TARGET}
|
||||
if _SCP81_MODE == 'passthru':
|
||||
# No listener and no pinned target: every channel dials the
|
||||
# destination the card requests (per-channel targets in the
|
||||
# BIP status).
|
||||
return {'mode': 'passthru'}
|
||||
return None
|
||||
if isinstance(_SCP81_LISTENER, scp81.PskTlsServer):
|
||||
return {'mode': 'tls', 'host': _SCP81_LISTENER.host, 'port': _SCP81_LISTENER.port,
|
||||
@@ -2010,18 +2018,28 @@ def _scp81_bip_control(body):
|
||||
_SCP81_LISTENER.stop()
|
||||
_SCP81_LISTENER = None
|
||||
_BIP.disable()
|
||||
if mode == 'passthru':
|
||||
# No local listener: the card's BIP channels connect straight to the
|
||||
# external platform (e.g. a production HTTP OTA server), which
|
||||
# terminates TLS and runs the administration dialog. The target is a
|
||||
# configured address, never the address the card requests.
|
||||
if mode == 'redirect':
|
||||
# No local listener: the card's BIP channels are redirected straight
|
||||
# to the configured target (e.g. a production HTTP OTA server), which
|
||||
# terminates TLS and runs the administration dialog. The address the
|
||||
# card requests is only logged.
|
||||
if not body.get('host') or body.get('port') in (None, ''):
|
||||
return {'ok': False,
|
||||
'error': 'passthru mode requires the target host and port'}
|
||||
_SCP81_MODE = 'passthru'
|
||||
'error': 'redirect mode requires the target host and port'}
|
||||
_SCP81_MODE = 'redirect'
|
||||
_SCP81_TARGET = (host, port)
|
||||
_BIP.on_data = _bip_data_available
|
||||
_BIP.enable(host, port)
|
||||
_BIP.enable(host, port, mode='redirect')
|
||||
return {'ok': True, 'bip': _BIP.status(),
|
||||
'listener': _scp81_listener_status()}
|
||||
if mode == 'passthru':
|
||||
# No local listener and no pinned target: every BIP channel dials the
|
||||
# destination the card requests in OPEN CHANNEL (Other address +
|
||||
# Transport level port, TCP client only). Host and port are unused.
|
||||
_SCP81_MODE = 'passthru'
|
||||
_SCP81_TARGET = None
|
||||
_BIP.on_data = _bip_data_available
|
||||
_BIP.enable(mode='passthru')
|
||||
return {'ok': True, 'bip': _BIP.status(),
|
||||
'listener': _scp81_listener_status()}
|
||||
if mode == 'tls':
|
||||
@@ -2103,7 +2121,7 @@ def _scp81_bip_control(body):
|
||||
_SCP81_MODE = 'tls'
|
||||
_SCP81_TARGET = (_SCP81_LISTENER.host, _SCP81_LISTENER.port)
|
||||
_BIP.on_data = _bip_data_available
|
||||
_BIP.enable(host, _SCP81_LISTENER.port)
|
||||
_BIP.enable(host, _SCP81_LISTENER.port, mode='redirect')
|
||||
return {'ok': True, 'bip': _BIP.status(), 'listener': _scp81_listener_status(),
|
||||
'script': list(_SCP81_SCRIPT_BASE),
|
||||
'script_kind': _SCP81_SCRIPT_KIND,
|
||||
@@ -2121,7 +2139,7 @@ def _scp81_bip_control(body):
|
||||
on_log=lambda kind, **fields: _BIP.log(kind, **fields))
|
||||
_SCP81_MODE = 'dump'
|
||||
_SCP81_TARGET = (_SCP81_LISTENER.host, _SCP81_LISTENER.port)
|
||||
_BIP.enable(host, _SCP81_LISTENER.port)
|
||||
_BIP.enable(host, _SCP81_LISTENER.port, mode='redirect')
|
||||
return {'ok': True, 'bip': _BIP.status(), 'listener': _scp81_listener_status()}
|
||||
|
||||
|
||||
|
||||
+49
-4
@@ -152,17 +152,17 @@ class BipTerminalTest(unittest.TestCase):
|
||||
self.assertIn('close', kinds)
|
||||
peer.stop()
|
||||
|
||||
def test_passthru_mode_roundtrip_via_bip_control(self):
|
||||
# SCP81 passthru: the control API enables BIP with the external
|
||||
def test_redirect_mode_roundtrip_via_bip_control(self):
|
||||
# SCP81 redirect: the control API enables BIP with the external
|
||||
# platform as the target and starts no local listener; the card's
|
||||
# channel talks straight to that platform.
|
||||
peer = PeerServer(greeting=b'PLATFORM')
|
||||
peer.start()
|
||||
try:
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'passthru',
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'redirect',
|
||||
'host': '127.0.0.1', 'port': peer.port})
|
||||
self.assertTrue(resp['ok'], resp)
|
||||
self.assertEqual(resp['listener']['mode'], 'passthru')
|
||||
self.assertEqual(resp['listener']['mode'], 'redirect')
|
||||
self.assertEqual(server._BIP.target, ('127.0.0.1', peer.port))
|
||||
cid, err = server._BIP.open('10.9.9.9', 10174, 512)
|
||||
self.assertIsNone(err)
|
||||
@@ -184,6 +184,51 @@ class BipTerminalTest(unittest.TestCase):
|
||||
self.assertIsNone(cid)
|
||||
self.assertIn('disabled', err)
|
||||
|
||||
def test_passthru_dials_the_requested_destination(self):
|
||||
# passthru has no pinned target: the socket goes to the destination
|
||||
# the card requested in OPEN CHANNEL (TCP client, remote).
|
||||
peer = PeerServer(greeting=b'PLATFORM')
|
||||
peer.start()
|
||||
try:
|
||||
bip = httpota.BipTerminal()
|
||||
bip.enable(mode='passthru')
|
||||
self.assertIsNone(bip.target)
|
||||
self.assertEqual(bip.status()['mode'], 'passthru')
|
||||
cid, err = bip.open('127.0.0.1', peer.port, 512, proto=0x02)
|
||||
self.assertIsNone(err)
|
||||
self.assertEqual(bip.channels[cid].target, ('127.0.0.1', peer.port))
|
||||
self.assertTrue(bip.send(cid, b'CARDHELLO'))
|
||||
data = b''
|
||||
for _ in range(20):
|
||||
data = bip.receive(cid, 100)
|
||||
if data:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
self.assertEqual(data, b'PLATFORM')
|
||||
self.assertTrue(bip.close(cid))
|
||||
finally:
|
||||
peer.stop()
|
||||
|
||||
def test_passthru_rejects_incomplete_or_non_tcp_requests(self):
|
||||
# The specs define no default port (TS 102 223 8.59): anything but a
|
||||
# complete TCP-client remote request fails the channel (result 3A
|
||||
# upstream) with a visible log reason.
|
||||
srv = socket.socket()
|
||||
srv.bind(('127.0.0.1', 0))
|
||||
dead_port = srv.getsockname()[1]
|
||||
srv.close()
|
||||
bip = httpota.BipTerminal()
|
||||
bip.enable(mode='passthru')
|
||||
self.assertIsNone(bip.open('-', 0, 512, proto=0x02)[0]) # no address
|
||||
self.assertIsNone(bip.open('127.0.0.1', 0, 512, proto=0x02)[0]) # no port
|
||||
self.assertIsNone(bip.open('127.0.0.1', 1234, 512, proto=0x03)[0]) # TCP server mode
|
||||
self.assertIsNone(bip.open('127.0.0.1', dead_port, 512, proto=0x02)[0]) # refused
|
||||
reasons = [e.get('reason', '') for e in bip.entries_after(0)
|
||||
if e['kind'] == 'open-fail']
|
||||
self.assertTrue(any('TCP client' in r for r in reasons), reasons)
|
||||
self.assertTrue(any('valid port' in r for r in reasons), reasons)
|
||||
self.assertTrue(any('address' in r for r in reasons), reasons)
|
||||
|
||||
def test_peer_close_queues_channel_status_event(self):
|
||||
# TS 102 223 7.5.11: a link lost outside a proactive command must be
|
||||
# reported to the UICC (channel id, link not established, info 05).
|
||||
|
||||
+25
-9
@@ -682,38 +682,54 @@ class BipControlTest(unittest.TestCase):
|
||||
self.assertFalse(resp['ok'])
|
||||
self.assertIn('unsupported mode', resp['error'])
|
||||
|
||||
def test_passthru_mode_targets_the_external_server(self):
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'passthru',
|
||||
def test_redirect_mode_targets_the_external_server(self):
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'redirect',
|
||||
'host': '10.11.12.13', 'port': 10174})
|
||||
self.assertTrue(resp['ok'], resp)
|
||||
self.assertIsNone(server._SCP81_LISTENER) # no local listener
|
||||
self.assertEqual(resp['listener']['mode'], 'passthru')
|
||||
self.assertEqual(resp['listener']['mode'], 'redirect')
|
||||
self.assertEqual(resp['listener']['host'], '10.11.12.13')
|
||||
self.assertEqual(resp['listener']['port'], 10174)
|
||||
self.assertEqual(resp['listener']['target'], '10.11.12.13:10174')
|
||||
self.assertTrue(resp['bip']['enabled'])
|
||||
self.assertEqual(server._BIP.target, ('10.11.12.13', 10174))
|
||||
# the status endpoint sees the passthru mode while it runs ...
|
||||
self.assertEqual(server._scp81_listener_status()['mode'], 'passthru')
|
||||
# the status endpoint sees the redirect mode while it runs ...
|
||||
self.assertEqual(server._scp81_listener_status()['mode'], 'redirect')
|
||||
# ... and stopping clears it (no stale listener in the status)
|
||||
server._scp81_bip_control({'action': 'stop'})
|
||||
self.assertIsNone(server._scp81_listener_status())
|
||||
self.assertFalse(server._BIP.enabled)
|
||||
|
||||
def test_passthru_mode_requires_an_explicit_target(self):
|
||||
def test_redirect_mode_requires_an_explicit_target(self):
|
||||
# No defaults for a remote platform: the target must be configured.
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'passthru'})
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'redirect'})
|
||||
self.assertFalse(resp['ok'])
|
||||
self.assertIn('host and port', resp['error'])
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'passthru',
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'redirect',
|
||||
'host': '10.0.0.1'})
|
||||
self.assertFalse(resp['ok'])
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'passthru',
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'redirect',
|
||||
'port': 1234})
|
||||
self.assertFalse(resp['ok'])
|
||||
self.assertIsNone(server._SCP81_LISTENER)
|
||||
self.assertFalse(server._BIP.enabled)
|
||||
|
||||
def test_passthru_mode_needs_no_target(self):
|
||||
# Passthru: no listener and no pinned target - every BIP channel dials
|
||||
# the destination the card requests in OPEN CHANNEL.
|
||||
resp = server._scp81_bip_control({'action': 'start', 'mode': 'passthru'})
|
||||
self.assertTrue(resp['ok'], resp)
|
||||
self.assertIsNone(server._SCP81_LISTENER)
|
||||
self.assertEqual(resp['listener'], {'mode': 'passthru'})
|
||||
self.assertEqual(server._BIP.mode, 'passthru')
|
||||
self.assertIsNone(server._BIP.target)
|
||||
self.assertTrue(resp['bip']['enabled'])
|
||||
self.assertEqual(resp['bip']['mode'], 'passthru')
|
||||
self.assertEqual(server._scp81_listener_status(), {'mode': 'passthru'})
|
||||
server._scp81_bip_control({'action': 'stop'})
|
||||
self.assertIsNone(server._scp81_listener_status())
|
||||
self.assertFalse(server._BIP.enabled)
|
||||
|
||||
def test_start_accepts_explicit_script_list(self):
|
||||
server._SCP81_PSKS = {}
|
||||
server._SCP81_PSK_LEGACY = None
|
||||
|
||||
Reference in New Issue
Block a user