Compare commits
4 Commits
pre-rename
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f6e48f0e0 | |||
| 3dceb5c56b | |||
| 4a99db13d0 | |||
| ac18c3e2cf |
@@ -1,8 +1,15 @@
|
||||
# OTAMan — SIM OTA toolkit: PWA + local card server
|
||||
# SIMple — a UICC workbench: APDU · OTA · STK · profiling
|
||||
|
||||
OTAMan is an offline HTML/JS PWA for building APDU commands (SIM, USIM, GlobalPlatform RAM), assembling SCP80 secured packets per ETSI TS 102 225, and constructing Expanded Remote Application data format APDU per ETSI TS 102 226. A bundled [`pysim-otaman-server`](pysim_otaman_server/) exposes a local HTTP API over pySim for live card operations: file manager, raw APDU, SIM Toolkit menu browsing, and OTA (SCP80) delivery.
|
||||
> Formerly known as OTAMan — renamed to SIMple on 2026-09-19.
|
||||
|
||||
**Demo:** [otaman.atroshin.ru](https://otaman.atroshin.ru) — the PWA alone, for experimenting. Install the server (below) for card-reader functions.
|
||||
SIMple is a workbench for UICC/SIM card specialists: an offline HTML/JS PWA plus a local card server ([`pysim-simple-server`](pysim_simple_server/), a pySim/PC-SC bridge that also serves the PWA). It covers the full lab cycle:
|
||||
|
||||
- **APDU workbench** — SIM/USIM RFM and Expanded Script builders (TS 102 226), GlobalPlatform RAM commands, a C-APDU/R-APDU parser and a response decoder (ISO 7816 / TS 102 221 / GP);
|
||||
- **OTA lab** — SCP80 secured packets (TS 102 225) and Java Card RAM installation, plus the terminal side of HTTP OTA (SCP81): BIP channel emulation, a PSK TLS administration server and APDU script execution;
|
||||
- **Phone/CAT simulator** — SIM Toolkit menu browser, proactive command / event handling, TERMINAL PROFILE editing and the C-AT dialogue log;
|
||||
- **Card profiler** — filesystem capture (profiles and immutable snapshots with timings), exact snapshot comparison and FCP/FCI decoding.
|
||||
|
||||
**Demo:** [simple.atroshin.ru](https://simple.atroshin.ru) — the PWA alone, for experimenting. Install the server (below) for card-reader functions.
|
||||
|
||||
## Quick start
|
||||
|
||||
@@ -11,8 +18,8 @@ OTAMan is an offline HTML/JS PWA for building APDU commands (SIM, USIM, GlobalPl
|
||||
**Full (PWA + card server):**
|
||||
|
||||
```sh
|
||||
git clone https://github.com/anttro/otaman.git
|
||||
cd otaman
|
||||
git clone https://github.com/anttro/simple.git
|
||||
cd simple
|
||||
./setup.sh # or setup.bat on Windows — creates .venv, installs pysim + server
|
||||
./start.sh # or start.bat — starts the server (it serves the PWA too)
|
||||
```
|
||||
@@ -431,28 +438,6 @@ Delivery PoR (SPI2 `01`) is simpler — the card returns the PoR directly in the
|
||||
- ISO 9797-1: MAC algorithms
|
||||
- NIST SP 800-38B: CMAC
|
||||
|
||||
### Cards
|
||||
|
||||
Stores saved card configurations (presets) in `localStorage`. A preset holds the cryptographic keys, SPI settings, TAR and replay counter for SCP80 operations, plus the **PSK identity / PSK key** pair used by the SCP81 HTTP OTA listener. Cards is a **top-level tab**. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in both SCP80 views.
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| Name | Human-readable label (required) |
|
||||
| ICCID | Optional card identifier |
|
||||
| SPI1 / SPI2 | Security level and PoR settings |
|
||||
| KIc / KID index | Key version number (required together with the keys) |
|
||||
| KIc / KID key | Encryption and MAC key hex |
|
||||
| TAR | Toolkit Application Reference (3 bytes) |
|
||||
| Counter (CNTR) | 10-digit hex replay counter, auto-incremented after each successful SCP80 send |
|
||||
| PSK identity | SCP81 HTTP OTA: the identity the card sends in the TLS handshake |
|
||||
| PSK key | SCP81 HTTP OTA: 32 hex chars (16 bytes); the listener picks it by the identity the card presents |
|
||||
|
||||
The **SCP81** column shows whether the preset supplies a usable PSK pair: **✓** (identity and key), **⚠** (only one of the two — the listener ignores such a preset), **—** (no PSK). Identity and key must be set together.
|
||||
|
||||
**Add a card:** fill in the name, ICCID (optional — **From card** fills it from the equipped card's EF.ICCID), SPI1/SPI2, KIc/KID keys and indices, TAR, the SCP81 PSK pair (optional) and click **Add**. A duplicate ICCID (compared ignoring spaces and the raw-hex form) is refused, naming the conflicting preset. The card appears in the list and becomes available in the RAM tab's **Card preset** dropdown.
|
||||
|
||||
**Edit / remove:** **Edit** loads a preset into the form (the Add button becomes **Save**; **Cancel** clears the form); **Remove** deletes the row from `localStorage`. A successful SCP80 send advances and stores the replay counter, and edits are pushed into a running SCP81 listener automatically.
|
||||
|
||||
### RAM
|
||||
|
||||
All RAM operations are delivered as SCP80 secured packets (ETSI TS 102 225) via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES) for secure transport.
|
||||
@@ -478,11 +463,35 @@ Delete confirms via a browser prompt before sending the GP `DELETE` command via
|
||||
|
||||
---
|
||||
|
||||
## Cards
|
||||
|
||||
Stores saved card configurations (presets) in `localStorage`. A preset holds the cryptographic keys, SPI settings, TAR and replay counter for SCP80 operations, plus the **PSK identity / PSK key** pair used by the SCP81 HTTP OTA listener. Cards is a **top-level tab**. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in both SCP80 views.
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| Name | Human-readable label (required) |
|
||||
| ICCID | Optional card identifier |
|
||||
| SPI1 / SPI2 | Security level and PoR settings |
|
||||
| KIc / KID index | Key version number (required together with the keys) |
|
||||
| KIc / KID key | Encryption and MAC key hex |
|
||||
| TAR | Toolkit Application Reference (3 bytes) |
|
||||
| Counter (CNTR) | 10-digit hex replay counter, auto-incremented after each successful SCP80 send |
|
||||
| PSK identity | SCP81 HTTP OTA: the identity the card sends in the TLS handshake |
|
||||
| PSK key | SCP81 HTTP OTA: 32 hex chars (16 bytes); the listener picks it by the identity the card presents |
|
||||
|
||||
The **SCP81** column shows whether the preset supplies a usable PSK pair: **✓** (identity and key), **⚠** (only one of the two — the listener ignores such a preset), **—** (no PSK). Identity and key must be set together.
|
||||
|
||||
**Add a card:** fill in the name, ICCID (optional — **From card** fills it from the equipped card's EF.ICCID), SPI1/SPI2, KIc/KID keys and indices, TAR, the SCP81 PSK pair (optional) and click **Add**. A duplicate ICCID (compared ignoring spaces and the raw-hex form) is refused, naming the conflicting preset. The card appears in the list and becomes available in the RAM tab's **Card preset** dropdown.
|
||||
|
||||
**Edit / remove:** **Edit** loads a preset into the form (the Add button becomes **Save**; **Cancel** clears the form); **Remove** deletes the row from `localStorage`. A successful SCP80 send advances and stores the replay counter, and edits are pushed into a running SCP81 listener automatically.
|
||||
|
||||
---
|
||||
|
||||
## Card Reader (pySim integration)
|
||||
|
||||
Connects to the bundled [`pysim-otaman-server`](pysim_otaman_server/) for live card operations.
|
||||
Connects to the bundled [`pysim-simple-server`](pysim_simple_server/) for live card operations.
|
||||
|
||||
> **Browser restriction:** when the PWA is served from a public HTTPS host, reaching the local server (`http://127.0.0.1:8080`) requires two things: the server must send `Access-Control-Allow-Private-Network: true` (pysim-otaman-server ≥ 1.6.1 does this automatically), and the browser must be allowed to access the local network — in Chrome/Edge/Vivaldi: Site settings → Local network access → allow the site (or accept the permission prompt). Without the browser permission, the request to `127.0.0.1` is blocked before any preflight is sent.
|
||||
> **Browser restriction:** when the PWA is served from a public HTTPS host, reaching the local server (`http://127.0.0.1:8080`) requires two things: the server must send `Access-Control-Allow-Private-Network: true` (pysim-simple-server ≥ 1.6.1 does this automatically), and the browser must be allowed to access the local network — in Chrome/Edge/Vivaldi: Site settings → Local network access → allow the site (or accept the permission prompt). Without the browser permission, the request to `127.0.0.1` is blocked before any preflight is sent.
|
||||
|
||||
### File Browser
|
||||
|
||||
@@ -605,7 +614,7 @@ The state line shows the listener, the negotiated identity and live channels (by
|
||||
|
||||
### Scripts
|
||||
|
||||
The **Scripts** pill manages named APDU lists stored in `localStorage` (`otaman_scripts`) and sent to the server when a listener starts. Each list is a sequence of hex C-APDUs, one per line (`#`/`;` comments allowed). **New** creates one from a template:
|
||||
The **Scripts** pill manages named APDU lists stored in `localStorage` (`simple_scripts`) and sent to the server when a listener starts. Each list is a sequence of hex C-APDUs, one per line (`#`/`;` comments allowed). **New** creates one from a template:
|
||||
|
||||
| Template | What it builds |
|
||||
|---|---|
|
||||
@@ -618,7 +627,7 @@ The table lists each script with kind, APDU count and creation time; **Edit** op
|
||||
|
||||
## PWA
|
||||
|
||||
OTAMan is a Progressive Web App and can be installed for offline use. Use the **INSTALL PWA** button in the header, or use the browser's install prompt.
|
||||
SIMple is a Progressive Web App and can be installed for offline use. Use the **INSTALL PWA** button in the header, or use the browser's install prompt.
|
||||
|
||||
- Service worker pre-caches all assets on first visit
|
||||
- App icons at 192×192 and 512×512
|
||||
@@ -631,9 +640,9 @@ A dark theme is included. It follows the system preference and can be toggled ma
|
||||
|
||||
The interface is in English with Russian support. The language is detected from `navigator.language`; the header toggle (EN/RU) stores the choice in `localStorage`. Switching the language also re-renders visible dynamic views (profile lists, check reports, snapshots, cards, proactive views).
|
||||
|
||||
## Server (pysim-otaman-server)
|
||||
## Server (pysim-simple-server)
|
||||
|
||||
The bundled Python server wraps [pySim](https://osmocom.org/projects/pysim/wiki) and serves both the OTAMan PWA (from `frontend/`) and a JSON API under `/api/*`.
|
||||
The bundled Python server wraps [pySim](https://osmocom.org/projects/pysim/wiki) and serves both the SIMple PWA (from `frontend/`) and a JSON API under `/api/*`.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@@ -657,7 +666,7 @@ python3 -m venv .venv
|
||||
source .venv/bin/activate # Linux/macOS (Windows: .venv\Scripts\activate)
|
||||
pip install git+https://github.com/osmocom/pysim.git
|
||||
pip install -e . # editable — serves frontend/ from the source tree
|
||||
pysim-otaman-server --http-port 8080
|
||||
pysim-simple-server --http-port 8080
|
||||
```
|
||||
|
||||
### CLI options
|
||||
@@ -689,20 +698,12 @@ pysim-otaman-server --http-port 8080
|
||||
|
||||
See [docs/api.md](docs/api.md) for the full endpoint reference.
|
||||
|
||||
## Theme
|
||||
|
||||
Dark theme is supported. The app follows the OS preference on first visit, and a manual toggle button (🌙/☀️) at the top-right corner persists the choice in `localStorage`.
|
||||
|
||||
## Localisation
|
||||
|
||||
The UI is in English with Russian language support. Language is detected from the browser's `navigator.language` preference. A manual toggle button (EN/RU) in the header persists the choice in `localStorage`.
|
||||
|
||||
## Version compatibility
|
||||
|
||||
| PWA (OTAMan) | Server | Status |
|
||||
| PWA (SIMple) | Server | Status |
|
||||
|-------------|--------|--------|
|
||||
| 1.x.x | 1.x.x | ✅ Compatible |
|
||||
| 1.x.x | 0.x.x | ❌ Outdated — update server |
|
||||
| 1.x.x | 2.x.x+ | ⚠️ Server newer — update PWA |
|
||||
| any | same major | ✅ Compatible |
|
||||
| any | older major | ❌ Outdated — update server |
|
||||
| any | newer major | ⚠️ Server newer — update PWA |
|
||||
|
||||
The PWA checks the server version on connect via `GET /api/version` and warns if versions are incompatible.
|
||||
The PWA checks the server version on connect via `GET /api/version` and compares the major version (e.g. a 2.x PWA with a 2.x server; a 1.x server is flagged as outdated).
|
||||
|
||||
+47
-38
@@ -1,8 +1,15 @@
|
||||
# OTAMan — SIM OTA toolkit: PWA + локальный сервер карт
|
||||
# SIMple — мастерская для UICC/SIM: APDU · OTA · STK · профилирование
|
||||
|
||||
OTAMan — автономный offline-PWA (HTML/JS) для создания APDU-команд (SIM, USIM, GlobalPlatform RAM), сборки защищённых пакетов SCP80 по ETSI TS 102 225 и построения Expanded Remote Application data format APDU по ETSI TS 102 226. В комплекте — [`pysim-otaman-server`](pysim_otaman_server/) — локальный HTTP-сервер поверх pySim для работы с картой: файловый менеджер, сырые APDU, меню SIM Toolkit и доставка OTA.
|
||||
> Прежнее название — OTAMan; переименован в SIMple 2026-09-19.
|
||||
|
||||
**Демо:** [otaman.atroshin.ru](https://otaman.atroshin.ru) — только PWA, для экспериментов. Для функций картридера установите сервер (ниже).
|
||||
SIMple — инструментарий для специалистов по UICC/SIM-картам: автономный HTML/JS PWA и локальный сервер карт ([`pysim-simple-server`](pysim_simple_server/) — мост к pySim/PC-SC, он же раздаёт PWA). Закрывает весь цикл лабораторной работы:
|
||||
|
||||
- **APDU-верстак** — конструкторы SIM/USIM RFM и Expanded Script (TS 102 226), команды GlobalPlatform RAM, парсер C-APDU/R-APDU и декодер ответов (ISO 7816 / TS 102 221 / GP);
|
||||
- **OTA-лаборатория** — защищённые пакеты SCP80 (TS 102 225) и установка Java Card RAM, а также терминальная сторона HTTP OTA (SCP81): эмуляция BIP-каналов, PSK-TLS-сервер администрирования и выполнение скриптов APDU;
|
||||
- **Симулятор телефона** — браузер меню SIM Toolkit, обработка proactive-команд и событий, редактирование TERMINAL PROFILE и журнал C-AT-диалога;
|
||||
- **Профайлер карты** — захват файловой системы (профили и неизменяемые снимки с таймингами), точное сравнение снимков и декодер FCP/FCI.
|
||||
|
||||
**Демо:** [simple.atroshin.ru](https://simple.atroshin.ru) — только PWA, для экспериментов. Для функций картридера установите сервер (ниже).
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
@@ -11,8 +18,8 @@ OTAMan — автономный offline-PWA (HTML/JS) для создания AP
|
||||
**Полная установка (PWA + сервер карт):**
|
||||
|
||||
```sh
|
||||
git clone https://github.com/anttro/otaman.git
|
||||
cd otaman
|
||||
git clone https://github.com/anttro/simple.git
|
||||
cd simple
|
||||
./setup.sh # или setup.bat в Windows — создаёт .venv, ставит pysim + сервер
|
||||
./start.sh # или start.bat — запускает сервер (он же раздаёт PWA)
|
||||
```
|
||||
@@ -405,28 +412,6 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
||||
- ISO 9797-1
|
||||
- NIST SP 800-38B (CMAC)
|
||||
|
||||
### Карты
|
||||
|
||||
Хранит сохранённые конфигурации карт (пресеты) в `localStorage`. Пресет содержит криптографические ключи, настройки SPI, TAR и счётчик повторов для SCP80-операций, а также пару **PSK identity / PSK key** для слушателя SCP81 HTTP OTA. «Карты» — **верхнеуровневая вкладка**. При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается в обоих видах SCP80.
|
||||
|
||||
| Поле | Описание |
|
||||
|---|---|
|
||||
| Name | Понятная метка (обязательна) |
|
||||
| ICCID | Опциональный идентификатор карты; кнопка **С карты** подставляет EF.ICCID подключённой карты (активна, только когда ICCID читается) |
|
||||
| SPI1 / SPI2 | Уровень безопасности и настройки PoR |
|
||||
| Индекс KIc / KID | Номер версии ключа (задаётся вместе с ключами) |
|
||||
| Ключ KIc / KID | Hex-ключи шифрования и MAC |
|
||||
| TAR | Toolkit Application Reference (3 байта) |
|
||||
| Счётчик (CNTR) | 10-значный hex-счётчик повторов, автоматически увеличивается после каждой успешной отправки SCP80 |
|
||||
| PSK identity | SCP81 HTTP OTA: идентификатор, который карта присылает в TLS-рукопожатии |
|
||||
| PSK key | SCP81 HTTP OTA: 32 hex-символа (16 байт); ключ выбирается по идентификатору, который предъявляет карта |
|
||||
|
||||
Столбец **SCP81** показывает, задана ли в пресете рабочая пара PSK: **✓** (идентификатор и ключ), **⚠** (только одно из двух — слушатель такой пресет игнорирует), **—** (PSK нет). Идентификатор и ключ задаются вместе.
|
||||
|
||||
**Добавить карту:** заполните имя, ICCID (опционально — кнопка **С карты** подставляет EF.ICCID подключённой карты), SPI1/SPI2, ключи и индексы KIc/KID, TAR, при необходимости пару PSK и нажмите **Add**. Дубликат ICCID (сравнение без пробелов и с учётом сырой hex-формы) отклоняется с указанием конфликтующего пресета. Карта появится в списке и станет доступна в выпадающем списке **Card preset** на вкладке RAM.
|
||||
|
||||
**Изменить/удалить:** **Edit** загружает пресет в форму (кнопка Add становится **Save**; **Cancel** очищает форму); **Remove** удаляет строку из `localStorage`. Успешная отправка SCP80 увеличивает и сохраняет счётчик повторов, а изменения сразу передаются работающему слушателю SCP81.
|
||||
|
||||
### RAM
|
||||
|
||||
Все операции RAM отправляются как защищённые пакеты SCP80 (ETSI TS 102 225) через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES) для безопасной транспортировки.
|
||||
@@ -452,11 +437,35 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
||||
|
||||
---
|
||||
|
||||
## Карты
|
||||
|
||||
Хранит сохранённые конфигурации карт (пресеты) в `localStorage`. Пресет содержит криптографические ключи, настройки SPI, TAR и счётчик повторов для SCP80-операций, а также пару **PSK identity / PSK key** для слушателя SCP81 HTTP OTA. «Карты» — **верхнеуровневая вкладка**. При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается в обоих видах SCP80.
|
||||
|
||||
| Поле | Описание |
|
||||
|---|---|
|
||||
| Name | Понятная метка (обязательна) |
|
||||
| ICCID | Опциональный идентификатор карты; кнопка **С карты** подставляет EF.ICCID подключённой карты (активна, только когда ICCID читается) |
|
||||
| SPI1 / SPI2 | Уровень безопасности и настройки PoR |
|
||||
| Индекс KIc / KID | Номер версии ключа (задаётся вместе с ключами) |
|
||||
| Ключ KIc / KID | Hex-ключи шифрования и MAC |
|
||||
| TAR | Toolkit Application Reference (3 байта) |
|
||||
| Счётчик (CNTR) | 10-значный hex-счётчик повторов, автоматически увеличивается после каждой успешной отправки SCP80 |
|
||||
| PSK identity | SCP81 HTTP OTA: идентификатор, который карта присылает в TLS-рукопожатии |
|
||||
| PSK key | SCP81 HTTP OTA: 32 hex-символа (16 байт); ключ выбирается по идентификатору, который предъявляет карта |
|
||||
|
||||
Столбец **SCP81** показывает, задана ли в пресете рабочая пара PSK: **✓** (идентификатор и ключ), **⚠** (только одно из двух — слушатель такой пресет игнорирует), **—** (PSK нет). Идентификатор и ключ задаются вместе.
|
||||
|
||||
**Добавить карту:** заполните имя, ICCID (опционально — кнопка **С карты** подставляет EF.ICCID подключённой карты), SPI1/SPI2, ключи и индексы KIc/KID, TAR, при необходимости пару PSK и нажмите **Add**. Дубликат ICCID (сравнение без пробелов и с учётом сырой hex-формы) отклоняется с указанием конфликтующего пресета. Карта появится в списке и станет доступна в выпадающем списке **Card preset** на вкладке RAM.
|
||||
|
||||
**Изменить/удалить:** **Edit** загружает пресет в форму (кнопка Add становится **Save**; **Cancel** очищает форму); **Remove** удаляет строку из `localStorage`. Успешная отправка SCP80 увеличивает и сохраняет счётчик повторов, а изменения сразу передаются работающему слушателю SCP81.
|
||||
|
||||
---
|
||||
|
||||
## Card Reader (интеграция с pySim)
|
||||
|
||||
Подключение к встроенному [`pysim-otaman-server`](pysim_otaman_server/) для работы с картой.
|
||||
Подключение к встроенному [`pysim-simple-server`](pysim_simple_server/) для работы с картой.
|
||||
|
||||
> **Ограничение браузера:** если PWA раздаётся с публичного HTTPS-хоста, для доступа к локальному серверу (`http://127.0.0.1:8080`) нужны два условия: сервер должен отправлять `Access-Control-Allow-Private-Network: true` (pysim-otaman-server ≥ 1.6.1 делает это автоматически), и браузеру должно быть разрешено обращаться к локальной сети — в Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → разрешить сайт (или подтвердить запрос). Без разрешения браузера запрос к `127.0.0.1` блокируется ещё до отправки preflight.
|
||||
> **Ограничение браузера:** если PWA раздаётся с публичного HTTPS-хоста, для доступа к локальному серверу (`http://127.0.0.1:8080`) нужны два условия: сервер должен отправлять `Access-Control-Allow-Private-Network: true` (pysim-simple-server ≥ 1.6.1 делает это автоматически), и браузеру должно быть разрешено обращаться к локальной сети — в Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → разрешить сайт (или подтвердить запрос). Без разрешения браузера запрос к `127.0.0.1` блокируется ещё до отправки preflight.
|
||||
|
||||
### Файловый менеджер
|
||||
|
||||
@@ -575,7 +584,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
||||
|
||||
### Скрипты
|
||||
|
||||
Подвкладка **Scripts** управляет именованными списками APDU, которые хранятся в `localStorage` (`otaman_scripts`) и передаются серверу при старте слушателя. Каждый список — последовательность hex C-APDU, по одной в строке (допускаются комментарии `#`/`;`). Кнопка **New** создаёт список из шаблона:
|
||||
Подвкладка **Scripts** управляет именованными списками APDU, которые хранятся в `localStorage` (`simple_scripts`) и передаются серверу при старте слушателя. Каждый список — последовательность hex C-APDU, по одной в строке (допускаются комментарии `#`/`;`). Кнопка **New** создаёт список из шаблона:
|
||||
|
||||
| Шаблон | Что строит |
|
||||
|---|---|
|
||||
@@ -588,7 +597,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
||||
|
||||
## PWA
|
||||
|
||||
OTAMan — Progressive Web App. Можно установить для offline-использования через кнопку **INSTALL PWA** или через браузер.
|
||||
SIMple — Progressive Web App. Можно установить для offline-использования через кнопку **INSTALL PWA** или через браузер.
|
||||
|
||||
- Service worker кеширует все ресурсы при первом посещении
|
||||
- Иконки 192×192 и 512×512
|
||||
@@ -603,17 +612,17 @@ OTAMan — Progressive Web App. Можно установить для offline-
|
||||
|
||||
## Совместимость версий
|
||||
|
||||
| PWA (OTAMan) | Сервер | Статус |
|
||||
| PWA (SIMple) | Сервер | Статус |
|
||||
|---|---|---|
|
||||
| 1.x.x | 1.x.x | ✅ Совместимы |
|
||||
| 1.x.x | 0.x.x | ❌ Сервер устарел |
|
||||
| 1.x.x | 2.x.x+ | ⚠️ Сервер новее — обновите PWA |
|
||||
| любая | та же мажорная | ✅ Совместимы |
|
||||
| любая | старее мажорная | ❌ Сервер устарел — обновите сервер |
|
||||
| любая | новее мажорная | ⚠️ Сервер новее — обновите PWA |
|
||||
|
||||
PWA проверяет версию сервера при подключении через `GET /api/version`.
|
||||
PWA проверяет версию сервера при подключении через `GET /api/version` и сравнивает мажорную версию (например, PWA 2.x с сервером 2.x; сервер 1.x помечается как устаревший).
|
||||
|
||||
---
|
||||
|
||||
## Сервер (pysim-otaman-server)
|
||||
## Сервер (pysim-simple-server)
|
||||
|
||||
Встроенный Python-сервер оборачивает [pySim](https://osmocom.org/projects/pysim/wiki) и раздаёт как PWA (из `frontend/`), так и JSON API по `/api/*`.
|
||||
|
||||
@@ -639,7 +648,7 @@ python3 -m venv .venv
|
||||
source .venv/bin/activate # Linux/macOS (Windows: .venv\Scripts\activate)
|
||||
pip install git+https://github.com/osmocom/pysim.git
|
||||
pip install -e . # editable — раздаёт frontend/ из исходного дерева
|
||||
pysim-otaman-server --http-port 8080
|
||||
pysim-simple-server --http-port 8080
|
||||
```
|
||||
|
||||
### Параметры CLI
|
||||
|
||||
+14
-6
@@ -1,4 +1,4 @@
|
||||
# pysim-otaman-server — HTTP API reference
|
||||
# pysim-simple-server — HTTP API reference
|
||||
|
||||
The server exposes a JSON HTTP API under `/api/*`. All responses carry
|
||||
`Access-Control-Allow-Origin: *` (plus `Access-Control-Allow-Private-Network: true`
|
||||
@@ -6,14 +6,15 @@ on the preflight), so the API is reachable from a separately-hosted PWA.
|
||||
|
||||
## Version compatibility
|
||||
|
||||
| Server | PWA (OTAMan) | Status |
|
||||
| Server | PWA (SIMple) | Status |
|
||||
|--------|-------------|--------|
|
||||
| 1.x.x | 1.x.x | ✅ Compatible |
|
||||
| 0.x.x | 1.x.x | ❌ Outdated — update server |
|
||||
| 2.x.x+ | 1.x.x | ⚠️ Server newer — update PWA |
|
||||
| same major | any | ✅ Compatible |
|
||||
| older major | any | ❌ Outdated — update server |
|
||||
| newer major | any | ⚠️ Server newer — update PWA |
|
||||
|
||||
The server reports its version via `GET /api/version`. The PWA checks this on
|
||||
connect and warns if versions are incompatible.
|
||||
connect and compares the major version (a 1.x server is flagged as outdated by
|
||||
a 2.x PWA).
|
||||
|
||||
## Endpoints
|
||||
|
||||
@@ -23,6 +24,7 @@ connect and warns if versions are incompatible.
|
||||
| `/api/status` | GET | Card reader + card info + current selection |
|
||||
| `/api/command` | POST | pySim command (equip, status, tree, etc.) |
|
||||
| `/api/commands` | GET | List available pySim commands |
|
||||
| `/api/cardinfo` | GET | pySim `cardinfo` output |
|
||||
| `/api/tree` | POST | File tree browser for given FID/name |
|
||||
| `/api/select` | POST | Select a file by name or FID |
|
||||
| `/api/read` | POST | Read file content |
|
||||
@@ -87,6 +89,12 @@ with the same ICCID in both SCP80 views (Secured Packet and RAM).
|
||||
|
||||
List all available shell commands for the current card profile.
|
||||
|
||||
### `GET /api/cardinfo`
|
||||
|
||||
Runs pySim's `cardinfo` command and returns its output as `{"output": "..."}`
|
||||
(card type, ATR, ICCID and other information pySim reports for the equipped
|
||||
card). A shortcut for `POST /api/command` with `{"cmd": "cardinfo"}`.
|
||||
|
||||
### `POST /api/command`
|
||||
|
||||
Execute any pysim-shell command.
|
||||
|
||||
@@ -5,7 +5,7 @@ Purpose: record **every attempted configuration and its outcome**, so the same
|
||||
variations are not repeated. Add rows as tests are run; keep the confirmed
|
||||
rules section current.
|
||||
|
||||
Setup: `pysim_otaman_server` with a PC/SC reader, the PWA SCP81 tab (or
|
||||
Setup: `pysim_simple_server` with a PC/SC reader, the PWA SCP81 tab (or
|
||||
`POST /api/scp81/bip`), the card triggered by its SMS-PP push / the Location
|
||||
status event. Server log at `GET /api/scp81/log`, script state at
|
||||
`GET /api/scp81/script`, proactive history at `GET /api/proactive-log`.
|
||||
@@ -308,8 +308,7 @@ INSTALL [for install] -> registries.
|
||||
## Next tests / work
|
||||
|
||||
1. **UI:** group the per-page R-APDUs under their logical command in the
|
||||
SCP81 tab (page merging/decoding for ELF and application listings);
|
||||
expose the framing options in the tab.
|
||||
SCP81 tab (page merging/decoding for ELF and application listings).
|
||||
2. **Load/store over SCP81:** implemented - `POST /api/scp81/gen-install`
|
||||
takes a `.cap`, expands it with the shared `_cap_apdu_sequence` helper
|
||||
(INSTALL [for load] -> 240-byte LOAD blocks -> INSTALL [for install]) and
|
||||
|
||||
+20
-20
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OTAMan — Справка</title>
|
||||
<title>SIMple — Справка</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body class="bg-neutral-50 dark:bg-slate-900 text-gray-800 dark:text-slate-200">
|
||||
@@ -16,12 +16,12 @@
|
||||
</aside>
|
||||
|
||||
<main class="flex-1 min-w-0 px-6 py-8">
|
||||
<h1 class="text-2xl font-bold text-heading mb-6">OTAMan <span class="text-xs text-gray-400 dark:text-slate-500 ml-2">Документация</span></h1>
|
||||
<h1 class="text-2xl font-bold text-heading mb-6">SIMple <span class="text-xs text-gray-400 dark:text-slate-500 ml-2">Документация</span></h1>
|
||||
<section class="mb-10">
|
||||
<h2 id="overview" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">1. Обзор</h2>
|
||||
<p class="mb-3">OTAMan — Progressive Web App (PWA) для построения APDU-команд, сборки защищённых пакетов SCP80, просмотра меню SIM Toolkit (STK) и симуляции реальной сетевой среды для тестирования SIM/USIM/UICC-карт через PC/SC-ридер.</p>
|
||||
<p class="mb-3">Приложение — один статический файл <code class="font-mono text-sm">index.html</code>. Все вычисления выполняются в браузере; доступ к карте — через локальный HTTP-сервер (<code class="font-mono text-sm">pysim-otaman-server</code>), оборачивающий библиотеку <a href="https://osmocom.org/projects/pysim" class="text-blue-600 dark:text-blue-400 hover:underline">pySim</a>.</p>
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">Браузер (OTAMan PWA) → HTTP :8080 → pysim-otaman-server → pySim → PC/SC → ридер → UICC/SIM</pre>
|
||||
<p class="mb-3">SIMple — инструментарий для специалистов по UICC/SIM-картам: автономный Progressive Web App (PWA), закрывающий весь цикл лабораторной работы — <strong>APDU-верстак</strong> (конструкторы SIM/USIM RFM и Expanded Script, команды GlobalPlatform RAM, парсер C-APDU/R-APDU и декодер ответов), <strong>OTA-лаборатория</strong> (защищённые пакеты SCP80 и установка Java Card RAM, а также терминальная сторона HTTP OTA — SCP81: эмуляция BIP-каналов, PSK-TLS-сервер администрирования и выполнение скриптов APDU), <strong>симулятор телефона</strong> (меню SIM Toolkit, proactive-команды и события, редактирование TERMINAL PROFILE, журнал C-AT-диалога) и <strong>профайлер карты</strong> (профили и неизменяемые снимки с таймингами, точное сравнение снимков, декодер FCP/FCI).</p>
|
||||
<p class="mb-3">Приложение — один статический файл <code class="font-mono text-sm">index.html</code>. Все вычисления выполняются в браузере; доступ к карте — через локальный HTTP-сервер (<code class="font-mono text-sm">pysim-simple-server</code>), оборачивающий библиотеку <a href="https://osmocom.org/projects/pysim" class="text-blue-600 dark:text-blue-400 hover:underline">pySim</a>.</p>
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">Браузер (SIMple PWA) → HTTP :8080 → pysim-simple-server → pySim → PC/SC → ридер → UICC/SIM</pre>
|
||||
<p class="mb-3">Стандарты, на которые опирается приложение:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li>ETSI TS 102 221 — интерфейс UICC-терминал (CLA 00, файлы USIM)</li>
|
||||
@@ -331,7 +331,7 @@
|
||||
<p class="mb-2"><strong>Сценарий</strong> выбирает список команд, отдаваемый в сессии — <strong>Нет</strong> (оставить настроенный на сервере скрипт) или один из скриптов подвкладки <strong>«Скрипты»</strong>; выбранный список передаётся серверу при старте слушателя.</p>
|
||||
<p class="text-sm mb-3">Строка состояния показывает слушатель, согласованный идентификатор и активные каналы (байты in/out). Панель <strong>Script results (R-APDUs)</strong> перечисляет каждый отданный C-APDU с его R-APDU/SW и прогрессом <em>done/total</em>; журнал <strong>HTTP OTA log</strong> фиксирует OPEN/CLOSE CHANNEL, SEND/RECEIVE DATA и каждый шаг TLS/HTTP/скрипта (<code class="font-mono text-sm">tls-handshake</code>, <code class="font-mono text-sm">tls-request</code>, <code class="font-mono text-sm">script-send</code>, <code class="font-mono text-sm">script-rapdu</code>, <code class="font-mono text-sm">script-page</code>, <code class="font-mono text-sm">script-done</code>, <code class="font-mono text-sm">data-available</code>, <code class="font-mono text-sm">peer-close</code>), кнопка <strong>Clear</strong> очищает его.</p>
|
||||
<h3 id="scp81-scripts" class="text-lg font-medium mb-2">5.2 Скрипты</h3>
|
||||
<p class="text-sm mb-2">Именованные списки APDU хранятся локально (<code class="font-mono text-sm">otaman_scripts</code>) и передаются серверу при старте слушателя. Каждый список — последовательность hex C-APDU (по одной в строке; допускаются комментарии <code class="font-mono text-sm">#</code>/<code class="font-mono text-sm">;</code>). Кнопка <strong>Новый</strong> создаёт список из шаблона:</p>
|
||||
<p class="text-sm mb-2">Именованные списки APDU хранятся локально (<code class="font-mono text-sm">simple_scripts</code>) и передаются серверу при старте слушателя. Каждый список — последовательность hex C-APDU (по одной в строке; допускаются комментарии <code class="font-mono text-sm">#</code>/<code class="font-mono text-sm">;</code>). Кнопка <strong>Новый</strong> создаёт список из шаблона:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Empty</strong> — начать с пустого списка.</li>
|
||||
<li><strong>Explore</strong> — эталонная административная последовательность (<code class="font-mono text-sm">GET DATA FF21</code>, листинги GET STATUS ISD/ELF/приложений, <code class="font-mono text-sm">GET DATA 0085</code>); длинные листинги автоматически продолжаются страницами <code class="font-mono text-sm">SW 6310/CAFE</code>.</li>
|
||||
@@ -343,7 +343,7 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="card-reader" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">6. Вкладка «Картридер» (pySim)</h2>
|
||||
<p class="mb-3">Подключение к локальному <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> для работы с картой: введите URL сервера (по умолчанию <code class="font-mono text-sm">http://127.0.0.1:8080</code>) и нажмите <strong>Подключиться</strong>. Область статуса показывает состояние ридера/карты, а <strong>Подключить карту</strong> (пере)инициализирует карту после вставки. Подвкладки: <strong>Файловый менеджер</strong>, <strong>Командная строка pySim</strong> и <strong>Отправка APDU</strong>. <strong>«Профайлер»</strong> и <strong>«Симулятор телефона»</strong> — отдельные вкладки верхнего уровня.</p>
|
||||
<p class="mb-3">Подключение к локальному <a href="https://github.com/anttro/simple" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-simple-server</a> для работы с картой: введите URL сервера (по умолчанию <code class="font-mono text-sm">http://127.0.0.1:8080</code>) и нажмите <strong>Подключиться</strong>. Область статуса показывает состояние ридера/карты, а <strong>Подключить карту</strong> (пере)инициализирует карту после вставки. Подвкладки: <strong>Файловый менеджер</strong>, <strong>Командная строка pySim</strong> и <strong>Отправка APDU</strong>. <strong>«Профайлер»</strong> и <strong>«Симулятор телефона»</strong> — отдельные вкладки верхнего уровня.</p>
|
||||
|
||||
<h3 id="file-manager" class="text-lg font-medium mb-2">6.1 Файловый менеджер</h3>
|
||||
<p class="text-sm mb-2">Дерево файловой системы отображается слева; выбор файла открывает панель деталей справа. Элементы сгруппированы: DF выше EF, сортировка по <strong>FID</strong> или символьному <strong>имени</strong> (пиллы и кнопка <strong>«Проверить все файлы»</strong> закреплены над прокручиваемым деревом; выбор сохраняется в <code class="font-mono text-sm">localStorage</code>). При выборе файла над содержимым также показываются FID, тип файла, размер / структура записей и декодированный FCI.</p>
|
||||
@@ -478,7 +478,7 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="server" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">9. Установка сервера</h2>
|
||||
<p class="mb-3">Для работы с картой (вкладка «Картридер», «Симулятор телефона», доставка OTA) нужен локальный <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> — встроенный в OTAMan HTTP-сервер, оборачивающий pySim, работающий с ридером через PC/SC или serial и раздающий сам PWA (откройте <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
|
||||
<p class="mb-3">Для работы с картой (вкладка «Картридер», «Симулятор телефона», доставка OTA) нужен локальный <a href="https://github.com/anttro/simple" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-simple-server</a> — встроенный в SIMple HTTP-сервер, оборачивающий pySim, работающий с ридером через PC/SC или serial и раздающий сам PWA (откройте <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
|
||||
|
||||
<h3 id="prerequisites" class="text-lg font-medium mb-2">9.1 Требования</h3>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
@@ -489,15 +489,15 @@
|
||||
</ul>
|
||||
|
||||
<h3 id="quickstart-linux" class="text-lg font-medium mb-2">9.2 Быстрый старт — Linux / macOS</h3>
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
|
||||
cd otaman
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/simple.git
|
||||
cd simple
|
||||
chmod +x setup.sh start.sh
|
||||
./setup.sh # создаёт .venv, устанавливает pysim + сервер (однократно)
|
||||
./start.sh # запускает сервер (PWA + API, автоопределение ридера)</pre>
|
||||
|
||||
<h3 id="quickstart-windows" class="text-lg font-medium mb-2">9.3 Быстрый старт — Windows</h3>
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
|
||||
cd otaman
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/simple.git
|
||||
cd simple
|
||||
setup.bat # создаёт .venv, устанавливает pysim + сервер (однократно)
|
||||
start.bat # запускает сервер (PWA + API)</pre>
|
||||
|
||||
@@ -528,26 +528,26 @@ source .venv/bin/activate # Linux/macOS
|
||||
# Установить pysim
|
||||
pip install git+https://github.com/osmocom/pysim.git
|
||||
|
||||
# Установить pysim-otaman-server (editable — раздаёт встроенный PWA)
|
||||
# Установить pysim-simple-server (editable — раздаёт встроенный PWA)
|
||||
pip install -e .
|
||||
|
||||
# Запустить сервер (PWA + API)
|
||||
pysim-otaman-server --http-port 8080</pre>
|
||||
pysim-simple-server --http-port 8080</pre>
|
||||
<p class="text-sm mb-3">Подключите PC/SC-ридер с SIM-картой и откройте <code class="font-mono text-sm">http://127.0.0.1:8080</code> — PWA и API на одном origin, поэтому CORS не требуется.</p>
|
||||
<p class="text-sm mb-3">Если PWA раздаётся с публичного HTTPS-хоста (например, <code class="font-mono text-sm">https://otaman.example.com</code>), для доступа к локальному серверу карт нужны два условия: (1) сервер отвечает на preflight заголовком <code class="font-mono text-sm">Access-Control-Allow-Private-Network: true</code> (pysim-otaman-server ≥ 1.6.1 делает это автоматически), и (2) браузеру должно быть разрешено обращаться к локальной сети — в Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → разрешить сайт (или подтвердить запрос). Без разрешения браузера запрос к <code class="font-mono text-sm">127.0.0.1</code> блокируется ещё до отправки preflight.</p>
|
||||
<p class="text-sm mb-3">Если PWA раздаётся с публичного HTTPS-хоста (например, <code class="font-mono text-sm">https://simple.example.com</code>), для доступа к локальному серверу карт нужны два условия: (1) сервер отвечает на preflight заголовком <code class="font-mono text-sm">Access-Control-Allow-Private-Network: true</code> (pysim-simple-server ≥ 1.6.1 делает это автоматически), и (2) браузеру должно быть разрешено обращаться к локальной сети — в Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → разрешить сайт (или подтвердить запрос). Без разрешения браузера запрос к <code class="font-mono text-sm">127.0.0.1</code> блокируется ещё до отправки preflight.</p>
|
||||
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="compatibility" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">10. Совместимость версий</h2>
|
||||
<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">PWA (OTAMan)</th><th class="text-left py-1 px-2">Сервер</th><th class="text-left py-1 px-2">Статус</th></tr></thead>
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">PWA (SIMple)</th><th class="text-left py-1 px-2">Сервер</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 font-mono">1.x.x</td><td class="py-1 px-2 font-mono">1.x.x</td><td class="py-1 px-2">✅ Совместимы</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">1.x.x</td><td class="py-1 px-2 font-mono">0.x.x</td><td class="py-1 px-2">❌ Устарел — обновите сервер</td></tr>
|
||||
<tr><td class="py-1 px-2 font-mono">1.x.x</td><td class="py-1 px-2 font-mono">2.x.x+</td><td class="py-1 px-2">⚠️ Сервер новее — обновите PWA</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">любая</td><td class="py-1 px-2 font-mono">та же мажорная</td><td class="py-1 px-2">✅ Совместимы</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">любая</td><td class="py-1 px-2 font-mono">старее мажорная</td><td class="py-1 px-2">❌ Устарел — обновите сервер</td></tr>
|
||||
<tr><td class="py-1 px-2 font-mono">любая</td><td class="py-1 px-2 font-mono">новее мажорная</td><td class="py-1 px-2">⚠️ Сервер новее — обновите PWA</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-sm">PWA проверяет версию сервера при подключении через <code class="font-mono text-sm">GET /api/version</code> и предупреждает о несовместимости.</p>
|
||||
<p class="text-sm">PWA проверяет версию сервера при подключении через <code class="font-mono text-sm">GET /api/version</code> и сравнивает мажорную версию (например, PWA 2.x с сервером 2.x; сервер 1.x помечается как устаревший).</p>
|
||||
|
||||
|
||||
</main>
|
||||
|
||||
+20
-20
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OTAMan — Help</title>
|
||||
<title>SIMple — Help</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body class="bg-neutral-50 dark:bg-slate-900 text-gray-800 dark:text-slate-200">
|
||||
@@ -16,12 +16,12 @@
|
||||
</aside>
|
||||
|
||||
<main class="flex-1 min-w-0 px-6 py-8">
|
||||
<h1 class="text-2xl font-bold text-heading mb-6">OTAMan <span class="text-xs text-gray-400 dark:text-slate-500 ml-2">Documentation</span></h1>
|
||||
<h1 class="text-2xl font-bold text-heading mb-6">SIMple <span class="text-xs text-gray-400 dark:text-slate-500 ml-2">Documentation</span></h1>
|
||||
<section class="mb-10">
|
||||
<h2 id="overview" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">1. Overview</h2>
|
||||
<p class="mb-3">OTAMan is a Progressive Web App (PWA) for building APDU commands, assembling SCP80 secured packets, browsing the SIM Toolkit (STK) menu, and simulating a real network environment against a SIM/USIM/UICC card via a PC/SC reader.</p>
|
||||
<p class="mb-3">The application is a single static <code class="font-mono text-sm">index.html</code> file. All computation runs in the browser; the card is accessed through a local HTTP server (<code class="font-mono text-sm">pysim-otaman-server</code>) that wraps the <a href="https://osmocom.org/projects/pysim" class="text-blue-600 dark:text-blue-400 hover:underline">pySim</a> library.</p>
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">Browser (OTAMan PWA) → HTTP :8080 → pysim-otaman-server → pySim → PC/SC → card reader → UICC/SIM</pre>
|
||||
<p class="mb-3">SIMple is a workbench for UICC/SIM card specialists: an offline Progressive Web App (PWA) covering the full lab cycle — an <strong>APDU workbench</strong> (SIM/USIM RFM and Expanded Script builders, GlobalPlatform RAM commands, a C-APDU/R-APDU parser and a response decoder), an <strong>OTA lab</strong> (SCP80 secured packets and Java Card RAM installation, plus the terminal side of HTTP OTA — SCP81: BIP channel emulation, a PSK TLS administration server and APDU script execution), a <strong>phone/CAT simulator</strong> (SIM Toolkit menu, proactive commands and events, TERMINAL PROFILE editing, the C-AT dialogue log) and a <strong>card profiler</strong> (profiles and immutable snapshots with timings, exact snapshot comparison, FCP/FCI decoding).</p>
|
||||
<p class="mb-3">The application is a single static <code class="font-mono text-sm">index.html</code> file. All computation runs in the browser; the card is accessed through a local HTTP server (<code class="font-mono text-sm">pysim-simple-server</code>) that wraps the <a href="https://osmocom.org/projects/pysim" class="text-blue-600 dark:text-blue-400 hover:underline">pySim</a> library.</p>
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">Browser (SIMple PWA) → HTTP :8080 → pysim-simple-server → pySim → PC/SC → card reader → UICC/SIM</pre>
|
||||
<p class="mb-3">Standards referenced across the application:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li>ETSI TS 102 221 — UICC-Terminal interface (CLA 00, USIM files)</li>
|
||||
@@ -331,7 +331,7 @@
|
||||
<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>
|
||||
<p class="text-sm mb-3">The state line shows the listener, the negotiated identity and live channels (bytes in/out). <strong>Script results (R-APDUs)</strong> lists each served C-APDU with its R-APDU/SW and a <em>done/total</em> progress; the <strong>HTTP OTA log</strong> records OPEN/CLOSE CHANNEL, SEND/RECEIVE DATA and every TLS/HTTP/script step (<code class="font-mono text-sm">tls-handshake</code>, <code class="font-mono text-sm">tls-request</code>, <code class="font-mono text-sm">script-send</code>, <code class="font-mono text-sm">script-rapdu</code>, <code class="font-mono text-sm">script-page</code>, <code class="font-mono text-sm">script-done</code>, <code class="font-mono text-sm">data-available</code>, <code class="font-mono text-sm">peer-close</code>), with <strong>Clear</strong>.</p>
|
||||
<h3 id="scp81-scripts" class="text-lg font-medium mb-2">5.2 Scripts</h3>
|
||||
<p class="text-sm mb-2">Named APDU lists stored locally (<code class="font-mono text-sm">otaman_scripts</code>) and sent to the server when a listener starts. Each list is a sequence of hex C-APDUs (one per line; <code class="font-mono text-sm">#</code>/<code class="font-mono text-sm">;</code> comments allowed). <strong>New</strong> creates one from a template:</p>
|
||||
<p class="text-sm mb-2">Named APDU lists stored locally (<code class="font-mono text-sm">simple_scripts</code>) and sent to the server when a listener starts. Each list is a sequence of hex C-APDUs (one per line; <code class="font-mono text-sm">#</code>/<code class="font-mono text-sm">;</code> comments allowed). <strong>New</strong> creates one from a template:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Empty</strong> — start from scratch.</li>
|
||||
<li><strong>Explore</strong> — the reference administration sequence (<code class="font-mono text-sm">GET DATA FF21</code>, GET STATUS ISD/ELF/application listings, <code class="font-mono text-sm">GET DATA 0085</code>); long listings auto-continue through the <code class="font-mono text-sm">SW 6310/CAFE</code> pages.</li>
|
||||
@@ -343,7 +343,7 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="card-reader" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">6. Card reader (pySim) tab</h2>
|
||||
<p class="mb-3">Connects to a local <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> for live card operations: enter the server URL (default <code class="font-mono text-sm">http://127.0.0.1:8080</code>) and press <strong>Connect</strong>. The status area shows the reader/card state, and <strong>Equip card</strong> (re)initializes the card after insertion. Sub-tabs: <strong>File manager</strong>, <strong>pySim command line</strong>, and <strong>Raw APDU</strong>. The <strong>Profiler</strong> and <strong>Phone simulator</strong> are separate top-level tabs.</p>
|
||||
<p class="mb-3">Connects to a local <a href="https://github.com/anttro/simple" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-simple-server</a> for live card operations: enter the server URL (default <code class="font-mono text-sm">http://127.0.0.1:8080</code>) and press <strong>Connect</strong>. The status area shows the reader/card state, and <strong>Equip card</strong> (re)initializes the card after insertion. Sub-tabs: <strong>File manager</strong>, <strong>pySim command line</strong>, and <strong>Raw APDU</strong>. The <strong>Profiler</strong> and <strong>Phone simulator</strong> are separate top-level tabs.</p>
|
||||
|
||||
<h3 id="file-manager" class="text-lg font-medium mb-2">6.1 File manager</h3>
|
||||
<p class="text-sm mb-2">The file system tree is displayed on the left; selecting a file opens its detail pane on the right. Entries are grouped with DFs above EFs and sorted by <strong>FID</strong> or symbolic <strong>Name</strong> (pills pinned above the scrolling tree together with <strong>Probe all files</strong>; the choice is remembered in <code class="font-mono text-sm">localStorage</code>). Selecting a file also shows its FID, file type, size / record layout and the decoded FCI above the content pane.</p>
|
||||
@@ -478,7 +478,7 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="server" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">9. Server installation</h2>
|
||||
<p class="mb-3">Live card operations (Card reader tab, Phone simulator, OTA delivery) require the local <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> — a small HTTP server bundled with OTAMan that wraps pySim, talks to the reader over PC/SC or serial, and also serves the PWA itself (open <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
|
||||
<p class="mb-3">Live card operations (Card reader tab, Phone simulator, OTA delivery) require the local <a href="https://github.com/anttro/simple" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-simple-server</a> — a small HTTP server bundled with SIMple that wraps pySim, talks to the reader over PC/SC or serial, and also serves the PWA itself (open <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
|
||||
|
||||
<h3 id="prerequisites" class="text-lg font-medium mb-2">9.1 Prerequisites</h3>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
@@ -489,15 +489,15 @@
|
||||
</ul>
|
||||
|
||||
<h3 id="quickstart-linux" class="text-lg font-medium mb-2">9.2 Quick start — Linux / macOS</h3>
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
|
||||
cd otaman
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/simple.git
|
||||
cd simple
|
||||
chmod +x setup.sh start.sh
|
||||
./setup.sh # creates .venv, installs pysim + server (run once)
|
||||
./start.sh # starts the server (serves PWA + API, auto-detects reader)</pre>
|
||||
|
||||
<h3 id="quickstart-windows" class="text-lg font-medium mb-2">9.3 Quick start — Windows</h3>
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
|
||||
cd otaman
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/simple.git
|
||||
cd simple
|
||||
setup.bat # creates .venv, installs pysim + server (run once)
|
||||
start.bat # starts the server (serves PWA + API)</pre>
|
||||
|
||||
@@ -528,26 +528,26 @@ source .venv/bin/activate # Linux/macOS
|
||||
# Install pysim
|
||||
pip install git+https://github.com/osmocom/pysim.git
|
||||
|
||||
# Install pysim-otaman-server (editable, so it serves the bundled PWA)
|
||||
# Install pysim-simple-server (editable, so it serves the bundled PWA)
|
||||
pip install -e .
|
||||
|
||||
# Start the server (serves PWA + API)
|
||||
pysim-otaman-server --http-port 8080</pre>
|
||||
pysim-simple-server --http-port 8080</pre>
|
||||
<p class="text-sm mb-3">Connect a PC/SC reader with a SIM card, then open <code class="font-mono text-sm">http://127.0.0.1:8080</code> — the PWA and API share one origin, so no CORS is involved.</p>
|
||||
<p class="text-sm mb-3">If the PWA is served from a public HTTPS host (e.g. <code class="font-mono text-sm">https://otaman.example.com</code>), two things are required to reach a local card server: (1) the server must answer the preflight with <code class="font-mono text-sm">Access-Control-Allow-Private-Network: true</code> (pysim-otaman-server ≥ 1.6.1 does this automatically), and (2) the browser must be allowed to access the local network — in Chrome/Edge/Vivaldi: Site settings → Local network access → allow the site (or accept the permission prompt). Without the browser permission, the request to <code class="font-mono text-sm">127.0.0.1</code> is blocked before any preflight is sent.</p>
|
||||
<p class="text-sm mb-3">If the PWA is served from a public HTTPS host (e.g. <code class="font-mono text-sm">https://simple.example.com</code>), two things are required to reach a local card server: (1) the server must answer the preflight with <code class="font-mono text-sm">Access-Control-Allow-Private-Network: true</code> (pysim-simple-server ≥ 1.6.1 does this automatically), and (2) the browser must be allowed to access the local network — in Chrome/Edge/Vivaldi: Site settings → Local network access → allow the site (or accept the permission prompt). Without the browser permission, the request to <code class="font-mono text-sm">127.0.0.1</code> is blocked before any preflight is sent.</p>
|
||||
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="compatibility" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">10. Version compatibility</h2>
|
||||
<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">PWA (OTAMan)</th><th class="text-left py-1 px-2">Server</th><th class="text-left py-1 px-2">Status</th></tr></thead>
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">PWA (SIMple)</th><th class="text-left py-1 px-2">Server</th><th class="text-left py-1 px-2">Status</th></tr></thead>
|
||||
<tbody>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">1.x.x</td><td class="py-1 px-2 font-mono">1.x.x</td><td class="py-1 px-2">✅ Compatible</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">1.x.x</td><td class="py-1 px-2 font-mono">0.x.x</td><td class="py-1 px-2">❌ Outdated — update server</td></tr>
|
||||
<tr><td class="py-1 px-2 font-mono">1.x.x</td><td class="py-1 px-2 font-mono">2.x.x+</td><td class="py-1 px-2">⚠️ Server newer — update PWA</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">any</td><td class="py-1 px-2 font-mono">same major</td><td class="py-1 px-2">✅ Compatible</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">any</td><td class="py-1 px-2 font-mono">older major</td><td class="py-1 px-2">❌ Outdated — update server</td></tr>
|
||||
<tr><td class="py-1 px-2 font-mono">any</td><td class="py-1 px-2 font-mono">newer major</td><td class="py-1 px-2">⚠️ Server newer — update PWA</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-sm">The PWA checks the server version on connect via <code class="font-mono text-sm">GET /api/version</code> and warns if versions are incompatible.</p>
|
||||
<p class="text-sm">The PWA checks the server version on connect via <code class="font-mono text-sm">GET /api/version</code> and compares the major version (e.g. 2.x PWA with a 2.x server; a 1.x server is flagged as outdated).</p>
|
||||
|
||||
|
||||
</main>
|
||||
|
||||
+37
-33
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OTAMan</title>
|
||||
<title>SIMple</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||
<link rel="icon" type="image/png" href="sim.png" sizes="462x462">
|
||||
@@ -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.21</span></h1>
|
||||
<h1 class="text-2xl font-bold text-heading">SIMple <span id="app-version" class="text-xs text-gray-400 dark:text-slate-500 ml-1"></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>
|
||||
@@ -26,7 +26,7 @@
|
||||
<span id="state-indicator-adm" class="hidden ml-0.5 text-xs font-mono" title=""></span>
|
||||
</span>
|
||||
<button id="install-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" style="display:none">INSTALL PWA [for offline use]</button>
|
||||
<a href="https://github.com/anttro/otaman" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a>
|
||||
<a href="https://github.com/anttro/simple" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a>
|
||||
<a id="help-link" href="help.html" target="_blank" rel="noopener" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300" data-l10n="help">help</a>
|
||||
<button onclick="toggleLang()" id="lang-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700">EN</button>
|
||||
<button onclick="toggleTheme()" id="theme-btn" class="px-2 py-1 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700">🌙</button>
|
||||
@@ -815,8 +815,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="pysim-connect-info" class="mb-3 text-xs text-gray-500 dark:text-slate-400 leading-relaxed">
|
||||
<p data-l10n="To access local card reader, you need pysim-otaman-server running locally.">To access local card reader, you need pysim-otaman-server running locally.</p>
|
||||
<p><span data-l10n="To connect, install">To connect, install</span> <a href="https://github.com/anttro/otaman" target="_blank" class="text-blue-600 dark:text-blue-400 underline">pysim-otaman-server</a>: <code class="font-mono bg-gray-100 dark:bg-slate-700 px-1 rounded">git clone https://github.com/anttro/otaman.git</code> <span data-l10n="and start it.">and start it.</span><br><span data-l10n="Refer to pysim-otaman-server README files for details.">Refer to pysim-otaman-server README files for details.</span> <span data-l10n="Click">Click</span> <b data-l10n="Connect">Connect</b> <span data-l10n="when pysim-otaman-server is ready">when pysim-otaman-server is ready</span></p>
|
||||
<p data-l10n="To access local card reader, you need pysim-simple-server running locally.">To access local card reader, you need pysim-simple-server running locally.</p>
|
||||
<p><span data-l10n="To connect, install">To connect, install</span> <a href="https://github.com/anttro/simple" target="_blank" class="text-blue-600 dark:text-blue-400 underline">pysim-simple-server</a>: <code class="font-mono bg-gray-100 dark:bg-slate-700 px-1 rounded">git clone https://github.com/anttro/simple.git</code> <span data-l10n="and start it.">and start it.</span><br><span data-l10n="Refer to pysim-simple-server README files for details.">Refer to pysim-simple-server README files for details.</span> <span data-l10n="Click">Click</span> <b data-l10n="Connect">Connect</b> <span data-l10n="when pysim-simple-server is ready">when pysim-simple-server is ready</span></p>
|
||||
<div id="pysim-connect-msg" class="text-xs"></div>
|
||||
</div>
|
||||
<div id="pysim-connected-row" class="mb-3 hidden">
|
||||
@@ -1302,6 +1302,12 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ===== Version =====
|
||||
// Single source of truth for the PWA version: shown in the header and used
|
||||
// by the server version check in pysimConnect().
|
||||
const SIMPLE_VERSION = '2.3.1';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
function switchTab(name) {
|
||||
if (_scp81Timer && name !== 'scp81') { clearInterval(_scp81Timer); _scp81Timer = null; }
|
||||
@@ -4679,7 +4685,7 @@ function decodeResponse() {
|
||||
|
||||
// ===== pysim integration =====
|
||||
// Default to a local server on 127.0.0.1:8080. If this PWA is served by
|
||||
// pysim-otaman-server itself (same origin), detect the co-located API once
|
||||
// pysim-simple-server itself (same origin), detect the co-located API once
|
||||
// and switch to a relative base; otherwise keep the default and wait for the
|
||||
// user to point at a server.
|
||||
let pysimBase = 'http://127.0.0.1:8080';
|
||||
@@ -4912,14 +4918,15 @@ async function pysimConnect() {
|
||||
if (!ver.version) throw new Error('No version');
|
||||
_pysimServerAvailable = true;
|
||||
const sv = (ver.version || '').split('.').map(Number);
|
||||
if (sv[0] < 1 || (sv[0] === 1 && sv[1] < 0)) {
|
||||
msgEl.innerHTML = '<span class="text-red-500">Server version ' + esc(ver.version) + ' is outdated. Please update pysim-otaman-server.</span>';
|
||||
const pv = SIMPLE_VERSION.split('.').map(Number);
|
||||
if (sv[0] < pv[0]) {
|
||||
msgEl.innerHTML = '<span class="text-red-500">Server version ' + esc(ver.version) + ' is outdated. Please update pysim-simple-server.</span>';
|
||||
btn.textContent = t('Connect');
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
if (sv[0] > 1) {
|
||||
msgEl.innerHTML = '<span class="text-yellow-500">Warning: server version ' + esc(ver.version) + ' is newer than expected. Update OTAMan for full compatibility.</span>';
|
||||
if (sv[0] > pv[0]) {
|
||||
msgEl.innerHTML = '<span class="text-yellow-500">Warning: server version ' + esc(ver.version) + ' is newer than expected. Update SIMple for full compatibility.</span>';
|
||||
}
|
||||
const status = await pysimFetch('/api/status');
|
||||
if (!status.app_ready) {
|
||||
@@ -5947,7 +5954,7 @@ let _cardsEditIdx = null;
|
||||
|
||||
function cardsLoad() {
|
||||
try {
|
||||
const d = localStorage.getItem('otaman_cards');
|
||||
const d = localStorage.getItem('simple_cards');
|
||||
cards = d ? JSON.parse(d) : [];
|
||||
} catch (e) { cards = []; }
|
||||
cardsRebuildSelect();
|
||||
@@ -5955,7 +5962,7 @@ function cardsLoad() {
|
||||
}
|
||||
|
||||
function cardsSave() {
|
||||
localStorage.setItem('otaman_cards', JSON.stringify(cards));
|
||||
localStorage.setItem('simple_cards', JSON.stringify(cards));
|
||||
cardsRebuildSelect();
|
||||
cardsRender();
|
||||
scp81PushPskMap();
|
||||
@@ -6225,7 +6232,7 @@ function downloadJson(name, obj) {
|
||||
}
|
||||
|
||||
function cardsExportFile() {
|
||||
downloadJson('otaman-cards.json', cards);
|
||||
downloadJson('simple-cards.json', cards);
|
||||
}
|
||||
|
||||
async function cardsImport(text) {
|
||||
@@ -6511,7 +6518,7 @@ function pysimCustomRefreshTree() {
|
||||
pysimFsRenderTree();
|
||||
}
|
||||
|
||||
let pysimFsSort = localStorage.getItem('otaman_fs_sort') === 'name' ? 'name' : 'fid';
|
||||
let pysimFsSort = localStorage.getItem('simple_fs_sort') === 'name' ? 'name' : 'fid';
|
||||
|
||||
function pysimFsSortChildren(children, mode) {
|
||||
return (children || []).slice().sort((a, b) => {
|
||||
@@ -6536,7 +6543,7 @@ function pysimFsSortChildren(children, mode) {
|
||||
|
||||
function pysimFsSetSort(mode) {
|
||||
pysimFsSort = mode === 'name' ? 'name' : 'fid';
|
||||
localStorage.setItem('otaman_fs_sort', pysimFsSort);
|
||||
localStorage.setItem('simple_fs_sort', pysimFsSort);
|
||||
document.querySelectorAll('.pysim-fs-sort-pill').forEach(p => {
|
||||
const active = p.dataset.fsSort === pysimFsSort;
|
||||
p.classList.toggle('bg-blue-600', active);
|
||||
@@ -8036,7 +8043,7 @@ function scp81OptionsBadge() {
|
||||
|
||||
function scp81OptionsLoad() {
|
||||
let saved = {};
|
||||
try { saved = JSON.parse(localStorage.getItem('otaman_scp81_opts') || '{}') || {}; } catch (e) { saved = {}; }
|
||||
try { saved = JSON.parse(localStorage.getItem('simple_scp81_opts') || '{}') || {}; } catch (e) { saved = {}; }
|
||||
const opt = Object.assign({}, SCP81_OPT_DEFAULTS, saved);
|
||||
if (saved.nextUriOn === undefined) opt.nextUriOn = (opt.nextUri || '') !== '';
|
||||
const set = (id, v) => { const el = document.getElementById(id); if (el) el.value = v; };
|
||||
@@ -8057,13 +8064,13 @@ function scp81OptionsLoad() {
|
||||
}
|
||||
|
||||
function scp81OptionsPersist() {
|
||||
localStorage.setItem('otaman_scp81_opts', JSON.stringify(scp81OptionsState()));
|
||||
localStorage.setItem('simple_scp81_opts', JSON.stringify(scp81OptionsState()));
|
||||
scp81OptionsSyncDisabled();
|
||||
scp81OptionsBadge();
|
||||
}
|
||||
|
||||
function scp81OptionsReset() {
|
||||
localStorage.removeItem('otaman_scp81_opts');
|
||||
localStorage.removeItem('simple_scp81_opts');
|
||||
scp81OptionsLoad();
|
||||
}
|
||||
|
||||
@@ -8602,14 +8609,14 @@ let _scriptsNewKind = 'empty';
|
||||
|
||||
function scriptsLoad() {
|
||||
try {
|
||||
const d = localStorage.getItem('otaman_scripts');
|
||||
const d = localStorage.getItem('simple_scripts');
|
||||
scripts = d ? JSON.parse(d) : [];
|
||||
} catch (e) { scripts = []; }
|
||||
scriptsRebuildSelect();
|
||||
}
|
||||
|
||||
function scriptsPersist() {
|
||||
localStorage.setItem('otaman_scripts', JSON.stringify(scripts));
|
||||
localStorage.setItem('simple_scripts', JSON.stringify(scripts));
|
||||
scriptsRebuildSelect();
|
||||
scriptsRender();
|
||||
}
|
||||
@@ -9444,7 +9451,7 @@ function pysimCustomExport() {
|
||||
}
|
||||
|
||||
function pysimCustomExportFile() {
|
||||
downloadJson('otaman-custom-files.json', pysimCustomFiles);
|
||||
downloadJson('simple-custom-files.json', pysimCustomFiles);
|
||||
}
|
||||
|
||||
async function pysimCustomImport(text) {
|
||||
@@ -9529,25 +9536,25 @@ const PROFILER_MASK_PREFIX4_FIDS = {
|
||||
|
||||
function profilerLoad() {
|
||||
try {
|
||||
const d = localStorage.getItem('otaman_profiles');
|
||||
const d = localStorage.getItem('simple_profiles');
|
||||
profiles = d ? JSON.parse(d) : [];
|
||||
} catch (e) { profiles = []; }
|
||||
}
|
||||
|
||||
function profilerSave() {
|
||||
localStorage.setItem('otaman_profiles', JSON.stringify(profiles));
|
||||
localStorage.setItem('simple_profiles', JSON.stringify(profiles));
|
||||
}
|
||||
|
||||
function snapshotsLoad() {
|
||||
try {
|
||||
const d = localStorage.getItem('otaman_snapshots');
|
||||
const d = localStorage.getItem('simple_snapshots');
|
||||
snapshots = d ? JSON.parse(d) : [];
|
||||
} catch (e) { snapshots = []; }
|
||||
}
|
||||
|
||||
function snapshotsSave() {
|
||||
try {
|
||||
localStorage.setItem('otaman_snapshots', JSON.stringify(snapshots));
|
||||
localStorage.setItem('simple_snapshots', JSON.stringify(snapshots));
|
||||
} catch (e) {
|
||||
alert('Snapshot save failed: ' + e.message);
|
||||
}
|
||||
@@ -11400,7 +11407,6 @@ const LANG_RU = {
|
||||
'Response data (hex)': 'Данные ответа (hex)',
|
||||
'— Select command —': '— Выберите команду —',
|
||||
'— select preset —': '— выберите предустановленный —',
|
||||
'SIM OTA with a Human Face': 'SIM OTA с человеческим лицом',
|
||||
'Error: specify SELECT parameters': 'Ошибка: укажите параметры SELECT',
|
||||
'Error: unknown command': 'Ошибка: неизвестная команда',
|
||||
'Error: specify data': 'Ошибка: укажите данные',
|
||||
@@ -11499,12 +11505,12 @@ const LANG_RU = {
|
||||
'Install Package (.cap file)': 'Установка пакета (.cap файл)',
|
||||
'Server URL': 'URL сервера',
|
||||
'Connect': 'Подключиться',
|
||||
'To access local card reader, you need pysim-otaman-server running locally.': 'Для доступа к локальному картридеру необходим запущенный pysim-otaman-server.',
|
||||
'To access local card reader, you need pysim-simple-server running locally.': 'Для доступа к локальному картридеру необходим запущенный pysim-simple-server.',
|
||||
'To connect, install': 'Для подключения установите',
|
||||
'and start it.': 'и запустите его.',
|
||||
'Refer to pysim-otaman-server README files for details.': 'Подробности — в README файлах pysim-otaman-server.',
|
||||
'Refer to pysim-simple-server README files for details.': 'Подробности — в README файлах pysim-simple-server.',
|
||||
'Click': 'Нажмите кнопку',
|
||||
'when pysim-otaman-server is ready': 'когда pysim-otaman-server будет готов',
|
||||
'when pysim-simple-server is ready': 'когда pysim-simple-server будет готов',
|
||||
'Equip card': 'Подключить карту',
|
||||
'Check status': 'Проверить статус',
|
||||
'Reset card': 'Сброс карты',
|
||||
@@ -11532,8 +11538,8 @@ const LANG_RU = {
|
||||
'Checking card...': 'Проверка карты...',
|
||||
'Connected': 'Подключено',
|
||||
'Connection failed:': 'Ошибка подключения:',
|
||||
'pysim-blocked-hint': 'Браузер, вероятно, заблокировал доступ к локальному серверу карт. Разрешите доступ к локальной сети для этого сайта (Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → Разрешить). Также проверьте, что pysim-otaman-server запущен по адресу {url}.',
|
||||
'pysim-unreachable-hint': 'Не удалось подключиться к серверу карт. Проверьте, что pysim-otaman-server запущен и адрес указан верно ({url}). Если эта страница открыта по HTTPS, также разрешите доступ к локальной сети в браузере (Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → Разрешить).',
|
||||
'pysim-blocked-hint': 'Браузер, вероятно, заблокировал доступ к локальному серверу карт. Разрешите доступ к локальной сети для этого сайта (Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → Разрешить). Также проверьте, что pysim-simple-server запущен по адресу {url}.',
|
||||
'pysim-unreachable-hint': 'Не удалось подключиться к серверу карт. Проверьте, что pysim-simple-server запущен и адрес указан верно ({url}). Если эта страница открыта по HTTPS, также разрешите доступ к локальной сети в браузере (Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → Разрешить).',
|
||||
'No card detected. Insert card and click Equip card button': 'Карта не обнаружена. Вставьте карту и нажмите Подключить карту',
|
||||
'Card inserted — press Equip': 'Карта вставлена — нажмите «Подключить карту»',
|
||||
'Card inserted — initializing...': 'Карта вставлена — инициализация...',
|
||||
@@ -11890,8 +11896,6 @@ function translatePage() {
|
||||
const key = el.getAttribute('data-l10n');
|
||||
el.textContent = t(key);
|
||||
});
|
||||
const slogan = document.getElementById('slogan');
|
||||
if (slogan) slogan.textContent = t('SIM OTA with a Human Face');
|
||||
}
|
||||
|
||||
function isViewVisible(id) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "OTAMan",
|
||||
"short_name": "OTAMan",
|
||||
"description": "Offline APDU helper for SIM/USIM/RAM commands",
|
||||
"name": "SIMple",
|
||||
"short_name": "SIMple",
|
||||
"description": "UICC/SIM workbench: APDU toolkit, SIM OTA (SCP80/SCP81), STK/CAT simulator, card profiler",
|
||||
"start_url": "index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "#fafafa",
|
||||
|
||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "otaman",
|
||||
"name": "simple",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "otaman",
|
||||
"name": "simple",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"aes-js": "^3.1.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "otaman",
|
||||
"name": "simple",
|
||||
"version": "1.0.0",
|
||||
"description": "Standalone offline HTML/JS tool for building APDU commands for SIM, USIM, and GlobalPlatform RAM, plus encoding conversions.",
|
||||
"description": "SIMple — a UICC workbench: APDU toolkit, SCP80/SCP81 OTA lab, STK/CAT (phone) simulator and card profiler.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "npx tailwindcss -i src/style.css -o style.css --config tailwind.config.js && cat src/contrast.css >> style.css",
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/anttro/otaman.git"
|
||||
"url": "https://github.com/anttro/simple.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"type": "commonjs",
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'otaman-v188';
|
||||
const CACHE = 'simple-v190';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -167,9 +167,9 @@ test('load/persist/reset round-trip through localStorage', () => {
|
||||
'opt-targeted-app': '//aid/A000000151000000'
|
||||
});
|
||||
scp81OptionsPersist();
|
||||
assert.ok(store['otaman_scp81_opts'].includes('"chunkSize":100'));
|
||||
assert.ok(store['otaman_scp81_opts'].includes('"targetedAppOn":true'));
|
||||
assert.ok(!store['otaman_scp81_opts'].includes('keepAlive'));
|
||||
assert.ok(store['simple_scp81_opts'].includes('"chunkSize":100'));
|
||||
assert.ok(store['simple_scp81_opts'].includes('"targetedAppOn":true'));
|
||||
assert.ok(!store['simple_scp81_opts'].includes('keepAlive'));
|
||||
// a reload restores the saved setup
|
||||
setup({});
|
||||
scp81OptionsLoad();
|
||||
@@ -184,7 +184,7 @@ test('load/persist/reset round-trip through localStorage', () => {
|
||||
assert.strictEqual(get('opt-targeted-app').value, '//aid/A000000151000000');
|
||||
// reset clears the saved entry and restores the defaults
|
||||
scp81OptionsReset();
|
||||
assert.ok(!('otaman_scp81_opts' in store));
|
||||
assert.ok(!('simple_scp81_opts' in store));
|
||||
scp81OptionsLoad();
|
||||
assert.strictEqual(get('opt-chunked').checked, true);
|
||||
assert.strictEqual(get('opt-next-uri').checked, true);
|
||||
|
||||
+5
-5
@@ -3,16 +3,16 @@ requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-otaman-server"
|
||||
version = "2.2.21"
|
||||
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
||||
name = "pysim-simple-server"
|
||||
version = "2.3.1"
|
||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
# Declaring it here would make pip resolve its full tree (including the SMPP
|
||||
# bridge -> Twisted -> twisted-iocpsupport, which needs MSVC on Windows).
|
||||
|
||||
[project.scripts]
|
||||
pysim-otaman-server = "pysim_otaman_server.__main__:main"
|
||||
pysim-simple-server = "pysim_simple_server.__main__:main"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["pysim_otaman_server*"]
|
||||
include = ["pysim_simple_server*"]
|
||||
@@ -33,11 +33,11 @@ def main():
|
||||
_server_start = time.time()
|
||||
mod = load_pysim_app()
|
||||
parser = mod.option_parser
|
||||
parser.description = 'pysim-otaman-server — HTTP API for pysim'
|
||||
parser.description = 'pysim-simple-server — HTTP API for pysim'
|
||||
parser.add_argument('--http-host', default='127.0.0.1', help='Bind address (default: 127.0.0.1)')
|
||||
parser.add_argument('--http-port', type=int, default=8080, help='Bind port (default: 8080)')
|
||||
parser.add_argument('--web-dir', default=_default_web_dir(), metavar='PATH',
|
||||
help='Directory with the OTAMan PWA static files to serve (default: <repo>/frontend)')
|
||||
help='Directory with the SIMple PWA static files to serve (default: <repo>/frontend)')
|
||||
parser.add_argument('--log-requests', action='store_true', default=False, help='Log request/response payloads to stderr')
|
||||
parser.add_argument('--sms-oa', default='12345', metavar='DIGITS',
|
||||
help='TP-Originating-Address (SMSC number) for the SMS-DELIVER TPDU (default: 12345)')
|
||||
@@ -222,14 +222,14 @@ def main():
|
||||
server.iccid = iccid
|
||||
server.equipping = False
|
||||
# Set server reference for polling timer and mark the card session state
|
||||
import pysim_otaman_server.server
|
||||
pysim_otaman_server.server._server_ref = server
|
||||
pysim_otaman_server.server._CARD_CONNECTED = card is not None
|
||||
import pysim_simple_server.server
|
||||
pysim_simple_server.server._server_ref = server
|
||||
pysim_simple_server.server._CARD_CONNECTED = card is not None
|
||||
if opts.poll_interval is not None:
|
||||
pysim_otaman_server.server._set_poll_interval(opts.poll_interval)
|
||||
pysim_simple_server.server._set_poll_interval(opts.poll_interval)
|
||||
# Auto-enable polling if card initialized successfully (unless interval is 0)
|
||||
if server.scc and server.card and opts.poll_interval != 0:
|
||||
pysim_otaman_server.server._poll_enable()
|
||||
pysim_simple_server.server._poll_enable()
|
||||
# Start presence monitoring only after the startup init: pyscard reports an
|
||||
# already-present card as "added" on the first pass, and we must not
|
||||
# auto-equip over a session we just initialized. If startup init failed,
|
||||
@@ -237,8 +237,8 @@ def main():
|
||||
if sl is not None and getattr(sl, '_reader', None) is not None:
|
||||
start_card_monitor(str(sl._reader))
|
||||
print("─" * 70)
|
||||
print(" pysim-otaman-server v%s listening on http://%s:%s" % (VERSION, opts.http_host, opts.http_port))
|
||||
print(" Open http://%s:%s in your browser for the OTAMan UI (served by this server)."
|
||||
print(" pysim-simple-server v%s listening on http://%s:%s" % (VERSION, opts.http_host, opts.http_port))
|
||||
print(" Open http://%s:%s in your browser for the SIMple UI (served by this server)."
|
||||
% (opts.http_host, opts.http_port))
|
||||
print("─" * 70)
|
||||
try:
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Fast card initialization for pysim-otaman-server.
|
||||
"""Fast card initialization for pysim-simple-server.
|
||||
|
||||
pySim's ``init_card()`` performs several physical card resets: one per profile
|
||||
candidate tried by ``CardProfile.pick()`` plus one at the end of
|
||||
@@ -11,8 +11,8 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from io import StringIO
|
||||
from pySim.transport import ApduTracer, ProactiveHandler
|
||||
from pySim.cards import UiccCardBase
|
||||
from pysim_otaman_server import httpota
|
||||
from pysim_otaman_server import scp81
|
||||
from pysim_simple_server import httpota
|
||||
from pysim_simple_server import scp81
|
||||
from smartcard.CardMonitoring import CardMonitor, CardObserver
|
||||
|
||||
import gsm0338 # registers 'gsm03.38' codec
|
||||
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '2.2.21'
|
||||
VERSION = '2.3.1'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
@echo off
|
||||
REM pysim-otaman-server setup script for Windows
|
||||
REM pysim-simple-server setup script for Windows
|
||||
REM Creates a venv and installs pysim and its dependencies.
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set VENV_DIR=%~dp0.venv
|
||||
|
||||
echo === pysim-otaman-server setup ===
|
||||
echo === pysim-simple-server setup ===
|
||||
echo.
|
||||
|
||||
REM Check Python
|
||||
@@ -71,11 +71,11 @@ if %errorlevel% neq 0 (
|
||||
)
|
||||
echo.
|
||||
|
||||
REM Install pysim-otaman-server
|
||||
echo === Installing pysim-otaman-server ===
|
||||
REM Install pysim-simple-server
|
||||
echo === Installing pysim-simple-server ===
|
||||
pip install -e "%~dp0."
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Failed to install pysim-otaman-server.
|
||||
echo Error: Failed to install pysim-simple-server.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# pysim-otaman-server setup script
|
||||
# pysim-simple-server setup script
|
||||
# Creates a venv and installs pysim and its dependencies.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
@@ -30,9 +30,9 @@ echo ""
|
||||
echo "=== Installing pysim ==="
|
||||
$PIP install git+https://github.com/osmocom/pysim.git
|
||||
|
||||
# Install pysim-otaman-server
|
||||
# Install pysim-simple-server
|
||||
echo ""
|
||||
echo "=== Installing pysim-otaman-server ==="
|
||||
echo "=== Installing pysim-simple-server ==="
|
||||
$PIP install -e "$SCRIPT_DIR"
|
||||
|
||||
# Check for PC/SC
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
@echo off
|
||||
REM pysim-otaman-server start script for Windows
|
||||
REM pysim-simple-server start script for Windows
|
||||
REM Starts the server, preferring the venv if it exists.
|
||||
REM PC/SC is built into Windows, so defaults to reader 0.
|
||||
|
||||
set VENV_DIR=%~dp0.venv
|
||||
|
||||
if exist "%VENV_DIR%\Scripts\pysim-otaman-server.exe" (
|
||||
echo Starting pysim-otaman-server from venv on http://127.0.0.1:8080
|
||||
if exist "%VENV_DIR%\Scripts\pysim-simple-server.exe" (
|
||||
echo Starting pysim-simple-server from venv on http://127.0.0.1:8080
|
||||
echo Press Ctrl+C to stop.
|
||||
"%VENV_DIR%\Scripts\pysim-otaman-server.exe" --http-port 8080 -p 0
|
||||
"%VENV_DIR%\Scripts\pysim-simple-server.exe" --http-port 8080 -p 0
|
||||
goto :eof
|
||||
)
|
||||
|
||||
if exist "%~dp0pysim_otaman_server\__main__.py" (
|
||||
echo Starting pysim-otaman-server from source on http://127.0.0.1:8080
|
||||
if exist "%~dp0pysim_simple_server\__main__.py" (
|
||||
echo Starting pysim-simple-server from source on http://127.0.0.1:8080
|
||||
echo Press Ctrl+C to stop.
|
||||
python -m pysim_otaman_server --http-port 8080 -p 0
|
||||
python -m pysim_simple_server --http-port 8080 -p 0
|
||||
goto :eof
|
||||
)
|
||||
|
||||
where pysim-otaman-server >nul 2>&1
|
||||
where pysim-simple-server >nul 2>&1
|
||||
if %errorlevel% equ 0 (
|
||||
echo Starting pysim-otaman-server on http://127.0.0.1:8080
|
||||
echo Starting pysim-simple-server on http://127.0.0.1:8080
|
||||
echo Press Ctrl+C to stop.
|
||||
pysim-otaman-server --http-port 8080 -p 0
|
||||
pysim-simple-server --http-port 8080 -p 0
|
||||
goto :eof
|
||||
)
|
||||
|
||||
echo Error: pysim-otaman-server not installed.
|
||||
echo Error: pysim-simple-server not installed.
|
||||
echo Run setup.bat first or install manually:
|
||||
echo pip install pysim-otaman-server
|
||||
echo pip install pysim-simple-server
|
||||
pause
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# pysim-otaman-server start script
|
||||
# pysim-simple-server start script
|
||||
# Starts the server, preferring the venv if it exists.
|
||||
# Auto-detects PC/SC reader if available.
|
||||
|
||||
@@ -21,21 +21,21 @@ if command -v pcscd > /dev/null 2>&1; then
|
||||
fi
|
||||
|
||||
SERVER=""
|
||||
if [ -f "$VENV_DIR/bin/pysim-otaman-server" ]; then
|
||||
SERVER="$VENV_DIR/bin/pysim-otaman-server"
|
||||
elif command -v pysim-otaman-server &> /dev/null; then
|
||||
SERVER="pysim-otaman-server"
|
||||
elif [ -f "$SCRIPT_DIR/pysim_otaman_server/__main__.py" ]; then
|
||||
echo "Starting pysim-otaman-server from source on http://127.0.0.1:8080"
|
||||
cd "$SCRIPT_DIR" && python3 -m pysim_otaman_server --http-port 8080 $READER_ARGS
|
||||
if [ -f "$VENV_DIR/bin/pysim-simple-server" ]; then
|
||||
SERVER="$VENV_DIR/bin/pysim-simple-server"
|
||||
elif command -v pysim-simple-server &> /dev/null; then
|
||||
SERVER="pysim-simple-server"
|
||||
elif [ -f "$SCRIPT_DIR/pysim_simple_server/__main__.py" ]; then
|
||||
echo "Starting pysim-simple-server from source on http://127.0.0.1:8080"
|
||||
cd "$SCRIPT_DIR" && python3 -m pysim_simple_server --http-port 8080 $READER_ARGS
|
||||
exit $?
|
||||
else
|
||||
echo "Error: pysim-otaman-server not installed."
|
||||
echo "Error: pysim-simple-server not installed."
|
||||
echo "Run setup.sh first or install manually:"
|
||||
echo " pip install pysim-otaman-server"
|
||||
echo " pip install pysim-simple-server"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting pysim-otaman-server on http://127.0.0.1:8080"
|
||||
echo "Starting pysim-simple-server on http://127.0.0.1:8080"
|
||||
echo "Press Ctrl+C to stop."
|
||||
$SERVER --http-port 8080 $READER_ARGS
|
||||
|
||||
@@ -13,7 +13,7 @@ PY_SIM = PROJECTS / 'pysim'
|
||||
if str(PY_SIM) not in sys.path:
|
||||
sys.path.insert(0, str(PY_SIM))
|
||||
|
||||
import pysim_otaman_server.server as S
|
||||
import pysim_simple_server.server as S
|
||||
|
||||
|
||||
class TestClassifyApdu(unittest.TestCase):
|
||||
|
||||
@@ -12,7 +12,7 @@ PY_SIM = PROJECTS / 'pysim'
|
||||
if str(PY_SIM) not in sys.path:
|
||||
sys.path.insert(0, str(PY_SIM))
|
||||
|
||||
import pysim_otaman_server.server as S
|
||||
import pysim_simple_server.server as S
|
||||
|
||||
|
||||
class FakeCard:
|
||||
|
||||
@@ -15,8 +15,8 @@ if str(PY_SIM) not in sys.path:
|
||||
from pySim.exceptions import SwMatchError
|
||||
from pySim.ts_102_221 import CardProfileUICC
|
||||
|
||||
import pysim_otaman_server.fastinit as fastinit
|
||||
from pysim_otaman_server.fastinit import (
|
||||
import pysim_simple_server.fastinit as fastinit
|
||||
from pysim_simple_server.fastinit import (
|
||||
FastRuntimeState,
|
||||
do_reset_fast,
|
||||
pick_profile_no_reset,
|
||||
|
||||
@@ -20,8 +20,8 @@ PY_SIM = PROJECTS / 'pysim'
|
||||
if str(PY_SIM) not in sys.path:
|
||||
sys.path.insert(0, str(PY_SIM))
|
||||
|
||||
from pysim_otaman_server import httpota
|
||||
import pysim_otaman_server.server as server
|
||||
from pysim_simple_server import httpota
|
||||
import pysim_simple_server.server as server
|
||||
|
||||
|
||||
OPEN_LOCALHOST = bytes.fromhex(
|
||||
@@ -480,7 +480,7 @@ if __name__ == '__main__':
|
||||
# A >127-byte channel data TLV must use the BER long form (0x81 len),
|
||||
# as the reference terminal traces do (`36 81 ed` for 237 bytes).
|
||||
import types
|
||||
server = __import__('pysim_otaman_server.server', fromlist=['x'])
|
||||
server = __import__('pysim_simple_server.server', fromlist=['x'])
|
||||
big = bytes(range(256)) * 1 # 256 bytes; take a slice below
|
||||
ch = types.SimpleNamespace(rx=bytearray(b'\xAA' * 237))
|
||||
class FakeBip:
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@ PY_SIM = PROJECTS / 'pysim'
|
||||
if str(PY_SIM) not in sys.path:
|
||||
sys.path.insert(0, str(PY_SIM))
|
||||
|
||||
from pysim_otaman_server import server
|
||||
from pysim_otaman_server.server import _decode_iccid, _read_iccid
|
||||
from pysim_simple_server import server
|
||||
from pysim_simple_server.server import _decode_iccid, _read_iccid
|
||||
|
||||
|
||||
class FakeFile:
|
||||
|
||||
@@ -12,7 +12,7 @@ PY_SIM = PROJECTS / 'pysim'
|
||||
if str(PY_SIM) not in sys.path:
|
||||
sys.path.insert(0, str(PY_SIM))
|
||||
|
||||
import pysim_otaman_server.server as S
|
||||
import pysim_simple_server.server as S
|
||||
|
||||
|
||||
class TestMenuTimeout(unittest.TestCase):
|
||||
|
||||
+36
-36
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Unit tests for the OTA helper functions in pysim_otaman_server.server.
|
||||
"""Unit tests for the OTA helper functions in pysim_simple_server.server.
|
||||
|
||||
Reference vectors are key-free: synthetic dummy keys plus the already-public
|
||||
sysmocom sample-key vectors that ship in pySim's own tests/unittests/test_ota.py.
|
||||
@@ -19,7 +19,7 @@ PY_SIM = PROJECTS / 'pysim'
|
||||
if str(PY_SIM) not in sys.path:
|
||||
sys.path.insert(0, str(PY_SIM))
|
||||
|
||||
from pysim_otaman_server.server import (
|
||||
from pysim_simple_server.server import (
|
||||
_build_sms_tpdu,
|
||||
_build_tr,
|
||||
_decode_cmd,
|
||||
@@ -106,7 +106,7 @@ class TestBuildSmsTpdu(unittest.TestCase):
|
||||
SCTS = bytes.fromhex('24051215173000')
|
||||
|
||||
def _build(self, *args, **kwargs):
|
||||
with mock.patch('pysim_otaman_server.server._encode_scts', return_value=self.SCTS):
|
||||
with mock.patch('pysim_simple_server.server._encode_scts', return_value=self.SCTS):
|
||||
return _build_sms_tpdu(*args, **kwargs)
|
||||
|
||||
def test_single_message_with_cpi(self):
|
||||
@@ -273,7 +273,7 @@ class TestProactiveDecode(unittest.TestCase):
|
||||
"""Server-side proactive command/TR decode helpers (v1.8.0 log feature)."""
|
||||
|
||||
def setUp(self):
|
||||
import pysim_otaman_server.server as srv
|
||||
import pysim_simple_server.server as srv
|
||||
srv._PROACTIVE_SESSION_START = 1234.0
|
||||
srv._PLI_DATA[0x00] = '93055210011000'
|
||||
|
||||
@@ -397,14 +397,14 @@ class TestProactiveDecode(unittest.TestCase):
|
||||
self.assertEqual(r[1], {'label': 'Data bytes', 'value': '3'})
|
||||
|
||||
def test_parse_proactive_header_plain_tags(self):
|
||||
import pysim_otaman_server.server as srv
|
||||
import pysim_simple_server.server as srv
|
||||
raw = bytes.fromhex('d00c010301270102028182240103')
|
||||
self.assertEqual(srv._parse_proactive_header(raw), (1, 0x27, 0x81, 0x82, 0x01))
|
||||
|
||||
def test_default_handler_logs_timer_management(self):
|
||||
# pySim's auto-handler path: the parsed command object (not the empty
|
||||
# collection) is re-encoded for the log and used for the response.
|
||||
import pysim_otaman_server.server as srv
|
||||
import pysim_simple_server.server as srv
|
||||
from pySim.cat import ProactiveCommand
|
||||
from pySim.utils import h2b
|
||||
srv._PROACTIVE_LOG.clear()
|
||||
@@ -420,7 +420,7 @@ class TestProactiveDecode(unittest.TestCase):
|
||||
self.assertEqual(entry['tr_result'], '00')
|
||||
|
||||
def test_default_handler_pli_includes_dict_data(self):
|
||||
import pysim_otaman_server.server as srv
|
||||
import pysim_simple_server.server as srv
|
||||
from pySim.cat import ProactiveCommand
|
||||
from pySim.utils import h2b
|
||||
srv._PROACTIVE_LOG.clear()
|
||||
@@ -529,7 +529,7 @@ class TestEventDownload(unittest.TestCase):
|
||||
"""ENVELOPE (EVENT DOWNLOAD) assembly, TS 102 223 7.5.11."""
|
||||
|
||||
def _send(self, event_type, event_data):
|
||||
import pysim_otaman_server.server as srv
|
||||
import pysim_simple_server.server as srv
|
||||
calls = []
|
||||
|
||||
class Tp:
|
||||
@@ -565,30 +565,30 @@ class TestTimerManagement(unittest.TestCase):
|
||||
START = bytes.fromhex('d011810301270082028182a40101a503001000')
|
||||
|
||||
def tearDown(self):
|
||||
import pysim_otaman_server.server as srv
|
||||
import pysim_simple_server.server as srv
|
||||
srv._timer_cancel()
|
||||
|
||||
def test_hms_bcd_roundtrip(self):
|
||||
import pysim_otaman_server.server as srv
|
||||
import pysim_simple_server.server as srv
|
||||
self.assertEqual(srv._hms_bcd(60).hex(), '001000')
|
||||
self.assertEqual(srv._hms_bcd(3723).hex(), '102030')
|
||||
self.assertEqual([srv._bcd_swap(b) for b in srv._hms_bcd(3723)], [1, 2, 3])
|
||||
|
||||
def test_start_returns_result_only_and_arms_timer(self):
|
||||
import pysim_otaman_server.server as srv
|
||||
import pysim_simple_server.server as srv
|
||||
tr = srv._handle_timer_command(1, 0x27, 0x00, self.START, 0x81, 0x82)
|
||||
self.assertEqual(tr.hex(), '810301270082028281030100')
|
||||
remaining = srv._timer_remaining(1)
|
||||
self.assertTrue(55 <= remaining <= 60, remaining)
|
||||
|
||||
def test_get_returns_remaining_value(self):
|
||||
import pysim_otaman_server.server as srv
|
||||
import pysim_simple_server.server as srv
|
||||
srv._handle_timer_command(1, 0x27, 0x00, self.START, 0x81, 0x82)
|
||||
tr = srv._handle_timer_command(1, 0x27, 0x02, self.START, 0x81, 0x82)
|
||||
self.assertEqual(tr.hex(), '810301270282028281a40101a503001000030100')
|
||||
|
||||
def test_deactivate_stops_and_reports_value(self):
|
||||
import pysim_otaman_server.server as srv
|
||||
import pysim_simple_server.server as srv
|
||||
srv._handle_timer_command(1, 0x27, 0x00, self.START, 0x81, 0x82)
|
||||
tr = srv._handle_timer_command(1, 0x27, 0x01, self.START, 0x81, 0x82)
|
||||
self.assertTrue(tr.hex().startswith('8103012701'), tr.hex())
|
||||
@@ -596,12 +596,12 @@ class TestTimerManagement(unittest.TestCase):
|
||||
self.assertIsNone(srv._timer_remaining(1))
|
||||
|
||||
def test_get_on_stopped_timer_is_contradiction(self):
|
||||
import pysim_otaman_server.server as srv
|
||||
import pysim_simple_server.server as srv
|
||||
tr = srv._handle_timer_command(1, 0x27, 0x02, self.START, 0x81, 0x82)
|
||||
self.assertEqual(tr.hex(), '810301270282028281030124')
|
||||
|
||||
def test_timer_expiration_envelope(self):
|
||||
import pysim_otaman_server.server as srv
|
||||
import pysim_simple_server.server as srv
|
||||
calls = []
|
||||
|
||||
class Tp:
|
||||
@@ -618,7 +618,7 @@ class TestTimerManagement(unittest.TestCase):
|
||||
self.assertEqual(calls, ['80c200000ed70c82028281a40101a503001000'])
|
||||
|
||||
def test_cancelled_timer_does_not_report(self):
|
||||
import pysim_otaman_server.server as srv
|
||||
import pysim_simple_server.server as srv
|
||||
calls = []
|
||||
|
||||
class Tp:
|
||||
@@ -704,7 +704,7 @@ class TestSmsConcat(unittest.TestCase):
|
||||
|
||||
def test_no_udh_sms_submit(self):
|
||||
"""SMS-SUBMIT without TP-UDHI: entire UD is payload."""
|
||||
from pysim_otaman_server.server import _parse_sms_concat
|
||||
from pysim_simple_server.server import _parse_sms_concat
|
||||
# First octet 0x01: MTI=01 (SUBMIT), no UDH, no VP
|
||||
# MR=00, DA_len=05, DA_type=90, DA=2143F5, PID=00, DCS=04, UDL=03, UD=AABBCC
|
||||
tpdu = bytes.fromhex('0100' # first octet + MR
|
||||
@@ -722,7 +722,7 @@ class TestSmsConcat(unittest.TestCase):
|
||||
|
||||
def test_8bit_concat_iei_0x00(self):
|
||||
"""SMS-SUBMIT with IEI 0x00 (8-bit reference concatenation)."""
|
||||
from pysim_otaman_server.server import _parse_sms_concat
|
||||
from pysim_simple_server.server import _parse_sms_concat
|
||||
# First octet 0x41: MTI=01 (SUBMIT), TP-UDHI=1, no VP
|
||||
# MR=00, DA_len=05, DA_type=90, DA=2143F5, PID=00, DCS=04
|
||||
# UDL=09, UDHL=05, UDH: 00 03 04 04 01 (concat IE), payload=AABBCC
|
||||
@@ -746,7 +746,7 @@ class TestSmsConcat(unittest.TestCase):
|
||||
|
||||
def test_16bit_concat_iei_0x08(self):
|
||||
"""SMS-SUBMIT with IEI 0x08 (16-bit reference concatenation)."""
|
||||
from pysim_otaman_server.server import _parse_sms_concat
|
||||
from pysim_simple_server.server import _parse_sms_concat
|
||||
# First octet 0x41: MTI=01, TP-UDHI=1
|
||||
# UDH: 06 (UDHL) 08 04 01 02 03 04 (16-bit concat: ref=0x0102, total=3, num=4)
|
||||
# payload=FF
|
||||
@@ -770,7 +770,7 @@ class TestSmsConcat(unittest.TestCase):
|
||||
|
||||
def test_udh_with_cpi(self):
|
||||
"""UDH with concatenation IE + CPI IE (0x70)."""
|
||||
from pysim_otaman_server.server import _parse_sms_concat
|
||||
from pysim_simple_server.server import _parse_sms_concat
|
||||
# First octet 0x41: MTI=01, TP-UDHI=1
|
||||
# UDHL=07, UDH: 00 03 04 04 01 (concat) + 70 00 (CPI)
|
||||
tpdu = bytes.fromhex('4100'
|
||||
@@ -794,7 +794,7 @@ class TestSmsConcat(unittest.TestCase):
|
||||
|
||||
def test_empty_payload(self):
|
||||
"""Segment with empty payload after UDH."""
|
||||
from pysim_otaman_server.server import _parse_sms_concat
|
||||
from pysim_simple_server.server import _parse_sms_concat
|
||||
# First octet 0x41: MTI=01, TP-UDHI=1
|
||||
tpdu = bytes.fromhex('4100'
|
||||
'05'
|
||||
@@ -815,7 +815,7 @@ class TestSmsConcat(unittest.TestCase):
|
||||
|
||||
def test_short_tpdu(self):
|
||||
"""Truncated TPDU returns gracefully."""
|
||||
from pysim_otaman_server.server import _parse_sms_concat
|
||||
from pysim_simple_server.server import _parse_sms_concat
|
||||
ref, total, num, payload = _parse_sms_concat(b'\x01')
|
||||
self.assertIsNone(ref)
|
||||
self.assertIsNone(total)
|
||||
@@ -823,7 +823,7 @@ class TestSmsConcat(unittest.TestCase):
|
||||
|
||||
def test_none_input(self):
|
||||
"""None input returns empty payload."""
|
||||
from pysim_otaman_server.server import _parse_sms_concat
|
||||
from pysim_simple_server.server import _parse_sms_concat
|
||||
ref, total, num, payload = _parse_sms_concat(None)
|
||||
self.assertIsNone(ref)
|
||||
self.assertIsNone(total)
|
||||
@@ -832,7 +832,7 @@ class TestSmsConcat(unittest.TestCase):
|
||||
|
||||
def test_short_tpdu(self):
|
||||
"""Truncated TPDU returns gracefully."""
|
||||
from pysim_otaman_server.server import _parse_sms_concat
|
||||
from pysim_simple_server.server import _parse_sms_concat
|
||||
ref, total, num, payload = _parse_sms_concat(b'\x44')
|
||||
self.assertIsNone(ref)
|
||||
self.assertIsNone(total)
|
||||
@@ -840,7 +840,7 @@ class TestSmsConcat(unittest.TestCase):
|
||||
|
||||
def test_none_input(self):
|
||||
"""None input returns empty payload."""
|
||||
from pysim_otaman_server.server import _parse_sms_concat
|
||||
from pysim_simple_server.server import _parse_sms_concat
|
||||
ref, total, num, payload = _parse_sms_concat(None)
|
||||
self.assertIsNone(ref)
|
||||
self.assertIsNone(total)
|
||||
@@ -853,7 +853,7 @@ class TestSmsReassembly(unittest.TestCase):
|
||||
|
||||
def test_single_segment_no_concat(self):
|
||||
"""Single segment without UDH → submit_tpdu_hex is set directly."""
|
||||
from pysim_otaman_server.server import PoRSubmitHandler, _find_sms_tpdu, _parse_sms_concat
|
||||
from pysim_simple_server.server import PoRSubmitHandler, _find_sms_tpdu, _parse_sms_concat
|
||||
handler = PoRSubmitHandler()
|
||||
# Build a simple D0 with tag 8B containing an SMS-SUBMIT without UDH
|
||||
sms_tpdu = bytes.fromhex('040005902143F50004' # SMS-SUBMIT header
|
||||
@@ -874,7 +874,7 @@ class TestSmsReassembly(unittest.TestCase):
|
||||
|
||||
def test_multi_segment_reassembly(self):
|
||||
"""3 segments with IEI 0x00 in random order → assembled in correct order."""
|
||||
from pysim_otaman_server.server import PoRSubmitHandler
|
||||
from pysim_simple_server.server import PoRSubmitHandler
|
||||
handler = PoRSubmitHandler()
|
||||
|
||||
# Segment payloads (after UDH)
|
||||
@@ -897,7 +897,7 @@ class TestSmsReassembly(unittest.TestCase):
|
||||
|
||||
def test_independent_references(self):
|
||||
"""Two different reference numbers are independent."""
|
||||
from pysim_otaman_server.server import PoRSubmitHandler
|
||||
from pysim_simple_server.server import PoRSubmitHandler
|
||||
handler = PoRSubmitHandler()
|
||||
|
||||
# Ref 0x01: 2 segments
|
||||
@@ -947,7 +947,7 @@ class CapApduSequenceTest(unittest.TestCase):
|
||||
return buf.getvalue().hex().upper()
|
||||
|
||||
def test_cap_parse(self):
|
||||
from pysim_otaman_server.server import _cap_parse
|
||||
from pysim_simple_server.server import _cap_parse
|
||||
loadfile_aid, module_aid, data = _cap_parse(self._mini_cap())
|
||||
self.assertEqual(loadfile_aid, 'A00000010001')
|
||||
self.assertEqual(module_aid, 'A000000100')
|
||||
@@ -956,7 +956,7 @@ class CapApduSequenceTest(unittest.TestCase):
|
||||
self.assertIn('03000A01', data)
|
||||
|
||||
def test_sequence_install_load_install(self):
|
||||
from pysim_otaman_server.server import _cap_apdu_sequence
|
||||
from pysim_simple_server.server import _cap_apdu_sequence
|
||||
seq = _cap_apdu_sequence('A00000010001', 'A000000100', 'AABBCCDD')
|
||||
# INSTALL [for load]: lv(pkg aid) + lv(ISD) + 000000
|
||||
self.assertEqual(seq[0],
|
||||
@@ -969,7 +969,7 @@ class CapApduSequenceTest(unittest.TestCase):
|
||||
self.assertIn('06A00000010001' + '05A000000100' + '05A000000100' + '0100', seq[2])
|
||||
|
||||
def test_load_blocks_split_and_counter(self):
|
||||
from pysim_otaman_server.server import _cap_apdu_sequence, _ber_len as _ber_len_lower
|
||||
from pysim_simple_server.server import _cap_apdu_sequence, _ber_len as _ber_len_lower
|
||||
data = ''.join('%02X' % (i % 256) for i in range(700))
|
||||
seq = _cap_apdu_sequence('A00000010001', 'A000000100', data)
|
||||
self.assertEqual(len(seq), 5) # INSTALL + 3 LOAD + INSTALL
|
||||
@@ -991,7 +991,7 @@ class CapApduSequenceTest(unittest.TestCase):
|
||||
# A smaller block size (SCP80: fit one SMS) slices the load file TLV
|
||||
# into consecutive chunks of that size, the last block marked P1=0x80
|
||||
# with the block counter in P2.
|
||||
from pysim_otaman_server.server import _cap_apdu_sequence
|
||||
from pysim_simple_server.server import _cap_apdu_sequence
|
||||
data = ''.join('%02X' % (i % 256) for i in range(700)) # TLV = 704 bytes
|
||||
seq = _cap_apdu_sequence('A00000010001', 'A000000100', data, block_size=100)
|
||||
self.assertEqual(len(seq), 10) # INSTALL + 8 LOAD + INSTALL
|
||||
@@ -1011,7 +1011,7 @@ class CapApduSequenceTest(unittest.TestCase):
|
||||
def test_gen_install_returns_the_apdu_list(self):
|
||||
# /api/scp81/gen-install: build the INSTALL/LOAD/INSTALL list for a
|
||||
# .cap without touching any listener or script state.
|
||||
from pysim_otaman_server.server import _scp81_gen_install
|
||||
from pysim_simple_server.server import _scp81_gen_install
|
||||
resp = _scp81_gen_install({'cap_hex': self._mini_cap(), 'privileges': '01'})
|
||||
self.assertTrue(resp['ok'], resp)
|
||||
self.assertEqual(resp['load_file_aid'], 'A00000010001')
|
||||
@@ -1023,7 +1023,7 @@ class CapApduSequenceTest(unittest.TestCase):
|
||||
self.assertNotIn('queued', resp) # generation only, no queueing
|
||||
|
||||
def test_gen_install_rejects_bad_input(self):
|
||||
from pysim_otaman_server.server import _scp81_gen_install
|
||||
from pysim_simple_server.server import _scp81_gen_install
|
||||
self.assertFalse(_scp81_gen_install({})['ok'])
|
||||
resp = _scp81_gen_install({'cap_hex': '00'})
|
||||
self.assertFalse(resp['ok'])
|
||||
@@ -1034,7 +1034,7 @@ class TerminalProfileTest(unittest.TestCase):
|
||||
"""Runtime TERMINAL PROFILE: hex validation and re-send."""
|
||||
|
||||
def test_validate_tp_hex(self):
|
||||
from pysim_otaman_server.server import _validate_tp_hex
|
||||
from pysim_simple_server.server import _validate_tp_hex
|
||||
self.assertEqual(_validate_tp_hex('ff 00 80'), ('FF0080', None))
|
||||
self.assertEqual(_validate_tp_hex('80FF'), ('80FF', None))
|
||||
for bad in ('', ' ', 'F', 'XYZ', 'FF0', 'FF' * 256):
|
||||
@@ -1044,7 +1044,7 @@ class TerminalProfileTest(unittest.TestCase):
|
||||
|
||||
def test_resend_terminal_profile_resets_state_and_sends(self):
|
||||
import types
|
||||
from pysim_otaman_server import server as srv
|
||||
from pysim_simple_server import server as srv
|
||||
server_obj = types.SimpleNamespace(
|
||||
terminal_profile='FF00', stk_pending={'type': 'display_text'},
|
||||
menu_active=True, event_list=[0x03], sim_menu='old')
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ PY_SIM = PROJECTS / 'pysim'
|
||||
if str(PY_SIM) not in sys.path:
|
||||
sys.path.insert(0, str(PY_SIM))
|
||||
|
||||
import pysim_otaman_server.server as S
|
||||
import pysim_simple_server.server as S
|
||||
|
||||
|
||||
class TestPollInterval(unittest.TestCase):
|
||||
|
||||
+6
-6
@@ -19,8 +19,8 @@ PY_SIM = PROJECTS / 'pysim'
|
||||
if str(PY_SIM) not in sys.path:
|
||||
sys.path.insert(0, str(PY_SIM))
|
||||
|
||||
from pysim_otaman_server import scp81
|
||||
import pysim_otaman_server.server as server
|
||||
from pysim_simple_server import scp81
|
||||
import pysim_simple_server.server as server
|
||||
|
||||
PSK = bytes.fromhex('00112233445566778899aabbccddeeff')
|
||||
IDENT = '89012345678901234567'
|
||||
@@ -194,7 +194,7 @@ class PskTlsServerTest(unittest.TestCase):
|
||||
tls = self._connect(srv)
|
||||
tls.sendall(b'POST /api/scp81 HTTP/1.1\r\nHost: 127.0.0.1\r\n'
|
||||
b'X-Admin-Protocol: globalplatform-remote-admin/1.0\r\n'
|
||||
b'X-Admin-From: otaman\r\n\r\n')
|
||||
b'X-Admin-From: simple\r\n\r\n')
|
||||
reply = self._read_http(tls)
|
||||
self.assertIn(b'HTTP/1.1 200 OK', reply)
|
||||
self.assertIn(b'X-Admin-Next-URI: /api/scp81?req=1', reply)
|
||||
@@ -1194,7 +1194,7 @@ class QueueScriptTest(unittest.TestCase):
|
||||
|
||||
class ScriptBodyLengthTest(unittest.TestCase):
|
||||
def test_long_c_apdu_uses_ber_long_form(self):
|
||||
from pysim_otaman_server.server import _scp81_command_body
|
||||
from pysim_simple_server.server import _scp81_command_body
|
||||
apdu = '80E80000F0' + 'AB' * 239 + '00' # exactly 245-byte LOAD
|
||||
body = _scp81_command_body(apdu)
|
||||
# AE 80 22 81 F5 <245 bytes> 00 00
|
||||
@@ -1203,12 +1203,12 @@ class ScriptBodyLengthTest(unittest.TestCase):
|
||||
self.assertEqual(len(body), 5 + 245 + 2)
|
||||
|
||||
def test_short_apdu_stays_short_form(self):
|
||||
from pysim_otaman_server.server import _scp81_command_body
|
||||
from pysim_simple_server.server import _scp81_command_body
|
||||
self.assertEqual(_scp81_command_body('80CAFF2100').hex().upper(),
|
||||
'AE80220580CAFF21000000')
|
||||
|
||||
def test_definite_variant_ber_lengths(self):
|
||||
from pysim_otaman_server.server import _scp81_command_body
|
||||
from pysim_simple_server.server import _scp81_command_body
|
||||
apdu = 'AB' * 130
|
||||
body = _scp81_command_body(apdu, definite=True)
|
||||
# AA 81 85 22 81 82 <130 bytes> (outer 1+2+130 = 133 = 0x85)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Unit tests for the parent-scoped select helpers in pysim_otaman_server.server.
|
||||
"""Unit tests for the parent-scoped select helpers in pysim_simple_server.server.
|
||||
|
||||
The helpers must resolve every model-known file strictly within the requested
|
||||
parent (no pySim global selectables, no probe_file model injection) and must
|
||||
@@ -16,7 +16,7 @@ PY_SIM = PROJECTS / 'pysim'
|
||||
if str(PY_SIM) not in sys.path:
|
||||
sys.path.insert(0, str(PY_SIM))
|
||||
|
||||
from pysim_otaman_server.server import (
|
||||
from pysim_simple_server.server import (
|
||||
_app_by_sel,
|
||||
_fid4,
|
||||
_file_by_sel,
|
||||
|
||||
@@ -21,8 +21,8 @@ if str(PY_SIM) not in sys.path:
|
||||
|
||||
from pySim.exceptions import NoCardError, SwMatchError
|
||||
|
||||
from pysim_otaman_server import __main__ as srv_main
|
||||
from pysim_otaman_server.server import _LineFilter
|
||||
from pysim_simple_server import __main__ as srv_main
|
||||
from pysim_simple_server.server import _LineFilter
|
||||
|
||||
CARDLESS_LINE = 'INIT: no card in the reader — server ready; insert a card or press Equip'
|
||||
|
||||
@@ -84,7 +84,7 @@ class StartupNoCardTest(unittest.TestCase):
|
||||
fake_fastinit.install = mock.Mock()
|
||||
fake_http.return_value.serve_forever.side_effect = KeyboardInterrupt
|
||||
try:
|
||||
with mock.patch('sys.argv', ['pysim-otaman-server']):
|
||||
with mock.patch('sys.argv', ['pysim-simple-server']):
|
||||
srv_main.main()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
@@ -135,7 +135,7 @@ class StartupNoCardTest(unittest.TestCase):
|
||||
fake_fastinit.install = mock.Mock()
|
||||
fake_http.return_value.serve_forever.side_effect = KeyboardInterrupt
|
||||
try:
|
||||
with mock.patch('sys.argv', ['pysim-otaman-server']):
|
||||
with mock.patch('sys.argv', ['pysim-simple-server']):
|
||||
srv_main.main()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user