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 <repo>/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.
This commit is contained in:
1 parent
8b7b10b8fa
commit
b900a8a3e1
16 files changed
+1707
-18
No files matched your search
@@ -7,3 +7,4 @@ build/
|
|||||||
.venv/
|
.venv/
|
||||||
decode-errors.log
|
decode-errors.log
|
||||||
auth-test.log
|
auth-test.log
|
||||||
|
.DS_Store
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
# simtrace2-pysniff
|
# simtrace2-pysniff
|
||||||
|
|
||||||
Python-based replacement for `simtrace2-sniff` — SIM card communication sniffer
|
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.
|
**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
|
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
|
Stores captured APDU traffic in SQLite and serves it over HTTP, together with
|
||||||
[simtrace-analyser](https://github.com/anttro/simtrace-analyser) PWA.
|
the bundled **simtrace-analyser** PWA (`frontend/`) on the same origin. Two
|
||||||
Two capture modes:
|
capture modes:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Listen for GSMTAP from simtrace2-pysniff (or original simtrace2-sniff):
|
# Listen for GSMTAP from simtrace2-pysniff (or original simtrace2-sniff):
|
||||||
@@ -73,9 +75,24 @@ simtrace2-pysniff-server --capture gsmtap
|
|||||||
simtrace2-pysniff-server --capture direct
|
simtrace2-pysniff-server --capture direct
|
||||||
```
|
```
|
||||||
|
|
||||||
The PWA then connects to `http://127.0.0.1:8081` for live viewing and
|
Then open **http://127.0.0.1:8081/** in a browser — the PWA is served by the
|
||||||
session management. See the [simtrace-analyser](https://github.com/anttro/simtrace-analyser)
|
server, so the UI and the API share an origin and no CORS/Private-Network-Access
|
||||||
repo for the browser interface.
|
setup is involved. Use `--web-dir PATH` to serve a different PWA directory
|
||||||
|
(default: `<repo>/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)
|
### Python module (cross-platform)
|
||||||
|
|
||||||
@@ -183,7 +200,5 @@ Use `--inactivity-timeout` to also trigger reconnect on silent firmware hangs.
|
|||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- **[simtrace-analyser](https://github.com/anttro/simtrace-analyser)** — a
|
- **[simtrace2](https://gitea.osmocom.org/sim-card/simtrace2)** — the upstream
|
||||||
browser-based PWA for viewing live captures and browsing/exporting saved
|
SIMtrace2 hardware/firmware project that this tool sniffs from.
|
||||||
sessions. Connects to `simtrace2-pysniff-server`.
|
|
||||||
Try it now: **https://simtrace.atroshin.ru**
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
+1451
File diff suppressed because it is too large.
Load diff
@@ -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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
shape-rendering="geometricPrecision"
|
||||||
|
text-rendering="geometricPrecision"
|
||||||
|
image-rendering="optimizeQuality"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
viewBox="0 0 507 511.73"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="sim-analyser.svg"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#3b82f6"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.0446382"
|
||||||
|
inkscape:cx="206.29151"
|
||||||
|
inkscape:cy="241.23184"
|
||||||
|
inkscape:window-width="1707"
|
||||||
|
inkscape:window-height="1007"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<path
|
||||||
|
d="M224.639 0c59.668 0 116.663 23.625 158.855 65.816 58.277 58.264 79.952 144.187 56.478 223.076-5.44 18.308-13.169 35.634-22.82 51.652l87.013 94.825c4.008 4.376 3.736 11.192-.627 15.2l-63.894 58.345c-4.362 4.008-11.15 3.694-15.145-.668l-83.237-91.553c-35.253 21.471-75.345 32.676-116.623 32.676-59.681 0-116.678-23.624-158.869-65.816a224.953 224.953 0 01-48.762-72.959C-17.808 226.73 1.985 129.628 65.798 65.843 107.907 23.611 165.012 0 224.639 0zM351.39 97.92C186.06-67.451-70.51 183.202 97.833 351.436c51.257 51.134 128.646 66.579 195.362 38.974 67.083-27.864 110.692-93.107 110.692-165.739 0-24.32-4.812-47.508-13.523-68.542A180.032 180.032 0 00351.39 97.92z"
|
||||||
|
fill="#3b82f6" id="path1" />
|
||||||
|
<path
|
||||||
|
style="fill:#3b82f6;stroke-width:4.8596"
|
||||||
|
d="m 128.04356,356.87391 c -13.07564,-2.62909 -24.733,-12.68507 -29.511929,-25.45795 -2.596909,-6.94081 -2.6389,-8.86274 -2.29299,-104.97903 l 0.35231,-97.90875 3.788399,-6.8388 c 4.61261,-8.3267 10.50929,-13.68042 19.93961,-18.10361 l 7.13036,-3.34441 h 97.07534 97.0753 l 7.13037,3.34441 c 9.43036,4.42319 15.32699,9.77691 19.93961,18.10361 l 3.78839,6.8388 0.35231,97.90875 c 0.34602,96.11629 0.30373,98.03822 -2.29294,104.97903 -3.50512,9.36825 -10.87912,17.4984 -20.10814,22.17001 l -7.52385,3.80852 -95.14666,0.19674 c -52.33067,0.10843 -97.19362,-0.2148 -99.69555,-0.71758 z m 46.33625,-48.98143 v -28.28685 h -28.28682 -28.28687 v 22.40224 22.40224 l 3.47213,4.12639 c 5.66491,6.73236 9.07721,7.53679 32.20788,7.59246 l 20.89368,0.0505 z m 78.43168,0.0486 v -28.23905 l -28.28799,-0.36934 -28.288,-0.36933 0.001,28.60829 0.001,28.60828 h 28.28682 28.28683 z m 67.88799,26.4325 c 1.90962,-0.99356 4.91852,-3.35297 6.68645,-5.24315 3.14502,-3.3625 3.22311,-3.93424 3.61699,-26.48038 l 0.40286,-23.04367 -24.5107,-0.16089 c -13.48091,-0.0884 -25.3786,-0.16089 -26.43935,-0.16089 -6.0649,0 -5.78596,-1.37936 -5.78596,28.60828 v 28.28683 h 21.27901 c 15.77042,0 22.17779,-0.46749 24.75093,-1.80651 z M 331.24322,228.81866 V 199.88895 H 255.9675 180.69175 l -3.15598,-3.15595 -3.15596,-3.15602 v -36.05986 -36.05988 h -20.61354 c -22.40511,0 -26.44906,0.88289 -32.10286,7.0087 l -3.21436,3.4828 -0.35671,62.89983 -0.3567,62.89984 h 106.7539 106.75388 z m -78.43173,-78.43245 v -28.92976 h -28.28683 -28.28682 v 28.07258 c 0,15.43993 0.38586,28.45827 0.85719,28.92974 0.47138,0.4714 13.20053,0.85713 28.28682,0.85713 h 27.42964 z m 78.43173,6.61896 c 0,-21.05484 -0.16425,-22.55671 -2.91862,-26.67962 -5.20034,-7.78442 -9.192,-8.86481 -32.76137,-8.86705 l -20.89371,-0.002 v 28.07259 c 0,15.43993 0.38585,28.45827 0.85718,28.92974 0.47138,0.47139 13.20053,0.85712 28.28683,0.85712 h 27.42969 z"
|
||||||
|
id="path1-5"
|
||||||
|
sodipodi:nodetypes="csscsscccsscccsccccccccccscccccccccccccscccsscsccccccccccssccccccccccscsccscccscscs" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -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))
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Tests — SIMtrace Analyser</title>
|
||||||
|
<body>
|
||||||
|
<h1>HTML Validator Test</h1>
|
||||||
|
<p>Page loads cleanly with no JS errors — check browser console.</p>
|
||||||
|
<script>
|
||||||
|
const tests = [];
|
||||||
|
|
||||||
|
// Test INS name mapping
|
||||||
|
function testInsNames() {
|
||||||
|
const n = (h) => ({'a4':'SELECT','b0':'READ BINARY','c2':'ENVELOPE','14':'TERMINAL RESPONSE',
|
||||||
|
'88':'AUTHENTICATE','f2':'STATUS','12':'FETCH','20':'VERIFY'}[h] || '?' + h);
|
||||||
|
console.assert(n('a4') === 'SELECT', 'INS a4 -> SELECT');
|
||||||
|
console.assert(n('c2') === 'ENVELOPE', 'INS c2 -> ENVELOPE');
|
||||||
|
console.assert(n('14') === 'TERMINAL RESPONSE', 'INS 14 -> TERMINAL RESPONSE');
|
||||||
|
console.assert(n('ff') !== '?', 'Unknown INS returns placeholder');
|
||||||
|
tests.push('testInsNames: OK');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test elapsed formatting
|
||||||
|
function testFmtElapsed() {
|
||||||
|
const fmt = (e) => {
|
||||||
|
const s = Math.floor(e);
|
||||||
|
const ms = Math.round((e - s) * 1000);
|
||||||
|
return String(s).padStart(4, '0') + '.' + String(ms).padStart(3, '0');
|
||||||
|
};
|
||||||
|
console.assert(fmt(0) === '0000.000', '0 -> 0000.000');
|
||||||
|
console.assert(fmt(1.5) === '0001.500', '1.5 -> 0001.500');
|
||||||
|
console.assert(fmt(12.034) === '0012.034', '12.034 -> 0012.034');
|
||||||
|
tests.push('testFmtElapsed: OK');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test data formatter
|
||||||
|
function testFmtData() {
|
||||||
|
const fmt = (hex, limit) => {
|
||||||
|
const pairs = (hex.match(/.{1,2}/g) || []);
|
||||||
|
const shown = pairs.slice(0, limit || 32);
|
||||||
|
let s = shown.join(' ');
|
||||||
|
if (pairs.length > (limit || 32)) s += ' ...';
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
const r = fmt('a0a40000023f00', 16);
|
||||||
|
console.assert(r === 'a0 a4 00 00 02 3f 00', 'Short data');
|
||||||
|
const r2 = fmt('00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff', 4);
|
||||||
|
console.assert(r2.startsWith('00 11 22 33 ...'), 'Long data truncated');
|
||||||
|
tests.push('testFmtData: OK');
|
||||||
|
}
|
||||||
|
|
||||||
|
testInsNames();
|
||||||
|
testFmtElapsed();
|
||||||
|
testFmtData();
|
||||||
|
|
||||||
|
document.body.innerHTML += `<pre>${tests.join('\n')}</pre>
|
||||||
|
<p style="color:green; font-weight:bold;">All ${tests.length} tests passed.</p>`;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "simtrace2-pysniff"
|
name = "simtrace2-pysniff"
|
||||||
version = "1.0.0"
|
version = "1.1.0"
|
||||||
description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)"
|
description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)"
|
||||||
readme = {file = "README.md", content-type = "text/markdown"}
|
readme = {file = "README.md", content-type = "text/markdown"}
|
||||||
license = {text = "GPL-2.0-or-later"}
|
license = {text = "GPL-2.0-or-later"}
|
||||||
|
|||||||
@@ -72,4 +72,5 @@ ARGS+=(--port "$PORT")
|
|||||||
ARGS+=("${EXTRA_ARGS[@]}")
|
ARGS+=("${EXTRA_ARGS[@]}")
|
||||||
|
|
||||||
echo "Starting simtrace2-pysniff-server..." >&2
|
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[@]}"
|
exec env PYTHONPATH="$PROJECT_DIR" python3 -m simtrace2_pysniff.server "${ARGS[@]}"
|
||||||
@@ -6,7 +6,7 @@ from .gsmtap import GsmtapSender, GsmtapReceiver
|
|||||||
from .pcap import PcapWriter
|
from .pcap import PcapWriter
|
||||||
from .dump import FileDumper, format_message
|
from .dump import FileDumper, format_message
|
||||||
|
|
||||||
__version__ = '1.0.0'
|
__version__ = '1.1.0'
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'SniffSession',
|
'SniffSession',
|
||||||
'DeviceDisconnected',
|
'DeviceDisconnected',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Usage:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
from http.server import HTTPServer
|
from http.server import HTTPServer
|
||||||
|
|
||||||
@@ -14,10 +15,16 @@ from .database import Database, DEFAULT_DB_PATH
|
|||||||
from .capture import CaptureManager, GsmtapListener, DirectSniffer
|
from .capture import CaptureManager, GsmtapListener, DirectSniffer
|
||||||
|
|
||||||
|
|
||||||
|
def _default_web_dir():
|
||||||
|
# <repo>/frontend, whether run from source or an editable install.
|
||||||
|
# __file__ is <repo>/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():
|
def main():
|
||||||
p = argparse.ArgumentParser(
|
p = argparse.ArgumentParser(
|
||||||
prog='simtrace2-pysniff-server',
|
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,
|
p.add_argument('--port', type=int, default=8081,
|
||||||
help='HTTP server port (default: 8081)')
|
help='HTTP server port (default: 8081)')
|
||||||
p.add_argument('--db', default=DEFAULT_DB_PATH,
|
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)')
|
help='Capture mode: gsmtap (listen UDP 4729) or direct (SIMtrace2 USB)')
|
||||||
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('--web-dir', default=_default_web_dir(), metavar='PATH',
|
||||||
|
help='Directory with the simtrace-analyser PWA static files to serve (default: <repo>/frontend)')
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
|
|
||||||
db = Database(args.db)
|
db = Database(args.db)
|
||||||
@@ -41,12 +50,14 @@ def main():
|
|||||||
RequestHandler.capture = capture
|
RequestHandler.capture = capture
|
||||||
|
|
||||||
server = HTTPServer(('127.0.0.1', args.port), RequestHandler)
|
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'simtrace2-pysniff-server — http://127.0.0.1:{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 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://127.0.0.1:{args.port}/', file=sys.stderr)
|
||||||
print(file=sys.stderr)
|
print(file=sys.stderr)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""HTTP API server for simtrace-analyser PWA."""
|
"""HTTP API + static PWA server for simtrace2-pysniff."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -16,6 +16,23 @@ from ..pcap import build_pcap, parse_pcap, parse_pcapng
|
|||||||
from .. import __version__
|
from .. import __version__
|
||||||
|
|
||||||
|
|
||||||
|
# Static file serving (the PWA lives in <repo>/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 = "<script>window.SIMTRACE_EMBEDDED='1'</script>"
|
||||||
|
|
||||||
|
|
||||||
def _content_disposition(filename):
|
def _content_disposition(filename):
|
||||||
"""RFC 6266/5987 Content-Disposition filename (ASCII fallback + UTF-8)."""
|
"""RFC 6266/5987 Content-Disposition filename (ASCII fallback + UTF-8)."""
|
||||||
fallback = filename.encode('ascii', 'replace').decode('ascii').replace('"', '_')
|
fallback = filename.encode('ascii', 'replace').decode('ascii').replace('"', '_')
|
||||||
@@ -71,6 +88,36 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|||||||
path = parsed.path.rstrip('/')
|
path = parsed.path.rstrip('/')
|
||||||
return path, parse_qs(parsed.query)
|
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'</head>', _EMBEDDED_MARKER.encode() + b'</head>', 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 ---
|
# --- CORS preflight ---
|
||||||
def do_OPTIONS(self):
|
def do_OPTIONS(self):
|
||||||
self.send_response(204)
|
self.send_response(204)
|
||||||
@@ -110,7 +157,7 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|||||||
msg_id = int(path.split('/')[-1])
|
msg_id = int(path.split('/')[-1])
|
||||||
self._handle_get_apdu(msg_id)
|
self._handle_get_apdu(msg_id)
|
||||||
else:
|
else:
|
||||||
self._send_error(404, 'Not found')
|
self._serve_static()
|
||||||
|
|
||||||
# --- POST ---
|
# --- POST ---
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
|
|||||||
@@ -140,7 +140,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__, '1.0.0')
|
self.assertEqual(__version__, '1.1.0')
|
||||||
|
|
||||||
def test_status_includes_version(self):
|
def test_status_includes_version(self):
|
||||||
import os
|
import os
|
||||||
|
|||||||
Reference in new issue
Block a user