Files
proxy-god/proxy_chain_manager/tray.py
Dr Jones 792b320257
Some checks failed
CI / Test Python 3.10 (push) Has been cancelled
CI / Test Python 3.11 (push) Has been cancelled
CI / Test Python 3.12 (push) Has been cancelled
fix: address P0/P1/P2 audit findings — security, reliability, CI, docs
P0 - Critical:
- config.py: add _mask() credential-redaction helper, SETTINGS_SCHEMA_VERSION,
  plaintext-storage warning; corrupt settings.json backed up before defaults
- gost_util.py: pin SHA256 for gost_3.2.6_windows_amd64.zip; verify before
  extraction; _add_defender_exclusion now logs warning on failure
- firewall.py: add emergency_disengage() for atexit/signal use
- service.py: register fw_emergency_disengage via atexit + SIGTERM/SIGINT;
  stop() calls emergency_disengage if thread hangs past 15s timeout
- app.py: wrap main() in top-level except with CTk error dialog + log
- LICENSE: add MIT license file

P1 - Important:
- app.py: switch to RotatingFileHandler (5 MB / 3 backups)
- config.py: settings_version + migrate(); save_settings() writes .bak before
  overwrite; _is_safe_https_url() strips RFC-1918 sources/ip_check_url
- service.py: threading.Lock on _settings; _signal_handler; graceful stop
- requirements.txt: pin exact versions; add cryptography==48.0.0
- .gitignore: add settings.json, credential JSON files, screenshot noise
- tray.py, dns_leak.py, gost_util.py: replace bare except:pass with logging
- CHANGELOG.md: document all session changes

P2 - Nice to have:
- .github/workflows/test.yml: CI on Python 3.10/3.11/3.12 windows-latest
- run.py: --version / -V flag
- docs/OPERATOR_RUNBOOK.md: emergency disengage, proxy leak, GOST, settings

Tests: 47/47 passed (python -m unittest discover -s tests -v)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 23:49:02 -07:00

147 lines
4.8 KiB
Python

"""System tray: LED-style proxy on/off + hover tooltip with exit IP."""
from __future__ import annotations
import logging
import threading
from typing import Any, Callable
log = logging.getLogger(__name__)
import pystray
from PIL import Image, ImageDraw, ImageFilter
_SIZE = 64
def _hex_rgb(h: str) -> tuple[int, int, int]:
h = h.lstrip("#")
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
def _led_icon(led: str, glow: str, rim: str = "#2d3748") -> Image.Image:
"""Traffic-light style icon: dark housing + glowing LED."""
base = Image.new("RGBA", (_SIZE, _SIZE), (0, 0, 0, 0))
draw = ImageDraw.Draw(base)
cx, cy = _SIZE // 2, _SIZE // 2
# Outer housing (rounded rect feel via ellipse)
draw.ellipse([4, 4, _SIZE - 4, _SIZE - 4], fill="#0d0d1a", outline=rim, width=2)
draw.ellipse([10, 10, _SIZE - 10, _SIZE - 10], fill="#12122a", outline="#1a1a3e", width=1)
glow_layer = Image.new("RGBA", (_SIZE, _SIZE), (0, 0, 0, 0))
g = ImageDraw.Draw(glow_layer)
lr, lg, lb = _hex_rgb(glow)
for radius, alpha in ((22, 35), (16, 70), (11, 120)):
g.ellipse(
[cx - radius, cy - radius, cx + radius, cy + radius],
fill=(lr, lg, lb, alpha),
)
glow_layer = glow_layer.filter(ImageFilter.GaussianBlur(radius=2))
base = Image.alpha_composite(base, glow_layer)
draw = ImageDraw.Draw(base)
dr, dg, db = _hex_rgb(led)
draw.ellipse([cx - 9, cy - 9, cx + 9, cy + 9], fill=(dr, dg, db, 255))
draw.ellipse([cx - 5, cy - 6, cx + 2, cy - 1], fill=(255, 255, 255, 90))
return base
ICONS = {
"green": _led_icon("#00e676", "#00e676"),
"red": _led_icon("#ff1744", "#ff1744"),
"yellow": _led_icon("#ffab00", "#ffab00"),
"gray": _led_icon("#4a5568", "#3d4a5c", rim="#374151"),
}
_STATE_LABEL = {
"green": "PROXY ON",
"red": "PROXY OFF / ERROR",
"yellow": "CONNECTING…",
"gray": "STOPPED",
}
def _tooltip(state: str, exit_ip: str | None) -> str:
label = _STATE_LABEL.get(state, "Proxy God")
if exit_ip:
return f"Proxy God — {label}\nExit IP: {exit_ip}"
if state == "green":
return f"Proxy God — {label}\nExit IP: (checking…)"
return f"Proxy God — {label}\nExit IP: —"
class TrayIcon:
def __init__(
self,
on_show: Callable[[], None],
on_quit: Callable[[], None],
on_rotate: Callable[[], None],
) -> None:
self._on_show = on_show
self._on_quit = on_quit
self._on_rotate = on_rotate
self._icon: pystray.Icon | None = None
self._thread: threading.Thread | None = None
self._state = "gray"
self._exit_ip: str | None = None
self._lock = threading.Lock()
def start(self) -> None:
if self._thread and self._thread.is_alive():
return
menu = pystray.Menu(
pystray.MenuItem("Show Proxy God", self._show, default=True),
pystray.MenuItem("Rotate chain now", self._rotate),
pystray.Menu.SEPARATOR,
pystray.MenuItem("Quit", self._quit),
)
with self._lock:
tip = _tooltip(self._state, self._exit_ip)
self._icon = pystray.Icon(
"ProxyGod",
icon=ICONS[self._state],
title=tip,
menu=menu,
)
self._thread = threading.Thread(target=self._icon.run, daemon=True)
self._thread.start()
def stop(self) -> None:
if self._icon:
try:
self._icon.stop()
except Exception as exc:
log.warning("Tray icon stop failed: %s", exc)
def set_state(self, state: str, exit_ip: str | None = None) -> None:
"""state: green (on), red (error), yellow (connecting), gray (stopped).
Pass exit_ip to refresh the hover tooltip. Use exit_ip=None to keep the last IP.
"""
if state not in ICONS:
state = "gray"
with self._lock:
self._state = state
if exit_ip is not None:
self._exit_ip = exit_ip.strip() if exit_ip else None
if self._icon:
self._icon.icon = ICONS[state]
self._icon.title = _tooltip(state, self._exit_ip)
def set_exit_ip(self, exit_ip: str | None) -> None:
"""Update tooltip only (e.g. after health check, same LED color)."""
with self._lock:
self._exit_ip = exit_ip.strip() if exit_ip else None
if self._icon:
self._icon.title = _tooltip(self._state, self._exit_ip)
def _show(self, icon: Any = None, item: Any = None) -> None:
self._on_show()
def _rotate(self, icon: Any = None, item: Any = None) -> None:
self._on_rotate()
def _quit(self, icon: Any = None, item: Any = None) -> None:
self._on_quit()