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
@@ -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():
|
||||
# <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():
|
||||
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: <repo>/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:
|
||||
|
||||
@@ -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 <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):
|
||||
"""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'</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 ---
|
||||
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):
|
||||
|
||||
Reference in new issue
Block a user