pySim's lchan.select() resolves names against global selectables (self + parent chain + MF children + applications) and falls back to probe_file(), which blindly SELECTs an unknown FID and permanently injects a dynamically named DF.XXXX/EF.XXXX into the running filesystem model. Probing a whole tree or scanning a snapshot with custom files therefore polluted the model, made tree branches show children of the wrong object, and could persist phantom files into snapshots. - server: new _select_with_parent()/_select_path() walk the requested parent path (new parent_path field, parent_sel kept as legacy fallback) strictly through the model and call lchan.select_file() only; model-unknown 4-hex segments are probed only with allow_probe and the temporary child pySim adds is detached again via the cleanup callable that the four handlers (/api/tree|select|read|write) now run in a finally block - frontend: getParentPath() builds the segment chain (MF, ADF names, FIDs) and all tree/select/read/write bodies plus the snapshot/profile walker send parent_path; allow_probe is set only for custom files; the blind retries in the file manager were dropped - tests: tests/test_select_scope.py (duplicate-FID resolution, no APDU for unknown non-custom files, probe+detach, model unchanged); fs_load/fs_probe assertions for parent_path and allow_probe; docs/api.md and AGENTS.md document the contract; SW cache v117 -> v118.
14 KiB
pysim-otaman-server — HTTP API reference
The server exposes a JSON HTTP API under /api/*. All responses carry
Access-Control-Allow-Origin: * (plus Access-Control-Allow-Private-Network: true
on the preflight), so the API is reachable from a separately-hosted PWA.
Version compatibility
| Server | PWA (OTAMan) | Status |
|---|---|---|
| 1.x.x | 1.x.x | ✅ Compatible |
| 0.x.x | 1.x.x | ❌ Outdated — update server |
| 2.x.x+ | 1.x.x | ⚠️ Server newer — update PWA |
The server reports its version via GET /api/version. The PWA checks this on
connect and warns if versions are incompatible.
Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/api/version |
GET | Server version string |
/api/status |
GET | Card reader + card info + current selection |
/api/command |
POST | pySim command (equip, status, tree, etc.) |
/api/commands |
GET | List available pySim commands |
/api/tree |
POST | File tree browser for given FID/name |
/api/select |
POST | Select a file by name or FID |
/api/read |
POST | Read file content |
/api/write |
POST | Write raw hex data to a file |
/api/apdu |
POST | Raw APDU send |
/api/help |
POST | pySim help for a given command |
/api/send-ota |
POST | SCP80 OTA secured packet delivery |
/api/ram-install |
POST | Install a Java Card .cap file via SCP80 (INSTALL[for load] → LOAD ×N → INSTALL[for install]) |
/api/sp-verify |
POST | Verify secured packet against pySim reference |
/api/menu |
GET | Current STK menu (title + items + active) |
/api/menu-select |
POST | ENVELOPE(Menu Selection) with item_id |
/api/menu-respond |
POST | TERMINAL RESPONSE for paused STK command |
/api/stk-status |
GET | STK session state (active/pending/type) |
/api/events |
GET | Event list from SET UP EVENT LIST |
/api/event-send |
POST | Send ENVELOPE(Event Download) |
/api/proactive-log |
GET | Last 50 proactive commands |
/api/status-poll |
POST | Manual STATUS poll + FETCH if 91XX |
/api/rescue |
POST | Re-send TERMINAL PROFILE to recover CAT session |
/api/poll-status |
GET | Background STATUS polling state |
/api/poll-toggle |
POST | Enable/disable background polling |
/api/pli-qualifiers |
GET | List of qualifier codes with descriptions |
/api/pli-dict |
GET | Current dictionary (hex values per qualifier) |
/api/pli-dict |
POST | Update dictionary entries |
Endpoint details
GET /api/version
Returns server version for compatibility checking.
Example response:
{"version": "2.0.0"}
GET /api/status
Card reader, card type, current selection, and card state.
GET /api/commands
List all available shell commands for the current card profile.
POST /api/command
Execute any pysim-shell command.
{"cmd": "select MF"}
Returns:
{"output": "..."}
POST /api/apdu
Send a raw APDU to the card.
{"apdu": "00A4040000..."}
Returns:
{"response": "...", "sw": "9000"}
POST /api/help
Get structured help for a shell command.
{"cmd": "apdu"}
Returns:
{"usage": "apdu [-h] [--expect-sw EXPECT_SW] [--raw] APDU", "description": "...", "args": [{"name": "APDU", "type": "positional", "help": "..."}]}
POST /api/send-ota
Send an OTA command (SCP80) to the card via SMS-PP-DOWNLOAD ENVELOPE. The secured packet is delivered in an SMS-DELIVER TPDU wrapped in an ENVELOPE command.
Request body:
{
"sp": "00201516011515b00000...",
"spi1": "16",
"spi2": "01",
"kic": "15",
"kid": "15",
"tar": "b00000",
"cntr": "0000000001",
"kicKey": "D6FCC023...",
"kidKey": "1B07E7E0..."
}
Response (delivery PoR):
{"success": true, "sw": "9000", "response_data": "027100000e0a...",
"por": {"response_status": "por_ok", "tar": "B00000", "pcntr": 0,
"decoded": {"number_of_commands": 1, "last_status_word": "6e00",
"last_response_data": ""}}}
Response (submit PoR): PoR is extracted from the SMS-SUBMIT TPDU
fetched via a proactive command (FETCH). The response contains the
same por structure if decoding succeeds.
The SPI2 por_in_submit bit (0x20) selects submit-mode PoR.
POST /api/ram-install
Install a Java Card .cap file on the card via GlobalPlatform commands (INSTALL[for load] → LOAD ×N → INSTALL[for install (+ make selectable)]) wrapped in SCP80 secured packets. Each step is sent via ENVELOPE and its PoR is checked; the sequence aborts on the first PoR error. The .cap archive (a ZIP of nested components) is parsed server-side in _cap_parse; no external tooling is required.
Request body:
{
"cap_hex": "DECAFFED...",
"sd_aid": "A000000003000000",
"install_params": "C90000",
"stk_params": "",
"nv_quota": 0,
"volatile_quota": 0,
"make_selectable": true,
"spi1": "0E", "spi2": "01",
"kic": "15", "kid": "15",
"tar": "000000",
"cntr": "0000000001",
"kicKey": "D6FCC023...",
"kidKey": "1B07E7E0..."
}
| Field | Req | Description |
|---|---|---|
cap_hex |
yes | Even-length hex of the .cap file (zipped Java Card CAP), max 48 kB (98304 hex chars) |
sd_aid |
no | Security Domain AID for INSTALL[for load]; empty → default ISD A000000003000000 |
install_params |
no | Hex C9 TLV install parameters; if empty, gen_install_parameters() is used with the quota/stk params |
stk_params |
no | Hex CA TLV (TS 102 226 §8.2.1.3.2.1) for SIM toolkit app-specific params |
nv_quota / volatile_quota |
no | Integer memory quotas (bytes) for gen_install_parameters() |
make_selectable |
no | If true (default), final INSTALL uses P1=0C (install + make selectable) |
Response (success):
{"success": true, "failed_step": null,
"steps": [{"name": "install_for_load", "apdu": "80E60200...", "por_status": "por_ok", "sw": "9000"},
{"name": "load_0", "apdu": "80E80000...", "por_status": "por_ok", "sw": "9000"},
{"name": "install_for_install", "apdu": "80E60C00...", "por_status": "por_ok", "sw": "9000"}],
"final_cntr": "0000000004",
"load_file_aid": "A000000003000000",
"module_aid": "A000000003000000",
"application_aid": "A000000003000000"}
Response (failure):
{"success": false, "failed_step": "load_1",
"steps": [{"name": "install_for_load", "por_status": "por_ok", "sw": "9000"},
{"name": "load_1", "por_status": "rc_error", "sw": null}],
"error": "..."}
The steps array contains one entry per GP command. final_cntr is the counter value after all successful steps (use it to update the card preset). The response is not streamed — all steps run server-side before the JSON is returned.
POST /api/sp-verify
Cross-check a secured packet against pySim's OtaDialectSms.encode_cmd
reference. Returns the JS-generated packet, pySim reference, a match flag,
and the decoded SPI fields.
{"spi1": "16", "spi2": "01", "kic": "15", "kid": "15", "tar": "b00000",
"cntr": "0000000001", "apdu": "00a40000023f00",
"kicKey": "D6FCC023...", "kidKey": "1B07E7E0..."}
Response:
{"js_sp": "...", "py_sp": "...", "match": true,
"diffs": [], "spi": {"counter": "counter_must_be_higher", ...}}
GET /api/menu
Returns the SIM Toolkit SETUP MENU captured from the card's TERMINAL PROFILE
response at startup. Empty {"items": []} if the card didn't send a menu.
Response:
{"command_number": 1, "items": [{"id": 128, "text": "Настройки/Settings"}],
"title": "Alfa Mobile", "active": false}
POST /api/menu-select
Sends an ENVELOPE(MENU SELECTION) with the selected item ID, then handles
the card's proactive response (DISPLAY TEXT or SELECT ITEM).
{"item_id": 128}
Response:
{"type": "display_text", "text": "Hello", "sw": "9122"}
or
{"type": "select_item", "items": [{"id": 1, "text": "Sub-menu"}], "sw": "9122"}
POST /api/menu-respond
Sends TERMINAL RESPONSE to the current proactive command with the given result
code. Continues the proactive chain if the card responds with 91XX. If no
response arrives within --menu-timeout seconds (default 60, 0 disables), the
server watchdog sends the timeout result itself.
{"result": "ok", "item_id": 1}
result |
TERMINAL RESPONSE code | Meaning |
|---|---|---|
ok |
0x00 |
Command performed successfully |
cancel |
0x10 |
Proactive session terminated by the user |
back |
0x11 |
Backward move in the proactive session requested by the user |
timeout |
0x12 |
No response from the user |
GET /api/stk-status
Returns the current STK session state.
{"active": true, "pending": true, "pending_type": "select_item"}
POST /api/read
Read file content. Auto-detects transparent vs record files.
{"name": "EF.ICCID", "fid": "2FE2", "parent_path": ["MF"], "mode": "raw"}
Returns transparent data:
{"success": true, "sw": "9000", "file_type": "transparent", "data": "...",
"apdu_times": [{"type": "select", "ms": 12}, {"type": "read_binary", "ms": 9}]}
Returns records:
{"success": true, "sw": "9000", "file_type": "linear_fixed",
"records": [{"num": 1, "data": "..."}, {"num": 2, "data": "..."}],
"apdu_times": [{"type": "select", "ms": 12},
{"type": "read_record", "ms": 11}, {"type": "read_record", "ms": 13}]}
apdu_times reports each command's duration (command sent to response
received) classified as select, read_binary or read_record; the PWA uses
it for snapshot timing statistics. Other commands are not reported.
POST /api/write
Write raw hex data to a file.
{"name": "EF.ICCID", "fid": "2FE2", "data": "A0A1A2...", "parent_path": ["MF"]}
For record files:
{"name": "EF.ADN", "fid": "6F3A", "data": "A0A1...", "record_nr": 1, "parent_path": ["MF", "7F10"]}
Returns:
{"success": true, "sw": "9000"}
POST /api/select
Select a file by name or FID, with optional parent selection.
{"name": "EF.ICCID", "fid": "2FE2", "parent_path": ["MF"]}
parent_path lists the path segments from MF to the parent (ADF names or
FIDs); the legacy single-segment parent_sel is still accepted but is only
unambiguous for ADFs. Resolution is strictly parent-scoped: model-known files
are selected through the requested parent only (pySim select_file()), never
via pySim's global selectables or its probe_file() model injection, so a
same-FID file under another parent is never picked and the filesystem model
is not modified. allow_probe: true (PWA custom files) additionally allows a
model-unknown 4-hex FID to be selected directly; any temporary model object
created for it is detached again before the response is sent.
Returns:
{"name": "EF.ICCID", "fid": "2FE2", "file_type": "transparent",
"file_size": 10, "record_len": null, "num_of_rec": null,
"fci_hex": "621082024021...",
"apdu_times": [{"type": "select", "ms": 12}], "exists": true}
fci_hex is the raw FCP template ('62') from the SELECT response, used by
the PWA's Exact FCI checks; file_size / record_len / num_of_rec drive
the profiler's size and record checks. When the file does not exist the
endpoint responds 404 with {"error": "...", "exists": false}.
POST /api/tree
Get directory listing with typed children.
{"name": "MF", "fid": "3F00"}
Use parent_path (or the legacy parent_sel) to list a subdirectory, e.g.
{"name": "DF.GSM-ACCESS", "fid": "5F3B", "parent_path": ["MF", "ADF.USIM"]}.
Returns:
{"exists": true, "name": "MF", "fid": "3F00", "file_type": "df", "children": [{"name": "EF.ICCID", "fid": "2fe2", "isDir": false}]}
GET /api/events
Returns the event list captured from the card's SET UP EVENT LIST (an array of
event byte values, or [] when none was received).
POST /api/event-send
Sends an ENVELOPE(Event Download) for a subscribed event.
{"event_type": 4, "event_data": "01A0"}
event_type is required (the SET UP EVENT LIST event byte); event_data is
optional hex for events that carry data. Returns the SW and any response data:
{"sw": "9000", "data": "..."}
GET /api/proactive-log
Returns the last 50 proactive commands fetched during CAT sessions, newest first:
[{"type_hex": "25", "type_name": "SET UP MENU", "elapsed": 3.2, "bytes": 97}]
POST /api/status-poll
Manually sends STATUS (F2) and, if the card answers 91XX, runs the
proactive chain (FETCH → TERMINAL RESPONSE) until it settles. Returns:
{"sw": "9000", "proactive": true}
POST /api/rescue
Recovers a stuck CAT session by clearing the pending state and re-sending the TERMINAL PROFILE. Returns whether a menu and event list were captured again:
{"menu": true, "events": [4, 5]}
GET /api/poll-status
Background STATUS polling state.
{"enabled": true, "interval": 300}
POST /api/poll-toggle
Turns background STATUS polling on or off.
{"enabled": true}
Returns the new state ({"enabled": ..., "interval": ...}).
GET /api/pli-qualifiers
Lists the PROVIDE LOCAL INFORMATION qualifier codes with their names.
[{"code": "00", "name": "Location Information"}, {"code": "0A", "name": "Battery Charge Level"}]
GET /api/pli-dict
Returns the current PLI data dictionary as a qualifier-code map.
{"00": "0291...", "0A": "64"}
POST /api/pli-dict
Updates dictionary entries. Body is a map of qualifier code to hex value; keys must be known qualifiers and values valid hex, otherwise they are ignored. Returns the updated dictionary.