12 Commits
Author SHA1 Message Date
catarrh 33f0ee3449 docs: related-tools section, language links, and RU wording polish
- Add a 'Related tools' section (EN + RU) introducing SIMple, SIMtrace
  Analyser and sigrok_iso7816_stream as independent tools.
- Cross-link the two READMEs (English/Russian) at the top.
- Russian: replace the 'pill' calque with 'button' (README_RUS + help-ru),
  and tighten the PC/SC-fallback, init-output and canonical-path wording.
- Bump the SW cache for the help-ru.html change.
2026-10-27 23:37:38 +03:00
catarrh 215d0a0ba0 frontend: fix equipped-card preset auto-selection race
release / release (push) Waiting to run
cardsAutoSelectByIccid latched its 'already handled' guard even when no
matching preset was found, so a card equipped before /api/presets had
loaded (the connect-time race with the status/availability polls) never
got auto-selected: navigating to SCP80/RAM showed no preset until the card
was re-inserted.  The guard is now latched only after a successful
selection, and cardsFetch re-runs the auto-selection once the store
arrives, so the card's preset is picked as soon as both resources are
available.

Bumps VERSION to 3.40.3 and the SW cache to simple-v488.
2026-10-27 18:58:18 +03:00
catarrh 8f63368008 docs: move Prerequisites before Quick start
Prerequisites is step 0 for the install, so it now leads the docs as a
top-level section ahead of Quick start (removed from the Server section).
2026-10-27 14:56:32 +03:00
catarrh 29d762a662 docs: quick start guides only the server install
Drop the 'Frontend only (client-side tools)' alternative — the quick
start is the default path and that installs the server.
2026-10-27 14:53:10 +03:00
catarrh eeea7ca509 docs: drop the SMPP bridge mentions (not installed, not used) 2026-10-27 14:50:58 +03:00
catarrh ad4289eabe docs: clarify the Windows prerequisite, drop the redundant PSK-TLS note
The Windows bullet now states the pyscard wheel situation plainly (3.13
= wheels, no compile; 3.14 = build from source) without repeating the
SCP81 TLS-PSK 3.13+ requirement already given in the Python bullet.
2026-10-27 14:50:03 +03:00
catarrh 04859051d3 docs: say 'frontend' instead of PWA where the app's PWA-ness is irrelevant
PWA is kept only where it matters: the first occurrence (unpacked as
'Progressive Web App') and the '## PWA' install/offline section.  The
RU text declines 'фронтенд' correctly.
2026-10-27 11:08:08 +03:00
catarrh 2154912d2b docs: drop the CORS mention from Quick start
The UI and API are served from one origin by default, so CORS is a
non-issue — the quick start no longer calls it out.  Not re-added
elsewhere (kept unobtrusive).
2026-10-27 11:01:05 +03:00
catarrh 6750f72cd5 card reader: move Services pill next to File manager 2026-10-26 22:49:47 +03:00
catarrh 465a43eef2 docs: link the project website alongside the Telegram channel
release / release (push) Waiting to run
2026-10-26 22:11:38 +03:00
catarrh fdbff77188 release: 3.40.2 — file-filter reset (✕) button
A red ✕ next to the filter selector appears whenever the filter is not
"All" and resets it to "All" in one click (pysimFsFilterClear).  Bump
VERSION/pyproject/PWA header to 3.40.2 and the SW cache to simple-v487.
2026-10-26 21:01:29 +03:00
catarrh 1f8fa54c49 release: 3.40.1 — filter uses per-spec service names
release / release (push) Canceled after 0s
Drop the invented "SMS-related" / "CB-related" filter categories: those
files are service-declared and are already reached through their
per-service options (per-spec service names).  Bump VERSION/pyproject/
PWA header to 3.40.1 and the SW cache to simple-v486.
2026-10-26 19:41:32 +03:00
10 changed files with 254 additions and 135 deletions

No files matched your search

+31
View File
@@ -9,6 +9,37 @@ every change carries a descriptive commit message.
## [Unreleased]
## [3.40.3] — 2026-10-27
### Fixed
- **Equipped card not always auto-selected in SCP80/RAM.** The ICCID-based
preset auto-selection latched its "already handled" guard even when no
matching preset was found, so a card equipped before `/api/presets` had
loaded (the connect-time race) never got auto-selected — navigating to
SCP80/RAM showed no preset until the card was re-inserted. The guard is
now latched only after a successful selection, and `cardsFetch` re-runs
the auto-selection once the store arrives.
## [3.40.2] — 2026-10-26
### Added
- **File filter: reset (✕) button.** A red ✕ to the right of the filter
selector appears whenever the filter is not "All" and resets it to "All"
in one click.
## [3.40.1] — 2026-10-26
### Changed
- **File filter: drop the "SMS-related" / "CB-related" categories.** Those
files are service-declared and already reachable through their per-service
filter options, which carry the per-spec service names ("Short Message
Storage (SMS)", "SMS Parameters (SMSP)", "Short Message Status Reports
(SMSR)", "Cell Broadcast Message Identifier (CBMI)", "Cell Broadcast
Message Identifier Ranges (CBMIR)").
## [3.40.0] — 2026-10-26
### Added
+48 -38
View File
@@ -1,23 +1,30 @@
# SIMple — a UICC workbench: APDU · OTA · STK · profiling
English | [Русский](README_RUS.md)
> Formerly known as OTAMan — renamed to SIMple on 2026-09-19.
📢 Project news and updates: [Telegram channel](https://t.me/youeyeseesee)
📢 Project news and updates: [website](https://atroshin.ru) · [Telegram channel](https://t.me/youeyeseesee)
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:
SIMple is a workbench for UICC/SIM card specialists: an offline HTML/JS frontend (PWA — a Progressive Web App) plus a local card server ([`pysim-simple-server`](pysim_simple_server/), a pySim/PC-SC bridge that also serves the frontend). 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.
**Demo:** [simple.atroshin.ru](https://simple.atroshin.ru) — the frontend alone, for experimenting. Install the server (below) for card-reader functions.
## Prerequisites
- **Python 3.9+** with `pip`, and **Git** — the app runs on 3.9+, but the **SCP81 TLS-PSK listener needs Python 3.13+** (`ssl`'s PSK callbacks).
- **Python venv support** — `setup.sh` creates `.venv`; if that fails with `ensurepip is not available` (Debian/Ubuntu ship the venv module separately), install it (`sudo apt install python3-venv`) and run `./setup.sh` again. An incomplete `.venv` left by the failed run is removed automatically.
- **Smart card reader** (PC/SC or serial/FTDI) — PC/SC is preferable (`pcsc-lite` + `ccid` on Linux). `pyscard` has no Linux wheels, so it builds from source: install the PC/SC header package too (`libpcsclite-dev` on Debian/Ubuntu, `pcsc-lite-devel` on Fedora; on Arch `pcsclite` includes them).
- **Windows** — use **Python 3.13 (strongly recommended)**: `pyscard` ships precompiled wheels for it, so nothing needs compiling; on **3.14** the `pyscard` and some pysim dependency wheels are missing and build from source (needs MSVC C++ Build Tools).
## Quick start
**PWA only (client-side tools):** open `frontend/index.html` in any browser, or serve `frontend/` with any static server. No Python required.
**Full (PWA + card server)** — two ways to get it:
**Install the server** — two ways to get it:
**1. Download the release package (recommended).** From the
[Releases](https://github.com/anttro/simple/releases) page take the latest package
@@ -25,18 +32,14 @@ SIMple is a workbench for UICC/SIM card specialists: an offline HTML/JS PWA plus
```sh
./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 frontend too)
```
The package is built from the tagged source — no git history, no development files.
Git is still required: `setup` installs **pysim** from its upstream repository
(nothing from pysim or its dependencies is bundled).
Git is still required: `setup` installs **pysim** from its upstream repository.
**Updates are fast:** `./update.sh` (or `update.bat`) downloads the latest release
package, replaces the application files in place and re-installs the app into the
existing `.venv` — pysim and its prerequisites are kept, so nothing is rebuilt.
`./update.sh --check` reports the current and latest versions, `--version X.Y.Z`
picks a specific release and `--zip file.zip` updates from a local package.
**Updates:** `./update.sh` (or `update.bat`) updates a packaged install in place
— pysim and its prerequisites are kept, so nothing is rebuilt.
**2. Clone the repository (for development, or changes not released yet).**
@@ -47,7 +50,7 @@ cd simple # `git pull` still updates it
./start.sh # or start.bat
```
Then open http://127.0.0.1:8080 — the UI and API share one origin, so no CORS or browser-permission setup is needed. The server starts fine with an empty reader (no card is a normal state): insert a card and it is initialized automatically (auto-equip), or press **Equip card** in the UI.
Then open http://127.0.0.1:8080 — the server starts fine with an empty reader (no card is a normal state): insert a card and it is initialized automatically (auto-equip), or press **Equip card** in the UI.
## Build
@@ -557,7 +560,7 @@ probe is refused when SPI1 has no counter check or the named preset does not def
## Cards
Stores saved card configurations (presets) in a **server-side store** — `~/.pysim-simple-server/card_presets.json` (`--card-presets` overrides it). A preset holds the cryptographic keys, SPI settings, TARs and the replay counters for SCP80 operations, the optional **ADM** key, plus the **PSK identity / PSK key** pair used by the SCP81 HTTP OTA listener. A preset carries **several keysets** — a keyset's number is the `b8..b5` nibble of its KIc/KID (TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same number (`01`–`0F`; A.2 rejects a mismatch), `00` (no security) is not a preset keyset, and each keyset has its own counter on the card (Annex A.1) — the server owns those counters, persists every consumed increment and appends it to an audit trail (`card_presets.json.audit.jsonl`). The list is loaded when the PWA connects; JSON export/import (which also accepts the old flat presets) is the migration path from the browser-local store. Cards is a **top-level tab**. The form groups the fields into two bordered blocks — **SCP80 (GSM 03.48, ETSI TS 102 225)** and **SCP81 (HTTP OTA)** — with the optional **ADM** field in the top row; the keysets are edited as rows (one per number, **Add keyset** / **Remove keyset**). When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches **no** preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The header shows gray **SCP80** / **SCP81** markers and a key glyph on the **ADM** badge when the preset matching the equipped card's ICCID has those settings filled in.
Stores saved card configurations (presets) in a **server-side store** — `~/.pysim-simple-server/card_presets.json` (`--card-presets` overrides it). A preset holds the cryptographic keys, SPI settings, TARs and the replay counters for SCP80 operations, the optional **ADM** key, plus the **PSK identity / PSK key** pair used by the SCP81 HTTP OTA listener. A preset carries **several keysets** — a keyset's number is the `b8..b5` nibble of its KIc/KID (TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same number (`01`–`0F`; A.2 rejects a mismatch), `00` (no security) is not a preset keyset, and each keyset has its own counter on the card (Annex A.1) — the server owns those counters, persists every consumed increment and appends it to an audit trail (`card_presets.json.audit.jsonl`). The list is loaded when the frontend connects; JSON export/import (which also accepts the old flat presets) is the migration path from the browser-local store. Cards is a **top-level tab**. The form groups the fields into two bordered blocks — **SCP80 (GSM 03.48, ETSI TS 102 225)** and **SCP81 (HTTP OTA)** — with the optional **ADM** field in the top row; the keysets are edited as rows (one per number, **Add keyset** / **Remove keyset**). When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches **no** preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The header shows gray **SCP80** / **SCP81** markers and a key glyph on the **ADM** badge when the preset matching the equipped card's ICCID has those settings filled in.
| Field | Description |
|---|---|
@@ -588,7 +591,7 @@ The **SCP81** column shows whether the preset supplies a usable PSK pair: **✓*
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-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.
> **Browser restriction:** when the frontend 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
@@ -618,7 +621,7 @@ A filesystem rule is defined by:
- **Path** — `MF`-rooted (e.g. `MF/7F10/6F3A`) or ADF AID-rooted (e.g. `A0000000871002/6F07`).
- **FCP/FCI check** — **Filetype only (FCP)**, **Filetype + size (FCP)** (adds file size, or record length/count for record files), or **Exact FCI** (byte-for-byte comparison of the raw SELECT FCP template `'62'`, catching FID/AID, life-cycle status, security-attribute, and proprietary-parameter changes).
- **Access rules** (optional) — verifies the file's access conditions as decoded rules: the FCI's `8B` reference is resolved against the card's (or snapshot's) EF.ARR copies and the decoded lines are compared as a set — record-number independent, so cards with a different EF.ARR layout still verify (missing/extra lines are listed; a lost reference fails; an unresolvable one is unverifiable). Captured automatically when a profile is created from a card or a snapshot; the editor shows each rule as structured rows (operation bits / RESIZE FILE / INCREASE plus security-condition selects, with a canonical preview line; `+ rule` adds one, raw template forms stay read-only) and the block is collapsible with None hiding it. With **Exact FCI**, *Ignore the EF.ARR reference in the exact FCI* compares the FCI without the `8B` value — a different record number no longer fails while the rules check owns its meaning.
- **Access rules** (optional) — verifies the file's access conditions as decoded rules: the FCI's `8B` reference is resolved against the card's (or snapshot's) EF.ARR copies and the decoded lines are compared as a set — record-number independent, so cards with a different EF.ARR layout still verify (missing/extra lines are listed; a lost reference fails; an unresolvable one is unverifiable). Captured automatically when a profile is created from a card or a snapshot; the editor shows each rule as structured rows (operation bits / RESIZE FILE / INCREASE plus security-condition selects, with a canonical preview line; `+ rule` adds one, raw template forms stay read-only) and the block is collapsible with None hiding it. With **Exact FCI**, *Ignore the EF.ARR reference in the exact FCI* compares the FCI without the `8B` value — a different record number does not fail while the rules check owns its meaning.
- **File attributes** — file type, size, record length and record count, taken from the FCP template (any may be left unset).
- **Check contents** (optional) — **Exact** hex equality, or **Mask** where `?` is a per-nibble wildcard (a mask with no `?` is a prefix match, e.g. `0891` for the IMSI MCC/MNC). Record files store a per-record list.
@@ -650,7 +653,7 @@ Files not in pysim's model can be added manually from the **Custom files** tab i
2. Enter the 4-hex **FID** and an alias (`EF.…`/`DF.…`; the prefix decides whether the entry is an EF or a DF)
3. Click **Add** — the file appears in the File manager tree; use **Edit** on a row to reload it into the form (the button becomes **Save**, **Cancel** aborts) or **Delete** to remove it (deleting a DF also deletes its child entries after a confirmation)
The canonical path removes the old ambiguity where the same file could be described both relatively and absolutely. Custom files persist in `localStorage` across sessions and are included in card scans under the same existence check. Export/import as JSON for sharing; legacy relative paths (e.g. `a153/4954`) are resolved on load, unresolvable ones are dropped and reported in the list.
The path is always canonical (MF- or ADF-rooted) — the same file is never described both relatively and absolutely. Custom files persist in `localStorage` across sessions and are included in card scans under the same existence check. Export/import as JSON for sharing; legacy relative paths (e.g. `a153/4954`) are resolved on load, unresolvable ones are dropped and reported in the list.
## Simulator
@@ -743,24 +746,17 @@ The interface is in English with Russian support. The language is detected from
## Server (pysim-simple-server)
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
- **Python 3.9+** with `pip`, and **Git** — the app runs on 3.9+, but the **SCP81 TLS-PSK listener needs Python 3.13+** (`ssl`'s PSK callbacks).
- **Python venv support** — `setup.sh` creates `.venv`; if that fails with `ensurepip is not available` (Debian/Ubuntu ship the venv module separately), install it (`sudo apt install python3-venv`) and run `./setup.sh` again. An incomplete `.venv` left by the failed run is removed automatically.
- **Smart card reader** (PC/SC or serial/FTDI) — PC/SC is preferable (`pcsc-lite` + `ccid` on Linux). `pyscard` has no Linux wheels, so it builds from source: install the PC/SC header package too (`libpcsclite-dev` on Debian/Ubuntu, `pcsc-lite-devel` on Fedora; on Arch `pcsclite` includes them).
- **Windows** — use **Python 3.13 (strongly recommended)**: `pyscard` has precompiled wheels up to 3.13 and the SCP81 TLS-PSK listener needs 3.13+. On 3.14 `pyscard` and some pysim dependency wheels are not available and build from source (needs MSVC C++ Build Tools). 3.9–3.12 run the app but not the SCP81 PSK listener. The SMPP bridge (`smpp.twisted3`) is intentionally skipped on Windows.
The bundled Python server wraps [pySim](https://osmocom.org/projects/pysim/wiki) and serves both the SIMple frontend (from `frontend/`) and a JSON API under `/api/*`.
### Scripts
| Script | What it does |
|--------|-------------|
| `setup.sh` / `setup.bat` | Creates `.venv/` (or repairs an incomplete one), installs pysim and the server. Run once after cloning or after unzipping the release package. |
| `start.sh` / `start.bat` | Starts the server from the venv (serves the PWA + API on `:8080`). |
| `start.sh` / `start.bat` | Starts the server from the venv (serves the frontend + API on `:8080`). |
| `update.sh` / `update.bat` | Updates a packaged install in place: fetches the latest release package, replaces the app files and re-installs the app into the existing `.venv` (pysim is kept). `--check`, `--version X.Y.Z`, `--zip FILE`, `--no-pip`, `--force`. |
`start.sh` checks PC/SC access before starting: if the user may not use `pcscd` (or the service is not running) it prints the cause with the group/polkit guidance and exits — the server **no longer falls back to a serial reader silently**. With access, the server auto-detects the first PC/SC reader; `start.bat` always passes `-p 0` (PC/SC is built into Windows). An explicit serial/FTDI reader is still possible with `-d <device>` (the pre-flight check is skipped). Extra arguments are passed through to the server, e.g. `./start.sh --gsmtap` or `start.bat --gsmtap 10.0.0.5:4729`. If no PC/SC reader is found the server reports it and exits — connect the reader and restart (a reader present but no card is still a normal cardless start).
`start.sh` checks PC/SC access before starting: if the user may not use `pcscd` (or the service is not running) it prints the cause with the group/polkit guidance and exits — the server **does not fall back to a serial reader silently**. With access, the server auto-detects the first PC/SC reader; `start.bat` always passes `-p 0` (PC/SC is built into Windows). An explicit serial/FTDI reader is still possible with `-d <device>` (the pre-flight check is skipped). Extra arguments are passed through to the server, e.g. `./start.sh --gsmtap` or `start.bat --gsmtap 10.0.0.5:4729`. If no PC/SC reader is found the server reports it and exits — connect the reader and restart (a reader present but no card is still a normal cardless start).
### Manual installation (Linux/macOS)
@@ -773,8 +769,8 @@ pysim-simple-server --http-port 8080
```
On Windows use `setup.bat` instead: it installs pysim's dependencies from
`requirements-pysim.txt` and pysim itself with `--no-deps`, so the SMPP bridge (and
its MSVC build chain) is not pulled in.
`requirements-pysim.txt` and pysim itself with `--no-deps`, so pysim's optional
dependencies are not pulled in.
### CLI options
@@ -782,7 +778,7 @@ its MSVC build chain) is not pulled in.
|--------|-------------|
| `--http-host` | Bind address (default: `127.0.0.1`) |
| `--http-port` | TCP port (default: `8080`) |
| `--web-dir` | Directory with PWA static files (default: `<repo>/frontend`) |
| `--web-dir` | Directory with frontend static files (default: `<repo>/frontend`) |
| `-p` / `--pcsc-device` | PC/SC reader slot number |
| `-d` / `--device` | Serial device path |
| `--no-card-init` | Skip card init to preserve the CAT session (no file manager) |
@@ -813,10 +809,10 @@ its MSVC build chain) is not pulled in.
```
then `sudo systemctl restart polkit` (or log out/in). Start the service if needed (`sudo systemctl enable --now pcscd`); `start.sh` and the server report this and exit instead of falling back to a serial reader, and `pcsc_scan` shows the same error until the rule is active. If your `pcscd` was built without PolicyKit and the socket is group-owned, adding the user to that group works instead.
- **"device file /dev/ttyUSB0 does not exist"** — only with an explicit `-d <device>`: the device is not connected (or the path is wrong). Without `-d` the server no longer uses the serial transport.
- **"device file /dev/ttyUSB0 does not exist"** — only with an explicit `-d <device>`: the device is not connected (or the path is wrong). Without `-d` the server uses the PC/SC transport only.
- **"no PC/SC reader found"** — connect a reader (`pcsc_scan` lists them) and restart; the server exits instead of starting without a reader.
- **"Service not available" (0x8010001D) / reader gone after a `pcscd` restart** — the server recovers by itself: a watchdog revives pyscard's presence monitor and the next equip (automatic after re-insertion, or the **Equip** button) recreates the PC/SC transport (releasing the old link first). No server restart needed.
- **"Card was removed" errors / auto-equip fails after a card swap** — a card insertion marks the PC/SC link for a rebuild (so the **first** attempt already uses a fresh link — on Windows the old one cannot connect after a swap), the auto-equip retries up to 3 times, and **every failed attempt rebuilds the transport** before the next one. A card removed while the server initializes at startup leaves it cardless with one line (no traceback), and the next insertion equips normally. The swallowed init error (with its traceback) is now printed as `AUTO-EQUIP: init output:`, and a card that is present but produced no card is a failure, not a silent "card gone". The watchdog re-arms with a doubling delay (5 s up to 60 s) while a card is present; a removed card clears the card state completely, so handlers answer "no card" instead of transmitting over the dead card.
- **"Card was removed" errors / auto-equip fails after a card swap** — a card insertion marks the PC/SC link for a rebuild (so the **first** attempt already uses a fresh link — on Windows the old one cannot connect after a swap), the auto-equip retries up to 3 times, and **every failed attempt rebuilds the transport** before the next one. A card removed while the server initializes at startup leaves it cardless with one line (no traceback), and the next insertion equips normally. A failed init is printed as `AUTO-EQUIP: init output:` (with its traceback), and a card that is present but produced no card is a failure, not a silent "card gone". The watchdog re-arms with a doubling delay (5 s up to 60 s) while a card is present; a removed card clears the card state completely, so handlers answer "no card" instead of transmitting over the dead card.
### API reference
@@ -824,13 +820,27 @@ See [docs/api.md](docs/api.md) for the full endpoint reference.
## Version compatibility
| PWA (SIMple) | Server | Status |
| frontend (SIMple) | Server | Status |
|-------------|--------|--------|
| any | same major | ✅ Compatible |
| any | older major | ❌ Outdated — update server |
| any | newer major | ⚠️ Server newer — update PWA |
| any | newer major | ⚠️ Server newer — update frontend |
The PWA checks the server version on connect via `GET /api/version` and compares the major version (e.g. a 3.x PWA with a 3.x server; a 2.x server is flagged as outdated).
The frontend checks the server version on connect via `GET /api/version` and compares the major version (e.g. a 3.x frontend with a 3.x server; a 2.x server is flagged as outdated).
## Related tools
SIMple, SIMtrace Analyser and sigrok_iso7816_stream are three standalone
tools for working with SIM/UICC cards, published together on the same
[website](https://atroshin.ru) and [news channel](https://t.me/youeyeseesee).
Each one is fully independent — install only the tools you need:
- **[SIMple](https://github.com/anttro/simple)** — a UICC workbench: APDU
construction, SCP80/SCP81 OTA, SIM Toolkit simulation and card profiling.
- **[SIMtrace Analyser](https://github.com/anttro/simtrace-analyser)** — an
APDU trace analyser for GSMTAP-SIM captures.
- **[sigrok_iso7816_stream](https://github.com/anttro/sigrok_iso7816_stream)** —
a passive ISO 7816 sniffer built on a cheap FX2 logic analyser.
## License
@@ -843,5 +853,5 @@ pySim itself is not included in this repository or in the release artifacts:
`setup.sh` / `setup.bat` install it from its upstream git repository.
Bundled third-party components (the MCC/MNC operator list and the AES/DES
bundles in the PWA) keep their own MIT licenses — see
bundles in the frontend) keep their own MIT licenses — see
[THIRD-PARTY-NOTICES](THIRD-PARTY-NOTICES).
+55 -44
View File
@@ -1,23 +1,30 @@
# SIMple — мастерская для UICC/SIM: APDU · OTA · STK · профилирование
[English](README.md) | Русский
> Прежнее название — OTAMan; переименован в SIMple 2026-09-19.
📢 Новости и обновления проекта: [Telegram-канал](https://t.me/youeyeseesee)
📢 Новости и обновления проекта: [сайт](https://atroshin.ru) · [Telegram-канал](https://t.me/youeyeseesee)
SIMple — инструментарий для специалистов по UICC/SIM-картам: автономный HTML/JS PWA и локальный сервер карт ([`pysim-simple-server`](pysim_simple_server/) — мост к pySim/PC-SC, он же раздаёт PWA). Закрывает весь цикл лабораторной работы:
SIMple — инструментарий для специалистов по UICC/SIM-картам: автономный HTML/JS-фронтенд (PWA — Progressive Web App) и локальный сервер карт ([`pysim-simple-server`](pysim_simple_server/) — мост к pySim/PC-SC, он же раздаёт фронтенд). Закрывает весь цикл лабораторной работы:
- **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, для экспериментов. Для функций картридера установите сервер (ниже).
**Демо:** [simple.atroshin.ru](https://simple.atroshin.ru) — только фронтенд, для экспериментов. Для функций картридера установите сервер (ниже).
## Требования
- **Python 3.9+** с `pip`, и **Git** — приложение работает на 3.9+, но **SCP81 TLS-PSK-слушателю нужен Python 3.13+** (PSK-callback'и `ssl`).
- **Поддержка venv в Python** — `setup.sh` создаёт `.venv`; если создание завершается с `ensurepip is not available` (в Debian/Ubuntu модуль venv поставляется отдельно), установите его (`sudo apt install python3-venv`) и запустите `./setup.sh` снова. Неполный `.venv`, оставшийся от неудачного запуска, удаляется автоматически.
- **Смарт-картридер** (PC/SC или serial/FTDI) — предпочтителен PC/SC (`pcsc-lite` + `ccid` на Linux). У `pyscard` нет Linux-wheel, поэтому он собирается из исходников: поставьте и пакет с заголовками PC/SC (`libpcsclite-dev` в Debian/Ubuntu, `pcsc-lite-devel` в Fedora; в Arch они входят в `pcsclite`).
- **Windows** — используйте **Python 3.13 (настоятельно рекомендуется)**: для него `pyscard` поставляет готовые wheel, так что ничего компилировать не нужно; на **3.14** wheel для `pyscard` и части зависимостей pysim отсутствуют, и они собираются из исходников (нужны MSVC C++ Build Tools).
## Быстрый старт
**Только PWA (клиентские функции):** откройте `frontend/index.html` в любом браузере или раздайте `frontend/` любым статическим сервером. Python не нужен.
**Полная установка (PWA + сервер карт)** — два способа:
**Установка сервера** — два способа:
**1. Скачать релизный пакет (рекомендуется).** На странице
[Releases](https://github.com/anttro/simple/releases) возьмите последний пакет
@@ -25,18 +32,14 @@ SIMple — инструментарий для специалистов по UIC
```sh
./setup.sh # или setup.bat в Windows — создаёт .venv, ставит pysim + сервер
./start.sh # или start.bat — запускает сервер (он же раздаёт PWA)
./start.sh # или start.bat — запускает сервер (он же раздаёт фронтенд)
```
Пакет собран из помеченного тегом исходного кода — без истории git и без файлов для
разработки. Git всё равно нужен: `setup` ставит **pysim** из его upstream-репозитория
(ни pysim, ни его зависимости в пакет не входят).
разработки. Git всё равно нужен: `setup` ставит **pysim** из его upstream-репозитория.
**Обновление быстрое:** `./update.sh` (или `update.bat`) скачивает последний релизный
пакет, заменяет файлы приложения на месте и переустанавливает приложение в существующий
`.venv` — pysim и его зависимости остаются, ничего не пересобирается.
`./update.sh --check` показывает текущую и последнюю версии, `--version X.Y.Z` —
конкретный релиз, `--zip file.zip` — обновление из локального пакета.
**Обновление:** `./update.sh` (или `update.bat`) обновляет пакетную установку на месте
— pysim и его зависимости остаются, ничего не пересобирается.
**2. Клонировать репозиторий (для разработки или ещё не выпущенных изменений).**
@@ -47,7 +50,7 @@ cd simple # `git pull` всё равно
./start.sh # или start.bat
```
Затем откройте http://127.0.0.1:8080 — интерфейс и API на одном origin, поэтому CORS и разрешения браузера не нужны. Сервер запускается и с пустым картридером (отсутствие карты — нормальное состояние): вставьте карту, и она инициализируется автоматически (auto-equip), либо нажмите **Equip card** в интерфейсе.
Затем откройте http://127.0.0.1:8080 — сервер запускается и с пустым картридером (отсутствие карты — нормальное состояние): вставьте карту, и она инициализируется автоматически (auto-equip), либо нажмите **Equip card** в интерфейсе.
## Сборка
@@ -61,7 +64,7 @@ npm run build
## Интерфейс
Семь вкладок верхнего уровня: **Remote APDU**, **SCP80**, **SCP81**, **Cards («Карты»)**, **Profiler**, **Card reader** и **Simulator**. Вкладки Remote APDU и SCP80 используют пиллы-подвкладки; вкладка **SCP81** — пиллы **Listener** и **Scripts**; во вкладке Card reader три подвкладки: **File manager**, **pySim command line** и **Raw APDU**; во вкладке Profiler — **Profiles**, **Card snapshots** и **Custom files**.
Семь вкладок верхнего уровня: **Remote APDU**, **SCP80**, **SCP81**, **Cards («Карты»)**, **Profiler**, **Card reader** и **Simulator**. Вкладки Remote APDU и SCP80 используют кнопки-подвкладки; вкладка **SCP81** — кнопки **Listener** и **Scripts**; во вкладке Card reader три подвкладки: **File manager**, **pySim command line** и **Raw APDU**; во вкладке Profiler — **Profiles**, **Card snapshots** и **Custom files**.
---
@@ -402,7 +405,7 @@ CLA = `80` (GlobalPlatform v2.3.1). Удалённое управление со
## Вкладка SCP80
Вкладка **SCP80** группирует SCP80-виды, переключаемые тремя пиллами: **Secured Packet**, **RAM** и **TAR-сканер**. Сборка защищённых пакетов по ETSI TS 102 225. При подключении карты сервер читает её EF.ICCID (2FE2); если в пресете указан тот же номер (цифрами или сырым hex из EF), он автоматически выбирается во всех трёх видах SCP80.
Вкладка **SCP80** группирует SCP80-виды, переключаемые тремя кнопками: **Secured Packet**, **RAM** и **TAR-сканер**. Сборка защищённых пакетов по ETSI TS 102 225. При подключении карты сервер читает её EF.ICCID (2FE2); если в пресете указан тот же номер (цифрами или сырым hex из EF), он автоматически выбирается во всех трёх видах SCP80.
### Secured Packet
@@ -518,7 +521,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
### TAR-сканер
Третья пилюля вкладки SCP80 показывает, **какие OTA-приложения зарегистрированы на карте**: отправляется по одному
Третья кнопка вкладки SCP80 показывает, **какие OTA-приложения зарегистрированы на карте**: отправляется по одному
защищённому пакету на каждый выбранный TAR — стандартные значения из TS 101 220 V18.3.0 Annex D, Table D.1
(предзаполнены, все отмечены) плюс свои TAR, добавляемые кнопкой **Добавить TAR** и хранимые в `localStorage` — с
безобидной C-APDU (по умолчанию `SELECT MF`), пресетом и keyset этого представления и SPI `16/01` (проверка
@@ -532,7 +535,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
## Карты
Хранит сохранённые конфигурации карт (пресеты) в **серверном хранилище** — `~/.pysim-simple-server/card_presets.json` (переопределяется `--card-presets`). Пресет содержит криптографические ключи, настройки SPI, TAR-ы и счётчики повторов для SCP80-операций, необязательный ключ **ADM**, а также пару **PSK identity / PSK key** для слушателя SCP81 HTTP OTA. Пресет содержит **несколько keysets** — номер keyset задаётся нибблом `b8..b5` его KIc/KID (TS 102 225 §5.1.2/A.2): KIc и KID одного keyset должны нести один и тот же номер (`01`–`0F`; A.2 отвергает несовпадение), `00` («без защиты») не является keyset пресета, и у каждого keyset на карте свой счётчик (Annex A.1) — счётчиками владеет сервер: он сохраняет каждый израсходованный инкремент и пишет его в журнал аудита (`card_presets.json.audit.jsonl`). Список загружается при подключении PWA; экспорт/импорт JSON (принимает и старые плоские пресеты) — путь миграции из браузерного хранилища. «Карты» — **верхнеуровневая вкладка**. Поля формы сгруппированы в два блока с рамкой — **SCP80 (GSM 03.48, ETSI TS 102 225)** и **SCP81 (HTTP OTA)**; необязательное поле **ADM** находится в верхней строке, а keysets редактируются строками (по одной на номер, **Добавить keyset** / **Удалить keyset**). При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается во всех трёх видах SCP80; если подключённая карта не совпадает ни с одним пресетом — или выбранный пресет принадлежит другой карте — все три вида SCP80 показывают жёлтое предупреждение над выбором пресета. В шапке отображаются серые маркеры **SCP80** / **SCP81** и значок ключа на бейдже **ADM**, когда в пресете с ICCID подключённой карты эти поля заполнены.
Хранит сохранённые конфигурации карт (пресеты) в **серверном хранилище** — `~/.pysim-simple-server/card_presets.json` (переопределяется `--card-presets`). Пресет содержит криптографические ключи, настройки SPI, TAR-ы и счётчики повторов для SCP80-операций, необязательный ключ **ADM**, а также пару **PSK identity / PSK key** для слушателя SCP81 HTTP OTA. Пресет содержит **несколько keysets** — номер keyset задаётся нибблом `b8..b5` его KIc/KID (TS 102 225 §5.1.2/A.2): KIc и KID одного keyset должны нести один и тот же номер (`01`–`0F`; A.2 отвергает несовпадение), `00` («без защиты») не является keyset пресета, и у каждого keyset на карте свой счётчик (Annex A.1) — счётчиками владеет сервер: он сохраняет каждый израсходованный инкремент и пишет его в журнал аудита (`card_presets.json.audit.jsonl`). Список загружается при подключении фронтенда; экспорт/импорт JSON (принимает и старые плоские пресеты) — путь миграции из браузерного хранилища. «Карты» — **верхнеуровневая вкладка**. Поля формы сгруппированы в два блока с рамкой — **SCP80 (GSM 03.48, ETSI TS 102 225)** и **SCP81 (HTTP OTA)**; необязательное поле **ADM** находится в верхней строке, а keysets редактируются строками (по одной на номер, **Добавить keyset** / **Удалить keyset**). При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается во всех трёх видах SCP80; если подключённая карта не совпадает ни с одним пресетом — или выбранный пресет принадлежит другой карте — все три вида SCP80 показывают жёлтое предупреждение над выбором пресета. В шапке отображаются серые маркеры **SCP80** / **SCP81** и значок ключа на бейдже **ADM**, когда в пресете с ICCID подключённой карты эти поля заполнены.
| Поле | Описание |
|---|---|
@@ -563,14 +566,14 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
Подключение к встроенному [`pysim-simple-server`](pysim_simple_server/) для работы с картой.
> **Ограничение браузера:** если 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.
> **Ограничение браузера:** если фронтенд раздаётся с публичного 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.
### Файловый менеджер
Дерево файлов UICC. Отображаются имена, FID и AID (для ADF). При выборе файла открывается панель деталей: FID, тип файла, размер / структура записей и декодированный FCI (в блоке с рамкой и именем файла) над панелью содержимого. Когда FCI ссылается на свои правила доступа через запись EF.ARR (атрибут безопасности `8B`), правила указанной записи показываются отступом под этой строкой — они берутся из кэшированных копий EF.ARR карты (MF, DF.TELECOM/DF.GSM и ADF.USIM, по цепочке DF файла; снимок разрешает их из собственной захваченной копии).
- Элементы сгруппированы (DF выше EF) и отсортированы по **FID** или символьному **имени** (пиллы закреплены над деревом вместе с **«Проверить все файлы»**, выбор сохраняется в `localStorage`)
- **Read raw** / **Read decoded** — чтение выбранного файла как hex-дампа или как таблицы декодированных полей (клиентские декодеры: IMSI, ICCID, SPN, списки PLMN, LOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); подсвеченная пилла — текущий вид, клик по любой из них перечитывает файл. таблица декодируется в браузере из тех же сырых байт (без серверного декодирования); **Read decoded** отключается, когда клиентские декодеры не покрывают файл (определяется по имени/FID до чтения)
- Элементы сгруппированы (DF выше EF) и отсортированы по **FID** или символьному **имени** (кнопки закреплены над деревом вместе с **«Проверить все файлы»**, выбор сохраняется в `localStorage`)
- **Read raw** / **Read decoded** — чтение выбранного файла как hex-дампа или как таблицы декодированных полей (клиентские декодеры: IMSI, ICCID, SPN, списки PLMN, LOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); подсвеченная кнопка — текущий вид, клик по любой из них перечитывает файл. таблица декодируется в браузере из тех же сырых байт (без серверного декодирования); **Read decoded** отключается, когда клиентские декодеры не покрывают файл (определяется по имени/FID до чтения)
- **Edit raw** — редактирование сырых hex-данных и **Save** для записи (или **Cancel**); для transparent-файлов — textarea, для record-файлов — по полю на запись с флажками выбора. **Edit raw** сначала переключается на hex и при необходимости читает файл
- **Edit decoded** — редактирование в собственной декодированной форме файла (клиентские кодировщики; запись обычным путём): **EF.SPN**, **EF.PNN**, списки PLMN **PLMNsel / FPLMN / EHPLMN** и **PLMNwAcT / OPLMNwAcT / HPLMNwAcT**, файлы номеров **ADN / FDN / SDN / MSISDN / LND / BDN**, таблицы сервисов **UST / IST / EST** (**SST** только для чтения до появления редактора allocated/activated), **ACC**, **PHASE**, **HPPLMN**, **AD**, **ICCID / IMSI** (с подтверждением) списки cell broadcast **CBMI / CBMID / CBMIR** и **EF.SMSP** (полная форма SMS-параметров: alpha, TP-DA/TS-SCA с их правилами адресации и TON, TP-PID, TP-DCS и относительный TP-VP; у каждого — флажок «отсутствует/по умолчанию» и живые подсказки по DCS и сроку действия). Для transparent-файла — одна форма (строки PLMN и элементы PNN с добавлением/удалением); для record-файла — форма на запись с флажком, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. **Save** проверяет все выбранные записи до записи; указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется. Обзор форматов и план охвата — в `docs/decoded-editing.md`
- **Проверить все файлы** — обход всего дерева (включая пользовательские) с пометкой «есть/нет», прогрессом *N / всего*, возможностью остановки и сводкой в конце; сам просмотр остаётся ленивым. Отсутствующие файлы показаны красным (✗); существующий пустой DF — `(пусто)`
@@ -593,7 +596,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
- **Путь** — от `MF` (напр. `MF/7F10/6F3A`) или от AID ADF (напр. `A0000000871002/6F07`).
- **Проверка FCP/FCI** — **Только тип файла (FCP)**, **Тип файла + размер (FCP)** (добавляет размер файла или длину/число записей) либо **Полный FCI** (побайтовое сравнение сырого шаблона FCP `'62'` из ответа SELECT — ловит изменения FID/AID, life-cycle, security attributes и проприетарных параметров).
- **Правила доступа** (необязательно) — проверяет условия доступа файла как декодированные правила: ссылка `8B` в FCI разрешается по копиям EF.ARR карты (или снимка), а декодированные строки сравниваются как множество — независимо от номера записи, поэтому карты с другой раскладкой EF.ARR всё равно проверяются (отсутствующие/лишние строки перечисляются; потерянная ссылка — ошибка; неразрешимая — непроверяемо). Заполняется автоматически при создании профиля с карты или из снимка; редактор показывает каждое правило структурированной строкой (биты операции / RESIZE FILE / INCREASE плюс селекторы условий безопасности, с предпросмотром канонической строки; `+ правило` добавляет, шаблонные формы остаются только для чтения), а блок сворачивается, «Нет» скрывает его. При **Полном FCI** флажок *«Игнорировать ссылку на EF.ARR в точном FCI»* сравнивает FCI без значения `8B` — другой номер записи больше не приводит к несовпадению, а смысл ссылки проверяют правила.
- **Правила доступа** (необязательно) — проверяет условия доступа файла как декодированные правила: ссылка `8B` в FCI разрешается по копиям EF.ARR карты (или снимка), а декодированные строки сравниваются как множество — независимо от номера записи, поэтому карты с другой раскладкой EF.ARR всё равно проверяются (отсутствующие/лишние строки перечисляются; потерянная ссылка — ошибка; неразрешимая — непроверяемо). Заполняется автоматически при создании профиля с карты или из снимка; редактор показывает каждое правило структурированной строкой (биты операции / RESIZE FILE / INCREASE плюс селекторы условий безопасности, с предпросмотром канонической строки; `+ правило` добавляет, шаблонные формы остаются только для чтения), а блок сворачивается, «Нет» скрывает его. При **Полном FCI** флажок *«Игнорировать ссылку на EF.ARR в точном FCI»* сравнивает FCI без значения `8B` — другой номер записи не приводит к несовпадению, а смысл ссылки проверяют правила.
- **Атрибуты файла** — тип, размер, длина и число записей из шаблона FCP (любое можно не задавать).
- **Проверка содержимого** (опционально) — **Exact** (точное равенство hex) или **Mask**, где `?` — пониббловый джокер (маска без `?` — префиксное совпадение, напр. `0891` для MCC/MNC IMSI). Для record-файлов хранится список записей.
@@ -625,7 +628,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
2. Введите 4-hex **FID** и псевдоним (`EF.…`/`DF.…`; префикс определяет, EF это или DF)
3. Нажмите **Add** — файл появится в дереве файлового менеджера; кнопка **Edit** загружает запись в форму (кнопка становится **Save**, **Cancel** отменяет), **Delete** удаляет запись (удаление DF удаляет и дочерние записи после подтверждения)
Канонический путь убирает прежнюю неоднозначность, когда один и тот же файл можно было описать и относительно, и абсолютно. Пользовательские файлы сохраняются в `localStorage` между сессиями и включаются в сканирование карты с той же проверкой существования. Экспорт/импорт в JSON для обмена; устаревшие относительные пути (напр. `a153/4954`) разрешаются при загрузке, неразрешимые отбрасываются и показываются в списке.
Путь всегда канонический (от MF или ADF) — один и тот же файл никогда не описывается и относительно, и абсолютно. Пользовательские файлы сохраняются в `localStorage` между сессиями и включаются в сканирование карты с той же проверкой существования. Экспорт/импорт в JSON для обмена; устаревшие относительные пути (напр. `a153/4954`) разрешаются при загрузке, неразрешимые отбрасываются и показываются в списке.
## Симулятор
@@ -715,36 +718,29 @@ SIMple — Progressive Web App. Можно установить для offline-
## Совместимость версий
| PWA (SIMple) | Сервер | Статус |
| фронтенд (SIMple) | Сервер | Статус |
|---|---|---|
| любая | та же мажорная | ✅ Совместимы |
| любая | старее мажорная | ❌ Сервер устарел — обновите сервер |
| любая | новее мажорная | ⚠️ Сервер новее — обновите PWA |
| любая | новее мажорная | ⚠️ Сервер новее — обновите фронтенд |
PWA проверяет версию сервера при подключении через `GET /api/version` и сравнивает мажорную версию (например, PWA 3.x с сервером 3.x; сервер 2.x помечается как устаревший).
фронтенд проверяет версию сервера при подключении через `GET /api/version` и сравнивает мажорную версию (например, фронтенд 3.x с сервером 3.x; сервер 2.x помечается как устаревший).
---
## Сервер (pysim-simple-server)
Встроенный Python-сервер оборачивает [pySim](https://osmocom.org/projects/pysim/wiki) и раздаёт как PWA (из `frontend/`), так и JSON API по `/api/*`.
### Требования
- **Python 3.9+** с `pip`, и **Git** — приложение работает на 3.9+, но **SCP81 TLS-PSK-слушателю нужен Python 3.13+** (PSK-callback'и `ssl`).
- **Поддержка venv в Python** — `setup.sh` создаёт `.venv`; если создание завершается с `ensurepip is not available` (в Debian/Ubuntu модуль venv поставляется отдельно), установите его (`sudo apt install python3-venv`) и запустите `./setup.sh` снова. Неполный `.venv`, оставшийся от неудачного запуска, удаляется автоматически.
- **Смарт-картридер** (PC/SC или serial/FTDI) — предпочтителен PC/SC (`pcsc-lite` + `ccid` на Linux). У `pyscard` нет Linux-wheel, поэтому он собирается из исходников: поставьте и пакет с заголовками PC/SC (`libpcsclite-dev` в Debian/Ubuntu, `pcsc-lite-devel` в Fedora; в Arch они входят в `pcsclite`).
- **Windows** — используйте **Python 3.13 (настоятельно рекомендуется)**: у `pyscard` есть готовые wheel до 3.13, и SCP81 TLS-PSK-слушателю нужен 3.13+. На 3.14 wheel для `pyscard` и части зависимостей pysim отсутствуют, они собираются из исходников (нужны MSVC C++ Build Tools). 3.9–3.12 запускают приложение, но не SCP81 PSK-слушатель. SMPP-мост (`smpp.twisted3`) в Windows сознательно не ставится.
Встроенный Python-сервер оборачивает [pySim](https://osmocom.org/projects/pysim/wiki) и раздаёт как фронтенд (из `frontend/`), так и JSON API по `/api/*`.
### Скрипты
| Скрипт | Назначение |
|--------|-------------|
| `setup.sh` / `setup.bat` | Создаёт `.venv/` (или исправляет неполный), ставит pysim и сервер. Запускать один раз после клонирования или распаковки релизного пакета. |
| `start.sh` / `start.bat` | Запускает сервер из venv (раздаёт PWA + API на `:8080`). |
| `start.sh` / `start.bat` | Запускает сервер из venv (раздаёт фронтенд + API на `:8080`). |
| `update.sh` / `update.bat` | Обновляет пакетную установку на месте: скачивает последний релизный пакет, заменяет файлы приложения и переустанавливает приложение в существующий `.venv` (pysim сохраняется). `--check`, `--version X.Y.Z`, `--zip FILE`, `--no-pip`, `--force`. |
`start.sh` перед запуском проверяет доступ к PC/SC: если пользователь не может использовать `pcscd` (или служба не запущена), он печатает причину и рекомендации (группа/polkit) и завершается — сервер **больше не откатывается на последовательный ридер молча**. При наличии доступа сервер сам находит первый PC/SC-ридер; `start.bat` всегда передаёт `-p 0` (в Windows PC/SC встроен). Явный serial/FTDI-ридер по-прежнему возможен через `-d <устройство>` (проверка пропускается). Дополнительные аргументы передаются серверу, например `./start.sh --gsmtap` или `start.bat --gsmtap 10.0.0.5:4729`. Если PC/SC-ридер не найден, сервер сообщает об этом и завершается — подключите ридер и перезапустите (ридер без карты — по-прежнему нормальный старт без карты).
`start.sh` перед запуском проверяет доступ к PC/SC: если пользователь не может использовать `pcscd` (или служба не запущена), он печатает причину и рекомендации (группа/polkit) и завершается — сервер **не откатывается на последовательный ридер молча**. При наличии доступа сервер сам находит первый PC/SC-ридер; `start.bat` всегда передаёт `-p 0` (в Windows PC/SC встроен). Явный serial/FTDI-ридер по-прежнему возможен через `-d <устройство>` (проверка пропускается). Дополнительные аргументы передаются серверу, например `./start.sh --gsmtap` или `start.bat --gsmtap 10.0.0.5:4729`. Если PC/SC-ридер не найден, сервер сообщает об этом и завершается — подключите ридер и перезапустите (ридер без карты — по-прежнему нормальный старт без карты).
### Ручная установка (Linux/macOS)
@@ -757,8 +753,8 @@ pysim-simple-server --http-port 8080
```
В Windows используйте `setup.bat`: он ставит зависимости pysim из
`requirements-pysim.txt`, а сам pysim — с `--no-deps`, поэтому SMPP-мост (и его сборка
через MSVC) не подтягивается.
`requirements-pysim.txt`, а сам pysim — с `--no-deps`, поэтому необязательные
зависимости pysim не подтягиваются.
### Параметры CLI
@@ -766,7 +762,7 @@ pysim-simple-server --http-port 8080
|----------|-------------|
| `--http-host` | Адрес привязки (по умолчанию `127.0.0.1`) |
| `--http-port` | Порт (по умолчанию `8080`) |
| `--web-dir` | Каталог со статикой PWA (по умолчанию `<repo>/frontend`) |
| `--web-dir` | Каталог со статикой фронтенда (по умолчанию `<repo>/frontend`) |
| `-p` / `--pcsc-device` | Номер слота PC/SC |
| `-d` / `--device` | Путь к serial-устройству |
| `--no-card-init` | Пропустить инициализацию карты (сохранить CAT-сессию) |
@@ -797,15 +793,30 @@ pysim-simple-server --http-port 8080
```
затем `sudo systemctl restart polkit` (или выйдите и войдите). При необходимости запустите службу (`sudo systemctl enable --now pcscd`); `start.sh` и сервер сообщают об этом и завершаются вместо отката на serial-ридер, а `pcsc_scan` показывает ту же ошибку, пока правило не начнёт действовать. Если `pcscd` собран без PolicyKit и сокет принадлежит отдельной группе, вместо этого можно добавить пользователя в неё.
- **"device file /dev/ttyUSB0 does not exist"** — только при явном `-d <устройство>`: устройство не подключено (или путь неверен). Без `-d` сервер больше не использует последовательный транспорт.
- **"device file /dev/ttyUSB0 does not exist"** — только при явном `-d <устройство>`: устройство не подключено (или путь неверен). Без `-d` сервер использует только транспорт PC/SC.
- **"no PC/SC reader found"** — подключите ридер (`pcsc_scan` покажет список) и перезапустите; сервер завершается, а не стартует без ридера.
- **"Service not available" (0x8010001D) / ридер пропал после перезапуска `pcscd`** — сервер восстанавливается сам: сторож перезапускает монитор присутствия pyscard, а следующий equip (автоматически после повторной вставки карты или кнопкой **Equip**) пересоздаёт транспорт PC/SC (сначала освобождая старый линк). Перезапуск сервера не нужен.
- **Ошибки "Card was removed" / equip не срабатывает после смены карты** — вставка карты помечает соединение PC/SC для пересоздания (поэтому **первая** попытка уже использует свежее соединение — в Windows старое после смены карты подключиться не может), авто-equip повторяется до 3 раз, и **каждая неудачная попытка пересоздаёт транспорт** перед следующей. Карта, снятая во время инициализации при старте сервера, оставляет его без карты с одной строкой (без трассировки), а следующая вставка инициализируется обычным образом. Проглоченная ошибка инициализации (с трассировкой) теперь печатается как `AUTO-EQUIP: init output:`, а «карта есть, но карта не создана» — это ошибка, а не тихое «карта пропала». Сторож повторяет с удвоением задержки (5 с … 60 с), пока карта в ридере; снятие карты полностью очищает состояние, поэтому обработчики отвечают «нет карты», а не шлют APDU мёртвой карте.
- **Ошибки "Card was removed" / equip не срабатывает после смены карты** — вставка карты помечает соединение PC/SC для пересоздания (поэтому **первая** попытка уже использует свежее соединение — в Windows старое после смены карты подключиться не может), авто-equip повторяется до 3 раз, и **каждая неудачная попытка пересоздаёт транспорт** перед следующей. Карта, снятая во время инициализации при старте сервера, оставляет его без карты с одной строкой (без трассировки), а следующая вставка инициализируется обычным образом. Неудачная инициализация печатается как `AUTO-EQUIP: init output:` (с трассировкой), а карта, которая есть в ридере, но не инициализировалась, — это ошибка, а не тихое «карта пропала». Сторож повторяет с удвоением задержки (5 с … 60 с), пока карта в ридере; снятие карты полностью очищает состояние, поэтому обработчики отвечают «нет карты», а не шлют APDU мёртвой карте.
### Справочник API
Полный справочник endpoints: [docs/api.md](docs/api.md).
## Связанные инструменты
SIMple, SIMtrace Analyser и sigrok_iso7816_stream — три автономных
инструмента для работы с SIM/UICC-картами, представленные на одном
[сайте](https://atroshin.ru) и в одном [Telegram-канале](https://t.me/youeyeseesee).
Каждый полностью независим — устанавливайте только нужные вам:
- **[SIMple](https://github.com/anttro/simple)** — мастерская для UICC/SIM:
конструкторы APDU, OTA SCP80/SCP81, симуляция SIM Toolkit и профилирование
карт.
- **[SIMtrace Analyser](https://github.com/anttro/simtrace-analyser)** —
анализатор APDU-трассировок GSMTAP-SIM.
- **[sigrok_iso7816_stream](https://github.com/anttro/sigrok_iso7816_stream)** —
пассивный ISO 7816-сниффер на дешёвом логическом анализаторе FX2.
## Лицензия
**GPL-2.0-or-later** — см. [LICENSE](LICENSE). Это та же лицензия, что у
@@ -816,5 +827,5 @@ pysim-simple-server --http-port 8080
Сам pySim не входит ни в этот репозиторий, ни в релизные артефакты: `setup.sh` /
`setup.bat` устанавливают его из его upstream git-репозитория.
Встроенные сторонние компоненты (список операторов MCC/MNC и AES/DES-бандлы в
PWA) сохраняют свои лицензии MIT — см. [THIRD-PARTY-NOTICES](THIRD-PARTY-NOTICES).
Встроенные сторонние компоненты (список операторов MCC/MNC и AES/DES-бандлы во
фронтенде) сохраняют свои лицензии MIT — см. [THIRD-PARTY-NOTICES](THIRD-PARTY-NOTICES).
+3 -3
View File
@@ -46,7 +46,7 @@
<li>Выбор языка и темы хранится в <code class="font-mono text-sm">localStorage</code> и сохраняется между перезагрузками.</li>
<li>Вкладки верхнего уровня: <strong>Remote APDU</strong> (<strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>RAM/GP</strong>, <strong>Expanded Script</strong>, <strong>&laquo;Пуш/триггер&raquo;</strong>, <strong>&laquo;Разбор&raquo;</strong> (<strong>«Разбор C-APDU»</strong>, <strong>«Разбор R-APDU»</strong>)), <strong>SCP80</strong> (<strong>Secured Packet</strong>, <strong>RAM</strong>, <strong>TAR-сканер</strong>), <strong>SCP81</strong> (<strong>&laquo;Слушатель&raquo;</strong>, <strong>&laquo;Скрипты&raquo;</strong>), <strong>&laquo;Карты&raquo;</strong>, <strong>&laquo;Профайлер&raquo;</strong> (вкладки <strong>&laquo;Профили&raquo;</strong>, <strong>&laquo;Снимки карт&raquo;</strong>, <strong>&laquo;Пользовательские файлы&raquo;</strong>), <strong>&laquo;Картридер&raquo;</strong> (<strong>Файловый менеджер</strong>, <strong>Командная строка pySim</strong>, <strong>Отправка APDU</strong>) и <strong>&laquo;Симулятор&raquo;</strong>.</li>
<li>Ссылка <strong>справка</strong> открывает эту документацию на разделе, соответствующем текущему представлению (например, вкладка &laquo;Профайлер&raquo; открывает &sect;7).</li>
<li>Кнопки браузера <strong>«Назад»</strong> / <strong>«Вперёд»</strong> проходят по собственному стеку представлений приложения &mdash; каждое переключение вкладки, пиллы и списка/редактора записывается (только на время сессии, URL не меняется); встроенные кнопки <em>«Отмена»</em>/<em>«Назад»</em> заменяют текущую запись, а не добавляют новую.</li>
<li>Кнопки браузера <strong>«Назад»</strong> / <strong>«Вперёд»</strong> проходят по собственному стеку представлений приложения &mdash; каждое переключение вкладки, кнопки и списка/редактора записывается (только на время сессии, URL не меняется); встроенные кнопки <em>«Отмена»</em>/<em>«Назад»</em> заменяют текущую запись, а не добавляют новую.</li>
</ul>
@@ -347,7 +347,7 @@
</section>
<section class="mb-10">
<h3 id="tar-probe" class="text-lg font-medium mb-2">3.3 TAR-сканер</h3>
<p class="mb-2">Третья пилюля вкладки SCP80 показывает, <strong>какие OTA-приложения зарегистрированы на карте</strong>: отправляется <strong>по одному защищённому пакету на каждый выбранный TAR</strong> с безобидной C-APDU (по умолчанию <code class="font-mono text-sm">SELECT MF</code>). v3.26.0: строки — это список TAR выбранного пресета (никаких произвольных TAR), каждая строка использует свой <strong>MSL</strong> и свой <strong>keyset по умолчанию</strong> (разрешается из хранилища пресета — селектор keyset у сканера убран), а TAR, чей MSL требует защиты и у которого нет keyset по умолчанию, отклоняется до отправки. Разрешённый SPI1 должен иметь проверку счётчика (b5b4 &ne; 00) и запрашивать PoR. Кнопка <strong>Execute</strong> недоступна, пока не выбран пресет карты.</p>
<p class="mb-2">Третья кнопка вкладки SCP80 показывает, <strong>какие OTA-приложения зарегистрированы на карте</strong>: отправляется <strong>по одному защищённому пакету на каждый выбранный TAR</strong> с безобидной C-APDU (по умолчанию <code class="font-mono text-sm">SELECT MF</code>). v3.26.0: строки — это список TAR выбранного пресета (никаких произвольных TAR), каждая строка использует свой <strong>MSL</strong> и свой <strong>keyset по умолчанию</strong> (разрешается из хранилища пресета — селектор keyset у сканера убран), а TAR, чей MSL требует защиты и у которого нет keyset по умолчанию, отклоняется до отправки. Разрешённый SPI1 должен иметь проверку счётчика (b5b4 &ne; 00) и запрашивать PoR. Кнопка <strong>Execute</strong> недоступна, пока не выбран пресет карты.</p>
<p class="mb-2">Список показывает записи TAR выбранного пресета (все отмечены; кнопки <strong>Выбрать все</strong> / <strong>Снять все</strong> переключают быстро); подсказка строки называет её MSL и keyset по умолчанию.</p>
<p class="mb-2">Каждый TAR классифицируется по ответу карты: <strong>зарегистрировано</strong> (ENVELOPE 9000 + PoR со SW и данными приложения), <strong>принято, нет ответа</strong> (PoR без R-APDU), <strong>принято, нет PoR</strong> (пакет молча отброшен) и <strong>отклонено</strong> (ENVELOPE не 9000, обычно <code class="font-mono text-sm">6200</code> для TAR без зарегистрированного приложения). Таблица результата появляется в окне операции; строка под кнопкой Execute показывает <code class="font-mono text-sm">зарегистрировано N / M</code>.</p>
<p class="mb-2">Каждый принятый картой пакет <strong>расходует счётчик</strong>, который сохраняется в keyset пресета &mdash; проба отклоняется, если в разрешённом SPI1 нет проверки счётчика, если в пресете нет keyset по умолчанию для строки (при ненулевом MSL он обязателен) или запись называет несуществующий номер keyset. На живой карте: <code class="font-mono text-sm">000000</code>, <code class="font-mono text-sm">B00000</code> и <code class="font-mono text-sm">B00001</code> зарегистрированы (приложения компактного формата), расширенный формат, USAT, мультиплексирование и CASD отклонены с <code class="font-mono text-sm">6200</code>.</p>
@@ -405,7 +405,7 @@
<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>
<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 — в рамке с символьным именем файла. Если FCI ссылается на свои правила доступа через запись <code class="font-mono text-sm">EF.ARR</code> (атрибут безопасности <code class="font-mono text-sm">8B</code>), правила указанной записи показываются с отступом под этой строкой: они берутся из копий <code class="font-mono text-sm">EF.ARR</code> карты &mdash; копии MF (<code class="font-mono text-sm">2F06</code>), копий DF.TELECOM/DF.GSM (<code class="font-mono text-sm">6F06</code>) и копии ADF.USIM (<code class="font-mono text-sm">6F06</code>): читаются один раз за сессию при equip, обновляются при сбросе/equip и после записи <code class="font-mono text-sm">EF.ARR</code>; снимок разрешает те же ссылки из собственной захваченной копии. Побеждает копия собственного DF файла, с подъёмом до MF; ссылка, для которой в этой цепочке копии нет, остаётся неразрешённой, если только кандидат не единственный.</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 — в рамке с символьным именем файла. Если FCI ссылается на свои правила доступа через запись <code class="font-mono text-sm">EF.ARR</code> (атрибут безопасности <code class="font-mono text-sm">8B</code>), правила указанной записи показываются с отступом под этой строкой: они берутся из копий <code class="font-mono text-sm">EF.ARR</code> карты &mdash; копии MF (<code class="font-mono text-sm">2F06</code>), копий DF.TELECOM/DF.GSM (<code class="font-mono text-sm">6F06</code>) и копии ADF.USIM (<code class="font-mono text-sm">6F06</code>): читаются один раз за сессию при equip, обновляются при сбросе/equip и после записи <code class="font-mono text-sm">EF.ARR</code>; снимок разрешает те же ссылки из собственной захваченной копии. Побеждает копия собственного DF файла, с подъёмом до MF; ссылка, для которой в этой цепочке копии нет, остаётся неразрешённой, если только кандидат не единственный.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Читать сырые данные / Декодировать</strong> — чтение выбранного файла в виде hex-дампа или таблицы декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); таблица декодируется в браузере из тех же сырых байт (без серверного декодирования). Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл. «Декодировать» недоступна, если для файла нет клиентского декодера (проверка по имени/FID до чтения).</li>
<li><strong>Редактировать сырые данные</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.</li>
+27 -14
View File
@@ -1104,9 +1104,9 @@
<div id="pysim-body" class="hidden">
<div class="flex flex-wrap justify-center gap-1 mb-2">
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-blue-600 text-white" data-pysim-sub="files" data-l10n="File manager">File manager</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600 disabled:opacity-40 disabled:cursor-not-allowed" data-pysim-sub="services" data-needs="card" data-l10n="Services">Services</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="cmd" data-l10n="pySim command line">pySim command line</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="apdu" data-l10n="Raw APDU">Raw APDU</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600 disabled:opacity-40 disabled:cursor-not-allowed" data-pysim-sub="services" data-needs="card" data-l10n="Services">Services</button>
</div>
<div id="pysim-sub-files">
@@ -1127,6 +1127,7 @@
<button type="button" id="pysim-fs-filter-btn" aria-haspopup="true" aria-expanded="false" onclick="pysimFsFilterToggle()" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 max-w-xs truncate">All</button>
<div id="pysim-fs-filter-menu" role="group" aria-label="Filter" class="hidden absolute left-0 mt-1 z-50 w-64 max-h-64 overflow-y-auto rounded border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-800 p-2 shadow-lg"></div>
</div>
<button type="button" id="pysim-fs-filter-clear" onclick="pysimFsFilterClear()" class="hidden px-1 text-xs text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300" data-l10n-title="Reset filter" aria-label="Reset filter">✕</button>
</div>
<button id="pysim-fs-probe-btn" data-needs="card" onclick="pysimFsProbeAll()" class="w-full mb-1 px-2 py-0.5 rounded bg-gray-600 text-white hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Probe all files">Probe all files</button>
<div id="pysim-fs-probe-status" class="hidden mb-1 text-gray-500 dark:text-slate-400"></div>
@@ -1989,7 +1990,7 @@
// ===== 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 = '3.40.0';
const SIMPLE_VERSION = '3.40.3';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
@@ -14314,6 +14315,11 @@ async function cardsFetch() {
cardsRebuildSelect();
cardsRender();
tarRender();
// A card may already be equipped when the presets arrive (the server
// starts with a card, or the fetch simply raced the first status poll):
// run the auto-selection now that the store is loaded. It is a no-op
// once the card's preset is already selected (the latched ICCID guard).
cardsAutoSelectByIccid(_pysimCardIccid);
scp81PushPskMap();
spPresetWarningRender();
return true;
@@ -15208,12 +15214,16 @@ let _cardsAutoIccid = null;
function cardsAutoSelectByIccid(iccid) {
// Act only when the equip reports a *new* ICCID: a manual preset choice
// for the same card must not be overridden by later status updates.
// The guard is latched only once the card's preset is actually found and
// selected - a miss (the preset store not loaded yet, or no preset for
// this card) is retried when the store arrives (cardsFetch) so a card
// equipped before the presets loaded still gets auto-selected.
const norm = cardsNormIccid(iccid);
if (!norm) { _cardsAutoIccid = null; return -1; }
if (norm === _cardsAutoIccid) return -1;
_cardsAutoIccid = norm;
const idx = cardsFindByIccid(norm);
if (idx < 0) return -1;
_cardsAutoIccid = norm;
const sp = document.getElementById('sp-card-sel');
if (sp) sp.value = String(idx);
const ram = document.getElementById('ram-card-sel');
@@ -16094,8 +16104,8 @@ function pysimFsSetSort(mode) {
// can belong to several criteria (EF.SPN is a provider name and the file of
// the "Service Provider Name" service). `all` is the exclusive default.
// The categories are shown under three headings (`group`): the subscriber
// data, the network-selection data, and services (the SMS files plus one
// option per file-backed feature of the service cross-reference).
// data, the network-selection data, and services (one option per file-backed
// feature of the service cross-reference).
const FS_FILTER_ALL = 'all';
// "Custom files" is not a name-based criterion: it matches the injected
// custom-file nodes by their `custom` flag (user-chosen names).
@@ -16111,12 +16121,6 @@ const FS_FILTER_GROUPS = [
{ id: 'wlan', label: 'WLAN / I-WLAN', group: 'PLMN', files: [
'EF.UPLMNWLAN', 'EF.OPLMNWLAN', 'EF.WRI', 'EF.HWSIDL', 'EF.WEHPLMNPI', 'EF.WHPI',
'EF.WLRPLMN', 'EF.HPLMNDAI'] },
// EF.PSISMSC is a standard name pySim does not model, so it has no tree
// node to match - it stays listed for completeness.
{ id: 'sms', label: 'SMS-related', group: 'Services', files: [
'EF.SMS', 'EF.SMSS', 'EF.SMSP', 'EF.SMSR', 'EF.PSISMSC'] },
{ id: 'cb', label: 'CB-related', group: 'Services', files: [
'EF.CBMI', 'EF.CBMID', 'EF.CBMIR'] },
];
const FS_FILTER_GROUP_ORDER = ['Identifiers', 'PLMN', 'Services'];
@@ -16276,6 +16280,7 @@ function pysimFsFilterRender() {
const menu = document.getElementById('pysim-fs-filter-menu');
const btn = document.getElementById('pysim-fs-filter-btn');
if (!menu) return;
const active = pysimFsFilter.size > 0 && !pysimFsFilter.has(FS_FILTER_ALL);
const services = fsFilterServiceOptions();
const item = (id, label) =>
'<label class="flex items-center gap-2 px-2 py-0.5 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-slate-700 text-xs text-gray-700 dark:text-slate-200">' +
@@ -16302,7 +16307,6 @@ function pysimFsFilterRender() {
if (btn) {
btn.textContent = pysimFsFilterSummary();
// a non-"All" selection stands out (blue, like the active sort pill)
const active = pysimFsFilter.size > 0 && !pysimFsFilter.has(FS_FILTER_ALL);
btn.classList.toggle('bg-blue-600', active);
btn.classList.toggle('text-white', active);
btn.classList.toggle('text-gray-700', !active);
@@ -16310,6 +16314,11 @@ function pysimFsFilterRender() {
btn.classList.toggle('hover:bg-gray-200', !active);
btn.classList.toggle('dark:hover:bg-slate-700', !active);
}
const clearBtn = document.getElementById('pysim-fs-filter-clear');
if (clearBtn) {
clearBtn.classList.toggle('hidden', !active);
clearBtn.setAttribute('aria-label', t('Reset filter'));
}
}
// Expand or collapse the menu (`force` overrides the toggle; the outside
@@ -16345,6 +16354,11 @@ function pysimFsFilterSetEntry(entry, on) {
pysimFsAutoExpandForFilter();
}
// The ✕ next to the filter selector: back to "All".
function pysimFsFilterClear() {
pysimFsFilterSetEntry(FS_FILTER_ALL, true);
}
// The cross-reference inverted to service number -> feature name, per file
// table. Both maps are declared later in this file, so they are read on the
// first call, never while the filter block itself is evaluated.
@@ -28106,13 +28120,12 @@ const LANG_RU = {
'Sort:': 'Сортировка:',
'Name': 'Имя',
'Filter:': 'Фильтр:',
'Reset filter': 'Сбросить фильтр',
'(no filters yet)': '(фильтров пока нет)',
'Identifiers': 'Идентификаторы',
'PLMN selection': 'Выбор PLMN',
'Provider names': 'Имена операторов',
'WLAN / I-WLAN': 'WLAN / I-WLAN',
'SMS-related': 'Связанное с SMS',
'CB-related': 'Связанное с CB',
'Custom files': 'Пользовательские файлы',
'Selected: {n}': 'Выбрано: {n}',
'Expand all': 'Развернуть все',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v485';
const CACHE = 'simple-v489';
const URLS = [
'index.html',
'help.html',
+31
View File
@@ -146,6 +146,30 @@ test('cardsAutoSelectByIccid reacts to a card swap and to unreadable ICCIDs', ()
assert.strictEqual(cardsAutoSelectByIccid('1234567890123456789'), -1);
});
test('a miss is not latched, so the store arriving later still auto-selects', () => {
// The card equips before /api/presets fills cards[] (the connect-time
// race): the auto-selection must not latch the ICCID on a miss, or the
// later cardsFetch would be a no-op and the preset would stay unselected
// until the card is re-inserted.
const els = fakeDoc();
const applied = [];
globalThis.cardsApply = idx => applied.push(idx);
globalThis.cards = []; // presets not loaded yet
globalThis._ramCardIdx = null;
globalThis._cardsAutoIccid = null;
assert.strictEqual(cardsAutoSelectByIccid(DIGITS), -1);
assert.strictEqual(globalThis._cardsAutoIccid, null,
'a missed auto-selection must not latch the ICCID');
// the presets arrive: the retry (from cardsFetch) finds and selects the card
globalThis.cards = [{ name: 'A', iccid: RAW_HEX }];
assert.strictEqual(cardsAutoSelectByIccid(DIGITS), 0);
assert.strictEqual(els['sp-card-sel'].value, '0');
assert.strictEqual(els['ram-card-sel'].value, '0');
assert.strictEqual(els['tar-card-sel'].value, '0');
assert.deepStrictEqual(applied, ['0']);
assert.strictEqual(globalThis._ramCardIdx, 0);
});
test('cardsFindDuplicateIccid catches duplicates across stored formats', () => {
globalThis.cards = [
{ name: 'A', iccid: '1111111111111111111' },
@@ -201,6 +225,13 @@ test('the card-state update wires the ICCID into the preset selection', () => {
assert.ok(html.includes("(data.iccid ? ' | ICCID: <b>' + esc(data.iccid) + '</b>' : '')"));
});
test('cardsFetch retries the auto-selection once the presets load', () => {
// The equip may be reported before /api/presets resolves, so the fetch
// must re-run the auto-selection (a no-op once the card is selected).
assert.ok(html.includes('cardsAutoSelectByIccid(_pysimCardIccid);'),
'cardsFetch must re-run the auto-selection after loading the store');
});
test('cardsRender highlights the equipped card preset', () => {
globalThis.t = s => s;
const els = { 'cards-tbody': { innerHTML: '' } };
+56 -33
View File
@@ -111,10 +111,11 @@ function fakeMenu() {
};
}
const dom = { menu: null, btn: null };
const dom = { menu: null, btn: null, clear: null };
globalThis.document = {
getElementById: id => (id === 'pysim-fs-filter-menu' ? dom.menu
: (id === 'pysim-fs-filter-btn' ? dom.btn : null)),
: (id === 'pysim-fs-filter-btn' ? dom.btn
: (id === 'pysim-fs-filter-clear' ? dom.clear : null))),
};
let code = '';
@@ -123,7 +124,7 @@ for (const f of ['fsFilterServiceOptions', 'fsFilterIndexBuild', 'fsFilterIndex'
'fsFilterPathsKeyOf', 'pysimFsFilterPathsInvalidate', 'pysimFsFilterMatchingPaths',
'pysimFsFilterMatch', 'pysimFsFilterShow', 'pysimFsFilterLabel',
'pysimFsFilterSummary', 'pysimFsFilterRender', 'pysimFsFilterToggle',
'pysimFsFilterSetEntry', 'pysimFsFilterRestore', 'pysimFsNodePath',
'pysimFsFilterSetEntry', 'pysimFsFilterClear', 'pysimFsFilterRestore', 'pysimFsNodePath',
'pysimFsExpandAll', 'pysimFsCollapseAll', 'pysimFsAutoExpandForFilter']) {
code += extractFunc(html, f) + '\n';
}
@@ -161,6 +162,31 @@ test('the file tree has a collapsed filter dropdown next to the sort pills', ()
assert.ok(!/pysimFsSetFilter\(/.test(html));
});
test('the ✕ next to the selector resets a non-All filter to All', () => {
// markup: a hidden red ✕ that clears the filter
assert.match(html, /id="pysim-fs-filter-clear"[^>]*onclick="pysimFsFilterClear\(\)"/);
assert.match(html, /id="pysim-fs-filter-clear"[^>]*data-l10n-title="Reset filter"/);
// the clear handler resets to All
const fn = extractFunc(html, 'pysimFsFilterClear');
assert.ok(fn.indexOf('pysimFsFilterSetEntry(FS_FILTER_ALL, true)') > 0, fn);
// the render shows it only when a non-All filter is active
dom.menu = fakeMenu();
dom.btn = fakeBtn();
dom.clear = fakeBtn();
withFilter(['plmn-selection'], () => {
pysimFsFilterRender();
assert.strictEqual(dom.clear.classes.has('hidden'), false);
assert.strictEqual(dom.clear.attrs['aria-label'], 'Reset filter');
});
withFilter(['all'], () => {
pysimFsFilterRender();
assert.strictEqual(dom.clear.classes.has('hidden'), true);
});
// the Russian entry exists
const dict = html.match(/const LANG_RU = \{([\s\S]*?)\n\};/)[1];
assert.ok(dict.includes("'Reset filter':"), 'missing Russian entry');
});
test('an outside click or Escape closes the menu', () => {
const click = html.match(/document\.addEventListener\('click', e => \{([\s\S]{0,600}?)\n\}\);/);
assert.ok(click, 'no document click listener');
@@ -173,9 +199,9 @@ test('an outside click or Escape closes the menu', () => {
assert.match(key[1], /pysimFsFilterToggle\(false\)/);
});
test('the six category groups name their files', () => {
test('the four category groups name their files', () => {
assert.deepStrictEqual(FS_FILTER_GROUPS.map(g => g.id),
['identifiers', 'plmn-selection', 'provider-names', 'wlan', 'sms', 'cb']);
['identifiers', 'plmn-selection', 'provider-names', 'wlan']);
const files = id => new Set(FS_FILTER_GROUPS.find(g => g.id === id).files);
assert.deepStrictEqual([...files('identifiers')],
['EF.ICCID', 'EF.IMSI', 'EF.MSISDN', 'EF.IMPI', 'EF.IMPU', 'EF.SUPI_NAI', 'EF.GID1', 'EF.GID2']);
@@ -187,8 +213,6 @@ test('the six category groups name their files', () => {
assert.deepStrictEqual([...files('wlan')],
['EF.UPLMNWLAN', 'EF.OPLMNWLAN', 'EF.WRI', 'EF.HWSIDL', 'EF.WEHPLMNPI', 'EF.WHPI',
'EF.WLRPLMN', 'EF.HPLMNDAI']);
assert.deepStrictEqual([...files('sms')], ['EF.SMS', 'EF.SMSS', 'EF.SMSP', 'EF.SMSR', 'EF.PSISMSC']);
assert.deepStrictEqual([...files('cb')], ['EF.CBMI', 'EF.CBMID', 'EF.CBMIR']);
assert.strictEqual(FS_FILTER_ALL, 'all');
assert.strictEqual(FS_FILTER_CUSTOM, 'custom');
});
@@ -212,12 +236,12 @@ test('the inverted index keys the uppercased names and merges memberships', () =
assert.ok(ids('EF.PLMNwAcT').includes('plmn-selection'));
assert.ok(ids('EF.SPN').includes('provider-names'));
assert.ok(ids('EF.SPN').includes('svc:Service Provider Name (SPN)'));
assert.ok(ids('EF.SMS').includes('sms'));
// SMS/CB files are covered by their per-service options, not a category
assert.ok(ids('EF.SMS').includes('svc:Short Message Storage (SMS)'));
assert.ok(ids('EF.CBMI').includes('cb'));
// EF.PSISMSC is only listed by the SMS category - pySim models no such
// file, so it never matches a tree node (no service lists it either)
assert.deepStrictEqual([...idx.get('EF.PSISMSC')], ['sms']);
assert.ok(ids('EF.CBMI').includes('svc:Cell Broadcast Message Identifier (CBMI)'));
// EF.PSISMSC is neither a category nor a file-backed service, so nothing
// maps it (pySim models no such file, and no service lists it either)
assert.ok(!idx.has('EF.PSISMSC'));
assert.ok(!fileBackedFeatures().some(n => (SERVICES_FS_MAP[n] || []).includes('EF.PSISMSC')));
});
@@ -232,12 +256,12 @@ test('a file matches the selected categories, case-insensitively', () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SPN' }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.PNN' }), false);
});
withFilter(['identifiers', 'sms'], () => {
withFilter(['identifiers', 'svc:Short Message Status Reports (SMSR)'], () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.IMSI' }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SMSR' }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SPN' }), false);
});
withFilter(['sms'], () => {
withFilter(['svc:Short Message Storage (SMS)'], () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ECC' }), false);
});
});
@@ -320,8 +344,8 @@ test('a directory shows only on the path of a file the filter selects', () => {
assert.strictEqual(pysimFsFilterShow(t.usim), true); // EF.IMSI/EF.MSISDN copies
assert.strictEqual(pysimFsFilterShow(t.wlan), false);
});
withFilter(['cb'], () => {
assert.strictEqual(pysimFsFilterShow(t.gsm), true); // EF.CBMI/CBMID/CBMIR
withFilter(['svc:Cell Broadcast Message Identifier (CBMI)'], () => {
assert.strictEqual(pysimFsFilterShow(t.gsm), true); // EF.CBMI
assert.strictEqual(pysimFsFilterShow(t.telecom), false);
assert.strictEqual(pysimFsFilterShow(t.wlan), false);
});
@@ -377,13 +401,11 @@ test('the standard list gives a name every tree it lives in', () => {
});
test('the selection resolves to the paths of its files', () => {
withFilter(['cb'], () => {
withFilter(['svc:Cell Broadcast Message Identifier (CBMI)'], () => {
const paths = pysimFsFilterMatchingPaths();
assert.ok(paths instanceof Set, 'expected a Set');
assert.deepStrictEqual(Array.from(paths).sort(), [
'ADF.USIM-NON-IMSI/6F45', 'ADF.USIM-NON-IMSI/6F48', 'ADF.USIM-NON-IMSI/6F50',
'ADF.USIM/6F45', 'ADF.USIM/6F48', 'ADF.USIM/6F50',
'MF/7F20/6F45', 'MF/7F20/6F48', 'MF/7F20/6F50',
'ADF.USIM-NON-IMSI/6F45', 'ADF.USIM/6F45', 'MF/7F20/6F45',
]);
// cached for the same selection
assert.strictEqual(pysimFsFilterMatchingPaths(), paths);
@@ -429,7 +451,8 @@ test('the menu lists all, custom, the three headings and the services', () => {
// the categories sit under their headings
assert.ok(out.indexOf('data-filter="provider-names"') < out.indexOf('>PLMN</div>'));
assert.ok(out.indexOf('data-filter="plmn-selection"') > out.indexOf('>PLMN</div>'));
assert.ok(out.indexOf('data-filter="sms"') > out.indexOf('>Services</div>'));
// the per-service options sit under the Services heading
assert.ok(out.indexOf('data-filter="svc:') > out.indexOf('>Services</div>'));
const boxes = renderedBoxes();
assert.strictEqual(boxes.length, 2 + FS_FILTER_GROUPS.length + fsFilterServiceOptions().length);
assert.strictEqual(boxes[0].id, 'all');
@@ -513,11 +536,11 @@ test('the button summary names the single entry or counts the selection', () =>
assert.strictEqual(summary(['all']), 'All');
// an empty selection also means everything
assert.strictEqual(summary([]), 'All');
assert.strictEqual(summary(['all', 'sms']), 'All');
assert.strictEqual(summary(['all', 'plmn-selection']), 'All');
assert.strictEqual(summary(['custom']), 'Custom files');
assert.strictEqual(summary(['plmn-selection']), 'PLMN selection');
assert.strictEqual(summary(['svc:Service Provider Name (SPN)']), 'Service Provider Name (SPN)');
assert.strictEqual(summary(['sms', 'cb']), 'Selected: 2');
assert.strictEqual(summary(['plmn-selection', 'wlan']), 'Selected: 2');
// an unknown id falls back to itself
assert.strictEqual(summary(['providers']), 'providers');
});
@@ -550,18 +573,18 @@ test('the checkbox rows keep "all" exclusive and persist the selection', () => {
assert.strictEqual(_store.simple_fs_filter, '["all"]');
assert.strictEqual(_treeRenders, 1);
// choosing a category drops "all"
pysimFsFilterSetEntry('sms', true);
assert.deepStrictEqual([...pysimFsFilter], ['sms']);
assert.strictEqual(_store.simple_fs_filter, '["sms"]');
pysimFsFilterSetEntry('provider-names', true);
assert.deepStrictEqual([...pysimFsFilter], ['provider-names']);
assert.strictEqual(_store.simple_fs_filter, '["provider-names"]');
assert.strictEqual(_treeRenders, 3); // render + the auto-expand's render
assert.strictEqual(box('sms').checked, true);
assert.strictEqual(box('provider-names').checked, true);
assert.strictEqual(box('all').checked, false);
assert.strictEqual(dom.btn.textContent, 'SMS-related');
assert.strictEqual(dom.btn.textContent, 'Provider names');
// several categories stay selected
pysimFsFilterSetEntry('identifiers', true);
assert.deepStrictEqual([...pysimFsFilter], ['sms', 'identifiers']);
assert.deepStrictEqual([...pysimFsFilter], ['provider-names', 'identifiers']);
// unchecking one keeps the other
pysimFsFilterSetEntry('sms', false);
pysimFsFilterSetEntry('provider-names', false);
assert.deepStrictEqual([...pysimFsFilter], ['identifiers']);
// unchecking the last one restores the default
pysimFsFilterSetEntry('identifiers', false);
@@ -569,7 +592,7 @@ test('the checkbox rows keep "all" exclusive and persist the selection', () => {
assert.strictEqual(_store.simple_fs_filter, '["all"]');
assert.strictEqual(box('all').checked, true);
// "all" is exclusive even when another entry is selected
pysimFsFilter = new Set(['sms']);
pysimFsFilter = new Set(['provider-names']);
pysimFsFilterSetEntry('all', true);
assert.deepStrictEqual([...pysimFsFilter], ['all']);
});
@@ -582,7 +605,7 @@ test('the saved selection is restored, defaulting to "all"', () => {
pysimFsFilterRestore();
assert.deepStrictEqual([...pysimFsFilter], ['providers', 'svc:Service Provider Name (SPN)']);
// `all` is exclusive; an empty or broken save falls back to it
_store.simple_fs_filter = JSON.stringify(['all', 'sms']);
_store.simple_fs_filter = JSON.stringify(['all', 'plmn-selection']);
pysimFsFilterRestore();
assert.deepStrictEqual([...pysimFsFilter], ['all']);
_store.simple_fs_filter = '[]';
@@ -636,7 +659,7 @@ test('the dropdown is re-rendered on language switch', () => {
test('the filter labels have Russian entries', () => {
const dict = html.match(/const LANG_RU = \{([\s\S]*?)\n\};/)[1];
for (const key of ['All', 'Custom files', 'Identifiers', 'PLMN', 'Services', 'PLMN selection',
'Provider names', 'WLAN / I-WLAN', 'SMS-related', 'CB-related', 'Selected: {n}']) {
'Provider names', 'WLAN / I-WLAN', 'Selected: {n}']) {
assert.ok(dict.includes("'" + key + "':"), 'missing Russian entry: ' + key);
}
});
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.40.0"
version = "3.40.3"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
license = { text = "GPL-2.0-or-later" }
requires-python = ">=3.9"
+1 -1
View File
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.40.0'
VERSION = '3.40.3'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE