Add PCAP/PCAPNG import via POST /api/sessions/import

- pcap.py: parse_pcap (micro/nano, both byte orders) and parse_pcapng
  (SHB byte-order, IDB linktype + if_tsresol, EPB/SPB) readers that
  unwrap Ethernet/RAW/IPv4/NULL/SLL/SLL2 to UDP and extract GSMTAP-SIM
  packets (APDU->tpdu, ATR->atr, PPS->pps), with a heuristic fallback
- database: insert_messages bulk-insert helper
- server: import endpoint reads raw body, auto-detects format, skips
  non-GSMTAP packets; 400 'No GSMTAP data found in trace' (no session)
  when empty, else creates a 'pcap' session named after the file

Tests: 102 (pcap round-trip, pcapng, no-GSMTAP, endpoint 200/400).
This commit is contained in:
catarrh committed 2026-08-13 10:15:22 +03:00
1 parent 6b2bcefbac
commit dd34c25899
4 files changed
+453 -3

No files matched your search

+8
View File
@@ -134,6 +134,14 @@ class Database:
self._conn.commit()
return cur.lastrowid
def insert_messages(self, session_id, rows):
"""Bulk-insert messages; *rows* is an iterable of
(elapsed, msg_type, data, flags) tuples. Returns the last rowid."""
self._conn.executemany(
'INSERT INTO messages (session_id, elapsed, type, data, flags) VALUES (?,?,?,?,?)',
[(session_id, elapsed, msg_type, data, flags) for elapsed, msg_type, data, flags in rows])
self._conn.commit()
def get_message(self, msg_id):
row = self._conn.execute(
'SELECT id, session_id, elapsed, type, data, flags FROM messages WHERE id=?',
+45 -2
View File
@@ -12,7 +12,7 @@ from urllib.parse import urlparse, parse_qs, quote
from .database import Database
from .capture import CaptureManager, GsmtapListener, DirectSniffer
from ..gsmtap import build_gsmtap_packet, GSMTAP_SIM_ATR, GSMTAP_SIM_APDU
from ..pcap import build_pcap
from ..pcap import build_pcap, parse_pcap, parse_pcapng
def _content_disposition(filename):
@@ -57,6 +57,12 @@ class RequestHandler(BaseHTTPRequestHandler):
return {}
return json.loads(self.rfile.read(length))
def _read_body(self):
length = int(self.headers.get('Content-Length', 0))
if length == 0:
return b''
return self.rfile.read(length)
def _parse_path(self):
parsed = urlparse(self.path)
path = parsed.path.rstrip('/')
@@ -104,12 +110,14 @@ class RequestHandler(BaseHTTPRequestHandler):
# --- POST ---
def do_POST(self):
path, _ = self._parse_path()
path, params = self._parse_path()
if path == '/api/capture/start':
self._handle_capture_start()
elif path == '/api/capture/stop':
self._handle_capture_stop()
elif path == '/api/sessions/import':
self._handle_import_pcap(params)
else:
self._send_error(404, 'Not found')
@@ -236,6 +244,41 @@ class RequestHandler(BaseHTTPRequestHandler):
'messages_count': self.db.count_messages(session_id),
})
def _handle_import_pcap(self, params):
raw = self._read_body()
name = (params.get('name', [''])[0]).strip()
if not raw:
self._send_error(400, 'Empty file')
return
if raw[:4] == b'\x0a\x0d\x0d\x0a':
packets = list(parse_pcapng(raw))
else:
packets = list(parse_pcap(raw))
if not packets:
self._send_error(400, 'No GSMTAP data found in trace')
return
session_id = self.db.create_session('pcap')
if name:
self.db.rename_session(session_id, name)
first_ts = packets[0][0]
rows = []
for ts, msg_type, payload in packets:
elapsed = round(ts - first_ts, 6) if ts is not None else 0.0
rows.append((max(0.0, elapsed), msg_type, payload, 0))
self.db.insert_messages(session_id, rows)
self.db.close_session(session_id)
self._send_json({
'session_id': session_id,
'name': name,
'message_count': len(packets),
})
def _handle_rename_session(self, session_id, name):
self.db.rename_session(session_id, name)
self._send_json({'ok': True})