Proxy auth UI, verbose Live logs, build script, docs
- Fixed exit and manual chain: optional user/pass fields with URL merge and redaction in UI/logs - config: split_proxy_for_edit, merge_proxy_credentials, redact_proxy_url, IPv6-style host bracketing - Live tab: UILogHandler to stream proxy_chain_manager logs; Verbose toggle; Clear log; httpx quiet - service/validator/fetcher: structured INFO/DEBUG for pool, GOST, validation, fetches - setup_and_build.ps1: pip upgrade, deps, PyInstaller, desktop copy + shortcut; build_exe.bat delegates - README: clone/pull to one-script desktop build flow Made-with: Cursor
This commit is contained in:
@@ -18,8 +18,11 @@ from .config import (
|
||||
OBFUSCATION_MODES,
|
||||
Settings,
|
||||
load_settings,
|
||||
merge_proxy_credentials,
|
||||
normalize_proxy_url,
|
||||
redact_proxy_url,
|
||||
save_settings,
|
||||
split_proxy_for_edit,
|
||||
)
|
||||
from .firewall import (
|
||||
disengage as fw_disengage,
|
||||
@@ -43,6 +46,21 @@ logging.basicConfig(
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class UILogHandler(logging.Handler):
|
||||
"""Forward ``proxy_chain_manager`` log records to the Live tab (via ``ui_q``). Thread-safe."""
|
||||
|
||||
def __init__(self, q: queue.Queue[dict[str, Any]]) -> None:
|
||||
super().__init__(level=logging.DEBUG)
|
||||
self._q = q
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
try:
|
||||
text = self.format(record)
|
||||
self._q.put_nowait({"type": "log", "text": text})
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
# ── palette ──────────────────────────────────────────────────────────────────
|
||||
BG = "#0d0d1a"
|
||||
CARD = "#12122a"
|
||||
@@ -104,6 +122,7 @@ def main() -> None:
|
||||
pulse_job["id"] = None
|
||||
|
||||
def _short(u: str) -> str:
|
||||
u = redact_proxy_url(u)
|
||||
u = u.replace("http://", "").replace("socks5://", "s5://").replace("socks4://", "s4://").replace("https://", "")
|
||||
return u[:28] + "…" if len(u) > 30 else u
|
||||
|
||||
@@ -405,16 +424,56 @@ def main() -> None:
|
||||
log_frame = ctk.CTkFrame(tab_live, fg_color=CARD, corner_radius=8)
|
||||
log_frame.pack(fill="both", expand=True)
|
||||
log_box = ctk.CTkTextbox(
|
||||
log_frame, font=("Consolas", 11),
|
||||
log_frame, font=("Consolas", 10),
|
||||
fg_color=BG, text_color=TEXT,
|
||||
scrollbar_button_color=ACCENT,
|
||||
)
|
||||
log_box.pack(fill="both", expand=True, padx=4, pady=4)
|
||||
log_box.pack(fill="both", expand=True, padx=4, pady=(0, 4))
|
||||
|
||||
def _log(line: str) -> None:
|
||||
log_box.insert("end", f"[{_ts()}] {line}\n")
|
||||
log_box.see("end")
|
||||
|
||||
log_tool = ctk.CTkFrame(log_frame, fg_color="transparent")
|
||||
log_tool.pack(fill="x", padx=8, pady=(6, 0), before=log_box)
|
||||
verbose_logs_var = ctk.BooleanVar(value=True)
|
||||
_pcm_logger = logging.getLogger("proxy_chain_manager")
|
||||
_ui_log_handler: UILogHandler | None = None
|
||||
for _h in _pcm_logger.handlers:
|
||||
if isinstance(_h, UILogHandler):
|
||||
_ui_log_handler = _h
|
||||
break
|
||||
if _ui_log_handler is None:
|
||||
_ui_log_handler = UILogHandler(ui_q)
|
||||
_ui_log_handler.setFormatter(
|
||||
logging.Formatter("%(levelname)-5s [%(name)s] %(message)s")
|
||||
)
|
||||
_pcm_logger.addHandler(_ui_log_handler)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
|
||||
def _sync_ui_log_level() -> None:
|
||||
on = bool(verbose_logs_var.get())
|
||||
lvl = logging.DEBUG if on else logging.INFO
|
||||
_pcm_logger.setLevel(lvl)
|
||||
if _ui_log_handler is not None:
|
||||
_ui_log_handler.setLevel(lvl)
|
||||
_log(f"UI log level: {'DEBUG (verbose)' if on else 'INFO'}")
|
||||
|
||||
ctk.CTkCheckBox(
|
||||
log_tool,
|
||||
text="Verbose logs (DEBUG in UI — file stays INFO)",
|
||||
variable=verbose_logs_var,
|
||||
command=_sync_ui_log_level,
|
||||
font=(FONT, 11),
|
||||
fg_color=ACCENT2,
|
||||
hover_color=ACCENT,
|
||||
text_color=TEXT,
|
||||
).pack(side="left", padx=(0, 12))
|
||||
_btn(log_tool, "Clear log", lambda: log_box.delete("1.0", "end"), w=88, h=26,
|
||||
fg_color=DIM, hover_color=ACCENT).pack(side="left", padx=(0, 6))
|
||||
_sync_ui_log_level()
|
||||
|
||||
# ─── TAB: CHAIN BUILDER ──────────────────────────────────────────────────
|
||||
cb_split = ctk.CTkFrame(tab_chain, fg_color="transparent")
|
||||
cb_split.pack(fill="x", pady=(4, 8))
|
||||
@@ -492,11 +551,37 @@ def main() -> None:
|
||||
height=28,
|
||||
)
|
||||
exit_proxy_entry.pack(fill="x", padx=10, pady=(0, 4))
|
||||
if s.manual_exit_proxy:
|
||||
exit_proxy_entry.insert(0, s.manual_exit_proxy)
|
||||
exit_auth_row = ctk.CTkFrame(exit_fix_frame, fg_color="transparent")
|
||||
exit_auth_row.pack(fill="x", padx=10, pady=(0, 4))
|
||||
ctk.CTkLabel(exit_auth_row, text="User", font=(FONT, 10), text_color=TEXT2, width=36).pack(
|
||||
side="left", padx=(0, 4)
|
||||
)
|
||||
exit_user_entry = ctk.CTkEntry(
|
||||
exit_auth_row, width=120, height=26, font=("Consolas", 10),
|
||||
fg_color=BG, border_color=ACCENT, placeholder_text="optional",
|
||||
)
|
||||
exit_user_entry.pack(side="left", padx=(0, 10))
|
||||
ctk.CTkLabel(exit_auth_row, text="Pass", font=(FONT, 10), text_color=TEXT2, width=36).pack(
|
||||
side="left", padx=(0, 4)
|
||||
)
|
||||
exit_pass_entry = ctk.CTkEntry(
|
||||
exit_auth_row, width=120, height=26, font=("Consolas", 10),
|
||||
fg_color=BG, border_color=ACCENT, placeholder_text="optional", show="*",
|
||||
)
|
||||
exit_pass_entry.pack(side="left", padx=(0, 0))
|
||||
|
||||
_eb, _eu, _epw = split_proxy_for_edit(s.manual_exit_proxy)
|
||||
if _eb:
|
||||
exit_proxy_entry.insert(0, _eb)
|
||||
if _eu:
|
||||
exit_user_entry.insert(0, _eu)
|
||||
if _epw:
|
||||
exit_pass_entry.insert(0, _epw)
|
||||
|
||||
ctk.CTkLabel(
|
||||
exit_fix_frame,
|
||||
text="Earlier hops still rotate randomly. Empty = fully automatic exit.\n"
|
||||
"Auth: use User/Pass (saved in settings) or paste user:pass@host in the URL field.\n"
|
||||
"Ignored while “Pin & use this chain” is enabled.",
|
||||
font=(FONT, 9),
|
||||
text_color=TEXT2,
|
||||
@@ -574,10 +659,21 @@ def main() -> None:
|
||||
add_row = ctk.CTkFrame(cb_right, fg_color="transparent")
|
||||
add_row.pack(fill="x", padx=12, pady=(0, 4))
|
||||
add_entry = ctk.CTkEntry(
|
||||
add_row, placeholder_text="http://ip:port or socks5://ip:port",
|
||||
add_row,
|
||||
placeholder_text="http://host:port or user:pass@host:port",
|
||||
font=(FONT, 10), fg_color=BG, border_color=ACCENT, height=28,
|
||||
)
|
||||
add_entry.pack(side="left", fill="x", expand=True, padx=(0, 4))
|
||||
add_user_entry = ctk.CTkEntry(
|
||||
add_row, width=88, height=28, font=("Consolas", 10),
|
||||
fg_color=BG, border_color=ACCENT, placeholder_text="user",
|
||||
)
|
||||
add_user_entry.pack(side="left", padx=(0, 2))
|
||||
add_pass_entry = ctk.CTkEntry(
|
||||
add_row, width=88, height=28, font=("Consolas", 10),
|
||||
fg_color=BG, border_color=ACCENT, placeholder_text="pass", show="*",
|
||||
)
|
||||
add_pass_entry.pack(side="left", padx=(0, 4))
|
||||
|
||||
def _add_proxy() -> None:
|
||||
v = add_entry.get().strip()
|
||||
@@ -585,9 +681,17 @@ def main() -> None:
|
||||
return
|
||||
if "://" not in v:
|
||||
v = "http://" + v
|
||||
au = add_user_entry.get().strip()
|
||||
ap = add_pass_entry.get().strip()
|
||||
if au or ap:
|
||||
v = merge_proxy_credentials(v, au, ap)
|
||||
else:
|
||||
v = normalize_proxy_url(v)
|
||||
manual_chain.append(v)
|
||||
_rebuild_chain_ui()
|
||||
add_entry.delete(0, "end")
|
||||
add_user_entry.delete(0, "end")
|
||||
add_pass_entry.delete(0, "end")
|
||||
|
||||
_btn(add_row, "+ Add", _add_proxy, w=68, h=28).pack(side="left")
|
||||
|
||||
@@ -696,7 +800,11 @@ def main() -> None:
|
||||
obfuscation_mode=mode_var.get(),
|
||||
use_pinned_chain=bool(use_manual_var.get()),
|
||||
pinned_chain=list(manual_chain),
|
||||
manual_exit_proxy=normalize_proxy_url(exit_proxy_entry.get()),
|
||||
manual_exit_proxy=merge_proxy_credentials(
|
||||
exit_proxy_entry.get(),
|
||||
exit_user_entry.get(),
|
||||
exit_pass_entry.get(),
|
||||
),
|
||||
health_check_seconds=min(3600, max(10, int(entries["health"].get().strip()))),
|
||||
full_refresh_seconds=min(86400, max(60, int(entries["refresh"].get().strip()))),
|
||||
validation_concurrency=max(1, int(entries["conc"].get().strip())),
|
||||
@@ -756,16 +864,33 @@ def main() -> None:
|
||||
|
||||
elif t == "phase":
|
||||
ph = str(m.get("phase", ""))
|
||||
phase_lbl.configure(text=ph)
|
||||
phase_labels = {
|
||||
"validate": "validate",
|
||||
"fetch": "fetch",
|
||||
"running": "running",
|
||||
"exit_check": "fixed exit",
|
||||
"verify_chain": "verify chain",
|
||||
"gost_start": "starting chain",
|
||||
}
|
||||
phase_lbl.configure(text=phase_labels.get(ph, ph or "idle"))
|
||||
if ph == "validate":
|
||||
prog_bar.configure(progress_color=YELLOW)
|
||||
elif ph == "fetch":
|
||||
prog_bar.configure(progress_color=ORANGE)
|
||||
prog_bar.set(0.15)
|
||||
elif ph == "exit_check":
|
||||
prog_bar.configure(progress_color=YELLOW)
|
||||
prog_bar.set(0.96)
|
||||
elif ph == "gost_start":
|
||||
prog_bar.configure(progress_color=GREEN)
|
||||
prog_bar.set(0.985)
|
||||
elif ph == "verify_chain":
|
||||
prog_bar.configure(progress_color=GREEN)
|
||||
prog_bar.set(0.98)
|
||||
elif ph == "running":
|
||||
prog_bar.configure(progress_color=GREEN)
|
||||
prog_bar.set(1.0)
|
||||
else:
|
||||
elif ph in ("idle", ""):
|
||||
prog_bar.set(0)
|
||||
|
||||
elif t == "validate_progress":
|
||||
@@ -849,6 +974,7 @@ def main() -> None:
|
||||
_log("Press START to fetch, validate and chain proxies.")
|
||||
_log("Use +/− in top bar to change hop count instantly.")
|
||||
_log("Chain Builder → Fixed exit: set the last hop (optional); rest still rotates.")
|
||||
_log("Verbose logs (Live tab): DEBUG lines from the engine + validator + fetcher (httpx kept quiet).")
|
||||
_log("─" * 60)
|
||||
_pump()
|
||||
root.mainloop()
|
||||
|
||||
Reference in New Issue
Block a user