gui.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. """A simple tkinter GUI to edit config.json and manage protector list.
  2. Features:
  3. - Edit base_url, conn_threshold, scan_interval (saved to config.json)
  4. - Manage protector_list entries (add/edit/remove) stored in protector_list.json
  5. - On saving base_url, attempt login via auth_session.login() and store sess_key
  6. """
  7. from __future__ import annotations
  8. import json
  9. import threading
  10. import time
  11. import tkinter as tk
  12. from tkinter import messagebox
  13. from pathlib import Path
  14. from auth_session import load_config, get_base_url, login, set_sess_key
  15. import protector_list
  16. ROOT = Path(__file__).resolve().parent
  17. CONFIG_PATH = ROOT / "config.json"
  18. def load_cfg():
  19. return load_config()
  20. def save_cfg(cfg: dict):
  21. with open(CONFIG_PATH, "w", encoding="utf-8") as f:
  22. json.dump(cfg, f, ensure_ascii=False, indent=2)
  23. class ProtectorGUI(tk.Tk):
  24. def __init__(self, protector_runner=None):
  25. super().__init__()
  26. self.title("Protector GUI")
  27. self.geometry("700x500")
  28. self.cfg = load_cfg()
  29. # Config frame
  30. cf = tk.LabelFrame(self, text="Config")
  31. cf.pack(fill="x", padx=8, pady=6)
  32. tk.Label(cf, text="base_url:").grid(row=0, column=0, sticky="w")
  33. self.base_entry = tk.Entry(cf, width=60)
  34. self.base_entry.grid(row=0, column=1, padx=4, pady=2)
  35. tk.Label(cf, text="conn_threshold:").grid(row=1, column=0, sticky="w")
  36. self.th_entry = tk.Entry(cf, width=10)
  37. self.th_entry.grid(row=1, column=1, sticky="w", padx=4, pady=2)
  38. tk.Label(cf, text="test_prefix:").grid(row=1, column=2, sticky="w", padx=(12,0))
  39. self.test_prefix_entry = tk.Entry(cf, width=15)
  40. self.test_prefix_entry.grid(row=1, column=3, sticky="w", padx=4, pady=2)
  41. tk.Label(cf, text="rule_ip_limit:").grid(row=2, column=2, sticky="w", padx=(12,0))
  42. self.rule_limit_entry = tk.Entry(cf, width=15)
  43. self.rule_limit_entry.grid(row=2, column=3, sticky="w", padx=4, pady=2)
  44. tk.Label(cf, text="scan_interval (s):").grid(row=2, column=0, sticky="w")
  45. self.interval_entry = tk.Entry(cf, width=10)
  46. self.interval_entry.grid(row=2, column=1, sticky="w", padx=4, pady=2)
  47. tk.Button(cf, text="Save Config", command=self.save_config).grid(row=3, column=1, sticky="w", pady=6)
  48. # Protector list frame
  49. lf = tk.LabelFrame(self, text="Protector List")
  50. lf.pack(fill="both", expand=True, padx=8, pady=6)
  51. self.listbox = tk.Listbox(lf)
  52. self.listbox.pack(side="left", fill="both", expand=True, padx=4, pady=4)
  53. self.listbox.bind("<<ListboxSelect>>", self.on_select)
  54. right = tk.Frame(lf)
  55. right.pack(side="right", fill="y", padx=4)
  56. tk.Label(right, text="target_ip:").pack(anchor="w")
  57. self.ip_entry = tk.Entry(right)
  58. self.ip_entry.pack(fill="x")
  59. tk.Label(right, text="src_port:").pack(anchor="w")
  60. self.port_entry = tk.Entry(right)
  61. self.port_entry.pack(fill="x")
  62. tk.Label(right, text="threshold (optional):").pack(anchor="w")
  63. self.entry_threshold = tk.Entry(right)
  64. self.entry_threshold.pack(fill="x")
  65. tk.Button(right, text="Add", command=self.add_entry).pack(fill="x", pady=4)
  66. tk.Button(right, text="Update", command=self.update_entry).pack(fill="x", pady=4)
  67. tk.Button(right, text="Remove", command=self.remove_entry).pack(fill="x", pady=4)
  68. self.load_values()
  69. # 自动在后台尝试登录(启动后立即)
  70. def _auto_login():
  71. try:
  72. resp, sess_cookie = login()
  73. if sess_cookie:
  74. set_sess_key(sess_cookie)
  75. # 不弹出大量通知,只有在需要时可打开下面这一行
  76. # messagebox.showinfo("登录", f"自动登录完成, 状态: {resp.status_code}")
  77. except Exception as e:
  78. # 登录失败不阻塞 GUI,记录到控制台/弹出一条错误提示
  79. try:
  80. messagebox.showerror("自动登录失败", str(e))
  81. except Exception:
  82. print("自动登录失败:", e)
  83. threading.Thread(target=_auto_login, daemon=True).start()
  84. # Protector runner instance (optional) for countdown and triggering runs
  85. self.protector_runner = protector_runner
  86. # countdown label
  87. self.countdown_var = tk.StringVar(value="Protector: idle")
  88. self.countdown_label = tk.Label(self, textvariable=self.countdown_var)
  89. self.countdown_label.pack(anchor="ne", padx=8, pady=4)
  90. # Pause/Resume button
  91. self.pause_btn = None
  92. if self.protector_runner is not None:
  93. self.pause_btn = tk.Button(self, text="Pause", command=self._toggle_pause)
  94. self.pause_btn.pack(anchor="ne", padx=8)
  95. # Blocked IPs frame (uses advanced_acl to query current Test_ prefixed rules)
  96. bf = tk.LabelFrame(self, text="Blocked IPs (来自高级 ACL)")
  97. bf.pack(fill="both", expand=False, padx=8, pady=6)
  98. self.block_listbox = tk.Listbox(bf, height=8)
  99. self.block_listbox.pack(side="left", fill="both", expand=True, padx=4, pady=4)
  100. block_right = tk.Frame(bf)
  101. block_right.pack(side="right", fill="y", padx=4)
  102. tk.Button(block_right, text="刷新阻断列表", command=self.refresh_blocked_ips).pack(fill="x", pady=2)
  103. tk.Label(block_right, text="手动加入 IP:").pack(anchor="w", pady=(8,0))
  104. self.manual_ip_entry = tk.Entry(block_right)
  105. self.manual_ip_entry.pack(fill="x")
  106. tk.Button(block_right, text="加入阻断", command=self.manual_add_ip).pack(fill="x", pady=2)
  107. tk.Button(block_right, text="从列表删除选中", command=self.manual_remove_selected).pack(fill="x", pady=2)
  108. # Register on_run_complete callback only after GUI widgets exist
  109. if self.protector_runner is not None:
  110. def _on_protector_run_complete():
  111. # schedule a GUI-thread refresh; guard if widget removed
  112. try:
  113. if hasattr(self, "block_listbox") and self.block_listbox is not None:
  114. self.after(100, self.refresh_blocked_ips)
  115. except Exception:
  116. pass
  117. try:
  118. self.protector_runner.on_run_complete = _on_protector_run_complete
  119. except Exception:
  120. # ignore if unable to set
  121. pass
  122. # start initial delayed run countdown (10 seconds) if runner provided
  123. if self.protector_runner is not None:
  124. self._initial_delay = 10
  125. self._countdown_seconds = self._initial_delay
  126. # schedule countdown update every 1 second via tkinter's after
  127. self.after(1000, self._update_countdown)
  128. def load_values(self):
  129. self.cfg = load_cfg()
  130. self.base_entry.delete(0, tk.END)
  131. self.base_entry.insert(0, self.cfg.get("base_url", ""))
  132. self.th_entry.delete(0, tk.END)
  133. self.th_entry.insert(0, str(self.cfg.get("conn_threshold", "")))
  134. # new fields
  135. self.test_prefix_entry.delete(0, tk.END)
  136. self.test_prefix_entry.insert(0, str(self.cfg.get("test_prefix", "Test_")))
  137. self.rule_limit_entry.delete(0, tk.END)
  138. self.rule_limit_entry.insert(0, str(self.cfg.get("rule_ip_limit", 1000)))
  139. self.interval_entry.delete(0, tk.END)
  140. self.interval_entry.insert(0, str(self.cfg.get("scan_interval", 60)))
  141. self.reload_listbox()
  142. def reload_listbox(self):
  143. self.listbox.delete(0, tk.END)
  144. items = protector_list.load_list()
  145. for i in items:
  146. self.listbox.insert(tk.END, f"{i['id']}: {i['target_ip']}:{i['src_port']} (th={i.get('threshold')})")
  147. # do NOT auto-refresh blocked IPs here (only refresh on explicit action or protector run)
  148. def refresh_blocked_ips(self):
  149. """Query advanced_acl.get_all_test_ips() and update the block_listbox."""
  150. # defensive: ensure listbox exists
  151. if not hasattr(self, "block_listbox") or self.block_listbox is None:
  152. # nothing to refresh
  153. return
  154. try:
  155. import advanced_acl
  156. except Exception as e:
  157. messagebox.showerror("错误", f"无法导入 advanced_acl: {e}")
  158. return
  159. try:
  160. # clear listbox
  161. try:
  162. self.block_listbox.delete(0, tk.END)
  163. except Exception:
  164. # if delete fails, just recreate a fresh listbox entry
  165. pass
  166. ips = advanced_acl.get_all_test_ips()
  167. # ips expected as list of dicts or simple ip strings
  168. if isinstance(ips, dict):
  169. # maybe returned as {"ips": [...]}
  170. ips_list = ips.get("ips") or ips.get("data") or []
  171. else:
  172. ips_list = ips or []
  173. for it in ips_list:
  174. if isinstance(it, dict):
  175. ip = it.get("ip") or it.get("dst_addr") or str(it)
  176. comment = it.get("comment") or it.get("rule") or ""
  177. self.block_listbox.insert(tk.END, f"{ip} {comment}")
  178. else:
  179. self.block_listbox.insert(tk.END, str(it))
  180. except Exception as e:
  181. # show more helpful error including type
  182. messagebox.showerror("错误", f"获取阻断列表失败: {type(e).__name__}: {e}")
  183. def manual_add_ip(self):
  184. ip = self.manual_ip_entry.get().strip()
  185. if not ip:
  186. messagebox.showerror("错误", "请输入要阻断的 IP")
  187. return
  188. try:
  189. import advanced_acl
  190. res = advanced_acl.add_ip(ip)
  191. # best-effort feedback
  192. messagebox.showinfo("加入阻断", f"尝试加入 {ip}: {res}")
  193. self.refresh_blocked_ips()
  194. except Exception as e:
  195. messagebox.showerror("错误", f"加入阻断失败: {e}")
  196. def manual_remove_selected(self):
  197. sel = self.block_listbox.curselection()
  198. if not sel:
  199. messagebox.showerror("错误", "先选择要删除的项")
  200. return
  201. idx = sel[0]
  202. item = self.block_listbox.get(idx)
  203. # try to parse ip from the line (assume leading token)
  204. ip = item.split()[0]
  205. try:
  206. import advanced_acl
  207. res = advanced_acl.del_ip(ip)
  208. messagebox.showinfo("删除阻断", f"尝试删除 {ip}: {res}")
  209. self.refresh_blocked_ips()
  210. except Exception as e:
  211. messagebox.showerror("错误", f"删除阻断失败: {e}")
  212. def save_config(self):
  213. try:
  214. self.cfg["base_url"] = self.base_entry.get().strip()
  215. self.cfg["conn_threshold"] = int(self.th_entry.get().strip())
  216. # optional new fields
  217. self.cfg["test_prefix"] = str(self.test_prefix_entry.get().strip())
  218. self.cfg["rule_ip_limit"] = int(self.rule_limit_entry.get().strip())
  219. self.cfg["scan_interval"] = int(self.interval_entry.get().strip())
  220. except Exception as e:
  221. messagebox.showerror("错误", f"配置输入无效: {e}")
  222. return
  223. save_cfg(self.cfg)
  224. # try login on a background thread
  225. def _login():
  226. try:
  227. resp, sess_cookie = login()
  228. if sess_cookie:
  229. set_sess_key(sess_cookie)
  230. messagebox.showinfo("登录", f"登录完成, 状态: {resp.status_code}")
  231. except Exception as e:
  232. messagebox.showerror("登录失败", str(e))
  233. threading.Thread(target=_login, daemon=True).start()
  234. def on_select(self, evt):
  235. sel = self.listbox.curselection()
  236. if not sel:
  237. return
  238. idx = sel[0]
  239. items = protector_list.load_list()
  240. if idx >= len(items):
  241. return
  242. item = items[idx]
  243. self.ip_entry.delete(0, tk.END)
  244. self.ip_entry.insert(0, item.get("target_ip", ""))
  245. self.port_entry.delete(0, tk.END)
  246. self.port_entry.insert(0, str(item.get("src_port", "")))
  247. self.entry_threshold.delete(0, tk.END)
  248. self.entry_threshold.insert(0, str(item.get("threshold", "")))
  249. def add_entry(self):
  250. ip = self.ip_entry.get().strip()
  251. port = self.port_entry.get().strip()
  252. th = self.entry_threshold.get().strip() or None
  253. if not ip or not port:
  254. messagebox.showerror("错误", "ip 与 port 为必填")
  255. return
  256. try:
  257. entry = protector_list.add_entry(ip, int(port), int(th) if th else None)
  258. self.reload_listbox()
  259. messagebox.showinfo("添加", f"已添加: {entry}")
  260. except Exception as e:
  261. messagebox.showerror("错误", str(e))
  262. def update_entry(self):
  263. sel = self.listbox.curselection()
  264. if not sel:
  265. messagebox.showerror("错误", "先选择一条")
  266. return
  267. idx = sel[0]
  268. items = protector_list.load_list()
  269. if idx >= len(items):
  270. return
  271. eid = items[idx]["id"]
  272. ip = self.ip_entry.get().strip()
  273. port = self.port_entry.get().strip()
  274. th = self.entry_threshold.get().strip() or None
  275. try:
  276. updated = protector_list.update_entry(eid, target_ip=ip, src_port=int(port), threshold=(int(th) if th else None))
  277. if updated is None:
  278. messagebox.showerror("错误", "更新失败")
  279. else:
  280. self.reload_listbox()
  281. messagebox.showinfo("更新", f"已更新: {updated}")
  282. except Exception as e:
  283. messagebox.showerror("错误", str(e))
  284. def remove_entry(self):
  285. sel = self.listbox.curselection()
  286. if not sel:
  287. messagebox.showerror("错误", "先选择一条")
  288. return
  289. idx = sel[0]
  290. items = protector_list.load_list()
  291. if idx >= len(items):
  292. return
  293. eid = items[idx]["id"]
  294. ok = protector_list.remove_entry(eid)
  295. if ok:
  296. self.reload_listbox()
  297. messagebox.showinfo("删除", "已删除")
  298. else:
  299. messagebox.showerror("错误", "删除失败")
  300. def _update_countdown(self):
  301. """Update countdown label every second and trigger protector run when it reaches zero."""
  302. try:
  303. if not self.protector_runner:
  304. return
  305. # Update label
  306. # compute seconds until next run based on protector_runner's schedule
  307. try:
  308. if self.protector_runner.is_paused():
  309. self.countdown_var.set("Protector: paused")
  310. else:
  311. next_ts = self.protector_runner.get_next_run_time()
  312. if next_ts is None:
  313. # fallback to internal counter
  314. secs = max(0, int(self._countdown_seconds))
  315. else:
  316. secs = max(0, int(round(next_ts - time.time())))
  317. self.countdown_var.set(f"下一次检测:{secs}s")
  318. except Exception:
  319. self.countdown_var.set("Protector: running")
  320. # if protector hasn't been started yet, use internal countdown
  321. try:
  322. started = getattr(self.protector_runner, "_started", False)
  323. except Exception:
  324. started = False
  325. if not started:
  326. # use internal countdown to start
  327. if self._countdown_seconds <= 0:
  328. do_start = True
  329. else:
  330. do_start = False
  331. else:
  332. # protector already started; rely on its internal schedule
  333. do_start = False
  334. if do_start:
  335. # If protector runner not started yet, start its loop first
  336. try:
  337. if not getattr(self.protector_runner, "_started", False):
  338. # start the background loop
  339. self.protector_runner._started = True
  340. threading.Thread(target=self.protector_runner.start, daemon=True).start()
  341. except Exception:
  342. pass
  343. # ProtectorRunner.start() will perform an initial run, so do not
  344. # explicitly call run_once() here to avoid double execution.
  345. # reset internal countdown to interval after start
  346. try:
  347. self._countdown_seconds = int(self.protector_runner.get_interval())
  348. except Exception:
  349. self._countdown_seconds = self._initial_delay
  350. # reset countdown to configured interval
  351. try:
  352. self._countdown_seconds = int(self.protector_runner.get_interval())
  353. except Exception:
  354. self._countdown_seconds = self._initial_delay
  355. # update pause button text
  356. try:
  357. if self.pause_btn:
  358. self.pause_btn.config(text=("Resume" if self.protector_runner.is_paused() else "Pause"))
  359. except Exception:
  360. pass
  361. else:
  362. # when protector already started, we won't decrement internal counter
  363. # keep internal counter but keep ticking for display purposes
  364. if not started:
  365. self._countdown_seconds -= 1
  366. except Exception as e:
  367. # keep ticking even on error
  368. print("倒计时更新出错:", e)
  369. # schedule next tick
  370. try:
  371. self.after(1000, self._update_countdown)
  372. except Exception:
  373. pass
  374. def _toggle_pause(self):
  375. if not self.protector_runner:
  376. return
  377. try:
  378. if self.protector_runner.is_paused():
  379. self.protector_runner.resume()
  380. if self.pause_btn:
  381. self.pause_btn.config(text="Pause")
  382. else:
  383. self.protector_runner.pause()
  384. if self.pause_btn:
  385. self.pause_btn.config(text="Resume")
  386. except Exception as e:
  387. messagebox.showerror("错误", f"切换暂停/恢复失败: {e}")
  388. def run_gui(protector_runner=None):
  389. app = ProtectorGUI(protector_runner=protector_runner)
  390. app.mainloop()
  391. if __name__ == "__main__":
  392. run_gui()