Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b83d21e64 | ||
|
|
9a661f5ab2 | ||
|
|
5a7768e6a9 | ||
|
|
7801c62ca3 | ||
|
|
f18a11edaf | ||
|
|
6fbe57052f | ||
|
|
9872913965 | ||
|
|
a863f66860 | ||
|
|
6ed3a7b7d6 | ||
|
|
903ead9791 | ||
|
|
c810ff50cf | ||
|
|
5fcd8e9fd6 | ||
|
|
e00a96b918 | ||
|
|
b19abbd514 | ||
|
|
0b3c483c0b | ||
|
|
9e46c64490 |
No files matched your search
@@ -0,0 +1,22 @@
|
|||||||
|
Install the wheel below — the PWA is bundled inside it:
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install ./simtrace_analyser-*.whl
|
||||||
|
simtrace-analyser-server --capture gsmtap
|
||||||
|
```
|
||||||
|
|
||||||
|
The UI is then served at http://127.0.0.1:8081/. If your system Python is
|
||||||
|
externally managed (PEP 668 — common on Arch and Fedora), install into a
|
||||||
|
virtual environment first (`python3 -m venv .venv && . .venv/bin/activate`).
|
||||||
|
|
||||||
|
The source archive is the same tree a clone would have: unzip it and run
|
||||||
|
`./start.sh` (Linux/macOS) or `start.bat` (Windows). Direct SIMtrace2 capture
|
||||||
|
on Linux additionally needs the attached `70-simtrace2-pysniff.rules` udev rule.
|
||||||
|
|
||||||
|
**Runtime zip (no installer)** — `simtrace-analyser-*.zip`
|
||||||
|
is the runtime tree only (packages, PWA, launchers, docs — no tests, no git):
|
||||||
|
extract it anywhere and run `start.bat` (Windows) or `./start.sh`
|
||||||
|
(Linux/macOS). Python 3 has to be on `PATH`; `pip install pyusb` is only
|
||||||
|
needed for direct SIMtrace2 capture.
|
||||||
|
|
||||||
|
The project is MIT-licensed (see `LICENSE`).
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
# A pushed v* tag publishes a GitHub release with the wheel, the sdist and the
|
||||||
|
# udev rule. A manual run (workflow_dispatch) does everything except publish,
|
||||||
|
# so the pipeline can be exercised without minting a release.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v*']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: The tag matches the version strings
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
run: |
|
||||||
|
version=$(python -c "from simtrace_analyser.version import __version__; print(__version__)")
|
||||||
|
if [ "$GITHUB_REF_NAME" != "v$version" ]; then
|
||||||
|
echo "tag $GITHUB_REF_NAME does not match version.py ($version)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -q "PWA_VERSION = '$version'" frontend/index.html
|
||||||
|
|
||||||
|
- name: Install build tooling and the runtime dependency
|
||||||
|
run: python -m pip install --upgrade pip pyusb build
|
||||||
|
|
||||||
|
- name: Python tests
|
||||||
|
run: python -m unittest discover -s tests
|
||||||
|
|
||||||
|
- name: PWA tests
|
||||||
|
working-directory: frontend
|
||||||
|
run: node --test
|
||||||
|
|
||||||
|
- name: Build the sdist and the wheel
|
||||||
|
run: python -m build --sdist --wheel
|
||||||
|
|
||||||
|
- name: Build the runtime zip
|
||||||
|
run: python scripts/build_release_zip.py --output dist
|
||||||
|
|
||||||
|
- name: The runtime zip is complete
|
||||||
|
run: |
|
||||||
|
python - <<'PY'
|
||||||
|
import glob
|
||||||
|
import zipfile
|
||||||
|
archive = glob.glob('dist/*.zip')[0]
|
||||||
|
names = zipfile.ZipFile(archive).namelist()
|
||||||
|
for required in ('start.bat', 'frontend/index.html'):
|
||||||
|
assert any(n.endswith('/' + required) for n in names), \
|
||||||
|
'%s is missing from %s' % (required, archive)
|
||||||
|
print('%s: %d entries, launchers and PWA bundled' % (archive, len(names)))
|
||||||
|
PY
|
||||||
|
|
||||||
|
- name: The wheel bundles the PWA
|
||||||
|
run: |
|
||||||
|
python - <<'PY'
|
||||||
|
import glob
|
||||||
|
import zipfile
|
||||||
|
wheel = glob.glob('dist/*.whl')[0]
|
||||||
|
names = zipfile.ZipFile(wheel).namelist()
|
||||||
|
for required in ('simtrace_analyser/web/index.html',
|
||||||
|
'simtrace_analyser/web/sw.js'):
|
||||||
|
assert required in names, '%s is missing from %s' % (required, wheel)
|
||||||
|
print('%s: %d entries, PWA bundled' % (wheel, len(names)))
|
||||||
|
PY
|
||||||
|
|
||||||
|
- name: The installed wheel serves the PWA
|
||||||
|
run: |
|
||||||
|
python -m venv /tmp/smoke
|
||||||
|
/tmp/smoke/bin/pip install --no-deps dist/*.whl
|
||||||
|
cd /tmp
|
||||||
|
/tmp/smoke/bin/simtrace-analyser-server --capture disabled \
|
||||||
|
--db /tmp/smoke.db --port 8099 &
|
||||||
|
server=$!
|
||||||
|
for _ in $(seq 1 20); do
|
||||||
|
if curl -fsS http://127.0.0.1:8099/ -o /tmp/served-index.html; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
kill "$server"
|
||||||
|
cmp /tmp/served-index.html "$GITHUB_WORKSPACE/frontend/index.html"
|
||||||
|
/tmp/smoke/bin/python -c \
|
||||||
|
"import simtrace_analyser.__main__ as m; p = m._default_web_dir(); assert p.endswith('/simtrace_analyser/web'), p"
|
||||||
|
|
||||||
|
- name: Keep the artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dist
|
||||||
|
path: dist/*
|
||||||
|
|
||||||
|
- name: Publish the release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
gh release create "$GITHUB_REF_NAME" \
|
||||||
|
--title "$GITHUB_REF_NAME" \
|
||||||
|
--generate-notes \
|
||||||
|
--notes-file .github/release-notes.md \
|
||||||
|
dist/*.whl dist/*.tar.gz dist/*.zip 70-simtrace2-pysniff.rules
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Anton Troshin
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
graft frontend
|
graft frontend
|
||||||
|
include LICENSE
|
||||||
include README.md README_RUS.md 70-simtrace2-pysniff.rules
|
include README.md README_RUS.md 70-simtrace2-pysniff.rules
|
||||||
include start.sh start.bat sniff-start.sh sniff-start.bat
|
include start.sh start.bat sniff-start.sh sniff-start.bat
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
# SIMtrace Analyser
|
# SIMtrace Analyser — an APDU trace analyser for GSMTAP-SIM captures
|
||||||
|
|
||||||
English | [Русский](README_RUS.md)
|
English | [Русский](README_RUS.md)
|
||||||
|
|
||||||
|
📢 Project news and updates: [website](https://atroshin.ru) · [Telegram channel](https://t.me/youeyeseesee)
|
||||||
|
|
||||||
APDU trace analyser for the GSMTAP-SIM format — captures SIM/UICC
|
APDU trace analyser for the GSMTAP-SIM format — captures SIM/UICC
|
||||||
communication from a UDP GSMTAP stream (or imports PCAP/PCAPNG files),
|
communication from a UDP GSMTAP stream (or imports PCAP/PCAPNG files),
|
||||||
decodes it, and serves an installable web app for browsing. The repository
|
decodes it, and serves an installable web app for browsing. The repository
|
||||||
@@ -9,128 +11,86 @@ also bundles **simtrace2-pysniff**, a Python replacement for
|
|||||||
`simtrace2-sniff` that captures from Osmocom SIMtrace2 hardware and can feed
|
`simtrace2-sniff` that captures from Osmocom SIMtrace2 hardware and can feed
|
||||||
GSMTAP into this analyser.
|
GSMTAP into this analyser.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Components
|

|
||||||
|
|
||||||
- **Analysis server + PWA** (`simtrace-analyser-server`) — stores captured
|
|
||||||
traffic in SQLite, decodes it, and serves the PWA ([`frontend/`](frontend/))
|
|
||||||
plus the HTTP API on a single origin. Three capture modes: a GSMTAP
|
|
||||||
listener, direct SIMtrace2 hardware capture, or disabled (browse existing
|
|
||||||
captures only).
|
|
||||||
- **Capture utility** (`simtrace2-pysniff`) — captures SIMtrace2 hardware
|
|
||||||
traffic and writes it to stdout, GSMTAP (Wireshark), or PCAP files. It is
|
|
||||||
one GSMTAP source of several that the analyser accepts (see also
|
|
||||||
[sigrok_iso7816_stream](https://github.com/anttro/sigrok_iso7816_stream)).
|
|
||||||
- **PWA** ([`frontend/`](frontend/)) — trace browser with decoded APDU,
|
|
||||||
SIM Toolkit, SMS and SCP80 OTA detail; sessions, PCAP and COMPRION trace
|
|
||||||
import, EN/RU UI.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
**APDU decoder** (server-side, rendered by the PWA):
|
|
||||||
|
|
||||||
- Full APDU decode — CLA per ISO 7816-4 / ETSI coding with the **logical
|
|
||||||
channel 0-19** (standard channels from b2-b1, the extended channels 4-19
|
|
||||||
from b4-b1 per TS 102 221 §10.3), secure messaging and command chaining,
|
|
||||||
INS names from a spec table, SFI-aware P1/P2 for READ/UPDATE BINARY and
|
|
||||||
record commands; MANAGE CHANNEL replies show the channel the card
|
|
||||||
assigned
|
|
||||||
- Status words — full ISO 7816-4 set plus UICC-specific values:
|
|
||||||
`91XX` (proactive command pending), `62F1/F2/F3`, `63F1/F2` (more data),
|
|
||||||
`9300` (SAT busy), `9850`, `9862–64`
|
|
||||||
- SIM Toolkit — SAT (GSM 11.14), CAT (TS 102 223) and USAT (TS 31.111)
|
|
||||||
proactive commands with decoded qualifiers, TERMINAL RESPONSE, ENVELOPE
|
|
||||||
(Menu Selection, Call Control, SMS-PP download…); tolerates known
|
|
||||||
non-compliant legacy-card quirks and preserves unrecognized TLVs
|
|
||||||
- TERMINAL CAPABILITY (TS 102 221 §11.1.19) — terminal power supply
|
|
||||||
(voltage class, maximum current, clock), extended logical channels,
|
|
||||||
additional interfaces (UICC-CLF) and eUICC device capabilities
|
|
||||||
(SGP.22/SGP.32)
|
|
||||||
- SMS TPDU — SMS-DELIVER/SUBMIT, SCTS timestamps, UDH information elements
|
|
||||||
- SCP80 OTA secured packets — SPI bits (ciphering, PoR requirement/mode,
|
|
||||||
RC/CC/DS, counter), KIc/KID algorithm + key set, TAR, CNTR/PCNTR;
|
|
||||||
Response Packet (PoR) decode on GET RESPONSE; **optional client-side
|
|
||||||
decryption** of ciphered packets — paste the card's 3DES/AES key in the
|
|
||||||
detail view to reveal the real CNTR and secured application data (the
|
|
||||||
key stays in the browser, in-memory only; ciphered on-wire CNTR/PCNTR/RC
|
|
||||||
are ciphertext and labeled as such); the secured application message is
|
|
||||||
decoded as a stream of **C-APDUs** (SELECT, UPDATE BINARY, …) with
|
|
||||||
full CLA/INS/P1/P2/Lc/Le breakdown — plaintext immediately, or after
|
|
||||||
decryption for ciphered packets
|
|
||||||
- ATR parse (clock-rate conversion, Fi/Di, T-bitmask, truncated-ATR safe)
|
|
||||||
and FCP/FCI TLV; the historical bytes are decoded as the UICC's
|
|
||||||
COMPACT-TLV objects (TS 102 221 §6.3.1) — card service data, the **card
|
|
||||||
capabilities** (ISO 7816-4 §8.1.1.2.7 Table 88: the assignment methods
|
|
||||||
and the announced maximum number of logical channels, `8 or more` for the
|
|
||||||
extended-capable cards) and the trailing status indicator (LCS/SW)
|
|
||||||
|
|
||||||
**SIMtrace Analyser PWA** ([`frontend/`](frontend/)):
|
|
||||||
|
|
||||||
- Trace view with Map/List modes — proactive, TERMINAL RESPONSE, ENVELOPE
|
|
||||||
and AUTH commands get distinctive styling in map view
|
|
||||||
- Sessions with deep links (`#session=N`), PCAP import (`.pcap/.pcapng/.cap`),
|
|
||||||
COMPRION trace import (the Device Manager *ISO Protocol Layer* text export),
|
|
||||||
search and a **multi-select command filter** — pick any number of commands
|
|
||||||
and/or the conversation presets (*Proactive sessions*: FETCH, TERMINAL
|
|
||||||
PROFILE and every command answered with SW 91xx; *File operations*: SELECT,
|
|
||||||
READ/UPDATE BINARY and READ/UPDATE RECORD, with the GET RESPONSE carrying a
|
|
||||||
selected command's data)
|
|
||||||
- Installable PWA, dark theme, English/Russian UI
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **Python 3.9+**
|
**Required**
|
||||||
- **PyUSB** — `pip install pyusb` (needed only for capture from SIMtrace2
|
|
||||||
hardware; GSMTAP/UDP capture and PCAP import work without it)
|
- **Python 3.9+** — GSMTAP capture, PCAP import and browsing use only the
|
||||||
|
standard library.
|
||||||
|
|
||||||
|
**Only for capturing from Osmocom SIMtrace2 hardware** (`--capture direct`, and
|
||||||
|
the bundled `simtrace2-pysniff` capture utility):
|
||||||
|
|
||||||
|
- **PyUSB** — `pip install pyusb`
|
||||||
- **libusb** — system library, present by default on Linux; on Windows the
|
- **libusb** — system library, present by default on Linux; on Windows the
|
||||||
SIMtrace2 driver must be swapped (see [USB Device Access](#usb-device-access))
|
SIMtrace2 driver must be swapped (see [USB Device Access](#usb-device-access))
|
||||||
- **SIMtrace2 hardware** running firmware in *trace* mode (only for direct
|
- **SIMtrace2 hardware** running firmware in *trace* mode (VID `1d50`, PID
|
||||||
capture; VID `1d50`, PID `60e3`, USB class `ff`, subclass `01`). For
|
`60e3`, USB class `ff`, subclass `01`); building and flashing trace firmware
|
||||||
building and flashing trace firmware, see the
|
is described in the
|
||||||
[upstream SIMtrace2 project](https://gitea.osmocom.org/sim-card/simtrace2).
|
[upstream SIMtrace2 project](https://gitea.osmocom.org/sim-card/simtrace2).
|
||||||
|
|
||||||
No other dependencies. PCAP, GSMTAP, and hex dump output use only Python stdlib.
|
The analyser needs none of this for `--capture gsmtap` (the GSMTAP source may
|
||||||
|
even run on another machine), `--capture disabled`, or PCAP imports.
|
||||||
|
|
||||||
## Install
|
## Quick start
|
||||||
|
|
||||||
There are two ways to get the analyser: install a release artifact (no git
|
Three ways to get it running; all of them serve the same web UI at
|
||||||
history), or clone the repository for development. Both bundle the same PWA.
|
http://127.0.0.1:8081/.
|
||||||
|
|
||||||
### From a release (no git history)
|
### 1. Wheel (pip)
|
||||||
|
|
||||||
Everything is on the
|
The wheel includes the UI, so the server serves it by itself:
|
||||||
[latest release](https://github.com/anttro/simtrace-analyser/releases/latest).
|
|
||||||
|
|
||||||
**Wheel** (recommended — an isolated install, the PWA is inside the package):
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pipx install ./simtrace_analyser-2.7.11-py3-none-any.whl
|
pip install ./simtrace_analyser-2.7.13-py3-none-any.whl
|
||||||
# or: pip install ./simtrace_analyser-2.7.11-py3-none-any.whl
|
|
||||||
|
|
||||||
# then run (the UI is served at http://127.0.0.1:8081/):
|
|
||||||
simtrace-analyser-server --capture gsmtap
|
simtrace-analyser-server --capture gsmtap
|
||||||
```
|
```
|
||||||
|
|
||||||
`pip` can also fetch the wheel straight from the release page (use the version
|
If your system Python is externally managed (PEP 668 — common on Arch and
|
||||||
shown there):
|
Fedora), install into a virtual environment first:
|
||||||
|
`python3 -m venv .venv && . .venv/bin/activate`.
|
||||||
|
|
||||||
```sh
|
Every artifact is on the
|
||||||
pip install https://github.com/anttro/simtrace-analyser/releases/download/v2.7.11/simtrace_analyser-2.7.11-py3-none-any.whl
|
[latest release](https://github.com/anttro/simtrace-analyser/releases/latest).
|
||||||
```
|
`pip` can fetch the wheel directly (use the version shown there), or the GitHub
|
||||||
|
CLI can pick the latest one:
|
||||||
With the GitHub CLI, download the latest wheel without knowing the version:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
pip install https://github.com/anttro/simtrace-analyser/releases/download/v2.7.13/simtrace_analyser-2.7.13-py3-none-any.whl
|
||||||
|
# or:
|
||||||
gh release download --pattern '*.whl'
|
gh release download --pattern '*.whl'
|
||||||
pipx install ./simtrace_analyser-*.whl
|
pip install ./simtrace_analyser-*.whl
|
||||||
```
|
```
|
||||||
|
|
||||||
**Source archive** — the *Source code (zip / tar.gz)* assets are the full tree
|
### 2. Zip (no installer)
|
||||||
without the git history: unzip, `cd` into it and use the launchers below
|
|
||||||
(`./start.sh` on Linux/macOS, `start.bat` on Windows).
|
|
||||||
|
|
||||||
### From a git clone (development)
|
Download `simtrace-analyser-2.7.13.zip` from the release page and extract it
|
||||||
|
anywhere: it is the runtime tree (the Python packages, the web UI, the launchers
|
||||||
|
and the docs — no tests, no git). The archive works on any OS; then run:
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
start.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux/macOS:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 3 has to be on `PATH`; `pip install pyusb` is only needed for direct
|
||||||
|
SIMtrace2 capture. The launchers accept the server's options
|
||||||
|
(`start.bat --capture direct`, `./start.sh --capture direct --port 8080`).
|
||||||
|
(The release's *Source code (zip / tar.gz)* archives are the full tree
|
||||||
|
including tests; they work like a clone below.)
|
||||||
|
|
||||||
|
### 3. Git clone (development)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/anttro/simtrace-analyser.git
|
git clone https://github.com/anttro/simtrace-analyser.git
|
||||||
@@ -138,31 +98,39 @@ cd simtrace-analyser
|
|||||||
./start.sh # Linux/macOS; on Windows: start.bat
|
./start.sh # Linux/macOS; on Windows: start.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
The launchers add the checkout to `PYTHONPATH` and run
|
The launchers put the checkout on `PYTHONPATH` and run
|
||||||
`python -m simtrace_analyser`, so no install step is needed for browsing and
|
`python -m simtrace_analyser`, so nothing has to be installed for browsing and
|
||||||
GSMTAP capture; they accept the server's options (`./start.sh --capture direct
|
GSMTAP capture; their options are the server's
|
||||||
--port 8080`). Direct hardware capture needs **PyUSB** (`pip install pyusb`).
|
(`./start.sh --capture direct --port 8080`). Direct hardware capture needs
|
||||||
|
**PyUSB** (`pip install pyusb`). To get the console scripts
|
||||||
To get the console scripts (`simtrace-analyser-server`, `simtrace2-pysniff`)
|
(`simtrace-analyser-server`, `simtrace2-pysniff`) install the package:
|
||||||
install the package instead:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pip install -e .
|
pip install -e .
|
||||||
simtrace-analyser-server --capture gsmtap
|
simtrace-analyser-server --capture gsmtap
|
||||||
```
|
```
|
||||||
|
|
||||||
On Windows the `.bat` launchers run the `python` command, so Python 3 has to be
|
### First run
|
||||||
on `PATH` (check with `python --version`); direct USB capture additionally
|
|
||||||
needs the driver swap described in [USB Device Access](#usb-device-access).
|
|
||||||
|
|
||||||
## Running
|
Open **http://127.0.0.1:8081/** in a browser. `Ctrl+C` stops the server.
|
||||||
|
|
||||||
### Analysis server + PWA (`simtrace-analyser-server`)
|
- `--capture gsmtap` (default) — listen for GSMTAP-SIM on UDP 4729, e.g. from
|
||||||
|
`simtrace2-pysniff` or `sigrok_iso7816_stream`;
|
||||||
|
- `--capture direct` — read SIMtrace2 hardware directly (needs PyUSB; Windows
|
||||||
|
also needs the driver swap, see [USB Device Access](#usb-device-access));
|
||||||
|
- `--capture disabled` — only browse stored captures.
|
||||||
|
|
||||||
|
The full runtime guide (server options, the UI views, the capture utility) is
|
||||||
|
in [Usage](#usage) below.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Analysis server + UI (`simtrace-analyser-server`)
|
||||||
|
|
||||||
Stores captured APDU traffic in SQLite and serves it over HTTP, together with
|
Stores captured APDU traffic in SQLite and serves it over HTTP, together with
|
||||||
the bundled **SIMtrace Analyser** PWA (`frontend/` in a source checkout,
|
the bundled **SIMtrace Analyser** frontend (`frontend/` in a source checkout,
|
||||||
bundled into the package on install) on the same origin. Three
|
bundled into the package on install) on the same origin. Three capture
|
||||||
capture modes:
|
modes:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Listen for GSMTAP from simtrace2-pysniff (or original simtrace2-sniff):
|
# Listen for GSMTAP from simtrace2-pysniff (or original simtrace2-sniff):
|
||||||
@@ -175,6 +143,27 @@ simtrace-analyser-server --capture direct
|
|||||||
simtrace-analyser-server --capture disabled
|
simtrace-analyser-server --capture disabled
|
||||||
```
|
```
|
||||||
|
|
||||||
|
From a source checkout or the runtime zip, the launchers run the same server
|
||||||
|
with the same options.
|
||||||
|
|
||||||
|
Linux/macOS:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./start.sh --capture gsmtap
|
||||||
|
./start.sh --capture direct
|
||||||
|
./start.sh --capture disabled
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
start.bat --capture gsmtap
|
||||||
|
start.bat --capture direct
|
||||||
|
start.bat --capture disabled
|
||||||
|
```
|
||||||
|
|
||||||
|
Options pass through unchanged (`./start.sh --capture direct --port 8080`).
|
||||||
|
|
||||||
The server logs its capture liveness to stderr (each line timestamped
|
The server logs its capture liveness to stderr (each line timestamped
|
||||||
with the local time): `Capture started` on begin, a periodic
|
with the local time): `Capture started` on begin, a periodic
|
||||||
`Capture alive: session=N messages=M bytes=B dropped=D` heartbeat, and
|
`Capture alive: session=N messages=M bytes=B dropped=D` heartbeat, and
|
||||||
@@ -184,7 +173,7 @@ datagram is dropped and counted (`dropped`) rather than silently
|
|||||||
killing the capture; a `tpdu` packet that is not APDU-shaped at all
|
killing the capture; a `tpdu` packet that is not APDU-shaped at all
|
||||||
(shorter than the header, the invalid CLA `FF` or an ISO-reserved class)
|
(shorter than the header, the invalid CLA `FF` or an ISO-reserved class)
|
||||||
is skipped and counted (`skipped`) — a character-level or mis-sampled
|
is skipped and counted (`skipped`) — a character-level or mis-sampled
|
||||||
stream no longer pollutes the decode. The heartbeat interval defaults to 60 s and is
|
stream does not pollute the decode. The heartbeat interval defaults to 60 s and is
|
||||||
tunable via `--log-interval SECONDS`.
|
tunable via `--log-interval SECONDS`.
|
||||||
|
|
||||||
The GSMTAP listener also accepts the **sigrok-iso7816-stream** decoder
|
The GSMTAP listener also accepts the **sigrok-iso7816-stream** decoder
|
||||||
@@ -212,7 +201,7 @@ The **Import COMPRION trace** button accepts the tab-separated **ISO Protocol
|
|||||||
Layer** export of COMPRION's Device Manager — the byte-accurate protocol view
|
Layer** export of COMPRION's Device Manager — the byte-accurate protocol view
|
||||||
(ATR, PPS, APDUs with their return codes). The *ISO Application Layer* export
|
(ATR, PPS, APDUs with their return codes). The *ISO Application Layer* export
|
||||||
carries no raw response bytes and is not needed; selecting it answers with a
|
carries no raw response bytes and is not needed; selecting it answers with a
|
||||||
pointer to the Protocol Layer file (the same check runs in the PWA before the
|
pointer to the Protocol Layer file (the same check runs in the UI before the
|
||||||
upload and on the server for API callers).
|
upload and on the server for API callers).
|
||||||
|
|
||||||
Exchanges are re-encoded into the same wire-shaped TPDUs a GSMTAP capture
|
Exchanges are re-encoded into the same wire-shaped TPDUs a GSMTAP capture
|
||||||
@@ -331,28 +320,32 @@ Server options:
|
|||||||
--db FILE SQLite database path (default: ~/.simtrace-analyser/sessions.db)
|
--db FILE SQLite database path (default: ~/.simtrace-analyser/sessions.db)
|
||||||
--capture MODE gsmtap | direct | disabled (default: gsmtap)
|
--capture MODE gsmtap | direct | disabled (default: gsmtap)
|
||||||
--gsmtap-port PORT UDP port for the GSMTAP listener (default: 4729)
|
--gsmtap-port PORT UDP port for the GSMTAP listener (default: 4729)
|
||||||
--web-dir PATH PWA static files directory (default: the PWA bundled in
|
--gsmtap-bind ADDR UDP bind address for the GSMTAP listener (default: 127.0.0.1;
|
||||||
the installed package, or <repo>/frontend from source)
|
use 0.0.0.0 to receive from a Docker bridge or another host)
|
||||||
|
--web-dir PATH static files directory for the web UI (default: the
|
||||||
|
frontend bundled in the installed package, or
|
||||||
|
<repo>/frontend from source)
|
||||||
```
|
```
|
||||||
|
|
||||||
Then open **http://127.0.0.1:8081/** in a browser — the PWA is served by the
|
Then open **http://127.0.0.1:8081/** in a browser. Use `--web-dir PATH` to
|
||||||
server, so the UI and the API share an origin and no CORS/Private-Network-Access
|
serve a different UI directory (default: the bundled frontend, or
|
||||||
setup is involved. Use `--web-dir PATH` to serve a different PWA directory
|
`<repo>/frontend` in a source checkout).
|
||||||
(default: the bundled PWA, or `<repo>/frontend` in a source checkout).
|
|
||||||
|
|
||||||
### Hosted PWA (landing page)
|
### Hosted UI (landing page)
|
||||||
|
|
||||||
A standalone copy of the PWA is hosted at **https://simtrace.atroshin.ru**.
|
A standalone copy of the UI is hosted at **https://simtrace.atroshin.ru**.
|
||||||
It is a pure frontend: point it (Settings → Server URL) at a locally running
|
It is a pure frontend: point it (Settings → Server URL) at a locally running
|
||||||
`simtrace-analyser-server`.
|
`simtrace-analyser-server`.
|
||||||
|
|
||||||
> **Browser restriction:** when the PWA is served from a public HTTPS host,
|
> **Browser restriction:** this is the one setup where the UI and the API have
|
||||||
> reaching a local server (`http://127.0.0.1:8081`) requires two things: the
|
> different origins, so the browser applies CORS and Private Network Access
|
||||||
> server must send `Access-Control-Allow-Private-Network: true` (this server
|
> rules. Reaching a local server (`http://127.0.0.1:8081`) requires two
|
||||||
> does), and the browser must be allowed to access the local network — in
|
> things: the server must send `Access-Control-Allow-Private-Network: true`
|
||||||
> Chrome/Edge/Vivaldi: Site settings → Local network access → allow the site
|
> (this server does), and the browser must be allowed to access the local
|
||||||
> (or accept the permission prompt). Without the browser permission, the
|
> network — in Chrome/Edge/Vivaldi: Site settings → Local network access →
|
||||||
> request to `127.0.0.1` is blocked before any preflight is sent.
|
> 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.
|
||||||
|
|
||||||
### Capture utility (`simtrace2-pysniff`)
|
### Capture utility (`simtrace2-pysniff`)
|
||||||
|
|
||||||
@@ -392,6 +385,71 @@ If installed via `pip install -e .`, use the console script directly:
|
|||||||
simtrace2-pysniff --format atr-time
|
simtrace2-pysniff --format atr-time
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Utility CLI Options
|
||||||
|
|
||||||
|
```
|
||||||
|
python -m simtrace2_pysniff [OPTIONS]
|
||||||
|
|
||||||
|
--format, -f FORMAT Output format (default: hex)
|
||||||
|
hex — plain hex dump
|
||||||
|
timestamp — TPDU lines prefixed with [HH:MM:SS.mmm]
|
||||||
|
atr-time — TPDU lines prefixed with [SSSS.mmm] from last ATR
|
||||||
|
--gsmtap HOST[:PORT] Send ATR/TPDU as GSMTAP over UDP (default port 4729)
|
||||||
|
--pcap FILE Write sniffed data to PCAP file for Wireshark
|
||||||
|
--output, -o FILE Write hex dump to file
|
||||||
|
--no-stdout Suppress hex dump to stdout
|
||||||
|
--vendor HEX USB vendor ID (default: 0x1d50)
|
||||||
|
--product HEX USB product ID (default: auto-detect)
|
||||||
|
|
||||||
|
Recovery options:
|
||||||
|
--no-reconnect Exit on USB disconnect instead of reconnecting
|
||||||
|
--reconnect-delay-min SEC Minimum reconnect delay (default: 1.0)
|
||||||
|
--reconnect-delay-max SEC Maximum reconnect delay cap (default: 30.0)
|
||||||
|
--backoff-factor N Exponential backoff multiplier (default: 1.5)
|
||||||
|
--inactivity-timeout SEC Reconnect after N seconds of silence (default: disabled)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output Formats
|
||||||
|
|
||||||
|
- **`hex`** (default): plain hex dump — `ATR: 3b 9e ...`, `TPDU: a0 a4 00 00 02 3f 00`
|
||||||
|
- **`timestamp`**: TPDU lines prefixed with local time — `[14:32:05.123] TPDU: a0 a4 00 00 02 3f 00`
|
||||||
|
- **`atr-time`**: TPDU lines prefixed with seconds since last ATR — `[0001.234] TPDU: a0 a4 00 00 02 3f 00`
|
||||||
|
|
||||||
|
Non-TPDU messages (ATR, PPS, card state changes, Fi/Di) print identically in all formats.
|
||||||
|
|
||||||
|
- **GSMTAP**: ATR and APDU messages over UDP (port 4729) to Wireshark
|
||||||
|
- **PCAP**: ATR/TPDU messages as Ethernet/IP/UDP/GSMTAP packets, openable in Wireshark
|
||||||
|
|
||||||
|
### Background / Daemon
|
||||||
|
|
||||||
|
SIGHUP is automatically ignored when stdin is not a terminal (i.e. run in
|
||||||
|
background or via a pipe). Foreground terminal sessions still respond to
|
||||||
|
SIGHUP normally.
|
||||||
|
|
||||||
|
To run persistently in the background with output to files:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nohup ./sniff-start.sh --output /tmp/sniff.log &> /tmp/sniff-err.log &
|
||||||
|
# or
|
||||||
|
FORMAT=timestamp nohup python -m simtrace2_pysniff --output /tmp/sniff.log &> /tmp/sniff-err.log &
|
||||||
|
```
|
||||||
|
|
||||||
|
### Library Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from simtrace2_pysniff import SniffSession
|
||||||
|
|
||||||
|
session = SniffSession(inactivity_timeout=30.0)
|
||||||
|
for msg in session.iter_messages():
|
||||||
|
print(f"{msg.type}: {msg.data.hex()}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recovery
|
||||||
|
|
||||||
|
The utility survives hardware resets, cable disconnects, and firmware hangs
|
||||||
|
by default — it reconnects with exponential backoff (1s → 30s cap).
|
||||||
|
Use `--inactivity-timeout` to also trigger reconnect on silent firmware hangs.
|
||||||
|
|
||||||
## USB Device Access
|
## USB Device Access
|
||||||
|
|
||||||
Required for capture from SIMtrace2 hardware (the utility and the server's
|
Required for capture from SIMtrace2 hardware (the utility and the server's
|
||||||
@@ -429,75 +487,95 @@ through: `start.bat --capture direct` starts the analyser server and
|
|||||||
Python 3 must be on `PATH` (verify with `python --version` in a new command
|
Python 3 must be on `PATH` (verify with `python --version` in a new command
|
||||||
prompt).
|
prompt).
|
||||||
|
|
||||||
## Utility CLI Options
|
## Components
|
||||||
|
|
||||||
```
|
- **Analysis server + UI** (`simtrace-analyser-server`) — stores captured
|
||||||
python -m simtrace2_pysniff [OPTIONS]
|
traffic in SQLite, decodes it, and serves the UI ([`frontend/`](frontend/))
|
||||||
|
plus the HTTP API on a single origin. Three capture modes: a GSMTAP
|
||||||
|
listener, direct SIMtrace2 hardware capture, or disabled (browse existing
|
||||||
|
captures only).
|
||||||
|
- **Capture utility** (`simtrace2-pysniff`) — captures SIMtrace2 hardware
|
||||||
|
traffic and writes it to stdout, GSMTAP (Wireshark), or PCAP files. It is
|
||||||
|
one GSMTAP source of several that the analyser accepts (see also
|
||||||
|
[sigrok_iso7816_stream](https://github.com/anttro/sigrok_iso7816_stream)).
|
||||||
|
- **UI** ([`frontend/`](frontend/)) — trace browser with decoded APDU,
|
||||||
|
SIM Toolkit, SMS and SCP80 OTA detail; sessions, PCAP and COMPRION trace
|
||||||
|
import, EN/RU UI.
|
||||||
|
|
||||||
--format, -f FORMAT Output format (default: hex)
|
## Features
|
||||||
hex — plain hex dump
|
|
||||||
timestamp — TPDU lines prefixed with [HH:MM:SS.mmm]
|
|
||||||
atr-time — TPDU lines prefixed with [SSSS.mmm] from last ATR
|
|
||||||
--gsmtap HOST[:PORT] Send ATR/TPDU as GSMTAP over UDP (default port 4729)
|
|
||||||
--pcap FILE Write sniffed data to PCAP file for Wireshark
|
|
||||||
--output, -o FILE Write hex dump to file
|
|
||||||
--no-stdout Suppress hex dump to stdout
|
|
||||||
--vendor HEX USB vendor ID (default: 0x1d50)
|
|
||||||
--product HEX USB product ID (default: auto-detect)
|
|
||||||
|
|
||||||
Recovery options:
|
**APDU decoder** (server-side, rendered by the UI):
|
||||||
--no-reconnect Exit on USB disconnect instead of reconnecting
|
|
||||||
--reconnect-delay-min SEC Minimum reconnect delay (default: 1.0)
|
|
||||||
--reconnect-delay-max SEC Maximum reconnect delay cap (default: 30.0)
|
|
||||||
--backoff-factor N Exponential backoff multiplier (default: 1.5)
|
|
||||||
--inactivity-timeout SEC Reconnect after N seconds of silence (default: disabled)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Output Formats
|
- Full APDU decode — CLA per ISO 7816-4 / ETSI coding with the **logical
|
||||||
|
channel 0-19** (standard channels from b2-b1, the extended channels 4-19
|
||||||
|
from b4-b1 per TS 102 221 §10.3), secure messaging and command chaining,
|
||||||
|
INS names from a spec table, SFI-aware P1/P2 for READ/UPDATE BINARY and
|
||||||
|
record commands; MANAGE CHANNEL replies show the channel the card
|
||||||
|
assigned
|
||||||
|
- Status words — full ISO 7816-4 set plus UICC-specific values:
|
||||||
|
`91XX` (proactive command pending), `62F1/F2/F3`, `63F1/F2` (more data),
|
||||||
|
`9300` (SAT busy), `9850`, `9862–64`
|
||||||
|
- SIM Toolkit — SAT (GSM 11.14), CAT (TS 102 223) and USAT (TS 31.111)
|
||||||
|
proactive commands with decoded qualifiers, TERMINAL RESPONSE, ENVELOPE
|
||||||
|
(Menu Selection, Call Control, SMS-PP download…); tolerates known
|
||||||
|
non-compliant legacy-card quirks and preserves unrecognized TLVs
|
||||||
|
- TERMINAL CAPABILITY (TS 102 221 §11.1.19) — terminal power supply
|
||||||
|
(voltage class, maximum current, clock), extended logical channels,
|
||||||
|
additional interfaces (UICC-CLF) and eUICC device capabilities
|
||||||
|
(SGP.22/SGP.32)
|
||||||
|
- SMS TPDU — SMS-DELIVER/SUBMIT, SCTS timestamps, UDH information elements
|
||||||
|
- SCP80 OTA secured packets — SPI bits (ciphering, PoR requirement/mode,
|
||||||
|
RC/CC/DS, counter), KIc/KID algorithm + key set, TAR, CNTR/PCNTR;
|
||||||
|
Response Packet (PoR) decode on GET RESPONSE; **optional client-side
|
||||||
|
decryption** of ciphered packets — paste the card's 3DES/AES key in the
|
||||||
|
detail view to reveal the real CNTR and secured application data (the
|
||||||
|
key stays in the browser, in-memory only; ciphered on-wire CNTR/PCNTR/RC
|
||||||
|
are ciphertext and labeled as such); the secured application message is
|
||||||
|
decoded as a stream of **C-APDUs** (SELECT, UPDATE BINARY, …) with
|
||||||
|
full CLA/INS/P1/P2/Lc/Le breakdown — plaintext immediately, or after
|
||||||
|
decryption for ciphered packets
|
||||||
|
- ATR parse (clock-rate conversion, Fi/Di, T-bitmask, truncated-ATR safe)
|
||||||
|
and FCP/FCI TLV; the historical bytes are decoded as the UICC's
|
||||||
|
COMPACT-TLV objects (TS 102 221 §6.3.1) — card service data, the **card
|
||||||
|
capabilities** (ISO 7816-4 §8.1.1.2.7 Table 88: the assignment methods
|
||||||
|
and the announced maximum number of logical channels, `8 or more` for the
|
||||||
|
extended-capable cards) and the trailing status indicator (LCS/SW)
|
||||||
|
|
||||||
- **`hex`** (default): plain hex dump — `ATR: 3b 9e ...`, `TPDU: a0 a4 00 00 02 3f 00`
|
**SIMtrace Analyser UI** ([`frontend/`](frontend/)):
|
||||||
- **`timestamp`**: TPDU lines prefixed with local time — `[14:32:05.123] TPDU: a0 a4 00 00 02 3f 00`
|
|
||||||
- **`atr-time`**: TPDU lines prefixed with seconds since last ATR — `[0001.234] TPDU: a0 a4 00 00 02 3f 00`
|
|
||||||
|
|
||||||
Non-TPDU messages (ATR, PPS, card state changes, Fi/Di) print identically in all formats.
|
- Trace view with Map/List modes — proactive, TERMINAL RESPONSE, ENVELOPE
|
||||||
|
and AUTH commands get distinctive styling in map view
|
||||||
|
- Sessions with deep links (`#session=N`), PCAP import (`.pcap/.pcapng/.cap`),
|
||||||
|
COMPRION trace import (the Device Manager *ISO Protocol Layer* text export),
|
||||||
|
search and a **multi-select command filter** — pick any number of commands
|
||||||
|
and/or the conversation presets (*Proactive sessions*: FETCH, TERMINAL
|
||||||
|
PROFILE and every command answered with SW 91xx; *File operations*: SELECT,
|
||||||
|
READ/UPDATE BINARY and READ/UPDATE RECORD, with the GET RESPONSE carrying a
|
||||||
|
selected command's data)
|
||||||
|
- Installable **PWA** (progressive web app: installs to the desktop or home
|
||||||
|
screen and works offline), dark theme, English/Russian UI
|
||||||
|
|
||||||
- **GSMTAP**: ATR and APDU messages over UDP (port 4729) to Wireshark
|
## Related tools
|
||||||
- **PCAP**: ATR/TPDU messages as Ethernet/IP/UDP/GSMTAP packets, openable in Wireshark
|
|
||||||
|
|
||||||
## Background / Daemon
|
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:
|
||||||
|
|
||||||
SIGHUP is automatically ignored when stdin is not a terminal (i.e. run in
|
- **[SIMple](https://github.com/anttro/simple)** — a UICC workbench: APDU
|
||||||
background or via a pipe). Foreground terminal sessions still respond to
|
construction, SCP80/SCP81 OTA, SIM Toolkit simulation and card profiling.
|
||||||
SIGHUP normally.
|
- **[SIMtrace Analyser](https://github.com/anttro/simtrace-analyser)** — an
|
||||||
|
APDU trace analyser for GSMTAP-SIM captures.
|
||||||
To run persistently in the background with output to files:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
nohup ./sniff-start.sh --output /tmp/sniff.log &> /tmp/sniff-err.log &
|
|
||||||
# or
|
|
||||||
FORMAT=timestamp nohup python -m simtrace2_pysniff --output /tmp/sniff.log &> /tmp/sniff-err.log &
|
|
||||||
```
|
|
||||||
|
|
||||||
## Library Usage
|
|
||||||
|
|
||||||
```python
|
|
||||||
from simtrace2_pysniff import SniffSession
|
|
||||||
|
|
||||||
session = SniffSession(inactivity_timeout=30.0)
|
|
||||||
for msg in session.iter_messages():
|
|
||||||
print(f"{msg.type}: {msg.data.hex()}")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Recovery
|
|
||||||
|
|
||||||
The utility survives hardware resets, cable disconnects, and firmware hangs
|
|
||||||
by default — it reconnects with exponential backoff (1s → 30s cap).
|
|
||||||
Use `--inactivity-timeout` to also trigger reconnect on silent firmware hangs.
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
- **[simtrace2](https://gitea.osmocom.org/sim-card/simtrace2)** — the upstream
|
|
||||||
SIMtrace2 hardware/firmware project that the capture utility sniffs from.
|
|
||||||
- **[sigrok_iso7816_stream](https://github.com/anttro/sigrok_iso7816_stream)** —
|
- **[sigrok_iso7816_stream](https://github.com/anttro/sigrok_iso7816_stream)** —
|
||||||
FX2LP-based passive ISO 7816 sniffer that emits the same GSMTAP-SIM stream
|
a passive ISO 7816 sniffer built on a cheap FX2 logic analyser.
|
||||||
on UDP 4729 (accepted by this project's GSMTAP listener).
|
|
||||||
|
The bundled capture utility also sniffs from the upstream
|
||||||
|
[simtrace2](https://gitea.osmocom.org/sim-card/simtrace2) hardware/firmware
|
||||||
|
project.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [LICENSE](LICENSE). The decoders and the UI's DES/3DES
|
||||||
|
implementation are original, written from the ISO 7816, ETSI and 3GPP
|
||||||
|
specifications; no third-party code is bundled. PyUSB (BSD-3-Clause) is a
|
||||||
|
runtime dependency for direct hardware capture.
|
||||||
+275
-202
@@ -1,7 +1,9 @@
|
|||||||
# SIMtrace Analyser
|
# SIMtrace Analyser — анализатор APDU-трассировок GSMTAP-SIM
|
||||||
|
|
||||||
[English](README.md) | Русский
|
[English](README.md) | Русский
|
||||||
|
|
||||||
|
📢 Новости и обновления проекта: [сайт](https://atroshin.ru) · [Telegram-канал](https://t.me/youeyeseesee)
|
||||||
|
|
||||||
Анализатор APDU-трассировок в формате GSMTAP-SIM — захватывает обмен с
|
Анализатор APDU-трассировок в формате GSMTAP-SIM — захватывает обмен с
|
||||||
SIM/UICC из UDP-потока GSMTAP (или импортирует файлы PCAP/PCAPNG),
|
SIM/UICC из UDP-потока GSMTAP (или импортирует файлы PCAP/PCAPNG),
|
||||||
декодирует его и отдаёт устанавливаемое веб-приложение для просмотра.
|
декодирует его и отдаёт устанавливаемое веб-приложение для просмотра.
|
||||||
@@ -9,134 +11,87 @@ SIM/UICC из UDP-потока GSMTAP (или импортирует файлы
|
|||||||
на Python, которая захватывает трафик с оборудования Osmocom SIMtrace2
|
на Python, которая захватывает трафик с оборудования Osmocom SIMtrace2
|
||||||
и может передавать GSMTAP этому анализатору.
|
и может передавать GSMTAP этому анализатору.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Компоненты
|

|
||||||
|
|
||||||
- **Анализирующий сервер + PWA** (`simtrace-analyser-server`) — сохраняет
|
|
||||||
захваченный трафик в SQLite, декодирует его и отдаёт PWA
|
|
||||||
([`frontend/`](frontend/)) вместе с HTTP API на одном origin. Три режима
|
|
||||||
захвата: GSMTAP-листенер, захват напрямую с оборудования SIMtrace2 или
|
|
||||||
отключённый захват (только просмотр существующих записей).
|
|
||||||
- **Утилита захвата** (`simtrace2-pysniff`) — захватывает трафик
|
|
||||||
оборудования SIMtrace2 и выводит его в stdout, GSMTAP (Wireshark) или
|
|
||||||
PCAP-файлы. Это один из нескольких источников GSMTAP, которые принимает
|
|
||||||
анализатор (см. также
|
|
||||||
[sigrok_iso7816_stream](https://github.com/anttro/sigrok_iso7816_stream)).
|
|
||||||
- **PWA** ([`frontend/`](frontend/)) — окно трассировки с декодированными
|
|
||||||
APDU, SIM Toolkit, SMS и SCP80 OTA; сессии, импорт PCAP и трасс COMPRION,
|
|
||||||
интерфейс EN/RU.
|
|
||||||
|
|
||||||
## Возможности
|
|
||||||
|
|
||||||
**Декодер APDU** (на стороне сервера, отображается в PWA):
|
|
||||||
|
|
||||||
- Полное декодирование APDU — CLA по ISO 7816-4 / ETSI с **логическим
|
|
||||||
каналом 0-19** (стандартные каналы из b2-b1, расширенные 4-19 из b4-b1
|
|
||||||
по TS 102 221 §10.3), secure messaging и chaining команд, имена INS из
|
|
||||||
таблицы спецификаций, учёт SFI в P1/P2 для READ/UPDATE BINARY и команд
|
|
||||||
с записями; ответ MANAGE CHANNEL показывает назначенный картой канал
|
|
||||||
- Статусные слова — полный набор ISO 7816-4 плюс специфичные для UICC
|
|
||||||
значения: `91XX` (ожидается proactive-команда), `62F1/F2/F3`,
|
|
||||||
`63F1/F2` (больше данных), `9300` (SAT busy), `9850`, `9862–64`
|
|
||||||
- SIM Toolkit — SAT (GSM 11.14), CAT (TS 102 223) и USAT (TS 31.111):
|
|
||||||
proactive-команды с декодированными квалификаторами, TERMINAL RESPONSE,
|
|
||||||
ENVELOPE (Menu Selection, Call Control, SMS-PP download…); учитываются
|
|
||||||
известные несоответствия старых карт, нераспознанные TLV сохраняются
|
|
||||||
- TERMINAL CAPABILITY (TS 102 221 §11.1.19) — питание терминала (класс
|
|
||||||
напряжения, максимальный ток, тактовая частота), расширенные логические
|
|
||||||
каналы, дополнительные интерфейсы (UICC-CLF) и возможности eUICC
|
|
||||||
(SGP.22/SGP.32)
|
|
||||||
- SMS TPDU — SMS-DELIVER/SUBMIT, метки времени SCTS, информационные
|
|
||||||
элементы UDH
|
|
||||||
- SCP80 OTA secured packets — биты SPI (шифрование, требование/режим PoR,
|
|
||||||
RC/CC/DS, счётчик), алгоритм KIc/KID + набор ключей, TAR, CNTR/PCNTR;
|
|
||||||
декодирование Response Packet (PoR) по GET RESPONSE; **опциональная
|
|
||||||
расшифровка на клиенте** зашифрованных пакетов — вставьте 3DES/AES-ключ
|
|
||||||
карты в панели деталей, чтобы увидеть настоящие CNTR и защищённые данные
|
|
||||||
приложения (ключ остаётся в браузере, только в памяти; зашифрованные
|
|
||||||
в эфире CNTR/PCNTR/RC — это шифротекст, и они так и помечены); защищённое
|
|
||||||
сообщение приложения декодируется как поток **C-APDU** (SELECT,
|
|
||||||
UPDATE BINARY, …) с полной разбивкой CLA/INS/P1/P2/Lc/Le — сразу для
|
|
||||||
открытого текста или после расшифровки для зашифрованных пакетов
|
|
||||||
- Разбор ATR (пересчёт тактовой частоты, Fi/Di, битовая маска T,
|
|
||||||
безопасен для усечённых ATR) и FCP/FCI TLV; исторические байты
|
|
||||||
разбираются как COMPACT-TLV объекты UICC (TS 102 221 §6.3.1) — card
|
|
||||||
service data, **card capabilities** (ISO 7816-4 §8.1.1.2.7 таблица 88:
|
|
||||||
способы назначения и заявленное максимальное число логических каналов,
|
|
||||||
`8 or more` у карт с расширенными каналами) и завершающий индикатор
|
|
||||||
состояния (LCS/SW)
|
|
||||||
|
|
||||||
**PWA SIMtrace Analyser** ([`frontend/`](frontend/)):
|
|
||||||
|
|
||||||
- Окно трассировки с режимами «Карта»/«Список» — proactive,
|
|
||||||
TERMINAL RESPONSE, ENVELOPE и AUTH-команды выделяются в режиме карты
|
|
||||||
- Сессии с прямыми ссылками (`#session=N`), импорт PCAP
|
|
||||||
(`.pcap/.pcapng/.cap`), импорт трасс COMPRION (текстовый экспорт
|
|
||||||
«ISO Protocol Layer» из Device Manager), поиск и **фильтр команд с
|
|
||||||
множественным выбором** — любые команды и/или готовые пресеты («Проактивные
|
|
||||||
сессии»: FETCH, TERMINAL PROFILE и все команды с ответом SW 91xx;
|
|
||||||
«Файловые операции»: SELECT,
|
|
||||||
READ/UPDATE BINARY и READ/UPDATE RECORD, вместе с GET RESPONSE, несущим
|
|
||||||
данные выбранной команды)
|
|
||||||
- Устанавливаемая PWA, тёмная тема, интерфейс на английском/русском
|
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
- **Python 3.9+**
|
**Обязательно**
|
||||||
- **PyUSB** — `pip install pyusb` (нужен только для захвата с оборудования
|
|
||||||
SIMtrace2; захват GSMTAP/UDP и импорт PCAP работают без него)
|
- **Python 3.9+** — для захвата GSMTAP, импорта PCAP и просмотра достаточно
|
||||||
|
стандартной библиотеки.
|
||||||
|
|
||||||
|
**Только для захвата с оборудования Osmocom SIMtrace2** (`--capture direct` и
|
||||||
|
встроенная утилита `simtrace2-pysniff`):
|
||||||
|
|
||||||
|
- **PyUSB** — `pip install pyusb`
|
||||||
- **libusb** — системная библиотека, обычно уже есть в Linux; в Windows
|
- **libusb** — системная библиотека, обычно уже есть в Linux; в Windows
|
||||||
нужно заменить драйвер SIMtrace2
|
нужно заменить драйвер SIMtrace2
|
||||||
(см. [Доступ к USB-устройству](#доступ-к-usb-устройству))
|
(см. [Доступ к USB-устройству](#доступ-к-usb-устройству))
|
||||||
- **Оборудование SIMtrace2** с прошивкой в режиме *trace* (только для
|
- **Оборудование SIMtrace2** с прошивкой в режиме *trace* (VID `1d50`, PID
|
||||||
прямого захвата; VID `1d50`, PID `60e3`, USB class `ff`, subclass `01`).
|
`60e3`, USB class `ff`, subclass `01`); сборка и прошивка trace-прошивки
|
||||||
О сборке и прошивке trace-прошивки см.
|
описаны в
|
||||||
[проект SIMtrace2](https://gitea.osmocom.org/sim-card/simtrace2).
|
[проекте SIMtrace2](https://gitea.osmocom.org/sim-card/simtrace2).
|
||||||
|
|
||||||
Других зависимостей нет. Вывод PCAP, GSMTAP и hex dump использует только
|
Для `--capture gsmtap` (источник GSMTAP может быть даже на другой машине),
|
||||||
стандартную библиотеку Python.
|
`--capture disabled` и импорта PCAP анализатору ничего из этого не нужно.
|
||||||
|
|
||||||
## Установка
|
## Быстрый старт
|
||||||
|
|
||||||
Есть два способа получить анализатор: установить артефакт релиза (без истории
|
Три способа запустить анализатор; все они отдают один и тот же веб-интерфейс на
|
||||||
git) или склонировать репозиторий для разработки. В обоих случаях в комплекте
|
http://127.0.0.1:8081/.
|
||||||
одна и та же PWA.
|
|
||||||
|
|
||||||
### Из релиза (без истории git)
|
### 1. Wheel (pip)
|
||||||
|
|
||||||
Всё находится на странице
|
Wheel содержит интерфейс внутри пакета, поэтому сервер отдаёт его сам:
|
||||||
[последнего релиза](https://github.com/anttro/simtrace-analyser/releases/latest).
|
|
||||||
|
|
||||||
**Wheel** (рекомендуется — изолированная установка, PWA внутри пакета):
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pipx install ./simtrace_analyser-2.7.11-py3-none-any.whl
|
pip install ./simtrace_analyser-2.7.13-py3-none-any.whl
|
||||||
# или: pip install ./simtrace_analyser-2.7.11-py3-none-any.whl
|
|
||||||
|
|
||||||
# затем запуск (интерфейс отдаётся на http://127.0.0.1:8081/):
|
|
||||||
simtrace-analyser-server --capture gsmtap
|
simtrace-analyser-server --capture gsmtap
|
||||||
```
|
```
|
||||||
|
|
||||||
`pip` также может скачать wheel прямо со страницы релиза (подставьте версию,
|
Если системный Python управляется извне (PEP 668 — обычное дело в Arch и
|
||||||
указанную там):
|
Fedora), сначала создайте виртуальное окружение:
|
||||||
|
`python3 -m venv .venv && . .venv/bin/activate`.
|
||||||
|
|
||||||
```sh
|
Все артефакты лежат на странице
|
||||||
pip install https://github.com/anttro/simtrace-analyser/releases/download/v2.7.11/simtrace_analyser-2.7.11-py3-none-any.whl
|
[последнего релиза](https://github.com/anttro/simtrace-analyser/releases/latest).
|
||||||
```
|
`pip` может скачать wheel напрямую (подставьте версию со страницы), а GitHub
|
||||||
|
CLI — выбрать последнюю:
|
||||||
С GitHub CLI можно скачать wheel последнего релиза, не зная версии:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
pip install https://github.com/anttro/simtrace-analyser/releases/download/v2.7.13/simtrace_analyser-2.7.13-py3-none-any.whl
|
||||||
|
# или:
|
||||||
gh release download --pattern '*.whl'
|
gh release download --pattern '*.whl'
|
||||||
pipx install ./simtrace_analyser-*.whl
|
pip install ./simtrace_analyser-*.whl
|
||||||
```
|
```
|
||||||
|
|
||||||
**Архив исходников** — ассеты *Source code (zip / tar.gz)* содержат полное
|
### 2. Zip (без установки)
|
||||||
дерево без истории git: распакуйте, перейдите в каталог и используйте скрипты
|
|
||||||
запуска ниже (`./start.sh` в Linux/macOS, `start.bat` в Windows).
|
|
||||||
|
|
||||||
### Из клона репозитория (для разработки)
|
Скачайте `simtrace-analyser-2.7.13.zip` со страницы релиза и распакуйте в любое
|
||||||
|
место: это рабочее дерево (пакеты Python, интерфейс, скрипты запуска и документация —
|
||||||
|
без тестов и git). Архив подходит для любой ОС. Затем запустите.
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
start.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux/macOS:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 3 должен быть в `PATH`; `pip install pyusb` нужен только для прямого
|
||||||
|
захвата с SIMtrace2. Скрипты принимают параметры сервера
|
||||||
|
(`start.bat --capture direct`, `./start.sh --capture direct --port 8080`).
|
||||||
|
(Архивы *Source code (zip / tar.gz)* из релиза — это полное дерево вместе с
|
||||||
|
тестами; они работают так же, как клон ниже.)
|
||||||
|
|
||||||
|
### 3. Клон репозитория (для разработки)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/anttro/simtrace-analyser.git
|
git clone https://github.com/anttro/simtrace-analyser.git
|
||||||
@@ -145,30 +100,38 @@ cd simtrace-analyser
|
|||||||
```
|
```
|
||||||
|
|
||||||
Скрипты запуска добавляют каталог в `PYTHONPATH` и запускают
|
Скрипты запуска добавляют каталог в `PYTHONPATH` и запускают
|
||||||
`python -m simtrace_analyser`, поэтому для просмотра и захвата GSMTAP
|
`python -m simtrace_analyser`, поэтому для просмотра и захвата GSMTAP ничего
|
||||||
установка не нужна; они принимают параметры сервера
|
устанавливать не нужно; их параметры — это параметры сервера
|
||||||
(`./start.sh --capture direct --port 8080`). Для прямого захвата с
|
(`./start.sh --capture direct --port 8080`). Для прямого захвата с
|
||||||
оборудования нужен **PyUSB** (`pip install pyusb`).
|
оборудования нужен **PyUSB** (`pip install pyusb`). Чтобы получить консольные
|
||||||
|
команды (`simtrace-analyser-server`, `simtrace2-pysniff`), установите пакет:
|
||||||
Чтобы получить консольные команды (`simtrace-analyser-server`,
|
|
||||||
`simtrace2-pysniff`), установите пакет:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pip install -e .
|
pip install -e .
|
||||||
simtrace-analyser-server --capture gsmtap
|
simtrace-analyser-server --capture gsmtap
|
||||||
```
|
```
|
||||||
|
|
||||||
В Windows скрипты `.bat` вызывают команду `python`, поэтому Python 3 должен
|
### Первый запуск
|
||||||
быть в `PATH` (проверьте `python --version`); для прямого захвата по USB
|
|
||||||
дополнительно нужна замена драйвера, описанная в разделе
|
|
||||||
[Доступ к USB-устройству](#доступ-к-usb-устройству).
|
|
||||||
|
|
||||||
## Запуск
|
Откройте **http://127.0.0.1:8081/** в браузере. `Ctrl+C` останавливает
|
||||||
|
сервер.
|
||||||
|
|
||||||
### Анализирующий сервер + PWA (`simtrace-analyser-server`)
|
- `--capture gsmtap` (по умолчанию) — слушать GSMTAP-SIM на UDP 4729,
|
||||||
|
например от `simtrace2-pysniff` или `sigrok_iso7816_stream`;
|
||||||
|
- `--capture direct` — читать оборудование SIMtrace2 напрямую (нужен PyUSB; в
|
||||||
|
Windows также нужна замена драйвера, см.
|
||||||
|
[Доступ к USB-устройству](#доступ-к-usb-устройству));
|
||||||
|
- `--capture disabled` — только просмотр сохранённых захватов.
|
||||||
|
|
||||||
|
Полное руководство по работе (параметры сервера, экраны интерфейса, утилита захвата)
|
||||||
|
находится в разделе [Использование](#использование).
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### Анализирующий сервер + интерфейс (`simtrace-analyser-server`)
|
||||||
|
|
||||||
Сохраняет захваченный APDU-трафик в SQLite и отдаёт его по HTTP вместе
|
Сохраняет захваченный APDU-трафик в SQLite и отдаёт его по HTTP вместе
|
||||||
со встроенной PWA **SIMtrace Analyser** (`frontend/` в исходном клоне, при
|
со встроенным фронтендом **SIMtrace Analyser** (`frontend/` в исходном клоне, при
|
||||||
установке — внутри пакета) на том же origin.
|
установке — внутри пакета) на том же origin.
|
||||||
Три режима захвата:
|
Три режима захвата:
|
||||||
|
|
||||||
@@ -183,6 +146,27 @@ simtrace-analyser-server --capture direct
|
|||||||
simtrace-analyser-server --capture disabled
|
simtrace-analyser-server --capture disabled
|
||||||
```
|
```
|
||||||
|
|
||||||
|
При запуске из клона или рабочего zip те же режимы доступны через скрипты
|
||||||
|
запуска.
|
||||||
|
|
||||||
|
Linux/macOS:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./start.sh --capture gsmtap
|
||||||
|
./start.sh --capture direct
|
||||||
|
./start.sh --capture disabled
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
start.bat --capture gsmtap
|
||||||
|
start.bat --capture direct
|
||||||
|
start.bat --capture disabled
|
||||||
|
```
|
||||||
|
|
||||||
|
Параметры передаются как есть (`./start.sh --capture direct --port 8080`).
|
||||||
|
|
||||||
Сервер пишет в stderr информацию о жизненном цикле захвата (каждая строка
|
Сервер пишет в stderr информацию о жизненном цикле захвата (каждая строка
|
||||||
с локальным временем): `Capture started` при старте, периодический
|
с локальным временем): `Capture started` при старте, периодический
|
||||||
heartbeat `Capture alive: session=N messages=M bytes=B dropped=D skipped=S`
|
heartbeat `Capture alive: session=N messages=M bytes=B dropped=D skipped=S`
|
||||||
@@ -192,7 +176,7 @@ heartbeat `Capture alive: session=N messages=M bytes=B dropped=D skipped=S`
|
|||||||
убивают захват; `tpdu`-пакет, вовсе не похожий на APDU (короче заголовка,
|
убивают захват; `tpdu`-пакет, вовсе не похожий на APDU (короче заголовка,
|
||||||
недопустимый CLA `FF` или зарезервированный ISO класс `001x`),
|
недопустимый CLA `FF` или зарезервированный ISO класс `001x`),
|
||||||
пропускается и считается (`skipped`) — посимвольный или миссемплированный
|
пропускается и считается (`skipped`) — посимвольный или миссемплированный
|
||||||
поток больше не засоряет декодирование. Интервал heartbeat по умолчанию
|
поток не засоряет декодирование. Интервал heartbeat по умолчанию
|
||||||
60 с, настраивается через `--log-interval SECONDS`.
|
60 с, настраивается через `--log-interval SECONDS`.
|
||||||
|
|
||||||
GSMTAP-листенер также принимает декодер **sigrok-iso7816-stream**
|
GSMTAP-листенер также принимает декодер **sigrok-iso7816-stream**
|
||||||
@@ -221,7 +205,7 @@ RST несёт измеренную частоту CLK (декодер v1.9.0+,
|
|||||||
(табулированный текст) из COMPRION Device Manager — байтовое представление
|
(табулированный текст) из COMPRION Device Manager — байтовое представление
|
||||||
протокола (ATR, PPS, APDU с кодами возврата). Экспорт *ISO Application
|
протокола (ATR, PPS, APDU с кодами возврата). Экспорт *ISO Application
|
||||||
Layer* не содержит необработанных байтов ответов и не нужен; при его выборе
|
Layer* не содержит необработанных байтов ответов и не нужен; при его выборе
|
||||||
выводится указание на файл Protocol Layer (та же проверка выполняется в PWA
|
выводится указание на файл Protocol Layer (та же проверка выполняется в интерфейсе
|
||||||
до загрузки и на сервере — для вызовов API).
|
до загрузки и на сервере — для вызовов API).
|
||||||
|
|
||||||
Обмены перекодируются в такие же «проводные» TPDU, какие несёт захват
|
Обмены перекодируются в такие же «проводные» TPDU, какие несёт захват
|
||||||
@@ -340,25 +324,27 @@ TS 51.011), поэтому FID, повторно используемый в р
|
|||||||
--db FILE путь к базе SQLite (по умолчанию: ~/.simtrace-analyser/sessions.db)
|
--db FILE путь к базе SQLite (по умолчанию: ~/.simtrace-analyser/sessions.db)
|
||||||
--capture MODE gsmtap | direct | disabled (по умолчанию: gsmtap)
|
--capture MODE gsmtap | direct | disabled (по умолчанию: gsmtap)
|
||||||
--gsmtap-port PORT UDP-порт листенера GSMTAP (по умолчанию: 4729)
|
--gsmtap-port PORT UDP-порт листенера GSMTAP (по умолчанию: 4729)
|
||||||
--web-dir PATH каталог статических файлов PWA (по умолчанию: PWA внутри
|
--gsmtap-bind ADDR UDP-адрес привязки листенера GSMTAP (по умолчанию: 127.0.0.1;
|
||||||
установленного пакета или <repo>/frontend при запуске из исходников)
|
используйте 0.0.0.0, чтобы принимать из Docker-сети или с другого хоста)
|
||||||
|
--web-dir PATH каталог статических файлов интерфейса (по умолчанию:
|
||||||
|
фронтенд внутри установленного пакета или
|
||||||
|
<repo>/frontend при запуске из исходников)
|
||||||
```
|
```
|
||||||
|
|
||||||
Затем откройте **http://127.0.0.1:8081/** в браузере — PWA отдаётся
|
Затем откройте **http://127.0.0.1:8081/** в браузере. Используйте
|
||||||
сервером, поэтому UI и API используют один origin, и настройка
|
`--web-dir PATH`, чтобы отдавать другой каталог интерфейса (по умолчанию:
|
||||||
CORS/Private-Network-Access не требуется. Используйте `--web-dir PATH`,
|
фронтенд внутри пакета или `<repo>/frontend` при запуске из исходников).
|
||||||
чтобы отдавать другой каталог PWA (по умолчанию: PWA внутри пакета или
|
|
||||||
`<repo>/frontend` при запуске из исходников).
|
|
||||||
|
|
||||||
### Размещённая PWA (лендинг)
|
### Размещённый интерфейс (лендинг)
|
||||||
|
|
||||||
Автономная копия PWA размещена на **https://simtrace.atroshin.ru**.
|
Автономная копия интерфейса размещена на **https://simtrace.atroshin.ru**.
|
||||||
Это чистый фронтенд: укажите в нём (Settings → Server URL) адрес
|
Это только интерфейс: укажите в нём (Settings → Server URL) адрес
|
||||||
локально запущенного `simtrace-analyser-server`.
|
локально запущенного `simtrace-analyser-server`.
|
||||||
|
|
||||||
> **Ограничение браузера:** когда PWA отдаётся с публичного HTTPS-хоста,
|
> **Ограничение браузера:** это единственная схема, где интерфейс и API
|
||||||
> доступ к локальному серверу (`http://127.0.0.1:8081`) требует двух
|
> находятся на разных origin, поэтому браузер применяет правила CORS и
|
||||||
> вещей: сервер должен отправлять
|
> Private Network Access. Доступ к локальному серверу
|
||||||
|
> (`http://127.0.0.1:8081`) требует двух вещей: сервер должен отправлять
|
||||||
> `Access-Control-Allow-Private-Network: true` (этот сервер отправляет),
|
> `Access-Control-Allow-Private-Network: true` (этот сервер отправляет),
|
||||||
> и браузер должен иметь разрешение на доступ к локальной сети —
|
> и браузер должен иметь разрешение на доступ к локальной сети —
|
||||||
> в Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети →
|
> в Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети →
|
||||||
@@ -404,6 +390,77 @@ python -m simtrace2_pysniff --pcap trace.pcap
|
|||||||
simtrace2-pysniff --format atr-time
|
simtrace2-pysniff --format atr-time
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Параметры CLI утилиты
|
||||||
|
|
||||||
|
```
|
||||||
|
python -m simtrace2_pysniff [OPTIONS]
|
||||||
|
|
||||||
|
--format, -f FORMAT Формат вывода (по умолчанию: hex)
|
||||||
|
hex — обычный hex-дамп
|
||||||
|
timestamp — строки TPDU с префиксом [ЧЧ:ММ:СС.мс]
|
||||||
|
atr-time — строки TPDU с префиксом [СССС.мс] от последнего ATR
|
||||||
|
--gsmtap HOST[:PORT] Отправлять ATR/TPDU как GSMTAP по UDP (порт по умолчанию 4729)
|
||||||
|
--pcap FILE Записывать перехваченные данные в PCAP для Wireshark
|
||||||
|
--output, -o FILE Записывать hex-дамп в файл
|
||||||
|
--no-stdout Не выводить hex-дамп в stdout
|
||||||
|
--vendor HEX USB vendor ID (по умолчанию: 0x1d50)
|
||||||
|
--product HEX USB product ID (по умолчанию: автоопределение)
|
||||||
|
|
||||||
|
Параметры восстановления:
|
||||||
|
--no-reconnect Выходить при отключении USB вместо переподключения
|
||||||
|
--reconnect-delay-min SEC Минимальная задержка переподключения (по умолчанию: 1.0)
|
||||||
|
--reconnect-delay-max SEC Максимальная задержка переподключения (по умолчанию: 30.0)
|
||||||
|
--backoff-factor N Множитель экспоненциальной задержки (по умолчанию: 1.5)
|
||||||
|
--inactivity-timeout SEC Переподключаться после N секунд тишины (по умолчанию: выключено)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Форматы вывода
|
||||||
|
|
||||||
|
- **`hex`** (по умолчанию): обычный hex-дамп — `ATR: 3b 9e ...`,
|
||||||
|
`TPDU: a0 a4 00 00 02 3f 00`
|
||||||
|
- **`timestamp`**: строки TPDU с локальным временем —
|
||||||
|
`[14:32:05.123] TPDU: a0 a4 00 00 02 3f 00`
|
||||||
|
- **`atr-time`**: строки TPDU с секундами от последнего ATR —
|
||||||
|
`[0001.234] TPDU: a0 a4 00 00 02 3f 00`
|
||||||
|
|
||||||
|
Сообщения, не являющиеся TPDU (ATR, PPS, изменения состояния карты, Fi/Di),
|
||||||
|
выводятся одинаково во всех форматах.
|
||||||
|
|
||||||
|
- **GSMTAP**: сообщения ATR и APDU по UDP (порт 4729) в Wireshark
|
||||||
|
- **PCAP**: сообщения ATR/TPDU как пакеты Ethernet/IP/UDP/GSMTAP,
|
||||||
|
открываются в Wireshark
|
||||||
|
|
||||||
|
### Фоновый режим / демон
|
||||||
|
|
||||||
|
SIGHUP автоматически игнорируется, когда stdin не является терминалом
|
||||||
|
(т.е. при запуске в фоне или через канал). Сессии в терминале на переднем
|
||||||
|
плане по-прежнему обрабатывают SIGHUP обычным образом.
|
||||||
|
|
||||||
|
Для постоянной работы в фоне с выводом в файлы:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nohup ./sniff-start.sh --output /tmp/sniff.log &> /tmp/sniff-err.log &
|
||||||
|
# или
|
||||||
|
FORMAT=timestamp nohup python -m simtrace2_pysniff --output /tmp/sniff.log &> /tmp/sniff-err.log &
|
||||||
|
```
|
||||||
|
|
||||||
|
### Использование как библиотеки
|
||||||
|
|
||||||
|
```python
|
||||||
|
from simtrace2_pysniff import SniffSession
|
||||||
|
|
||||||
|
session = SniffSession(inactivity_timeout=30.0)
|
||||||
|
for msg in session.iter_messages():
|
||||||
|
print(f"{msg.type}: {msg.data.hex()}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Восстановление
|
||||||
|
|
||||||
|
Утилита по умолчанию переживает сбросы оборудования, отключения кабеля
|
||||||
|
и зависания прошивки — она переподключается с экспоненциальной задержкой
|
||||||
|
(1с → 30с). Используйте `--inactivity-timeout`, чтобы также переподключаться
|
||||||
|
при «тихих» зависаниях прошивки.
|
||||||
|
|
||||||
## Доступ к USB-устройству
|
## Доступ к USB-устройству
|
||||||
|
|
||||||
Нужен для захвата с оборудования SIMtrace2 (утилита и режим
|
Нужен для захвата с оборудования SIMtrace2 (утилита и режим
|
||||||
@@ -442,82 +499,98 @@ sudo udevadm trigger
|
|||||||
Python 3 должен быть в `PATH` (проверьте `python --version` в новом окне
|
Python 3 должен быть в `PATH` (проверьте `python --version` в новом окне
|
||||||
команды).
|
команды).
|
||||||
|
|
||||||
## Параметры CLI утилиты
|
## Компоненты
|
||||||
|
|
||||||
```
|
- **Анализирующий сервер + интерфейс** (`simtrace-analyser-server`) — сохраняет
|
||||||
python -m simtrace2_pysniff [OPTIONS]
|
захваченный трафик в SQLite, декодирует его и отдаёт интерфейс
|
||||||
|
([`frontend/`](frontend/)) вместе с HTTP API на одном origin. Три режима
|
||||||
|
захвата: GSMTAP-листенер, захват напрямую с оборудования SIMtrace2 или
|
||||||
|
отключённый захват (только просмотр существующих записей).
|
||||||
|
- **Утилита захвата** (`simtrace2-pysniff`) — захватывает трафик
|
||||||
|
оборудования SIMtrace2 и выводит его в stdout, GSMTAP (Wireshark) или
|
||||||
|
PCAP-файлы. Это один из нескольких источников GSMTAP, которые принимает
|
||||||
|
анализатор (см. также
|
||||||
|
[sigrok_iso7816_stream](https://github.com/anttro/sigrok_iso7816_stream)).
|
||||||
|
- **Интерфейс** ([`frontend/`](frontend/)) — окно трассировки с декодированными
|
||||||
|
APDU, SIM Toolkit, SMS и SCP80 OTA; сессии, импорт PCAP и трасс COMPRION,
|
||||||
|
интерфейс EN/RU.
|
||||||
|
|
||||||
--format, -f FORMAT Формат вывода (по умолчанию: hex)
|
## Возможности
|
||||||
hex — обычный hex-дамп
|
|
||||||
timestamp — строки TPDU с префиксом [ЧЧ:ММ:СС.мс]
|
|
||||||
atr-time — строки TPDU с префиксом [СССС.мс] от последнего ATR
|
|
||||||
--gsmtap HOST[:PORT] Отправлять ATR/TPDU как GSMTAP по UDP (порт по умолчанию 4729)
|
|
||||||
--pcap FILE Записывать перехваченные данные в PCAP для Wireshark
|
|
||||||
--output, -o FILE Записывать hex-дамп в файл
|
|
||||||
--no-stdout Не выводить hex-дамп в stdout
|
|
||||||
--vendor HEX USB vendor ID (по умолчанию: 0x1d50)
|
|
||||||
--product HEX USB product ID (по умолчанию: автоопределение)
|
|
||||||
|
|
||||||
Параметры восстановления:
|
**Декодер APDU** (на стороне сервера, отображается в интерфейсе):
|
||||||
--no-reconnect Выходить при отключении USB вместо переподключения
|
|
||||||
--reconnect-delay-min SEC Минимальная задержка переподключения (по умолчанию: 1.0)
|
|
||||||
--reconnect-delay-max SEC Максимальная задержка переподключения (по умолчанию: 30.0)
|
|
||||||
--backoff-factor N Множитель экспоненциальной задержки (по умолчанию: 1.5)
|
|
||||||
--inactivity-timeout SEC Переподключаться после N секунд тишины (по умолчанию: выключено)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Форматы вывода
|
- Полное декодирование APDU — CLA по ISO 7816-4 / ETSI с **логическим
|
||||||
|
каналом 0-19** (стандартные каналы из b2-b1, расширенные 4-19 из b4-b1
|
||||||
|
по TS 102 221 §10.3), secure messaging и chaining команд, имена INS из
|
||||||
|
таблицы спецификаций, учёт SFI в P1/P2 для READ/UPDATE BINARY и команд
|
||||||
|
с записями; ответ MANAGE CHANNEL показывает назначенный картой канал
|
||||||
|
- Статусные слова — полный набор ISO 7816-4 плюс специфичные для UICC
|
||||||
|
значения: `91XX` (ожидается proactive-команда), `62F1/F2/F3`,
|
||||||
|
`63F1/F2` (больше данных), `9300` (SAT busy), `9850`, `9862–64`
|
||||||
|
- SIM Toolkit — SAT (GSM 11.14), CAT (TS 102 223) и USAT (TS 31.111):
|
||||||
|
proactive-команды с декодированными квалификаторами, TERMINAL RESPONSE,
|
||||||
|
ENVELOPE (Menu Selection, Call Control, SMS-PP download…); учитываются
|
||||||
|
известные несоответствия старых карт, нераспознанные TLV сохраняются
|
||||||
|
- TERMINAL CAPABILITY (TS 102 221 §11.1.19) — питание терминала (класс
|
||||||
|
напряжения, максимальный ток, тактовая частота), расширенные логические
|
||||||
|
каналы, дополнительные интерфейсы (UICC-CLF) и возможности eUICC
|
||||||
|
(SGP.22/SGP.32)
|
||||||
|
- SMS TPDU — SMS-DELIVER/SUBMIT, метки времени SCTS, информационные
|
||||||
|
элементы UDH
|
||||||
|
- SCP80 OTA secured packets — биты SPI (шифрование, требование/режим PoR,
|
||||||
|
RC/CC/DS, счётчик), алгоритм KIc/KID + набор ключей, TAR, CNTR/PCNTR;
|
||||||
|
декодирование Response Packet (PoR) по GET RESPONSE; **опциональная
|
||||||
|
расшифровка на клиенте** зашифрованных пакетов — вставьте 3DES/AES-ключ
|
||||||
|
карты в панели деталей, чтобы увидеть настоящие CNTR и защищённые данные
|
||||||
|
приложения (ключ остаётся в браузере, только в памяти; зашифрованные
|
||||||
|
в эфире CNTR/PCNTR/RC — это шифротекст, и они так и помечены); защищённое
|
||||||
|
сообщение приложения декодируется как поток **C-APDU** (SELECT,
|
||||||
|
UPDATE BINARY, …) с полной разбивкой CLA/INS/P1/P2/Lc/Le — сразу для
|
||||||
|
открытого текста или после расшифровки для зашифрованных пакетов
|
||||||
|
- Разбор ATR (пересчёт тактовой частоты, Fi/Di, битовая маска T,
|
||||||
|
безопасен для усечённых ATR) и FCP/FCI TLV; исторические байты
|
||||||
|
разбираются как COMPACT-TLV объекты UICC (TS 102 221 §6.3.1) — card
|
||||||
|
service data, **card capabilities** (ISO 7816-4 §8.1.1.2.7 таблица 88:
|
||||||
|
способы назначения и заявленное максимальное число логических каналов,
|
||||||
|
`8 or more` у карт с расширенными каналами) и завершающий индикатор
|
||||||
|
состояния (LCS/SW)
|
||||||
|
|
||||||
- **`hex`** (по умолчанию): обычный hex-дамп — `ATR: 3b 9e ...`,
|
**Интерфейс SIMtrace Analyser** ([`frontend/`](frontend/)):
|
||||||
`TPDU: a0 a4 00 00 02 3f 00`
|
|
||||||
- **`timestamp`**: строки TPDU с локальным временем —
|
|
||||||
`[14:32:05.123] TPDU: a0 a4 00 00 02 3f 00`
|
|
||||||
- **`atr-time`**: строки TPDU с секундами от последнего ATR —
|
|
||||||
`[0001.234] TPDU: a0 a4 00 00 02 3f 00`
|
|
||||||
|
|
||||||
Сообщения, не являющиеся TPDU (ATR, PPS, изменения состояния карты, Fi/Di),
|
- Окно трассировки с режимами «Карта»/«Список» — proactive,
|
||||||
выводятся одинаково во всех форматах.
|
TERMINAL RESPONSE, ENVELOPE и AUTH-команды выделяются в режиме карты
|
||||||
|
- Сессии с прямыми ссылками (`#session=N`), импорт PCAP
|
||||||
|
(`.pcap/.pcapng/.cap`), импорт трасс COMPRION (текстовый экспорт
|
||||||
|
«ISO Protocol Layer» из Device Manager), поиск и **фильтр команд с
|
||||||
|
множественным выбором** — любые команды и/или готовые пресеты («Проактивные
|
||||||
|
сессии»: FETCH, TERMINAL PROFILE и все команды с ответом SW 91xx;
|
||||||
|
«Файловые операции»: SELECT,
|
||||||
|
READ/UPDATE BINARY и READ/UPDATE RECORD, вместе с GET RESPONSE, несущим
|
||||||
|
данные выбранной команды)
|
||||||
|
- **PWA** (progressive web app): устанавливается на рабочий стол или домашний
|
||||||
|
экран и работает офлайн; тёмная тема, интерфейс на английском/русском
|
||||||
|
|
||||||
- **GSMTAP**: сообщения ATR и APDU по UDP (порт 4729) в Wireshark
|
## Связанные инструменты
|
||||||
- **PCAP**: сообщения ATR/TPDU как пакеты Ethernet/IP/UDP/GSMTAP,
|
|
||||||
открываются в Wireshark
|
|
||||||
|
|
||||||
## Фоновый режим / демон
|
SIMple, SIMtrace Analyser и sigrok_iso7816_stream — три автономных
|
||||||
|
инструмента для работы с SIM/UICC-картами, представленные на одном
|
||||||
|
[сайте](https://atroshin.ru) и в одном [Telegram-канале](https://t.me/youeyeseesee).
|
||||||
|
Каждый полностью независим — устанавливайте только нужные вам:
|
||||||
|
|
||||||
SIGHUP автоматически игнорируется, когда stdin не является терминалом
|
- **[SIMple](https://github.com/anttro/simple)** — мастерская для UICC/SIM:
|
||||||
(т.е. при запуске в фоне или через канал). Сессии в терминале на переднем
|
конструкторы APDU, OTA SCP80/SCP81, симуляция SIM Toolkit и профилирование
|
||||||
плане по-прежнему обрабатывают SIGHUP обычным образом.
|
карт.
|
||||||
|
- **[SIMtrace Analyser](https://github.com/anttro/simtrace-analyser)** —
|
||||||
Для постоянной работы в фоне с выводом в файлы:
|
анализатор APDU-трассировок GSMTAP-SIM.
|
||||||
|
|
||||||
```sh
|
|
||||||
nohup ./sniff-start.sh --output /tmp/sniff.log &> /tmp/sniff-err.log &
|
|
||||||
# или
|
|
||||||
FORMAT=timestamp nohup python -m simtrace2_pysniff --output /tmp/sniff.log &> /tmp/sniff-err.log &
|
|
||||||
```
|
|
||||||
|
|
||||||
## Использование как библиотеки
|
|
||||||
|
|
||||||
```python
|
|
||||||
from simtrace2_pysniff import SniffSession
|
|
||||||
|
|
||||||
session = SniffSession(inactivity_timeout=30.0)
|
|
||||||
for msg in session.iter_messages():
|
|
||||||
print(f"{msg.type}: {msg.data.hex()}")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Восстановление
|
|
||||||
|
|
||||||
Утилита по умолчанию переживает сбросы оборудования, отключения кабеля
|
|
||||||
и зависания прошивки — она переподключается с экспоненциальной задержкой
|
|
||||||
(1с → 30с). Используйте `--inactivity-timeout`, чтобы также переподключаться
|
|
||||||
при «тихих» зависаниях прошивки.
|
|
||||||
|
|
||||||
## Связанные проекты
|
|
||||||
|
|
||||||
- **[simtrace2](https://gitea.osmocom.org/sim-card/simtrace2)** — вышестоящий
|
|
||||||
проект оборудования/прошивки SIMtrace2, с которого утилита захвата
|
|
||||||
снимает трафик.
|
|
||||||
- **[sigrok_iso7816_stream](https://github.com/anttro/sigrok_iso7816_stream)** —
|
- **[sigrok_iso7816_stream](https://github.com/anttro/sigrok_iso7816_stream)** —
|
||||||
пассивный ISO 7816 сниффер на базе FX2LP, выдающий такой же поток
|
пассивный ISO 7816-сниффер на дешёвом логическом анализаторе FX2.
|
||||||
GSMTAP-SIM на UDP 4729 (принимается GSMTAP-листенером этого проекта).
|
|
||||||
|
Встроенная утилита захвата также снимает трафик с вышестоящего проекта
|
||||||
|
оборудования/прошивки [simtrace2](https://gitea.osmocom.org/sim-card/simtrace2).
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
MIT — см. [LICENSE](LICENSE). Декодеры и реализация DES/3DES в интерфейсе написаны
|
||||||
|
самостоятельно по спецификациям ISO 7816, ETSI и 3GPP; сторонний код в проект
|
||||||
|
не включён. PyUSB (BSD-3-Clause) — зависимость для прямого захвата с
|
||||||
|
оборудования.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="640" viewBox="0 0 960 640" role="img" aria-labelledby="diagram-title diagram-desc" font-family="'Segoe UI', 'Helvetica Neue', Arial, 'DejaVu Sans', sans-serif">
|
||||||
|
<title id="diagram-title">SIMtrace Analyser: capture and analysis</title>
|
||||||
|
<desc id="diagram-desc">simtrace2-pysniff (SIMtrace2 hardware) and sigrok_iso7816_stream (FX2 logic analyzer) stream GSMTAP-SIM to the listener on UDP :4729; alternatively the server reads SIMtrace2 hardware directly over USB, and PCAP/COMPRION traces are imported from files. simtrace-analyser-server decodes and stores the traffic in SQLite and serves the web UI.</desc>
|
||||||
|
<defs>
|
||||||
|
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||||
|
<path d="M0 0 L10 5 L0 10 Z" fill="#64748b"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
<rect x="1" y="1" width="958" height="638" rx="18" fill="#ffffff" stroke="#e2e8f0" stroke-width="1.5"/>
|
||||||
|
<text x="480" y="30" text-anchor="middle" font-size="14" fill="#475569">capture → analyse</text>
|
||||||
|
|
||||||
|
<rect x="120" y="48" width="320" height="76" rx="12" fill="#eff6ff" stroke="#93c5fd" stroke-width="1.5"/>
|
||||||
|
<text x="280" y="84" text-anchor="middle" font-size="16" font-weight="600" fill="#0f172a" font-family="'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace">simtrace2-pysniff</text>
|
||||||
|
<text x="280" y="108" text-anchor="middle" font-size="14" fill="#475569">SIMtrace2 hardware</text>
|
||||||
|
|
||||||
|
<rect x="520" y="48" width="320" height="76" rx="12" fill="#f5f3ff" stroke="#c4b5fd" stroke-width="1.5"/>
|
||||||
|
<text x="680" y="84" text-anchor="middle" font-size="16" font-weight="600" fill="#0f172a" font-family="'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace">sigrok_iso7816_stream</text>
|
||||||
|
<text x="680" y="108" text-anchor="middle" font-size="14" fill="#475569">FX2 logic analyzer</text>
|
||||||
|
|
||||||
|
<line x1="280" y1="124" x2="480" y2="210" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<line x1="680" y1="124" x2="480" y2="210" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
|
||||||
|
<rect x="40" y="210" width="270" height="72" rx="12" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.5"/>
|
||||||
|
<text x="175" y="242" text-anchor="middle" font-size="16" font-weight="600" fill="#0f172a">SIMtrace2 hardware</text>
|
||||||
|
<text x="175" y="264" text-anchor="middle" font-size="14" fill="#475569">direct USB</text>
|
||||||
|
|
||||||
|
<rect x="345" y="210" width="270" height="72" rx="12" fill="#fffbeb" stroke="#fcd34d" stroke-width="1.5"/>
|
||||||
|
<text x="480" y="242" text-anchor="middle" font-size="16" font-weight="600" fill="#0f172a" font-family="'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace">GSMTAP UDP :4729</text>
|
||||||
|
<text x="480" y="264" text-anchor="middle" font-size="14" fill="#475569">listener</text>
|
||||||
|
|
||||||
|
<rect x="650" y="210" width="270" height="72" rx="12" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.5"/>
|
||||||
|
<text x="785" y="242" text-anchor="middle" font-size="16" font-weight="600" fill="#0f172a">PCAP / COMPRION import</text>
|
||||||
|
<text x="785" y="264" text-anchor="middle" font-size="14" fill="#475569">files</text>
|
||||||
|
|
||||||
|
<line x1="175" y1="282" x2="400" y2="368" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<line x1="480" y1="282" x2="480" y2="368" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<line x1="785" y1="282" x2="560" y2="368" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
|
||||||
|
<rect x="270" y="368" width="420" height="84" rx="12" fill="#ecfdf5" stroke="#6ee7b7" stroke-width="1.5"/>
|
||||||
|
<text x="480" y="404" text-anchor="middle" font-size="19" font-weight="600" fill="#0f172a" font-family="'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace">simtrace-analyser-server</text>
|
||||||
|
<text x="480" y="430" text-anchor="middle" font-size="14" fill="#475569">decodes · stores</text>
|
||||||
|
|
||||||
|
<line x1="480" y1="452" x2="255" y2="516" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<line x1="480" y1="452" x2="705" y2="516" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
|
||||||
|
<rect x="120" y="516" width="270" height="76" rx="12" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.5"/>
|
||||||
|
<text x="255" y="554" text-anchor="middle" font-size="18" font-weight="600" fill="#0f172a">SQLite</text>
|
||||||
|
<text x="255" y="576" text-anchor="middle" font-size="14" fill="#475569">sessions.db</text>
|
||||||
|
|
||||||
|
<rect x="570" y="516" width="270" height="76" rx="12" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.5"/>
|
||||||
|
<text x="705" y="554" text-anchor="middle" font-size="18" font-weight="600" fill="#0f172a">Browser (web UI)</text>
|
||||||
|
<text x="705" y="576" text-anchor="middle" font-size="14" fill="#475569">installable PWA</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="640" viewBox="0 0 960 640" role="img" aria-labelledby="diagram-title diagram-desc" font-family="'Segoe UI', 'Helvetica Neue', Arial, 'DejaVu Sans', sans-serif">
|
||||||
|
<title id="diagram-title">SIMtrace Analyser: захват и анализ</title>
|
||||||
|
<desc id="diagram-desc">simtrace2-pysniff (оборудование SIMtrace2) и sigrok_iso7816_stream (логический анализатор FX2) передают GSMTAP-SIM слушателю UDP :4729; альтернативно сервер читает оборудование SIMtrace2 напрямую по USB, а PCAP/трассы COMPRION импортируются из файлов. simtrace-analyser-server декодирует и сохраняет трафик в SQLite и раздаёт веб-интерфейс.</desc>
|
||||||
|
<defs>
|
||||||
|
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||||
|
<path d="M0 0 L10 5 L0 10 Z" fill="#64748b"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
<rect x="1" y="1" width="958" height="638" rx="18" fill="#ffffff" stroke="#e2e8f0" stroke-width="1.5"/>
|
||||||
|
<text x="480" y="30" text-anchor="middle" font-size="14" fill="#475569">захват → анализ</text>
|
||||||
|
|
||||||
|
<rect x="120" y="48" width="320" height="76" rx="12" fill="#eff6ff" stroke="#93c5fd" stroke-width="1.5"/>
|
||||||
|
<text x="280" y="84" text-anchor="middle" font-size="16" font-weight="600" fill="#0f172a" font-family="'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace">simtrace2-pysniff</text>
|
||||||
|
<text x="280" y="108" text-anchor="middle" font-size="14" fill="#475569">оборудование SIMtrace2</text>
|
||||||
|
|
||||||
|
<rect x="520" y="48" width="320" height="76" rx="12" fill="#f5f3ff" stroke="#c4b5fd" stroke-width="1.5"/>
|
||||||
|
<text x="680" y="84" text-anchor="middle" font-size="16" font-weight="600" fill="#0f172a" font-family="'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace">sigrok_iso7816_stream</text>
|
||||||
|
<text x="680" y="108" text-anchor="middle" font-size="14" fill="#475569">логический анализатор FX2</text>
|
||||||
|
|
||||||
|
<line x1="280" y1="124" x2="480" y2="210" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<line x1="680" y1="124" x2="480" y2="210" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
|
||||||
|
<rect x="40" y="210" width="270" height="72" rx="12" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.5"/>
|
||||||
|
<text x="175" y="242" text-anchor="middle" font-size="16" font-weight="600" fill="#0f172a">Оборудование SIMtrace2</text>
|
||||||
|
<text x="175" y="264" text-anchor="middle" font-size="14" fill="#475569">прямой USB</text>
|
||||||
|
|
||||||
|
<rect x="345" y="210" width="270" height="72" rx="12" fill="#fffbeb" stroke="#fcd34d" stroke-width="1.5"/>
|
||||||
|
<text x="480" y="242" text-anchor="middle" font-size="16" font-weight="600" fill="#0f172a" font-family="'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace">GSMTAP UDP :4729</text>
|
||||||
|
<text x="480" y="264" text-anchor="middle" font-size="14" fill="#475569">слушатель</text>
|
||||||
|
|
||||||
|
<rect x="650" y="210" width="270" height="72" rx="12" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.5"/>
|
||||||
|
<text x="785" y="242" text-anchor="middle" font-size="16" font-weight="600" fill="#0f172a">Импорт PCAP / COMPRION</text>
|
||||||
|
<text x="785" y="264" text-anchor="middle" font-size="14" fill="#475569">файлы</text>
|
||||||
|
|
||||||
|
<line x1="175" y1="282" x2="400" y2="368" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<line x1="480" y1="282" x2="480" y2="368" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<line x1="785" y1="282" x2="560" y2="368" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
|
||||||
|
<rect x="270" y="368" width="420" height="84" rx="12" fill="#ecfdf5" stroke="#6ee7b7" stroke-width="1.5"/>
|
||||||
|
<text x="480" y="404" text-anchor="middle" font-size="19" font-weight="600" fill="#0f172a" font-family="'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace">simtrace-analyser-server</text>
|
||||||
|
<text x="480" y="430" text-anchor="middle" font-size="14" fill="#475569">декодирует · сохраняет</text>
|
||||||
|
|
||||||
|
<line x1="480" y1="452" x2="255" y2="516" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<line x1="480" y1="452" x2="705" y2="516" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
|
||||||
|
<rect x="120" y="516" width="270" height="76" rx="12" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.5"/>
|
||||||
|
<text x="255" y="554" text-anchor="middle" font-size="18" font-weight="600" fill="#0f172a">SQLite</text>
|
||||||
|
<text x="255" y="576" text-anchor="middle" font-size="14" fill="#475569">sessions.db</text>
|
||||||
|
|
||||||
|
<rect x="570" y="516" width="270" height="76" rx="12" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.5"/>
|
||||||
|
<text x="705" y="554" text-anchor="middle" font-size="18" font-weight="600" fill="#0f172a">Браузер (веб-интерфейс)</text>
|
||||||
|
<text x="705" y="576" text-anchor="middle" font-size="14" fill="#475569">устанавливаемое PWA</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.0 KiB |
+1
-1
@@ -3389,7 +3389,7 @@ function onSearchChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────── Versioning ────────────────────
|
// ──────────────────── Versioning ────────────────────
|
||||||
const PWA_VERSION = '2.7.11';
|
const PWA_VERSION = '2.7.13';
|
||||||
const MIN_SERVER_VERSION = '2.3.0';
|
const MIN_SERVER_VERSION = '2.3.0';
|
||||||
|
|
||||||
function parseVersion(v) {
|
function parseVersion(v) {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simtrace-analyser-v93';
|
const CACHE = 'simtrace-analyser-v95';
|
||||||
|
|
||||||
const PRECACHE = [
|
const PRECACHE = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
+4
-3
@@ -1,13 +1,14 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=64"]
|
requires = ["setuptools>=77"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "simtrace-analyser"
|
name = "simtrace-analyser"
|
||||||
version = "2.7.11"
|
version = "2.7.13"
|
||||||
description = "SIMtrace Analyser — APDU trace analyser for GSMTAP/PCAP captures (bundles the simtrace2-pysniff utility)"
|
description = "SIMtrace Analyser — APDU trace analyser for GSMTAP/PCAP captures (bundles the simtrace2-pysniff utility)"
|
||||||
readme = {file = "README.md", content-type = "text/markdown"}
|
readme = {file = "README.md", content-type = "text/markdown"}
|
||||||
license = {text = "GPL-2.0-or-later"}
|
license = "MIT"
|
||||||
|
license-files = ["LICENSE"]
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pyusb>=1.2",
|
"pyusb>=1.2",
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""Build the ready-to-run runtime zip for a GitHub release.
|
||||||
|
|
||||||
|
The wheel needs ``pip`` and the GitHub source archive carries the whole
|
||||||
|
repository (tests, CI, the gitignored ``tools/``). This zip is the runtime
|
||||||
|
tree only: extract it anywhere, run ``start.bat`` on Windows or
|
||||||
|
``./start.sh`` elsewhere, and the server serves the bundled PWA. Python 3
|
||||||
|
must be on ``PATH``; PyUSB is only needed for direct hardware capture.
|
||||||
|
|
||||||
|
The build is deliberately simple - the explicit include lists below are the
|
||||||
|
whole manifest, so nothing from ``tests/``, ``.github/``, ``pyproject.toml``
|
||||||
|
or a build directory can leak into the archive by accident.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parents[1]
|
||||||
|
# The script lives outside the packages; import the version from the checkout.
|
||||||
|
sys.path.insert(0, str(REPO))
|
||||||
|
|
||||||
|
from simtrace_analyser.version import __version__ # noqa: E402
|
||||||
|
|
||||||
|
# The Python packages (all modules, no caches).
|
||||||
|
PACKAGES = ('simtrace_analyser', 'simtrace2_pysniff')
|
||||||
|
|
||||||
|
# The PWA runtime files; ``tests/`` and ``package.json`` are dev-only.
|
||||||
|
FRONTEND = 'frontend'
|
||||||
|
FRONTEND_EXCLUDE = ('tests', 'package.json')
|
||||||
|
|
||||||
|
# Repo-root files that make the extracted tree usable. LICENSE is included
|
||||||
|
# once the repository has one.
|
||||||
|
ROOT_FILES = (
|
||||||
|
'start.sh',
|
||||||
|
'start.bat',
|
||||||
|
'sniff-start.sh',
|
||||||
|
'sniff-start.bat',
|
||||||
|
'70-simtrace2-pysniff.rules',
|
||||||
|
'README.md',
|
||||||
|
'README_RUS.md',
|
||||||
|
'frontend.png',
|
||||||
|
'architecture_en.svg',
|
||||||
|
'architecture_ru.svg',
|
||||||
|
'LICENSE',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Skip editor/byte-code droppings wherever they appear.
|
||||||
|
SKIP_DIRS = ('__pycache__',)
|
||||||
|
SKIP_NAMES = ('.DS_Store',)
|
||||||
|
SKIP_SUFFIXES = ('.pyc',)
|
||||||
|
|
||||||
|
|
||||||
|
def _files(root):
|
||||||
|
"""Yield the regular files under *root*, sorted and cache-free."""
|
||||||
|
for path in sorted(root.rglob('*')):
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
if any(part in SKIP_DIRS for part in path.parts):
|
||||||
|
continue
|
||||||
|
if path.name in SKIP_NAMES or path.suffix in SKIP_SUFFIXES:
|
||||||
|
continue
|
||||||
|
yield path
|
||||||
|
|
||||||
|
|
||||||
|
def _write_bat(zf, path, arcname):
|
||||||
|
"""Write a .bat entry with CRLF line endings.
|
||||||
|
|
||||||
|
The checkout stores the batch files with LF, and cmd.exe mishandles a
|
||||||
|
batch file whose lines end in a bare LF (labels and ``goto`` in
|
||||||
|
particular), so the archive gets the Windows form. The source files
|
||||||
|
stay untouched. ``writestr`` does not copy the file mode, so set it.
|
||||||
|
"""
|
||||||
|
data = path.read_bytes().replace(b'\r\n', b'\n').replace(b'\n', b'\r\n')
|
||||||
|
info = zipfile.ZipInfo.from_file(path, arcname)
|
||||||
|
info.external_attr = 0o644 << 16
|
||||||
|
info.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
zf.writestr(info, data)
|
||||||
|
|
||||||
|
|
||||||
|
def build_zip(output_dir, version=None):
|
||||||
|
"""Write ``simtrace-analyser-<version>.zip`` into *output_dir*.
|
||||||
|
|
||||||
|
Returns the archive path. The zip has a single top-level directory
|
||||||
|
``simtrace-analyser-<version>/`` holding the runtime tree.
|
||||||
|
"""
|
||||||
|
version = version or __version__
|
||||||
|
output_dir = Path(output_dir)
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
archive = output_dir / ('simtrace-analyser-%s.zip' % version)
|
||||||
|
top = 'simtrace-analyser-%s' % version
|
||||||
|
|
||||||
|
with zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for package in PACKAGES:
|
||||||
|
for path in _files(REPO / package):
|
||||||
|
zf.write(path, '%s/%s' % (top, path.relative_to(REPO).as_posix()))
|
||||||
|
|
||||||
|
for path in _files(REPO / FRONTEND):
|
||||||
|
rel = path.relative_to(REPO / FRONTEND)
|
||||||
|
if rel.parts[0] in FRONTEND_EXCLUDE:
|
||||||
|
continue
|
||||||
|
zf.write(path, '%s/%s' % (top, path.relative_to(REPO).as_posix()))
|
||||||
|
|
||||||
|
for name in ROOT_FILES:
|
||||||
|
path = REPO / name
|
||||||
|
if not path.is_file():
|
||||||
|
# LICENSE is not in the repository yet; include it once it is.
|
||||||
|
continue
|
||||||
|
arcname = '%s/%s' % (top, name)
|
||||||
|
if path.suffix == '.bat':
|
||||||
|
_write_bat(zf, path, arcname)
|
||||||
|
else:
|
||||||
|
zf.write(path, arcname)
|
||||||
|
|
||||||
|
return archive
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Build the runtime zip attached to a release')
|
||||||
|
parser.add_argument('--output', default='dist', metavar='DIR',
|
||||||
|
help='directory for the archive (default: dist)')
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
archive = build_zip(args.output)
|
||||||
|
with zipfile.ZipFile(archive) as zf:
|
||||||
|
entries = len(zf.namelist())
|
||||||
|
print(archive)
|
||||||
|
print('%d entries' % entries)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
"""simtrace-analyser-server — CLI entry point.
|
"""simtrace-analyser-server — CLI entry point.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
simtrace-analyser-server --capture gsmtap [--gsmtap-port 4729] [--port 8081]
|
simtrace-analyser-server --capture gsmtap [--gsmtap-bind 0.0.0.0]
|
||||||
|
[--gsmtap-port 4729] [--port 8081]
|
||||||
simtrace-analyser-server --capture direct [--port 8081]
|
simtrace-analyser-server --capture direct [--port 8081]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -46,6 +47,9 @@ def main():
|
|||||||
help='Capture mode: gsmtap (listen UDP 4729), direct (SIMtrace2 USB), or disabled (no capture)')
|
help='Capture mode: gsmtap (listen UDP 4729), direct (SIMtrace2 USB), or disabled (no capture)')
|
||||||
p.add_argument('--gsmtap-port', type=int, default=4729,
|
p.add_argument('--gsmtap-port', type=int, default=4729,
|
||||||
help='UDP port for GSMTAP listener (default: 4729)')
|
help='UDP port for GSMTAP listener (default: 4729)')
|
||||||
|
p.add_argument('--gsmtap-bind', default='127.0.0.1', metavar='ADDR',
|
||||||
|
help='UDP bind address for the GSMTAP listener '
|
||||||
|
'(default: 127.0.0.1; use 0.0.0.0 in a container)')
|
||||||
p.add_argument('--log-interval', type=float, default=60.0,
|
p.add_argument('--log-interval', type=float, default=60.0,
|
||||||
help='Seconds between capture liveness heartbeat logs '
|
help='Seconds between capture liveness heartbeat logs '
|
||||||
'(default: 60.0)')
|
'(default: 60.0)')
|
||||||
@@ -61,7 +65,7 @@ def main():
|
|||||||
|
|
||||||
capture = None
|
capture = None
|
||||||
if args.capture == 'gsmtap':
|
if args.capture == 'gsmtap':
|
||||||
backend = GsmtapListener(bind_port=args.gsmtap_port)
|
backend = GsmtapListener(bind_ip=args.gsmtap_bind, bind_port=args.gsmtap_port)
|
||||||
capture = CaptureManager(backend, db, log_interval=args.log_interval)
|
capture = CaptureManager(backend, db, log_interval=args.log_interval)
|
||||||
elif args.capture == 'direct':
|
elif args.capture == 'direct':
|
||||||
backend = DirectSniffer()
|
backend = DirectSniffer()
|
||||||
@@ -78,6 +82,7 @@ def main():
|
|||||||
print(f'simtrace-analyser-server v{__version__} — http://{args.host}:{args.port}', file=sys.stderr)
|
print(f'simtrace-analyser-server v{__version__} — http://{args.host}:{args.port}', file=sys.stderr)
|
||||||
print(f' capture mode: {args.capture}', file=sys.stderr)
|
print(f' capture mode: {args.capture}', file=sys.stderr)
|
||||||
if args.capture == 'gsmtap':
|
if args.capture == 'gsmtap':
|
||||||
|
print(f' GSMTAP bind: {args.gsmtap_bind}', file=sys.stderr)
|
||||||
print(f' GSMTAP port: {args.gsmtap_port}', file=sys.stderr)
|
print(f' GSMTAP port: {args.gsmtap_port}', file=sys.stderr)
|
||||||
print(f' database: {args.db}', file=sys.stderr)
|
print(f' database: {args.db}', file=sys.stderr)
|
||||||
print(f' PWA served at http://{args.host}:{args.port}/', file=sys.stderr)
|
print(f' PWA served at http://{args.host}:{args.port}/', file=sys.stderr)
|
||||||
|
|||||||
@@ -39,15 +39,17 @@ def gsmtap_msg_type(sub_type):
|
|||||||
|
|
||||||
|
|
||||||
class GsmtapListener:
|
class GsmtapListener:
|
||||||
def __init__(self, bind_port=4729):
|
def __init__(self, bind_port=4729, bind_ip='127.0.0.1'):
|
||||||
self._bind_port = bind_port
|
self._bind_port = bind_port
|
||||||
self._receiver = GsmtapReceiver(bind_port=bind_port)
|
self._bind_ip = bind_ip
|
||||||
|
self._receiver = GsmtapReceiver(bind_ip=bind_ip, bind_port=bind_port)
|
||||||
self._running = False
|
self._running = False
|
||||||
self._dropped = 0
|
self._dropped = 0
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if getattr(self._receiver, '_closed', False):
|
if getattr(self._receiver, '_closed', False):
|
||||||
self._receiver = GsmtapReceiver(bind_port=self._bind_port)
|
self._receiver = GsmtapReceiver(bind_ip=self._bind_ip,
|
||||||
|
bind_port=self._bind_port)
|
||||||
self._running = True
|
self._running = True
|
||||||
self._dropped = 0
|
self._dropped = 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""Single source of truth for the SIMtrace Analyser version."""
|
"""Single source of truth for the SIMtrace Analyser version."""
|
||||||
|
|
||||||
__version__ = '2.7.11'
|
__version__ = '2.7.13'
|
||||||
@@ -206,6 +206,41 @@ class TestGsmtapListenerRobustness(unittest.TestCase):
|
|||||||
self.assertEqual(listener.dropped, 1)
|
self.assertEqual(listener.dropped, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGsmtapListenerBind(unittest.TestCase):
|
||||||
|
def test_bind_ip_reaches_the_receiver(self):
|
||||||
|
from simtrace_analyser import capture
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class _FakeReceiver:
|
||||||
|
def __init__(self, bind_ip='127.0.0.1', bind_port=4729):
|
||||||
|
calls.append((bind_ip, bind_port))
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
real = capture.GsmtapReceiver
|
||||||
|
capture.GsmtapReceiver = _FakeReceiver
|
||||||
|
try:
|
||||||
|
listener = capture.GsmtapListener(bind_ip='0.0.0.0', bind_port=4729)
|
||||||
|
listener._receiver._closed = True # force start() to re-create it
|
||||||
|
listener.start()
|
||||||
|
finally:
|
||||||
|
capture.GsmtapReceiver = real
|
||||||
|
self.assertEqual(calls, [('0.0.0.0', 4729), ('0.0.0.0', 4729)])
|
||||||
|
|
||||||
|
def test_all_interfaces_bind(self):
|
||||||
|
from simtrace_analyser.capture import GsmtapListener
|
||||||
|
listener = GsmtapListener(bind_ip='0.0.0.0', bind_port=0)
|
||||||
|
self.addCleanup(listener.stop)
|
||||||
|
self.assertEqual(listener._receiver._sock.getsockname()[0], '0.0.0.0')
|
||||||
|
|
||||||
|
def test_default_bind_is_loopback(self):
|
||||||
|
from simtrace_analyser.capture import GsmtapListener
|
||||||
|
listener = GsmtapListener(bind_port=0)
|
||||||
|
self.addCleanup(listener.stop)
|
||||||
|
self.assertEqual(listener._receiver._sock.getsockname()[0], '127.0.0.1')
|
||||||
|
|
||||||
|
|
||||||
class TestCaptureLoopRobustness(unittest.TestCase):
|
class TestCaptureLoopRobustness(unittest.TestCase):
|
||||||
def test_capture_loop_survives_insert_error(self):
|
def test_capture_loop_survives_insert_error(self):
|
||||||
mgr = CaptureManager(_OneShotBackend(), _NullDb())
|
mgr = CaptureManager(_OneShotBackend(), _NullDb())
|
||||||
|
|||||||
@@ -70,6 +70,15 @@ class TestDistributionMetadata(unittest.TestCase):
|
|||||||
# The sdist carries frontend/, so a wheel built from it has the PWA.
|
# The sdist carries frontend/, so a wheel built from it has the PWA.
|
||||||
self.assertIn('graft frontend', self._read('MANIFEST.in'))
|
self.assertIn('graft frontend', self._read('MANIFEST.in'))
|
||||||
|
|
||||||
|
def test_license_is_mit(self):
|
||||||
|
# The project is MIT; the metadata and the file must not drift apart.
|
||||||
|
pyproject = self._read('pyproject.toml')
|
||||||
|
self.assertIn('license = "MIT"', pyproject)
|
||||||
|
self.assertIn('license-files = ["LICENSE"]', pyproject)
|
||||||
|
license_text = self._read('LICENSE')
|
||||||
|
self.assertTrue(license_text.startswith('MIT License\n'))
|
||||||
|
self.assertIn('Permission is hereby granted, free of charge', license_text)
|
||||||
|
|
||||||
def test_setup_py_bundles_the_pwa(self):
|
def test_setup_py_bundles_the_pwa(self):
|
||||||
setup_py = self._read('setup.py')
|
setup_py = self._read('setup.py')
|
||||||
self.assertIn('frontend', setup_py)
|
self.assertIn('frontend', setup_py)
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ class TestImportEndpoint(unittest.TestCase):
|
|||||||
class TestVersion(unittest.TestCase):
|
class TestVersion(unittest.TestCase):
|
||||||
def test_package_version(self):
|
def test_package_version(self):
|
||||||
from simtrace2_pysniff import __version__
|
from simtrace2_pysniff import __version__
|
||||||
self.assertEqual(__version__, '2.7.11')
|
self.assertEqual(__version__, '2.7.13')
|
||||||
|
|
||||||
def test_status_includes_version(self):
|
def test_status_includes_version(self):
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
"""Tests for the ready-to-run release zip (scripts/build_release_zip.py)."""
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from simtrace_analyser.version import __version__
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
# scripts/ is not a package, so load the builder from its path.
|
||||||
|
_spec = importlib.util.spec_from_file_location(
|
||||||
|
'build_release_zip', REPO / 'scripts' / 'build_release_zip.py')
|
||||||
|
build_release_zip = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(build_release_zip)
|
||||||
|
|
||||||
|
|
||||||
|
class TestReleaseZip(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls._tmp = tempfile.TemporaryDirectory()
|
||||||
|
cls.archive = build_release_zip.build_zip(cls._tmp.name)
|
||||||
|
with zipfile.ZipFile(cls.archive) as zf:
|
||||||
|
cls.names = zf.namelist()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
cls._tmp.cleanup()
|
||||||
|
|
||||||
|
def test_name_and_single_top_level_directory(self):
|
||||||
|
top = 'simtrace-analyser-%s' % __version__
|
||||||
|
self.assertEqual(self.archive.name, top + '.zip')
|
||||||
|
for name in self.names:
|
||||||
|
self.assertTrue(name.startswith(top + '/'),
|
||||||
|
'%s is outside %s/' % (name, top))
|
||||||
|
|
||||||
|
def test_required_entries(self):
|
||||||
|
top = 'simtrace-analyser-%s' % __version__
|
||||||
|
for rel in (
|
||||||
|
'simtrace_analyser/__main__.py',
|
||||||
|
'simtrace_analyser/version.py',
|
||||||
|
'simtrace2_pysniff/__main__.py',
|
||||||
|
'frontend/index.html',
|
||||||
|
'frontend/sw.js',
|
||||||
|
'frontend/manifest.json',
|
||||||
|
'start.sh',
|
||||||
|
'start.bat',
|
||||||
|
'sniff-start.sh',
|
||||||
|
'sniff-start.bat',
|
||||||
|
'70-simtrace2-pysniff.rules',
|
||||||
|
'LICENSE',
|
||||||
|
'README.md'):
|
||||||
|
self.assertIn('%s/%s' % (top, rel), self.names)
|
||||||
|
|
||||||
|
def test_dev_only_entries_are_excluded(self):
|
||||||
|
for name in self.names:
|
||||||
|
self.assertNotIn('/tests/', name)
|
||||||
|
self.assertNotIn('/__pycache__/', name)
|
||||||
|
self.assertNotIn('.pyc', name)
|
||||||
|
self.assertNotIn('/.git', name)
|
||||||
|
self.assertNotIn('/.github/', name)
|
||||||
|
self.assertNotIn(
|
||||||
|
'simtrace-analyser-%s/frontend/package.json' % __version__,
|
||||||
|
self.names)
|
||||||
|
self.assertNotIn(
|
||||||
|
'simtrace-analyser-%s/pyproject.toml' % __version__, self.names)
|
||||||
|
|
||||||
|
def test_precache_files_are_in_the_zip(self):
|
||||||
|
# Drift guard: every asset the service worker precaches must travel
|
||||||
|
# with the PWA, otherwise an offline install fails on a fresh extract.
|
||||||
|
text = (REPO / 'frontend' / 'sw.js').read_text()
|
||||||
|
match = re.search(r'PRECACHE\s*=\s*\[(.*?)\]', text, re.S)
|
||||||
|
self.assertIsNotNone(match, 'no PRECACHE array in frontend/sw.js')
|
||||||
|
assets = re.findall(r"'([^']+)'", match.group(1))
|
||||||
|
self.assertTrue(assets)
|
||||||
|
top = 'simtrace-analyser-%s' % __version__
|
||||||
|
for asset in assets:
|
||||||
|
self.assertIn('%s/frontend/%s' % (top, asset), self.names,
|
||||||
|
'precached %s is missing from the zip' % asset)
|
||||||
|
|
||||||
|
def test_readme_images_are_in_the_zip(self):
|
||||||
|
# Drift guard: the runtime tree's READMEs embed the screenshot and the
|
||||||
|
# architecture diagrams by relative path, so every referenced image
|
||||||
|
# must travel with the archive.
|
||||||
|
top = 'simtrace-analyser-%s' % __version__
|
||||||
|
for readme in ('README.md', 'README_RUS.md'):
|
||||||
|
text = (REPO / readme).read_text()
|
||||||
|
images = re.findall(r'!\[[^\]]*\]\(([^)\s]+)\)', text)
|
||||||
|
self.assertTrue(images, 'no images referenced by %s' % readme)
|
||||||
|
for image in images:
|
||||||
|
self.assertNotIn('://', image, image)
|
||||||
|
self.assertIn('%s/%s' % (top, image), self.names,
|
||||||
|
'%s references %s, which is missing from the zip'
|
||||||
|
% (readme, image))
|
||||||
|
|
||||||
|
def test_batch_files_use_crlf(self):
|
||||||
|
with zipfile.ZipFile(self.archive) as zf:
|
||||||
|
bats = [n for n in self.names if n.endswith('.bat')]
|
||||||
|
self.assertTrue(bats)
|
||||||
|
for name in bats:
|
||||||
|
data = zf.read(name)
|
||||||
|
self.assertIn(b'\r\n', data, name)
|
||||||
|
self.assertNotIn(b'\n', data.replace(b'\r\n', b''),
|
||||||
|
'%s has a bare LF' % name)
|
||||||
|
|
||||||
|
def test_shell_script_is_lf_and_executable(self):
|
||||||
|
name = 'simtrace-analyser-%s/start.sh' % __version__
|
||||||
|
with zipfile.ZipFile(self.archive) as zf:
|
||||||
|
info = zf.getinfo(name)
|
||||||
|
data = zf.read(name)
|
||||||
|
self.assertNotIn(b'\r\n', data)
|
||||||
|
self.assertTrue(info.external_attr >> 16 & 0o100,
|
||||||
|
'start.sh lost its executable bit')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user