4 Commits

Author SHA1 Message Date
catarrh 4f6e48f0e0 docs: fix version compatibility (PWA vs server majors), broaden descriptions, drop the header slogan (v2.3.1)
- version check in pysimConnect() now compares the server major against
  SIMPLE_VERSION (2.3.1) instead of the hardcoded 1.x assumption; the header
  version is rendered from the same constant
- removed the 'SIM OTA with a Human Face' slogan (span, i18n key, translate
  hook) to free header space
- broaden the PWA description in manifest.json/package.json and the help
  overview (EN/RU): APDU workbench, OTA lab, CAT/STK simulator, card profiler
- version compatibility tables in README/RUS, help EN/RU and docs/api.md now
  describe the major-version rule
- README/RUS: Cards promoted to a top-level section; EN duplicate
  Theme/Localisation sections removed
- docs: /api/cardinfo documented; SCP81 'expose framing options' next-work
  item dropped (done); local AGENTS.md not-implemented note corrected
- SW cache simple-v190
2026-09-19 08:54:33 +03:00
catarrh 3dceb5c56b readme: note the former OTAMan name (renamed to SIMple on 2026-09-19) 2026-09-19 08:43:48 +03:00
catarrh 4a99db13d0 readme: broaden the SIMple overview — APDU workbench, OTA lab, CAT/STK simulator, card profiler 2026-09-19 08:42:48 +03:00
catarrh ac18c3e2cf rename OTAMan → SIMple (v2.3.0)
- package pysim_otaman_server → pysim_simple_server; distribution and
  console script pysim-otaman-server → pysim-simple-server
- PWA branding SIMple: title, header, manifest, help EN/RU, GitHub links
- localStorage keys otaman_* → simple_* (clean break, no migration)
- export filenames simple-cards.json / simple-custom-files.json
- service worker cache simple-v189
- README/docs/setup/start scripts updated; no 'otaman' left in the tree
2026-09-19 08:25:20 +03:00
35 changed files with 309 additions and 288 deletions
+48 -47
View File
@@ -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 ## Quick start
@@ -11,8 +18,8 @@ OTAMan is an offline HTML/JS PWA for building APDU commands (SIM, USIM, GlobalPl
**Full (PWA + card server):** **Full (PWA + card server):**
```sh ```sh
git clone https://github.com/anttro/otaman.git git clone https://github.com/anttro/simple.git
cd otaman cd simple
./setup.sh # or setup.bat on Windows — creates .venv, installs pysim + server ./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) ./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 - ISO 9797-1: MAC algorithms
- NIST SP 800-38B: CMAC - 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 ### 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. 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) ## 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 ### File Browser
@@ -605,7 +614,7 @@ The state line shows the listener, the negotiated identity and live channels (by
### Scripts ### 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 | | Template | What it builds |
|---|---| |---|---|
@@ -618,7 +627,7 @@ The table lists each script with kind, APDU count and creation time; **Edit** op
## PWA ## 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 - Service worker pre-caches all assets on first visit
- App icons at 192×192 and 512×512 - 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). 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 ### Prerequisites
@@ -657,7 +666,7 @@ python3 -m venv .venv
source .venv/bin/activate # Linux/macOS (Windows: .venv\Scripts\activate) source .venv/bin/activate # Linux/macOS (Windows: .venv\Scripts\activate)
pip install git+https://github.com/osmocom/pysim.git pip install git+https://github.com/osmocom/pysim.git
pip install -e . # editable — serves frontend/ from the source tree pip install -e . # editable — serves frontend/ from the source tree
pysim-otaman-server --http-port 8080 pysim-simple-server --http-port 8080
``` ```
### CLI options ### CLI options
@@ -689,20 +698,12 @@ pysim-otaman-server --http-port 8080
See [docs/api.md](docs/api.md) for the full endpoint reference. 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 ## Version compatibility
| PWA (OTAMan) | Server | Status | | PWA (SIMple) | Server | Status |
|-------------|--------|--------| |-------------|--------|--------|
| 1.x.x | 1.x.x | ✅ Compatible | | any | same major | ✅ Compatible |
| 1.x.x | 0.x.x | ❌ Outdated — update server | | any | older major | ❌ Outdated — update server |
| 1.x.x | 2.x.x+ | ⚠️ Server newer — update PWA | | 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
View File
@@ -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 + сервер карт):** **Полная установка (PWA + сервер карт):**
```sh ```sh
git clone https://github.com/anttro/otaman.git git clone https://github.com/anttro/simple.git
cd otaman cd simple
./setup.sh # или setup.bat в Windows — создаёт .venv, ставит pysim + сервер ./setup.sh # или setup.bat в Windows — создаёт .venv, ставит pysim + сервер
./start.sh # или start.bat — запускает сервер (он же раздаёт PWA) ./start.sh # или start.bat — запускает сервер (он же раздаёт PWA)
``` ```
@@ -405,28 +412,6 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
- ISO 9797-1 - ISO 9797-1
- NIST SP 800-38B (CMAC) - 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
Все операции RAM отправляются как защищённые пакеты SCP80 (ETSI TS 102 225) через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES) для безопасной транспортировки. Все операции 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) ## 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 ## PWA
OTAMan — Progressive Web App. Можно установить для offline-использования через кнопку **INSTALL PWA** или через браузер. SIMple — Progressive Web App. Можно установить для offline-использования через кнопку **INSTALL PWA** или через браузер.
- Service worker кеширует все ресурсы при первом посещении - Service worker кеширует все ресурсы при первом посещении
- Иконки 192×192 и 512×512 - Иконки 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/*`. Встроенный 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) source .venv/bin/activate # Linux/macOS (Windows: .venv\Scripts\activate)
pip install git+https://github.com/osmocom/pysim.git pip install git+https://github.com/osmocom/pysim.git
pip install -e . # editable — раздаёт frontend/ из исходного дерева pip install -e . # editable — раздаёт frontend/ из исходного дерева
pysim-otaman-server --http-port 8080 pysim-simple-server --http-port 8080
``` ```
### Параметры CLI ### Параметры CLI
+14 -6
View File
@@ -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 The server exposes a JSON HTTP API under `/api/*`. All responses carry
`Access-Control-Allow-Origin: *` (plus `Access-Control-Allow-Private-Network: true` `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 ## Version compatibility
| Server | PWA (OTAMan) | Status | | Server | PWA (SIMple) | Status |
|--------|-------------|--------| |--------|-------------|--------|
| 1.x.x | 1.x.x | ✅ Compatible | | same major | any | ✅ Compatible |
| 0.x.x | 1.x.x | ❌ Outdated — update server | | older major | any | ❌ Outdated — update server |
| 2.x.x+ | 1.x.x | ⚠️ Server newer — update PWA | | newer major | any | ⚠️ Server newer — update PWA |
The server reports its version via `GET /api/version`. The PWA checks this on 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 ## Endpoints
@@ -23,6 +24,7 @@ connect and warns if versions are incompatible.
| `/api/status` | GET | Card reader + card info + current selection | | `/api/status` | GET | Card reader + card info + current selection |
| `/api/command` | POST | pySim command (equip, status, tree, etc.) | | `/api/command` | POST | pySim command (equip, status, tree, etc.) |
| `/api/commands` | GET | List available pySim commands | | `/api/commands` | GET | List available pySim commands |
| `/api/cardinfo` | GET | pySim `cardinfo` output |
| `/api/tree` | POST | File tree browser for given FID/name | | `/api/tree` | POST | File tree browser for given FID/name |
| `/api/select` | POST | Select a file by name or FID | | `/api/select` | POST | Select a file by name or FID |
| `/api/read` | POST | Read file content | | `/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. 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` ### `POST /api/command`
Execute any pysim-shell command. Execute any pysim-shell command.
+2 -3
View File
@@ -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 variations are not repeated. Add rows as tests are run; keep the confirmed
rules section current. 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 `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 status event. Server log at `GET /api/scp81/log`, script state at
`GET /api/scp81/script`, proactive history at `GET /api/proactive-log`. `GET /api/scp81/script`, proactive history at `GET /api/proactive-log`.
@@ -308,8 +308,7 @@ INSTALL [for install] -> registries.
## Next tests / work ## Next tests / work
1. **UI:** group the per-page R-APDUs under their logical command in the 1. **UI:** group the per-page R-APDUs under their logical command in the
SCP81 tab (page merging/decoding for ELF and application listings); SCP81 tab (page merging/decoding for ELF and application listings).
expose the framing options in the tab.
2. **Load/store over SCP81:** implemented - `POST /api/scp81/gen-install` 2. **Load/store over SCP81:** implemented - `POST /api/scp81/gen-install`
takes a `.cap`, expands it with the shared `_cap_apdu_sequence` helper takes a `.cap`, expands it with the shared `_cap_apdu_sequence` helper
(INSTALL [for load] -> 240-byte LOAD blocks -> INSTALL [for install]) and (INSTALL [for load] -> 240-byte LOAD blocks -> INSTALL [for install]) and
+20 -20
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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="stylesheet" href="style.css">
</head> </head>
<body class="bg-neutral-50 dark:bg-slate-900 text-gray-800 dark:text-slate-200"> <body class="bg-neutral-50 dark:bg-slate-900 text-gray-800 dark:text-slate-200">
@@ -16,12 +16,12 @@
</aside> </aside>
<main class="flex-1 min-w-0 px-6 py-8"> <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"> <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> <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">SIMple — инструментарий для специалистов по UICC/SIM-картам: автономный Progressive Web App (PWA), закрывающий весь цикл лабораторной работы &mdash; <strong>APDU-верстак</strong> (конструкторы SIM/USIM RFM и Expanded Script, команды GlobalPlatform RAM, парсер C-APDU/R-APDU и декодер ответов), <strong>OTA-лаборатория</strong> (защищённые пакеты SCP80 и установка Java Card RAM, а также терминальная сторона HTTP OTA &mdash; 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-otaman-server</code>), оборачивающий библиотеку <a href="https://osmocom.org/projects/pysim" class="text-blue-600 dark:text-blue-400 hover:underline">pySim</a>.</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">Браузер (OTAMan PWA) &rarr; HTTP :8080 &rarr; pysim-otaman-server &rarr; pySim &rarr; PC/SC &rarr; ридер &rarr; UICC/SIM</pre> <pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">Браузер (SIMple PWA) &rarr; HTTP :8080 &rarr; pysim-simple-server &rarr; pySim &rarr; PC/SC &rarr; ридер &rarr; UICC/SIM</pre>
<p class="mb-3">Стандарты, на которые опирается приложение:</p> <p class="mb-3">Стандарты, на которые опирается приложение:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3"> <ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li>ETSI TS 102 221 — интерфейс UICC-терминал (CLA 00, файлы USIM)</li> <li>ETSI TS 102 221 — интерфейс UICC-терминал (CLA 00, файлы USIM)</li>
@@ -331,7 +331,7 @@
<p class="mb-2"><strong>Сценарий</strong> выбирает список команд, отдаваемый в сессии — <strong>Нет</strong> (оставить настроенный на сервере скрипт) или один из скриптов подвкладки <strong>&laquo;Скрипты&raquo;</strong>; выбранный список передаётся серверу при старте слушателя.</p> <p class="mb-2"><strong>Сценарий</strong> выбирает список команд, отдаваемый в сессии — <strong>Нет</strong> (оставить настроенный на сервере скрипт) или один из скриптов подвкладки <strong>&laquo;Скрипты&raquo;</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> <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> <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"> <ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Empty</strong> — начать с пустого списка.</li> <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> <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"> <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. Вкладка &laquo;Картридер&raquo; (pySim)</h2> <h2 id="card-reader" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">6. Вкладка &laquo;Картридер&raquo; (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>&laquo;Профайлер&raquo;</strong> и <strong>&laquo;Симулятор телефона&raquo;</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>&laquo;Профайлер&raquo;</strong> и <strong>&laquo;Симулятор телефона&raquo;</strong> — отдельные вкладки верхнего уровня.</p>
<h3 id="file-manager" class="text-lg font-medium mb-2">6.1 Файловый менеджер</h3> <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> <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"> <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> <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">Для работы с картой (вкладка &laquo;Картридер&raquo;, &laquo;Симулятор телефона&raquo;, доставка 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">Для работы с картой (вкладка &laquo;Картридер&raquo;, &laquo;Симулятор телефона&raquo;, доставка 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> <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"> <ul class="list-disc list-inside text-sm space-y-1 mb-3">
@@ -489,15 +489,15 @@
</ul> </ul>
<h3 id="quickstart-linux" class="text-lg font-medium mb-2">9.2 Быстрый старт — Linux / macOS</h3> <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 <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 otaman cd simple
chmod +x setup.sh start.sh chmod +x setup.sh start.sh
./setup.sh # создаёт .venv, устанавливает pysim + сервер (однократно) ./setup.sh # создаёт .venv, устанавливает pysim + сервер (однократно)
./start.sh # запускает сервер (PWA + API, автоопределение ридера)</pre> ./start.sh # запускает сервер (PWA + API, автоопределение ридера)</pre>
<h3 id="quickstart-windows" class="text-lg font-medium mb-2">9.3 Быстрый старт — Windows</h3> <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 <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 otaman cd simple
setup.bat # создаёт .venv, устанавливает pysim + сервер (однократно) setup.bat # создаёт .venv, устанавливает pysim + сервер (однократно)
start.bat # запускает сервер (PWA + API)</pre> start.bat # запускает сервер (PWA + API)</pre>
@@ -528,26 +528,26 @@ source .venv/bin/activate # Linux/macOS
# Установить pysim # Установить pysim
pip install git+https://github.com/osmocom/pysim.git pip install git+https://github.com/osmocom/pysim.git
# Установить pysim-otaman-server (editable — раздаёт встроенный PWA) # Установить pysim-simple-server (editable — раздаёт встроенный PWA)
pip install -e . pip install -e .
# Запустить сервер (PWA + API) # Запустить сервер (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">Подключите 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 &ge; 1.6.1 делает это автоматически), и (2) браузеру должно быть разрешено обращаться к локальной сети &mdash; в Chrome/Edge/Vivaldi: Настройки сайта &rarr; Доступ к локальной сети &rarr; разрешить сайт (или подтвердить запрос). Без разрешения браузера запрос к <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 &ge; 1.6.1 делает это автоматически), и (2) браузеру должно быть разрешено обращаться к локальной сети &mdash; в Chrome/Edge/Vivaldi: Настройки сайта &rarr; Доступ к локальной сети &rarr; разрешить сайт (или подтвердить запрос). Без разрешения браузера запрос к <code class="font-mono text-sm">127.0.0.1</code> блокируется ещё до отправки preflight.</p>
<section class="mb-10"> <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> <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"> <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> <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">&#9989; Совместимы</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">&#9989; Совместимы</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">&#10060; Устарел &mdash; обновите сервер</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">&#10060; Устарел &mdash; обновите сервер</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">&#9888;&#65039; Сервер новее &mdash; обновите PWA</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">&#9888;&#65039; Сервер новее &mdash; обновите PWA</td></tr>
</tbody> </tbody>
</table> </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> </main>
+20 -20
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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"> <link rel="stylesheet" href="style.css">
</head> </head>
<body class="bg-neutral-50 dark:bg-slate-900 text-gray-800 dark:text-slate-200"> <body class="bg-neutral-50 dark:bg-slate-900 text-gray-800 dark:text-slate-200">
@@ -16,12 +16,12 @@
</aside> </aside>
<main class="flex-1 min-w-0 px-6 py-8"> <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"> <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> <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">SIMple is a workbench for UICC/SIM card specialists: an offline Progressive Web App (PWA) covering the full lab cycle &mdash; 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 &mdash; 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-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> <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 (OTAMan PWA) &rarr; HTTP :8080 &rarr; pysim-otaman-server &rarr; pySim &rarr; PC/SC &rarr; card reader &rarr; UICC/SIM</pre> <pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">Browser (SIMple PWA) &rarr; HTTP :8080 &rarr; pysim-simple-server &rarr; pySim &rarr; PC/SC &rarr; card reader &rarr; UICC/SIM</pre>
<p class="mb-3">Standards referenced across the application:</p> <p class="mb-3">Standards referenced across the application:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3"> <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> <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="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> <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> <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"> <ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Empty</strong> — start from scratch.</li> <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> <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"> <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> <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> <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> <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"> <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> <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> <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"> <ul class="list-disc list-inside text-sm space-y-1 mb-3">
@@ -489,15 +489,15 @@
</ul> </ul>
<h3 id="quickstart-linux" class="text-lg font-medium mb-2">9.2 Quick start — Linux / macOS</h3> <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 <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 otaman cd simple
chmod +x setup.sh start.sh chmod +x setup.sh start.sh
./setup.sh # creates .venv, installs pysim + server (run once) ./setup.sh # creates .venv, installs pysim + server (run once)
./start.sh # starts the server (serves PWA + API, auto-detects reader)</pre> ./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> <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 <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 otaman cd simple
setup.bat # creates .venv, installs pysim + server (run once) setup.bat # creates .venv, installs pysim + server (run once)
start.bat # starts the server (serves PWA + API)</pre> start.bat # starts the server (serves PWA + API)</pre>
@@ -528,26 +528,26 @@ source .venv/bin/activate # Linux/macOS
# Install pysim # Install pysim
pip install git+https://github.com/osmocom/pysim.git 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 . pip install -e .
# Start the server (serves PWA + API) # 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">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 &ge; 1.6.1 does this automatically), and (2) the browser must be allowed to access the local network &mdash; in Chrome/Edge/Vivaldi: Site settings &rarr; Local network access &rarr; 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 &ge; 1.6.1 does this automatically), and (2) the browser must be allowed to access the local network &mdash; in Chrome/Edge/Vivaldi: Site settings &rarr; Local network access &rarr; 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"> <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> <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"> <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> <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">&#9989; 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">same major</td><td class="py-1 px-2">&#9989; 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">&#10060; Outdated &mdash; update server</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">&#10060; Outdated &mdash; 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">&#9888;&#65039; Server newer &mdash; update PWA</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">&#9888;&#65039; Server newer &mdash; update PWA</td></tr>
</tbody> </tbody>
</table> </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> </main>
+37 -33
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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="stylesheet" href="style.css">
<link rel="icon" type="image/svg+xml" href="favicon.svg"> <link rel="icon" type="image/svg+xml" href="favicon.svg">
<link rel="icon" type="image/png" href="sim.png" sizes="462x462"> <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="max-w-7xl mx-auto px-6 py-2">
<div class="flex items-center justify-between mb-3"> <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"> <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" 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> <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 id="state-indicator-adm" class="hidden ml-0.5 text-xs font-mono" title=""></span>
</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> <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> <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="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> <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> </div>
<div id="pysim-connect-info" class="mb-3 text-xs text-gray-500 dark:text-slate-400 leading-relaxed"> <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 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/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><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 id="pysim-connect-msg" class="text-xs"></div>
</div> </div>
<div id="pysim-connected-row" class="mb-3 hidden"> <div id="pysim-connected-row" class="mb-3 hidden">
@@ -1302,6 +1302,12 @@
</div> </div>
<script> <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 ===== // ===== Tab switching =====
function switchTab(name) { function switchTab(name) {
if (_scp81Timer && name !== 'scp81') { clearInterval(_scp81Timer); _scp81Timer = null; } if (_scp81Timer && name !== 'scp81') { clearInterval(_scp81Timer); _scp81Timer = null; }
@@ -4679,7 +4685,7 @@ function decodeResponse() {
// ===== pysim integration ===== // ===== pysim integration =====
// Default to a local server on 127.0.0.1:8080. If this PWA is served by // 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 // and switch to a relative base; otherwise keep the default and wait for the
// user to point at a server. // user to point at a server.
let pysimBase = 'http://127.0.0.1:8080'; let pysimBase = 'http://127.0.0.1:8080';
@@ -4912,14 +4918,15 @@ async function pysimConnect() {
if (!ver.version) throw new Error('No version'); if (!ver.version) throw new Error('No version');
_pysimServerAvailable = true; _pysimServerAvailable = true;
const sv = (ver.version || '').split('.').map(Number); const sv = (ver.version || '').split('.').map(Number);
if (sv[0] < 1 || (sv[0] === 1 && sv[1] < 0)) { const pv = SIMPLE_VERSION.split('.').map(Number);
msgEl.innerHTML = '<span class="text-red-500">Server version ' + esc(ver.version) + ' is outdated. Please update pysim-otaman-server.</span>'; 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.textContent = t('Connect');
btn.disabled = false; btn.disabled = false;
return; return;
} }
if (sv[0] > 1) { if (sv[0] > pv[0]) {
msgEl.innerHTML = '<span class="text-yellow-500">Warning: server version ' + esc(ver.version) + ' is newer than expected. Update OTAMan for full compatibility.</span>'; 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'); const status = await pysimFetch('/api/status');
if (!status.app_ready) { if (!status.app_ready) {
@@ -5947,7 +5954,7 @@ let _cardsEditIdx = null;
function cardsLoad() { function cardsLoad() {
try { try {
const d = localStorage.getItem('otaman_cards'); const d = localStorage.getItem('simple_cards');
cards = d ? JSON.parse(d) : []; cards = d ? JSON.parse(d) : [];
} catch (e) { cards = []; } } catch (e) { cards = []; }
cardsRebuildSelect(); cardsRebuildSelect();
@@ -5955,7 +5962,7 @@ function cardsLoad() {
} }
function cardsSave() { function cardsSave() {
localStorage.setItem('otaman_cards', JSON.stringify(cards)); localStorage.setItem('simple_cards', JSON.stringify(cards));
cardsRebuildSelect(); cardsRebuildSelect();
cardsRender(); cardsRender();
scp81PushPskMap(); scp81PushPskMap();
@@ -6225,7 +6232,7 @@ function downloadJson(name, obj) {
} }
function cardsExportFile() { function cardsExportFile() {
downloadJson('otaman-cards.json', cards); downloadJson('simple-cards.json', cards);
} }
async function cardsImport(text) { async function cardsImport(text) {
@@ -6511,7 +6518,7 @@ function pysimCustomRefreshTree() {
pysimFsRenderTree(); 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) { function pysimFsSortChildren(children, mode) {
return (children || []).slice().sort((a, b) => { return (children || []).slice().sort((a, b) => {
@@ -6536,7 +6543,7 @@ function pysimFsSortChildren(children, mode) {
function pysimFsSetSort(mode) { function pysimFsSetSort(mode) {
pysimFsSort = mode === 'name' ? 'name' : 'fid'; 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 => { document.querySelectorAll('.pysim-fs-sort-pill').forEach(p => {
const active = p.dataset.fsSort === pysimFsSort; const active = p.dataset.fsSort === pysimFsSort;
p.classList.toggle('bg-blue-600', active); p.classList.toggle('bg-blue-600', active);
@@ -8036,7 +8043,7 @@ function scp81OptionsBadge() {
function scp81OptionsLoad() { function scp81OptionsLoad() {
let saved = {}; 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); const opt = Object.assign({}, SCP81_OPT_DEFAULTS, saved);
if (saved.nextUriOn === undefined) opt.nextUriOn = (opt.nextUri || '') !== ''; if (saved.nextUriOn === undefined) opt.nextUriOn = (opt.nextUri || '') !== '';
const set = (id, v) => { const el = document.getElementById(id); if (el) el.value = v; }; const set = (id, v) => { const el = document.getElementById(id); if (el) el.value = v; };
@@ -8057,13 +8064,13 @@ function scp81OptionsLoad() {
} }
function scp81OptionsPersist() { function scp81OptionsPersist() {
localStorage.setItem('otaman_scp81_opts', JSON.stringify(scp81OptionsState())); localStorage.setItem('simple_scp81_opts', JSON.stringify(scp81OptionsState()));
scp81OptionsSyncDisabled(); scp81OptionsSyncDisabled();
scp81OptionsBadge(); scp81OptionsBadge();
} }
function scp81OptionsReset() { function scp81OptionsReset() {
localStorage.removeItem('otaman_scp81_opts'); localStorage.removeItem('simple_scp81_opts');
scp81OptionsLoad(); scp81OptionsLoad();
} }
@@ -8602,14 +8609,14 @@ let _scriptsNewKind = 'empty';
function scriptsLoad() { function scriptsLoad() {
try { try {
const d = localStorage.getItem('otaman_scripts'); const d = localStorage.getItem('simple_scripts');
scripts = d ? JSON.parse(d) : []; scripts = d ? JSON.parse(d) : [];
} catch (e) { scripts = []; } } catch (e) { scripts = []; }
scriptsRebuildSelect(); scriptsRebuildSelect();
} }
function scriptsPersist() { function scriptsPersist() {
localStorage.setItem('otaman_scripts', JSON.stringify(scripts)); localStorage.setItem('simple_scripts', JSON.stringify(scripts));
scriptsRebuildSelect(); scriptsRebuildSelect();
scriptsRender(); scriptsRender();
} }
@@ -9444,7 +9451,7 @@ function pysimCustomExport() {
} }
function pysimCustomExportFile() { function pysimCustomExportFile() {
downloadJson('otaman-custom-files.json', pysimCustomFiles); downloadJson('simple-custom-files.json', pysimCustomFiles);
} }
async function pysimCustomImport(text) { async function pysimCustomImport(text) {
@@ -9529,25 +9536,25 @@ const PROFILER_MASK_PREFIX4_FIDS = {
function profilerLoad() { function profilerLoad() {
try { try {
const d = localStorage.getItem('otaman_profiles'); const d = localStorage.getItem('simple_profiles');
profiles = d ? JSON.parse(d) : []; profiles = d ? JSON.parse(d) : [];
} catch (e) { profiles = []; } } catch (e) { profiles = []; }
} }
function profilerSave() { function profilerSave() {
localStorage.setItem('otaman_profiles', JSON.stringify(profiles)); localStorage.setItem('simple_profiles', JSON.stringify(profiles));
} }
function snapshotsLoad() { function snapshotsLoad() {
try { try {
const d = localStorage.getItem('otaman_snapshots'); const d = localStorage.getItem('simple_snapshots');
snapshots = d ? JSON.parse(d) : []; snapshots = d ? JSON.parse(d) : [];
} catch (e) { snapshots = []; } } catch (e) { snapshots = []; }
} }
function snapshotsSave() { function snapshotsSave() {
try { try {
localStorage.setItem('otaman_snapshots', JSON.stringify(snapshots)); localStorage.setItem('simple_snapshots', JSON.stringify(snapshots));
} catch (e) { } catch (e) {
alert('Snapshot save failed: ' + e.message); alert('Snapshot save failed: ' + e.message);
} }
@@ -11400,7 +11407,6 @@ const LANG_RU = {
'Response data (hex)': 'Данные ответа (hex)', 'Response data (hex)': 'Данные ответа (hex)',
'— Select command —': '— Выберите команду —', '— Select command —': '— Выберите команду —',
'— select preset —': '— выберите предустановленный —', '— select preset —': '— выберите предустановленный —',
'SIM OTA with a Human Face': 'SIM OTA с человеческим лицом',
'Error: specify SELECT parameters': 'Ошибка: укажите параметры SELECT', 'Error: specify SELECT parameters': 'Ошибка: укажите параметры SELECT',
'Error: unknown command': 'Ошибка: неизвестная команда', 'Error: unknown command': 'Ошибка: неизвестная команда',
'Error: specify data': 'Ошибка: укажите данные', 'Error: specify data': 'Ошибка: укажите данные',
@@ -11499,12 +11505,12 @@ const LANG_RU = {
'Install Package (.cap file)': 'Установка пакета (.cap файл)', 'Install Package (.cap file)': 'Установка пакета (.cap файл)',
'Server URL': 'URL сервера', 'Server URL': 'URL сервера',
'Connect': 'Подключиться', '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': 'Для подключения установите', 'To connect, install': 'Для подключения установите',
'and start it.': 'и запустите его.', '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': 'Нажмите кнопку', 'Click': 'Нажмите кнопку',
'when pysim-otaman-server is ready': 'когда pysim-otaman-server будет готов', 'when pysim-simple-server is ready': 'когда pysim-simple-server будет готов',
'Equip card': 'Подключить карту', 'Equip card': 'Подключить карту',
'Check status': 'Проверить статус', 'Check status': 'Проверить статус',
'Reset card': 'Сброс карты', 'Reset card': 'Сброс карты',
@@ -11532,8 +11538,8 @@ const LANG_RU = {
'Checking card...': 'Проверка карты...', 'Checking card...': 'Проверка карты...',
'Connected': 'Подключено', 'Connected': 'Подключено',
'Connection failed:': 'Ошибка подключения:', 'Connection failed:': 'Ошибка подключения:',
'pysim-blocked-hint': 'Браузер, вероятно, заблокировал доступ к локальному серверу карт. Разрешите доступ к локальной сети для этого сайта (Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → Разрешить). Также проверьте, что pysim-otaman-server запущен по адресу {url}.', 'pysim-blocked-hint': 'Браузер, вероятно, заблокировал доступ к локальному серверу карт. Разрешите доступ к локальной сети для этого сайта (Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → Разрешить). Также проверьте, что pysim-simple-server запущен по адресу {url}.',
'pysim-unreachable-hint': 'Не удалось подключиться к серверу карт. Проверьте, что pysim-otaman-server запущен и адрес указан верно ({url}). Если эта страница открыта по HTTPS, также разрешите доступ к локальной сети в браузере (Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → Разрешить).', 'pysim-unreachable-hint': 'Не удалось подключиться к серверу карт. Проверьте, что pysim-simple-server запущен и адрес указан верно ({url}). Если эта страница открыта по HTTPS, также разрешите доступ к локальной сети в браузере (Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → Разрешить).',
'No card detected. Insert card and click Equip card button': 'Карта не обнаружена. Вставьте карту и нажмите Подключить карту', 'No card detected. Insert card and click Equip card button': 'Карта не обнаружена. Вставьте карту и нажмите Подключить карту',
'Card inserted — press Equip': 'Карта вставлена — нажмите «Подключить карту»', 'Card inserted — press Equip': 'Карта вставлена — нажмите «Подключить карту»',
'Card inserted — initializing...': 'Карта вставлена — инициализация...', 'Card inserted — initializing...': 'Карта вставлена — инициализация...',
@@ -11890,8 +11896,6 @@ function translatePage() {
const key = el.getAttribute('data-l10n'); const key = el.getAttribute('data-l10n');
el.textContent = t(key); el.textContent = t(key);
}); });
const slogan = document.getElementById('slogan');
if (slogan) slogan.textContent = t('SIM OTA with a Human Face');
} }
function isViewVisible(id) { function isViewVisible(id) {
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "OTAMan", "name": "SIMple",
"short_name": "OTAMan", "short_name": "SIMple",
"description": "Offline APDU helper for SIM/USIM/RAM commands", "description": "UICC/SIM workbench: APDU toolkit, SIM OTA (SCP80/SCP81), STK/CAT simulator, card profiler",
"start_url": "index.html", "start_url": "index.html",
"display": "standalone", "display": "standalone",
"background_color": "#fafafa", "background_color": "#fafafa",
+2 -2
View File
@@ -1,11 +1,11 @@
{ {
"name": "otaman", "name": "simple",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "otaman", "name": "simple",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"aes-js": "^3.1.2", "aes-js": "^3.1.2",
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "otaman", "name": "simple",
"version": "1.0.0", "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", "main": "index.js",
"scripts": { "scripts": {
"build": "npx tailwindcss -i src/style.css -o style.css --config tailwind.config.js && cat src/contrast.css >> style.css", "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": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/anttro/otaman.git" "url": "https://github.com/anttro/simple.git"
}, },
"keywords": [], "keywords": [],
"type": "commonjs", "type": "commonjs",
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'otaman-v188'; const CACHE = 'simple-v190';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+4 -4
View File
@@ -167,9 +167,9 @@ test('load/persist/reset round-trip through localStorage', () => {
'opt-targeted-app': '//aid/A000000151000000' 'opt-targeted-app': '//aid/A000000151000000'
}); });
scp81OptionsPersist(); scp81OptionsPersist();
assert.ok(store['otaman_scp81_opts'].includes('"chunkSize":100')); assert.ok(store['simple_scp81_opts'].includes('"chunkSize":100'));
assert.ok(store['otaman_scp81_opts'].includes('"targetedAppOn":true')); assert.ok(store['simple_scp81_opts'].includes('"targetedAppOn":true'));
assert.ok(!store['otaman_scp81_opts'].includes('keepAlive')); assert.ok(!store['simple_scp81_opts'].includes('keepAlive'));
// a reload restores the saved setup // a reload restores the saved setup
setup({}); setup({});
scp81OptionsLoad(); scp81OptionsLoad();
@@ -184,7 +184,7 @@ test('load/persist/reset round-trip through localStorage', () => {
assert.strictEqual(get('opt-targeted-app').value, '//aid/A000000151000000'); assert.strictEqual(get('opt-targeted-app').value, '//aid/A000000151000000');
// reset clears the saved entry and restores the defaults // reset clears the saved entry and restores the defaults
scp81OptionsReset(); scp81OptionsReset();
assert.ok(!('otaman_scp81_opts' in store)); assert.ok(!('simple_scp81_opts' in store));
scp81OptionsLoad(); scp81OptionsLoad();
assert.strictEqual(get('opt-chunked').checked, true); assert.strictEqual(get('opt-chunked').checked, true);
assert.strictEqual(get('opt-next-uri').checked, true); assert.strictEqual(get('opt-next-uri').checked, true);
+5 -5
View File
@@ -3,16 +3,16 @@ requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "pysim-otaman-server" name = "pysim-simple-server"
version = "2.2.21" version = "2.3.1"
description = "HTTP REST server wrapping pysim for the OTAMan PWA" description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8" requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. # 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 # Declaring it here would make pip resolve its full tree (including the SMPP
# bridge -> Twisted -> twisted-iocpsupport, which needs MSVC on Windows). # bridge -> Twisted -> twisted-iocpsupport, which needs MSVC on Windows).
[project.scripts] [project.scripts]
pysim-otaman-server = "pysim_otaman_server.__main__:main" pysim-simple-server = "pysim_simple_server.__main__:main"
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
include = ["pysim_otaman_server*"] include = ["pysim_simple_server*"]
@@ -33,11 +33,11 @@ def main():
_server_start = time.time() _server_start = time.time()
mod = load_pysim_app() mod = load_pysim_app()
parser = mod.option_parser 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-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('--http-port', type=int, default=8080, help='Bind port (default: 8080)')
parser.add_argument('--web-dir', default=_default_web_dir(), metavar='PATH', 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('--log-requests', action='store_true', default=False, help='Log request/response payloads to stderr')
parser.add_argument('--sms-oa', default='12345', metavar='DIGITS', parser.add_argument('--sms-oa', default='12345', metavar='DIGITS',
help='TP-Originating-Address (SMSC number) for the SMS-DELIVER TPDU (default: 12345)') help='TP-Originating-Address (SMSC number) for the SMS-DELIVER TPDU (default: 12345)')
@@ -222,14 +222,14 @@ def main():
server.iccid = iccid server.iccid = iccid
server.equipping = False server.equipping = False
# Set server reference for polling timer and mark the card session state # Set server reference for polling timer and mark the card session state
import pysim_otaman_server.server import pysim_simple_server.server
pysim_otaman_server.server._server_ref = server pysim_simple_server.server._server_ref = server
pysim_otaman_server.server._CARD_CONNECTED = card is not None pysim_simple_server.server._CARD_CONNECTED = card is not None
if opts.poll_interval 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) # Auto-enable polling if card initialized successfully (unless interval is 0)
if server.scc and server.card and opts.poll_interval != 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 # Start presence monitoring only after the startup init: pyscard reports an
# already-present card as "added" on the first pass, and we must not # 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, # 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: if sl is not None and getattr(sl, '_reader', None) is not None:
start_card_monitor(str(sl._reader)) start_card_monitor(str(sl._reader))
print("" * 70) print("" * 70)
print(" pysim-otaman-server v%s listening on http://%s:%s" % (VERSION, opts.http_host, opts.http_port)) 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 OTAMan UI (served by this server)." print(" Open http://%s:%s in your browser for the SIMple UI (served by this server)."
% (opts.http_host, opts.http_port)) % (opts.http_host, opts.http_port))
print("" * 70) print("" * 70)
try: 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 pySim's ``init_card()`` performs several physical card resets: one per profile
candidate tried by ``CardProfile.pick()`` plus one at the end of 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 io import StringIO
from pySim.transport import ApduTracer, ProactiveHandler from pySim.transport import ApduTracer, ProactiveHandler
from pySim.cards import UiccCardBase from pySim.cards import UiccCardBase
from pysim_otaman_server import httpota from pysim_simple_server import httpota
from pysim_otaman_server import scp81 from pysim_simple_server import scp81
from smartcard.CardMonitoring import CardMonitor, CardObserver from smartcard.CardMonitoring import CardMonitor, CardObserver
import gsm0338 # registers 'gsm03.38' codec import gsm0338 # registers 'gsm03.38' codec
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
from osmocom.tlv import BER_TLV_IE 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 MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
+5 -5
View File
@@ -1,12 +1,12 @@
@echo off @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. REM Creates a venv and installs pysim and its dependencies.
setlocal enabledelayedexpansion setlocal enabledelayedexpansion
set VENV_DIR=%~dp0.venv set VENV_DIR=%~dp0.venv
echo === pysim-otaman-server setup === echo === pysim-simple-server setup ===
echo. echo.
REM Check Python REM Check Python
@@ -71,11 +71,11 @@ if %errorlevel% neq 0 (
) )
echo. echo.
REM Install pysim-otaman-server REM Install pysim-simple-server
echo === Installing pysim-otaman-server === echo === Installing pysim-simple-server ===
pip install -e "%~dp0." pip install -e "%~dp0."
if %errorlevel% neq 0 ( if %errorlevel% neq 0 (
echo Error: Failed to install pysim-otaman-server. echo Error: Failed to install pysim-simple-server.
pause pause
exit /b 1 exit /b 1
) )
+3 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
# pysim-otaman-server setup script # pysim-simple-server setup script
# Creates a venv and installs pysim and its dependencies. # Creates a venv and installs pysim and its dependencies.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
@@ -30,9 +30,9 @@ echo ""
echo "=== Installing pysim ===" echo "=== Installing pysim ==="
$PIP install git+https://github.com/osmocom/pysim.git $PIP install git+https://github.com/osmocom/pysim.git
# Install pysim-otaman-server # Install pysim-simple-server
echo "" echo ""
echo "=== Installing pysim-otaman-server ===" echo "=== Installing pysim-simple-server ==="
$PIP install -e "$SCRIPT_DIR" $PIP install -e "$SCRIPT_DIR"
# Check for PC/SC # Check for PC/SC
+12 -12
View File
@@ -1,33 +1,33 @@
@echo off @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 Starts the server, preferring the venv if it exists.
REM PC/SC is built into Windows, so defaults to reader 0. REM PC/SC is built into Windows, so defaults to reader 0.
set VENV_DIR=%~dp0.venv set VENV_DIR=%~dp0.venv
if exist "%VENV_DIR%\Scripts\pysim-otaman-server.exe" ( if exist "%VENV_DIR%\Scripts\pysim-simple-server.exe" (
echo Starting pysim-otaman-server from venv on http://127.0.0.1:8080 echo Starting pysim-simple-server from venv on http://127.0.0.1:8080
echo Press Ctrl+C to stop. 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 goto :eof
) )
if exist "%~dp0pysim_otaman_server\__main__.py" ( if exist "%~dp0pysim_simple_server\__main__.py" (
echo Starting pysim-otaman-server from source on http://127.0.0.1:8080 echo Starting pysim-simple-server from source on http://127.0.0.1:8080
echo Press Ctrl+C to stop. 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 goto :eof
) )
where pysim-otaman-server >nul 2>&1 where pysim-simple-server >nul 2>&1
if %errorlevel% equ 0 ( 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. echo Press Ctrl+C to stop.
pysim-otaman-server --http-port 8080 -p 0 pysim-simple-server --http-port 8080 -p 0
goto :eof 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 Run setup.bat first or install manually:
echo pip install pysim-otaman-server echo pip install pysim-simple-server
pause pause
+11 -11
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
# pysim-otaman-server start script # pysim-simple-server start script
# Starts the server, preferring the venv if it exists. # Starts the server, preferring the venv if it exists.
# Auto-detects PC/SC reader if available. # Auto-detects PC/SC reader if available.
@@ -21,21 +21,21 @@ if command -v pcscd > /dev/null 2>&1; then
fi fi
SERVER="" SERVER=""
if [ -f "$VENV_DIR/bin/pysim-otaman-server" ]; then if [ -f "$VENV_DIR/bin/pysim-simple-server" ]; then
SERVER="$VENV_DIR/bin/pysim-otaman-server" SERVER="$VENV_DIR/bin/pysim-simple-server"
elif command -v pysim-otaman-server &> /dev/null; then elif command -v pysim-simple-server &> /dev/null; then
SERVER="pysim-otaman-server" SERVER="pysim-simple-server"
elif [ -f "$SCRIPT_DIR/pysim_otaman_server/__main__.py" ]; then elif [ -f "$SCRIPT_DIR/pysim_simple_server/__main__.py" ]; then
echo "Starting pysim-otaman-server from source on http://127.0.0.1:8080" echo "Starting pysim-simple-server from source on http://127.0.0.1:8080"
cd "$SCRIPT_DIR" && python3 -m pysim_otaman_server --http-port 8080 $READER_ARGS cd "$SCRIPT_DIR" && python3 -m pysim_simple_server --http-port 8080 $READER_ARGS
exit $? exit $?
else else
echo "Error: pysim-otaman-server not installed." echo "Error: pysim-simple-server not installed."
echo "Run setup.sh first or install manually:" echo "Run setup.sh first or install manually:"
echo " pip install pysim-otaman-server" echo " pip install pysim-simple-server"
exit 1 exit 1
fi 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." echo "Press Ctrl+C to stop."
$SERVER --http-port 8080 $READER_ARGS $SERVER --http-port 8080 $READER_ARGS
+1 -1
View File
@@ -13,7 +13,7 @@ PY_SIM = PROJECTS / 'pysim'
if str(PY_SIM) not in sys.path: if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM)) 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): class TestClassifyApdu(unittest.TestCase):
+1 -1
View File
@@ -12,7 +12,7 @@ PY_SIM = PROJECTS / 'pysim'
if str(PY_SIM) not in sys.path: if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM)) sys.path.insert(0, str(PY_SIM))
import pysim_otaman_server.server as S import pysim_simple_server.server as S
class FakeCard: class FakeCard:
+2 -2
View File
@@ -15,8 +15,8 @@ if str(PY_SIM) not in sys.path:
from pySim.exceptions import SwMatchError from pySim.exceptions import SwMatchError
from pySim.ts_102_221 import CardProfileUICC from pySim.ts_102_221 import CardProfileUICC
import pysim_otaman_server.fastinit as fastinit import pysim_simple_server.fastinit as fastinit
from pysim_otaman_server.fastinit import ( from pysim_simple_server.fastinit import (
FastRuntimeState, FastRuntimeState,
do_reset_fast, do_reset_fast,
pick_profile_no_reset, pick_profile_no_reset,
+3 -3
View File
@@ -20,8 +20,8 @@ PY_SIM = PROJECTS / 'pysim'
if str(PY_SIM) not in sys.path: if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM)) sys.path.insert(0, str(PY_SIM))
from pysim_otaman_server import httpota from pysim_simple_server import httpota
import pysim_otaman_server.server as server import pysim_simple_server.server as server
OPEN_LOCALHOST = bytes.fromhex( 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), # 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). # as the reference terminal traces do (`36 81 ed` for 237 bytes).
import types 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 big = bytes(range(256)) * 1 # 256 bytes; take a slice below
ch = types.SimpleNamespace(rx=bytearray(b'\xAA' * 237)) ch = types.SimpleNamespace(rx=bytearray(b'\xAA' * 237))
class FakeBip: class FakeBip:
+2 -2
View File
@@ -18,8 +18,8 @@ PY_SIM = PROJECTS / 'pysim'
if str(PY_SIM) not in sys.path: if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM)) sys.path.insert(0, str(PY_SIM))
from pysim_otaman_server import server from pysim_simple_server import server
from pysim_otaman_server.server import _decode_iccid, _read_iccid from pysim_simple_server.server import _decode_iccid, _read_iccid
class FakeFile: class FakeFile:
+1 -1
View File
@@ -12,7 +12,7 @@ PY_SIM = PROJECTS / 'pysim'
if str(PY_SIM) not in sys.path: if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM)) 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): class TestMenuTimeout(unittest.TestCase):
+36 -36
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/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 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. 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: if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM)) sys.path.insert(0, str(PY_SIM))
from pysim_otaman_server.server import ( from pysim_simple_server.server import (
_build_sms_tpdu, _build_sms_tpdu,
_build_tr, _build_tr,
_decode_cmd, _decode_cmd,
@@ -106,7 +106,7 @@ class TestBuildSmsTpdu(unittest.TestCase):
SCTS = bytes.fromhex('24051215173000') SCTS = bytes.fromhex('24051215173000')
def _build(self, *args, **kwargs): 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) return _build_sms_tpdu(*args, **kwargs)
def test_single_message_with_cpi(self): 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).""" """Server-side proactive command/TR decode helpers (v1.8.0 log feature)."""
def setUp(self): def setUp(self):
import pysim_otaman_server.server as srv import pysim_simple_server.server as srv
srv._PROACTIVE_SESSION_START = 1234.0 srv._PROACTIVE_SESSION_START = 1234.0
srv._PLI_DATA[0x00] = '93055210011000' srv._PLI_DATA[0x00] = '93055210011000'
@@ -397,14 +397,14 @@ class TestProactiveDecode(unittest.TestCase):
self.assertEqual(r[1], {'label': 'Data bytes', 'value': '3'}) self.assertEqual(r[1], {'label': 'Data bytes', 'value': '3'})
def test_parse_proactive_header_plain_tags(self): 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') raw = bytes.fromhex('d00c010301270102028182240103')
self.assertEqual(srv._parse_proactive_header(raw), (1, 0x27, 0x81, 0x82, 0x01)) self.assertEqual(srv._parse_proactive_header(raw), (1, 0x27, 0x81, 0x82, 0x01))
def test_default_handler_logs_timer_management(self): def test_default_handler_logs_timer_management(self):
# pySim's auto-handler path: the parsed command object (not the empty # pySim's auto-handler path: the parsed command object (not the empty
# collection) is re-encoded for the log and used for the response. # 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.cat import ProactiveCommand
from pySim.utils import h2b from pySim.utils import h2b
srv._PROACTIVE_LOG.clear() srv._PROACTIVE_LOG.clear()
@@ -420,7 +420,7 @@ class TestProactiveDecode(unittest.TestCase):
self.assertEqual(entry['tr_result'], '00') self.assertEqual(entry['tr_result'], '00')
def test_default_handler_pli_includes_dict_data(self): 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.cat import ProactiveCommand
from pySim.utils import h2b from pySim.utils import h2b
srv._PROACTIVE_LOG.clear() srv._PROACTIVE_LOG.clear()
@@ -529,7 +529,7 @@ class TestEventDownload(unittest.TestCase):
"""ENVELOPE (EVENT DOWNLOAD) assembly, TS 102 223 7.5.11.""" """ENVELOPE (EVENT DOWNLOAD) assembly, TS 102 223 7.5.11."""
def _send(self, event_type, event_data): def _send(self, event_type, event_data):
import pysim_otaman_server.server as srv import pysim_simple_server.server as srv
calls = [] calls = []
class Tp: class Tp:
@@ -565,30 +565,30 @@ class TestTimerManagement(unittest.TestCase):
START = bytes.fromhex('d011810301270082028182a40101a503001000') START = bytes.fromhex('d011810301270082028182a40101a503001000')
def tearDown(self): def tearDown(self):
import pysim_otaman_server.server as srv import pysim_simple_server.server as srv
srv._timer_cancel() srv._timer_cancel()
def test_hms_bcd_roundtrip(self): 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(60).hex(), '001000')
self.assertEqual(srv._hms_bcd(3723).hex(), '102030') self.assertEqual(srv._hms_bcd(3723).hex(), '102030')
self.assertEqual([srv._bcd_swap(b) for b in srv._hms_bcd(3723)], [1, 2, 3]) 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): 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) tr = srv._handle_timer_command(1, 0x27, 0x00, self.START, 0x81, 0x82)
self.assertEqual(tr.hex(), '810301270082028281030100') self.assertEqual(tr.hex(), '810301270082028281030100')
remaining = srv._timer_remaining(1) remaining = srv._timer_remaining(1)
self.assertTrue(55 <= remaining <= 60, remaining) self.assertTrue(55 <= remaining <= 60, remaining)
def test_get_returns_remaining_value(self): 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) srv._handle_timer_command(1, 0x27, 0x00, self.START, 0x81, 0x82)
tr = srv._handle_timer_command(1, 0x27, 0x02, self.START, 0x81, 0x82) tr = srv._handle_timer_command(1, 0x27, 0x02, self.START, 0x81, 0x82)
self.assertEqual(tr.hex(), '810301270282028281a40101a503001000030100') self.assertEqual(tr.hex(), '810301270282028281a40101a503001000030100')
def test_deactivate_stops_and_reports_value(self): 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) srv._handle_timer_command(1, 0x27, 0x00, self.START, 0x81, 0x82)
tr = srv._handle_timer_command(1, 0x27, 0x01, self.START, 0x81, 0x82) tr = srv._handle_timer_command(1, 0x27, 0x01, self.START, 0x81, 0x82)
self.assertTrue(tr.hex().startswith('8103012701'), tr.hex()) self.assertTrue(tr.hex().startswith('8103012701'), tr.hex())
@@ -596,12 +596,12 @@ class TestTimerManagement(unittest.TestCase):
self.assertIsNone(srv._timer_remaining(1)) self.assertIsNone(srv._timer_remaining(1))
def test_get_on_stopped_timer_is_contradiction(self): 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) tr = srv._handle_timer_command(1, 0x27, 0x02, self.START, 0x81, 0x82)
self.assertEqual(tr.hex(), '810301270282028281030124') self.assertEqual(tr.hex(), '810301270282028281030124')
def test_timer_expiration_envelope(self): def test_timer_expiration_envelope(self):
import pysim_otaman_server.server as srv import pysim_simple_server.server as srv
calls = [] calls = []
class Tp: class Tp:
@@ -618,7 +618,7 @@ class TestTimerManagement(unittest.TestCase):
self.assertEqual(calls, ['80c200000ed70c82028281a40101a503001000']) self.assertEqual(calls, ['80c200000ed70c82028281a40101a503001000'])
def test_cancelled_timer_does_not_report(self): def test_cancelled_timer_does_not_report(self):
import pysim_otaman_server.server as srv import pysim_simple_server.server as srv
calls = [] calls = []
class Tp: class Tp:
@@ -704,7 +704,7 @@ class TestSmsConcat(unittest.TestCase):
def test_no_udh_sms_submit(self): def test_no_udh_sms_submit(self):
"""SMS-SUBMIT without TP-UDHI: entire UD is payload.""" """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 # 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 # 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 tpdu = bytes.fromhex('0100' # first octet + MR
@@ -722,7 +722,7 @@ class TestSmsConcat(unittest.TestCase):
def test_8bit_concat_iei_0x00(self): def test_8bit_concat_iei_0x00(self):
"""SMS-SUBMIT with IEI 0x00 (8-bit reference concatenation).""" """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 # 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 # 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 # 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): def test_16bit_concat_iei_0x08(self):
"""SMS-SUBMIT with IEI 0x08 (16-bit reference concatenation).""" """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 # 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) # UDH: 06 (UDHL) 08 04 01 02 03 04 (16-bit concat: ref=0x0102, total=3, num=4)
# payload=FF # payload=FF
@@ -770,7 +770,7 @@ class TestSmsConcat(unittest.TestCase):
def test_udh_with_cpi(self): def test_udh_with_cpi(self):
"""UDH with concatenation IE + CPI IE (0x70).""" """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 # First octet 0x41: MTI=01, TP-UDHI=1
# UDHL=07, UDH: 00 03 04 04 01 (concat) + 70 00 (CPI) # UDHL=07, UDH: 00 03 04 04 01 (concat) + 70 00 (CPI)
tpdu = bytes.fromhex('4100' tpdu = bytes.fromhex('4100'
@@ -794,7 +794,7 @@ class TestSmsConcat(unittest.TestCase):
def test_empty_payload(self): def test_empty_payload(self):
"""Segment with empty payload after UDH.""" """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 # First octet 0x41: MTI=01, TP-UDHI=1
tpdu = bytes.fromhex('4100' tpdu = bytes.fromhex('4100'
'05' '05'
@@ -815,7 +815,7 @@ class TestSmsConcat(unittest.TestCase):
def test_short_tpdu(self): def test_short_tpdu(self):
"""Truncated TPDU returns gracefully.""" """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') ref, total, num, payload = _parse_sms_concat(b'\x01')
self.assertIsNone(ref) self.assertIsNone(ref)
self.assertIsNone(total) self.assertIsNone(total)
@@ -823,7 +823,7 @@ class TestSmsConcat(unittest.TestCase):
def test_none_input(self): def test_none_input(self):
"""None input returns empty payload.""" """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) ref, total, num, payload = _parse_sms_concat(None)
self.assertIsNone(ref) self.assertIsNone(ref)
self.assertIsNone(total) self.assertIsNone(total)
@@ -832,7 +832,7 @@ class TestSmsConcat(unittest.TestCase):
def test_short_tpdu(self): def test_short_tpdu(self):
"""Truncated TPDU returns gracefully.""" """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') ref, total, num, payload = _parse_sms_concat(b'\x44')
self.assertIsNone(ref) self.assertIsNone(ref)
self.assertIsNone(total) self.assertIsNone(total)
@@ -840,7 +840,7 @@ class TestSmsConcat(unittest.TestCase):
def test_none_input(self): def test_none_input(self):
"""None input returns empty payload.""" """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) ref, total, num, payload = _parse_sms_concat(None)
self.assertIsNone(ref) self.assertIsNone(ref)
self.assertIsNone(total) self.assertIsNone(total)
@@ -853,7 +853,7 @@ class TestSmsReassembly(unittest.TestCase):
def test_single_segment_no_concat(self): def test_single_segment_no_concat(self):
"""Single segment without UDH → submit_tpdu_hex is set directly.""" """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() handler = PoRSubmitHandler()
# Build a simple D0 with tag 8B containing an SMS-SUBMIT without UDH # Build a simple D0 with tag 8B containing an SMS-SUBMIT without UDH
sms_tpdu = bytes.fromhex('040005902143F50004' # SMS-SUBMIT header sms_tpdu = bytes.fromhex('040005902143F50004' # SMS-SUBMIT header
@@ -874,7 +874,7 @@ class TestSmsReassembly(unittest.TestCase):
def test_multi_segment_reassembly(self): def test_multi_segment_reassembly(self):
"""3 segments with IEI 0x00 in random order → assembled in correct order.""" """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() handler = PoRSubmitHandler()
# Segment payloads (after UDH) # Segment payloads (after UDH)
@@ -897,7 +897,7 @@ class TestSmsReassembly(unittest.TestCase):
def test_independent_references(self): def test_independent_references(self):
"""Two different reference numbers are independent.""" """Two different reference numbers are independent."""
from pysim_otaman_server.server import PoRSubmitHandler from pysim_simple_server.server import PoRSubmitHandler
handler = PoRSubmitHandler() handler = PoRSubmitHandler()
# Ref 0x01: 2 segments # Ref 0x01: 2 segments
@@ -947,7 +947,7 @@ class CapApduSequenceTest(unittest.TestCase):
return buf.getvalue().hex().upper() return buf.getvalue().hex().upper()
def test_cap_parse(self): 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()) loadfile_aid, module_aid, data = _cap_parse(self._mini_cap())
self.assertEqual(loadfile_aid, 'A00000010001') self.assertEqual(loadfile_aid, 'A00000010001')
self.assertEqual(module_aid, 'A000000100') self.assertEqual(module_aid, 'A000000100')
@@ -956,7 +956,7 @@ class CapApduSequenceTest(unittest.TestCase):
self.assertIn('03000A01', data) self.assertIn('03000A01', data)
def test_sequence_install_load_install(self): 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') seq = _cap_apdu_sequence('A00000010001', 'A000000100', 'AABBCCDD')
# INSTALL [for load]: lv(pkg aid) + lv(ISD) + 000000 # INSTALL [for load]: lv(pkg aid) + lv(ISD) + 000000
self.assertEqual(seq[0], self.assertEqual(seq[0],
@@ -969,7 +969,7 @@ class CapApduSequenceTest(unittest.TestCase):
self.assertIn('06A00000010001' + '05A000000100' + '05A000000100' + '0100', seq[2]) self.assertIn('06A00000010001' + '05A000000100' + '05A000000100' + '0100', seq[2])
def test_load_blocks_split_and_counter(self): 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)) data = ''.join('%02X' % (i % 256) for i in range(700))
seq = _cap_apdu_sequence('A00000010001', 'A000000100', data) seq = _cap_apdu_sequence('A00000010001', 'A000000100', data)
self.assertEqual(len(seq), 5) # INSTALL + 3 LOAD + INSTALL 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 # 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 # into consecutive chunks of that size, the last block marked P1=0x80
# with the block counter in P2. # 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 data = ''.join('%02X' % (i % 256) for i in range(700)) # TLV = 704 bytes
seq = _cap_apdu_sequence('A00000010001', 'A000000100', data, block_size=100) seq = _cap_apdu_sequence('A00000010001', 'A000000100', data, block_size=100)
self.assertEqual(len(seq), 10) # INSTALL + 8 LOAD + INSTALL 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): def test_gen_install_returns_the_apdu_list(self):
# /api/scp81/gen-install: build the INSTALL/LOAD/INSTALL list for a # /api/scp81/gen-install: build the INSTALL/LOAD/INSTALL list for a
# .cap without touching any listener or script state. # .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'}) resp = _scp81_gen_install({'cap_hex': self._mini_cap(), 'privileges': '01'})
self.assertTrue(resp['ok'], resp) self.assertTrue(resp['ok'], resp)
self.assertEqual(resp['load_file_aid'], 'A00000010001') self.assertEqual(resp['load_file_aid'], 'A00000010001')
@@ -1023,7 +1023,7 @@ class CapApduSequenceTest(unittest.TestCase):
self.assertNotIn('queued', resp) # generation only, no queueing self.assertNotIn('queued', resp) # generation only, no queueing
def test_gen_install_rejects_bad_input(self): 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']) self.assertFalse(_scp81_gen_install({})['ok'])
resp = _scp81_gen_install({'cap_hex': '00'}) resp = _scp81_gen_install({'cap_hex': '00'})
self.assertFalse(resp['ok']) self.assertFalse(resp['ok'])
@@ -1034,7 +1034,7 @@ class TerminalProfileTest(unittest.TestCase):
"""Runtime TERMINAL PROFILE: hex validation and re-send.""" """Runtime TERMINAL PROFILE: hex validation and re-send."""
def test_validate_tp_hex(self): 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('ff 00 80'), ('FF0080', None))
self.assertEqual(_validate_tp_hex('80FF'), ('80FF', None)) self.assertEqual(_validate_tp_hex('80FF'), ('80FF', None))
for bad in ('', ' ', 'F', 'XYZ', 'FF0', 'FF' * 256): 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): def test_resend_terminal_profile_resets_state_and_sends(self):
import types import types
from pysim_otaman_server import server as srv from pysim_simple_server import server as srv
server_obj = types.SimpleNamespace( server_obj = types.SimpleNamespace(
terminal_profile='FF00', stk_pending={'type': 'display_text'}, terminal_profile='FF00', stk_pending={'type': 'display_text'},
menu_active=True, event_list=[0x03], sim_menu='old') menu_active=True, event_list=[0x03], sim_menu='old')
+1 -1
View File
@@ -11,7 +11,7 @@ PY_SIM = PROJECTS / 'pysim'
if str(PY_SIM) not in sys.path: if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM)) 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): class TestPollInterval(unittest.TestCase):
+6 -6
View File
@@ -19,8 +19,8 @@ PY_SIM = PROJECTS / 'pysim'
if str(PY_SIM) not in sys.path: if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM)) sys.path.insert(0, str(PY_SIM))
from pysim_otaman_server import scp81 from pysim_simple_server import scp81
import pysim_otaman_server.server as server import pysim_simple_server.server as server
PSK = bytes.fromhex('00112233445566778899aabbccddeeff') PSK = bytes.fromhex('00112233445566778899aabbccddeeff')
IDENT = '89012345678901234567' IDENT = '89012345678901234567'
@@ -194,7 +194,7 @@ class PskTlsServerTest(unittest.TestCase):
tls = self._connect(srv) tls = self._connect(srv)
tls.sendall(b'POST /api/scp81 HTTP/1.1\r\nHost: 127.0.0.1\r\n' 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-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) reply = self._read_http(tls)
self.assertIn(b'HTTP/1.1 200 OK', reply) self.assertIn(b'HTTP/1.1 200 OK', reply)
self.assertIn(b'X-Admin-Next-URI: /api/scp81?req=1', 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): class ScriptBodyLengthTest(unittest.TestCase):
def test_long_c_apdu_uses_ber_long_form(self): 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 apdu = '80E80000F0' + 'AB' * 239 + '00' # exactly 245-byte LOAD
body = _scp81_command_body(apdu) body = _scp81_command_body(apdu)
# AE 80 22 81 F5 <245 bytes> 00 00 # AE 80 22 81 F5 <245 bytes> 00 00
@@ -1203,12 +1203,12 @@ class ScriptBodyLengthTest(unittest.TestCase):
self.assertEqual(len(body), 5 + 245 + 2) self.assertEqual(len(body), 5 + 245 + 2)
def test_short_apdu_stays_short_form(self): 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(), self.assertEqual(_scp81_command_body('80CAFF2100').hex().upper(),
'AE80220580CAFF21000000') 'AE80220580CAFF21000000')
def test_definite_variant_ber_lengths(self): 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 apdu = 'AB' * 130
body = _scp81_command_body(apdu, definite=True) body = _scp81_command_body(apdu, definite=True)
# AA 81 85 22 81 82 <130 bytes> (outer 1+2+130 = 133 = 0x85) # AA 81 85 22 81 82 <130 bytes> (outer 1+2+130 = 133 = 0x85)
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/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 The helpers must resolve every model-known file strictly within the requested
parent (no pySim global selectables, no probe_file model injection) and must 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: if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM)) sys.path.insert(0, str(PY_SIM))
from pysim_otaman_server.server import ( from pysim_simple_server.server import (
_app_by_sel, _app_by_sel,
_fid4, _fid4,
_file_by_sel, _file_by_sel,
+4 -4
View File
@@ -21,8 +21,8 @@ if str(PY_SIM) not in sys.path:
from pySim.exceptions import NoCardError, SwMatchError from pySim.exceptions import NoCardError, SwMatchError
from pysim_otaman_server import __main__ as srv_main from pysim_simple_server import __main__ as srv_main
from pysim_otaman_server.server import _LineFilter from pysim_simple_server.server import _LineFilter
CARDLESS_LINE = 'INIT: no card in the reader — server ready; insert a card or press Equip' 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_fastinit.install = mock.Mock()
fake_http.return_value.serve_forever.side_effect = KeyboardInterrupt fake_http.return_value.serve_forever.side_effect = KeyboardInterrupt
try: try:
with mock.patch('sys.argv', ['pysim-otaman-server']): with mock.patch('sys.argv', ['pysim-simple-server']):
srv_main.main() srv_main.main()
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
pass pass
@@ -135,7 +135,7 @@ class StartupNoCardTest(unittest.TestCase):
fake_fastinit.install = mock.Mock() fake_fastinit.install = mock.Mock()
fake_http.return_value.serve_forever.side_effect = KeyboardInterrupt fake_http.return_value.serve_forever.side_effect = KeyboardInterrupt
try: try:
with mock.patch('sys.argv', ['pysim-otaman-server']): with mock.patch('sys.argv', ['pysim-simple-server']):
srv_main.main() srv_main.main()
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
pass pass