v0.12.6 #10

Merged
serversdown merged 43 commits from seismo-lab-new into main 2026-05-04 13:22:56 -04:00
Showing only changes of commit 5cd5652560 - Show all commits
+287
View File
@@ -22,6 +22,7 @@ from __future__ import annotations
import datetime
import os
import queue
import socket
import subprocess
import sys
import threading
@@ -309,6 +310,284 @@ class BridgePanel(tk.Frame):
messagebox.showerror("Error", f"Failed to send mark:\n{e}")
# ─────────────────────────────────────────────────────────────────────────────
# TCP Bridge panel — MITM capture over IP
# ─────────────────────────────────────────────────────────────────────────────
class TcpBridgePanel(tk.Frame):
"""
TCP man-in-the-middle capture panel.
Listens on a local TCP port for an incoming Blastware connection, forwards
all traffic to the real device, and saves both directions to raw .bin files.
Calls on_bridge_started(raw_bw_path, raw_s3_path, None) when a session
begins so the Analyzer can wire up live mode — same signature as BridgePanel.
"""
def __init__(self, parent: tk.Widget, on_bridge_started, on_bridge_stopped, **kw):
super().__init__(parent, bg=BG2, **kw)
self._on_started = on_bridge_started
self._on_stopped = on_bridge_stopped
self._server: Optional[socket.socket] = None
self._stop_event = threading.Event()
self._log_q: queue.Queue[str] = queue.Queue()
self._build()
self._poll_log_q()
# ── build ─────────────────────────────────────────────────────────────
def _build(self) -> None:
pad = {"padx": 6, "pady": 4}
cfg = tk.Frame(self, bg=BG2)
cfg.pack(side=tk.TOP, fill=tk.X, padx=4, pady=4)
# Row 0: listen port + remote host + remote port
tk.Label(cfg, text="Listen port:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=0, sticky="e", **pad)
self.listen_port_var = tk.StringVar(value="9034")
tk.Entry(cfg, textvariable=self.listen_port_var, width=8,
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
font=MONO).grid(row=0, column=1, sticky="w", **pad)
tk.Label(cfg, text="Device host:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=2, sticky="e", **pad)
self.remote_host_var = tk.StringVar(value="63.43.212.232")
tk.Entry(cfg, textvariable=self.remote_host_var, width=18,
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
font=MONO).grid(row=0, column=3, sticky="w", **pad)
tk.Label(cfg, text="Port:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=4, sticky="e", **pad)
self.remote_port_var = tk.StringVar(value="9034")
tk.Entry(cfg, textvariable=self.remote_port_var, width=8,
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
font=MONO).grid(row=0, column=5, sticky="w", **pad)
# Row 1: log dir
tk.Label(cfg, text="Log dir:", bg=BG2, fg=FG, font=MONO).grid(row=1, column=0, sticky="e", **pad)
self.logdir_var = tk.StringVar(value=str(SCRIPT_DIR / "bridges" / "captures" / "mitm"))
tk.Entry(cfg, textvariable=self.logdir_var, width=40,
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
font=MONO).grid(row=1, column=1, columnspan=4, sticky="we", **pad)
tk.Button(cfg, text="Browse", bg=BG3, fg=FG, relief="flat", cursor="hand2",
font=MONO, command=self._choose_dir).grid(row=1, column=5, **pad)
# Row 2: capture checkboxes
self._raw_bw_on = tk.BooleanVar(value=True)
self._raw_s3_on = tk.BooleanVar(value=True)
tk.Checkbutton(cfg, text="Capture BW→device raw", variable=self._raw_bw_on,
bg=BG2, fg=FG, selectcolor=BG3, activebackground=BG2,
font=MONO).grid(row=2, column=0, columnspan=2, sticky="w", **pad)
tk.Checkbutton(cfg, text="Capture device→BW raw", variable=self._raw_s3_on,
bg=BG2, fg=FG, selectcolor=BG3, activebackground=BG2,
font=MONO).grid(row=2, column=2, columnspan=2, sticky="w", **pad)
# Row 3: buttons + status
btn_row = tk.Frame(self, bg=BG2)
btn_row.pack(side=tk.TOP, fill=tk.X, padx=4, pady=2)
self.start_btn = tk.Button(btn_row, text="Start Listening", bg=GREEN, fg="#000000",
relief="flat", padx=12, cursor="hand2", font=MONO_B,
command=self.start_server)
self.start_btn.pack(side=tk.LEFT, padx=6)
self.stop_btn = tk.Button(btn_row, text="Stop", bg=BG3, fg=FG,
relief="flat", padx=12, cursor="hand2", font=MONO,
command=self.stop_server, state="disabled")
self.stop_btn.pack(side=tk.LEFT, padx=4)
self.status_var = tk.StringVar(value="Idle")
tk.Label(btn_row, textvariable=self.status_var,
bg=BG2, fg=FG_DIM, font=MONO).pack(side=tk.LEFT, padx=10)
# Log output
self.log_view = scrolledtext.ScrolledText(
self, height=18, font=MONO_SM,
bg=BG, fg=FG, insertbackground=FG,
relief="flat", state="disabled",
)
self.log_view.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
# ── helpers ───────────────────────────────────────────────────────────
def _choose_dir(self) -> None:
path = filedialog.askdirectory(initialdir=self.logdir_var.get())
if path:
self.logdir_var.set(path)
def _append_log(self, text: str) -> None:
self.log_view.configure(state="normal")
self.log_view.insert(tk.END, text)
self.log_view.see(tk.END)
self.log_view.configure(state="disabled")
def _poll_log_q(self) -> None:
try:
while True:
msg = self._log_q.get_nowait()
if msg == "<<session_ended>>":
if self._server is not None:
self.status_var.set(f"Listening on :{self.listen_port_var.get()}")
self._on_stopped()
else:
self._append_log(msg)
except queue.Empty:
pass
finally:
self.after(100, self._poll_log_q)
# ── server control ────────────────────────────────────────────────────
def start_server(self) -> None:
try:
listen_port = int(self.listen_port_var.get().strip())
remote_host = self.remote_host_var.get().strip()
remote_port = int(self.remote_port_var.get().strip())
except ValueError:
messagebox.showerror("Error", "Invalid port number.")
return
if not remote_host:
messagebox.showerror("Error", "Please enter the device host.")
return
try:
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("0.0.0.0", listen_port))
srv.listen(5)
srv.settimeout(1.0)
except OSError as e:
messagebox.showerror("Error", f"Cannot bind to port {listen_port}:\n{e}")
return
self._server = srv
self._stop_event.clear()
self.start_btn.configure(state="disabled")
self.stop_btn.configure(state="normal", bg=RED)
self.status_var.set(f"Listening on :{listen_port}")
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
self._append_log(
f"== TCP Bridge started [{ts}]\n"
f" Listening on 0.0.0.0:{listen_port}\n"
f" Forwarding to {remote_host}:{remote_port}\n"
f" Point Blastware call-home to this machine on port {listen_port}\n==\n"
)
logdir = self.logdir_var.get().strip() or "."
raw_bw_on = self._raw_bw_on.get()
raw_s3_on = self._raw_s3_on.get()
threading.Thread(
target=self._accept_loop,
args=(srv, remote_host, remote_port, logdir, raw_bw_on, raw_s3_on),
daemon=True,
).start()
def stop_server(self) -> None:
self._stop_event.set()
if self._server:
try:
self._server.close()
except OSError:
pass
self._server = None
self.start_btn.configure(state="normal")
self.stop_btn.configure(state="disabled", bg=BG3)
self.status_var.set("Idle")
self._append_log("== TCP Bridge stopped ==\n")
self._on_stopped()
# ── accept / session threads ──────────────────────────────────────────
def _accept_loop(self, srv: socket.socket, remote_host: str, remote_port: int,
logdir: str, raw_bw_on: bool, raw_s3_on: bool) -> None:
while not self._stop_event.is_set():
try:
client_sock, addr = srv.accept()
except socket.timeout:
continue
except OSError:
break
peer = f"{addr[0]}:{addr[1]}"
self._log_q.put(f"[TCP] Blastware connected from {peer}\n")
try:
dev_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
dev_sock.connect((remote_host, remote_port))
except OSError as e:
self._log_q.put(f"[TCP] Cannot reach device {remote_host}:{remote_port}: {e}\n")
client_sock.close()
continue
self._log_q.put(f"[TCP] Connected to device at {remote_host}:{remote_port}\n")
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
os.makedirs(logdir, exist_ok=True)
raw_bw_path = os.path.join(logdir, f"raw_bw_{ts}.bin") if raw_bw_on else None
raw_s3_path = os.path.join(logdir, f"raw_s3_{ts}.bin") if raw_s3_on else None
self.after(0, self._notify_session_start, raw_bw_path, raw_s3_path, peer, remote_host, remote_port)
self._run_session(client_sock, dev_sock, raw_bw_path, raw_s3_path, ts)
self._log_q.put("<<session_ended>>")
def _notify_session_start(self, raw_bw_path, raw_s3_path, peer, remote_host, remote_port) -> None:
self.status_var.set(f"Active: {peer}{remote_host}:{remote_port}")
self._on_started(raw_bw_path, raw_s3_path, None)
def _run_session(self, bw_sock: socket.socket, dev_sock: socket.socket,
raw_bw_path: Optional[str], raw_s3_path: Optional[str],
ts: str) -> None:
bw_fh = open(raw_bw_path, "wb") if raw_bw_path else None
s3_fh = open(raw_s3_path, "wb") if raw_s3_path else None
bw_bytes = [0]
s3_bytes = [0]
def _pipe(src, dst, fh, counter):
try:
while True:
data = src.recv(4096)
if not data:
break
dst.sendall(data)
if fh:
fh.write(data)
fh.flush()
counter[0] += len(data)
except OSError:
pass
finally:
try:
dst.shutdown(socket.SHUT_WR)
except OSError:
pass
t_bw = threading.Thread(target=_pipe, args=(bw_sock, dev_sock, bw_fh, bw_bytes), daemon=True)
t_s3 = threading.Thread(target=_pipe, args=(dev_sock, bw_sock, s3_fh, s3_bytes), daemon=True)
t_bw.start()
t_s3.start()
t_bw.join()
t_s3.join()
bw_sock.close()
dev_sock.close()
if bw_fh:
bw_fh.close()
if s3_fh:
s3_fh.close()
self._log_q.put(
f"[TCP] Session {ts} done "
f"BW→dev: {bw_bytes[0]} bytes dev→BW: {s3_bytes[0]} bytes\n"
)
if raw_bw_path:
self._log_q.put(f"[TCP] BW capture: {raw_bw_path}\n")
if raw_s3_path:
self._log_q.put(f"[TCP] S3 capture: {raw_s3_path}\n")
# ─────────────────────────────────────────────────────────────────────────────
# Analyzer panel (tk.Frame — lives inside a notebook tab)
# Extracted from gui_analyzer.py; accepts external path injection.
@@ -1887,6 +2166,13 @@ class SeismoLab(tk.Tk):
)
nb.add(self._bridge_panel, text=" Bridge ")
self._tcp_bridge_panel = TcpBridgePanel(
nb,
on_bridge_started=self._on_bridge_started,
on_bridge_stopped=self._on_bridge_stopped,
)
nb.add(self._tcp_bridge_panel, text=" TCP Capture ")
self._analyzer_panel = AnalyzerPanel(nb, db=self._db)
nb.add(self._analyzer_panel, text=" Analyzer ")
@@ -1927,6 +2213,7 @@ class SeismoLab(tk.Tk):
def _on_close(self) -> None:
self._bridge_panel.stop_bridge()
self._tcp_bridge_panel.stop_server()
self._serial_watch_panel._stop()
self.destroy()