From b900a8a3e19bccc87579abab9d1f39500476dc77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Fri, 14 Aug 2026 16:26:28 +0300 Subject: [PATCH] Merge simtrace-analyser PWA into frontend/ (monorepo); serve it same-origin - frontend/: simtrace-analyser PWA (index.html, sw.js, manifest.json, icons, tests/index.test.html) copied in fresh - server.py: static file serving (_STATIC_MIME, _serve_static) with path-traversal guard; injects window.SIMTRACE_EMBEDDED='1' into index.html so the PWA defaults to a relative API base - server __main__: --web-dir (default /frontend) - frontend: embedded same-origin mode; sw.js relative precache paths + cache v4; GitHub link + version-warn point at simtrace2-pysniff - version 1.1.0 everywhere (__version__, pyproject, PWA_VERSION, manifest); MIN_SERVER_VERSION stays 1.0.0 - README/scripts/.gitignore updated Tests: 107. --- .gitignore | 1 + README.md | 39 +- frontend/favicon.png | Bin 0 -> 2289 bytes frontend/icon-192.png | Bin 0 -> 8849 bytes frontend/icon-512.png | Bin 0 -> 25048 bytes frontend/index.html | 1451 ++++++++++++++++++++++++++ frontend/manifest.json | 14 + frontend/sim-analyser.svg | 45 + frontend/sw.js | 45 + frontend/tests/index.test.html | 59 ++ pyproject.toml | 2 +- server-start.sh | 1 + simtrace2_pysniff/__init__.py | 2 +- simtrace2_pysniff/server/__main__.py | 13 +- simtrace2_pysniff/server/server.py | 51 +- tests/test_pcap_import.py | 2 +- 16 files changed, 1707 insertions(+), 18 deletions(-) create mode 100644 frontend/favicon.png create mode 100644 frontend/icon-192.png create mode 100644 frontend/icon-512.png create mode 100644 frontend/index.html create mode 100644 frontend/manifest.json create mode 100644 frontend/sim-analyser.svg create mode 100644 frontend/sw.js create mode 100644 frontend/tests/index.test.html diff --git a/.gitignore b/.gitignore index d720fdd..bd3d227 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build/ .venv/ decode-errors.log auth-test.log +.DS_Store diff --git a/README.md b/README.md index a52f0b8..485e224 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # simtrace2-pysniff Python-based replacement for `simtrace2-sniff` — SIM card communication sniffer -for Osmocom SIMtrace2 hardware (firmware in **trace** mode). +for Osmocom SIMtrace2 hardware (firmware in **trace** mode). It bundles the +**simtrace-analyser** PWA (in [`frontend/`](frontend/)) and an analysis server +that both captures APDU traffic and serves that PWA. **Single dependency**: PyUSB (libusb wrapper). No libosmocore, no libosmosim. @@ -59,11 +61,11 @@ OPTIONS can also be set via environment variables: FORMAT=timestamp GSMTAP=127.0.0.1 ./sniff-start.sh ``` -### Analysis server (`simtrace2-pysniff-server`) +### Analysis server + PWA (`simtrace2-pysniff-server`) -Stores captured APDU traffic in SQLite and serves it over HTTP for the -[simtrace-analyser](https://github.com/anttro/simtrace-analyser) PWA. -Two capture modes: +Stores captured APDU traffic in SQLite and serves it over HTTP, together with +the bundled **simtrace-analyser** PWA (`frontend/`) on the same origin. Two +capture modes: ```sh # Listen for GSMTAP from simtrace2-pysniff (or original simtrace2-sniff): @@ -73,9 +75,24 @@ simtrace2-pysniff-server --capture gsmtap simtrace2-pysniff-server --capture direct ``` -The PWA then connects to `http://127.0.0.1:8081` for live viewing and -session management. See the [simtrace-analyser](https://github.com/anttro/simtrace-analyser) -repo for the browser interface. +Then open **http://127.0.0.1:8081/** in a browser — the PWA is served by the +server, so the UI and the API share an origin and no CORS/Private-Network-Access +setup is involved. Use `--web-dir PATH` to serve a different PWA directory +(default: `/frontend`). + +### Hosted PWA (landing page) + +A standalone copy of the PWA is hosted at **https://simtrace.atroshin.ru**. +It is a pure frontend: point it (Settings → Server URL) at a locally running +`simtrace2-pysniff-server`. + +> **Browser restriction:** when the PWA is served from a public HTTPS host, +> reaching a local server (`http://127.0.0.1:8081`) requires two things: the +> server must send `Access-Control-Allow-Private-Network: true` (this server +> does), and the browser must be allowed to access the local network — in +> Chrome/Edge/Vivaldi: Site settings → Local network access → allow the site +> (or accept the permission prompt). Without the browser permission, the +> request to `127.0.0.1` is blocked before any preflight is sent. ### Python module (cross-platform) @@ -183,7 +200,5 @@ Use `--inactivity-timeout` to also trigger reconnect on silent firmware hangs. ## Related -- **[simtrace-analyser](https://github.com/anttro/simtrace-analyser)** — a - browser-based PWA for viewing live captures and browsing/exporting saved - sessions. Connects to `simtrace2-pysniff-server`. - Try it now: **https://simtrace.atroshin.ru** +- **[simtrace2](https://gitea.osmocom.org/sim-card/simtrace2)** — the upstream + SIMtrace2 hardware/firmware project that this tool sniffs from. diff --git a/frontend/favicon.png b/frontend/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..dd5bfd0cd7dc775a14de8df03a93fbef7053de75 GIT binary patch literal 2289 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 LNkvXXu0mjf!oV*T literal 0 HcmV?d00001 diff --git a/frontend/icon-192.png b/frontend/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..fcd79fb1aff9fcf2e4e7ecf37c36214c51990d38 GIT binary patch literal 8849 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 Tn0K@=00000NkvXXu0mjflooUy literal 0 HcmV?d00001 diff --git a/frontend/icon-512.png b/frontend/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..f37ebb7d89c284dab2d3dde875d4d13b42c9214b GIT binary patch literal 25048 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 AGXMYp literal 0 HcmV?d00001 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..a9ea738 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,1451 @@ + + + + + +SIMtrace Analyser + + + + + + + + +
+
+
Trace
+
Sessions
+
Settings
+
+
SIM-ME GSMTAP trace analyser
+
+ + GitHub + + +
+
+
+
+ + + + +
+
+
+
+
+
Click an APDU to see details
+
+
+
+

Command Stats

+
—
+
+
+
+ Not connected + 0 messages +
+
+ + +
+
+ + + + + +
+
+
+

No saved sessions

+

Start a capture from the Trace tab to create sessions.

+
+
+
+ + +
+
+ +
+ + +
+
+ ⬤ Not connected — ensure the server is running on the URL above +
+
+
+
+
+
+ + + + + + + diff --git a/frontend/manifest.json b/frontend/manifest.json new file mode 100644 index 0000000..fbc3875 --- /dev/null +++ b/frontend/manifest.json @@ -0,0 +1,14 @@ +{ + "name": "SIMtrace Analyser", + "short_name": "SIMtrace", + "description": "PWA frontend for browsing SIMtrace2 APDU traces", + "version": "1.1.0", + "start_url": "index.html", + "display": "standalone", + "background_color": "#0f172a", + "theme_color": "#0f172a", + "icons": [ + { "src": "icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "icon-512.png", "sizes": "512x512", "type": "image/png" } + ] +} diff --git a/frontend/sim-analyser.svg b/frontend/sim-analyser.svg new file mode 100644 index 0000000..70c99c5 --- /dev/null +++ b/frontend/sim-analyser.svg @@ -0,0 +1,45 @@ + + + + + + + diff --git a/frontend/sw.js b/frontend/sw.js new file mode 100644 index 0000000..2524dd8 --- /dev/null +++ b/frontend/sw.js @@ -0,0 +1,45 @@ +const CACHE = 'simtrace-analyser-v4'; + +const PRECACHE = [ + 'index.html', + 'manifest.json', + 'icon-192.png', + 'icon-512.png', + 'favicon.png', + 'sim-analyser.svg', +]; + +self.addEventListener('install', e => { + self.skipWaiting(); + e.waitUntil( + caches.open(CACHE) + .then(cache => cache.addAll(PRECACHE)) + .then(() => console.log('SW: precache done')) + .catch(err => console.error('SW: precache failed', err)) + ); +}); + +self.addEventListener('activate', e => { + e.waitUntil( + caches.keys() + .then(keys => Promise.all( + keys.filter(k => k !== CACHE).map(k => caches.delete(k)) + )) + .then(() => self.clients.claim()) + .then(() => console.log('SW: activated, controlling clients')) + ); +}); + +self.addEventListener('fetch', e => { + // Network-first: the local server is always up, so always serve fresh + // content and fall back to cache only when offline. + e.respondWith( + fetch(e.request) + .then(r => { + const clone = r.clone(); + caches.open(CACHE).then(c => c.put(e.request, clone)); + return r; + }) + .catch(() => caches.match(e.request)) + ); +}); diff --git a/frontend/tests/index.test.html b/frontend/tests/index.test.html new file mode 100644 index 0000000..e8b306e --- /dev/null +++ b/frontend/tests/index.test.html @@ -0,0 +1,59 @@ + + + +Tests — SIMtrace Analyser + +

HTML Validator Test

+

Page loads cleanly with no JS errors — check browser console.

+ + + diff --git a/pyproject.toml b/pyproject.toml index a954736..38e041f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend" [project] name = "simtrace2-pysniff" -version = "1.0.0" +version = "1.1.0" description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)" readme = {file = "README.md", content-type = "text/markdown"} license = {text = "GPL-2.0-or-later"} diff --git a/server-start.sh b/server-start.sh index 04c316a..7460b82 100755 --- a/server-start.sh +++ b/server-start.sh @@ -72,4 +72,5 @@ ARGS+=(--port "$PORT") ARGS+=("${EXTRA_ARGS[@]}") echo "Starting simtrace2-pysniff-server..." >&2 +echo "PWA (simtrace-analyser) will be served at http://127.0.0.1:$PORT/" >&2 exec env PYTHONPATH="$PROJECT_DIR" python3 -m simtrace2_pysniff.server "${ARGS[@]}" diff --git a/simtrace2_pysniff/__init__.py b/simtrace2_pysniff/__init__.py index c8e52d8..82cb9e9 100644 --- a/simtrace2_pysniff/__init__.py +++ b/simtrace2_pysniff/__init__.py @@ -6,7 +6,7 @@ from .gsmtap import GsmtapSender, GsmtapReceiver from .pcap import PcapWriter from .dump import FileDumper, format_message -__version__ = '1.0.0' +__version__ = '1.1.0' __all__ = [ 'SniffSession', 'DeviceDisconnected', diff --git a/simtrace2_pysniff/server/__main__.py b/simtrace2_pysniff/server/__main__.py index 7736de8..e52ced0 100644 --- a/simtrace2_pysniff/server/__main__.py +++ b/simtrace2_pysniff/server/__main__.py @@ -6,6 +6,7 @@ Usage: """ import argparse +import os import sys from http.server import HTTPServer @@ -14,10 +15,16 @@ from .database import Database, DEFAULT_DB_PATH from .capture import CaptureManager, GsmtapListener, DirectSniffer +def _default_web_dir(): + # /frontend, whether run from source or an editable install. + # __file__ is /simtrace2_pysniff/server/__main__.py → up three levels. + return os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'frontend') + + def main(): p = argparse.ArgumentParser( prog='simtrace2-pysniff-server', - description='HTTP API server for SIMtrace2 APDU capture and analysis') + description='HTTP API + PWA server for SIMtrace2 APDU capture and analysis') p.add_argument('--port', type=int, default=8081, help='HTTP server port (default: 8081)') p.add_argument('--db', default=DEFAULT_DB_PATH, @@ -26,6 +33,8 @@ def main(): help='Capture mode: gsmtap (listen UDP 4729) or direct (SIMtrace2 USB)') p.add_argument('--gsmtap-port', type=int, default=4729, help='UDP port for GSMTAP listener (default: 4729)') + p.add_argument('--web-dir', default=_default_web_dir(), metavar='PATH', + help='Directory with the simtrace-analyser PWA static files to serve (default: /frontend)') args = p.parse_args() db = Database(args.db) @@ -41,12 +50,14 @@ def main(): RequestHandler.capture = capture server = HTTPServer(('127.0.0.1', args.port), RequestHandler) + server.web_dir = args.web_dir print(f'simtrace2-pysniff-server — http://127.0.0.1:{args.port}', file=sys.stderr) print(f' capture mode: {args.capture}', file=sys.stderr) if args.capture == 'gsmtap': print(f' GSMTAP port: {args.gsmtap_port}', file=sys.stderr) print(f' database: {args.db}', file=sys.stderr) + print(f' PWA served at http://127.0.0.1:{args.port}/', file=sys.stderr) print(file=sys.stderr) try: diff --git a/simtrace2_pysniff/server/server.py b/simtrace2_pysniff/server/server.py index ddb834f..bfddafa 100644 --- a/simtrace2_pysniff/server/server.py +++ b/simtrace2_pysniff/server/server.py @@ -1,4 +1,4 @@ -"""HTTP API server for simtrace-analyser PWA.""" +"""HTTP API + static PWA server for simtrace2-pysniff.""" import json import os @@ -16,6 +16,23 @@ from ..pcap import build_pcap, parse_pcap, parse_pcapng from .. import __version__ +# Static file serving (the PWA lives in /frontend, served by this server +# so the UI and the API share an origin and no CORS/PNA is involved). +_STATIC_MIME = { + '.html': 'text/html; charset=utf-8', + '.css': 'text/css', + '.js': 'application/javascript', + '.json': 'application/json', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.webmanifest': 'application/manifest+json', + '.map': 'application/json', + '.wasm': 'application/wasm', +} +_EMBEDDED_MARKER = "" + + def _content_disposition(filename): """RFC 6266/5987 Content-Disposition filename (ASCII fallback + UTF-8).""" fallback = filename.encode('ascii', 'replace').decode('ascii').replace('"', '_') @@ -71,6 +88,36 @@ class RequestHandler(BaseHTTPRequestHandler): path = parsed.path.rstrip('/') return path, parse_qs(parsed.query) + def _serve_static(self): + web_dir = getattr(self.server, 'web_dir', None) + if not web_dir: + self._send_error(404, 'Not found') + return + rel = self.path.split('?', 1)[0].lstrip('/') + if rel in ('', '/'): + rel = 'index.html' + if '..' in rel.split('/') or rel.startswith('/'): + self._send_error(404, 'Not found') + return + fs_path = os.path.join(web_dir, rel) + if not os.path.isfile(fs_path): + self._send_error(404, 'Not found') + return + with open(fs_path, 'rb') as f: + data = f.read() + if rel == 'index.html': + # Tell the frontend it is served by this server (same origin), so + # it can default to a relative API base instead of 127.0.0.1:8081. + data = data.replace(b'', _EMBEDDED_MARKER.encode() + b'', 1) + content_type = _STATIC_MIME.get(os.path.splitext(rel)[1].lower(), 'application/octet-stream') + self.send_response(200) + self.send_header('Content-Type', content_type) + self.send_header('Content-Length', str(len(data))) + if rel in ('index.html', 'sw.js'): + self.send_header('Cache-Control', 'no-cache') + self.end_headers() + self.wfile.write(data) + # --- CORS preflight --- def do_OPTIONS(self): self.send_response(204) @@ -110,7 +157,7 @@ class RequestHandler(BaseHTTPRequestHandler): msg_id = int(path.split('/')[-1]) self._handle_get_apdu(msg_id) else: - self._send_error(404, 'Not found') + self._serve_static() # --- POST --- def do_POST(self): diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py index ced4e3a..dc3e09f 100644 --- a/tests/test_pcap_import.py +++ b/tests/test_pcap_import.py @@ -140,7 +140,7 @@ class TestImportEndpoint(unittest.TestCase): class TestVersion(unittest.TestCase): def test_package_version(self): from simtrace2_pysniff import __version__ - self.assertEqual(__version__, '1.0.0') + self.assertEqual(__version__, '1.1.0') def test_status_includes_version(self): import os