gui.py 18 KB

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