fix: sanitize settings JSON, asyncio get_running_loop, Defender exclusion, _btn cleanup, ASCII Save label

Made-with: Cursor
This commit is contained in:
Dr Jones
2026-04-15 13:39:27 -07:00
parent 1366a2f48e
commit 096c5094fb
4 changed files with 72 additions and 6 deletions

View File

@@ -111,7 +111,9 @@ def main() -> None:
# HELPERS
# ─────────────────────────────────────────────────────────────────────────
def _btn(parent: Any, label: str, cmd: Any, w: int = 80, h: int = 28, **kw: Any) -> ctk.CTkButton:
# Pop styling keys so they are never duplicated in **kw (CustomTkinter raises on duplicate font, etc.).
# Pull known keys out first so **kw never duplicates CTkButton parameters (font, width, …).
kw.pop("text", None)
kw.pop("command", None)
font = kw.pop("font", (FONT, 11))
fg_color = kw.pop("fg_color", ACCENT)
hover_color = kw.pop("hover_color", ACCENT2)
@@ -669,7 +671,7 @@ def main() -> None:
).pack(side="left")
ctk.CTkFrame(sf_outer, fg_color="transparent", height=4).pack()
_btn(sf_outer, "💾 Save All Settings",
_btn(sf_outer, "Save All Settings",
lambda: _save_settings(verbose=True), w=220, h=36,
font=(FONT, 13)).pack(anchor="w", padx=6, pady=8)

View File

@@ -75,6 +75,63 @@ class Settings:
return app_data_dir() / "settings.json"
def sanitize_settings(s: Settings) -> tuple[Settings, bool]:
"""Clamp invalid values from hand-edited JSON. Returns (settings, changed)."""
changed = False
try:
cl = int(s.chain_length)
if not (1 <= cl <= 8):
s.chain_length = max(1, min(8, cl))
changed = True
except (TypeError, ValueError):
s.chain_length = 3
changed = True
try:
p = int(s.local_port)
if not (1 <= p <= 65535):
s.local_port = max(1, min(65535, p))
changed = True
except (TypeError, ValueError):
s.local_port = 18888
changed = True
try:
if int(s.health_check_seconds) < 10:
s.health_check_seconds = 10
changed = True
except (TypeError, ValueError):
s.health_check_seconds = 180
changed = True
try:
if int(s.full_refresh_seconds) < 60:
s.full_refresh_seconds = 60
changed = True
except (TypeError, ValueError):
s.full_refresh_seconds = 1800
changed = True
try:
if int(s.validation_concurrency) < 1:
s.validation_concurrency = 1
changed = True
except (TypeError, ValueError):
s.validation_concurrency = 64
changed = True
try:
if int(s.max_candidates) < 10:
s.max_candidates = 10
changed = True
except (TypeError, ValueError):
s.max_candidates = 400
changed = True
try:
if float(s.validation_timeout_seconds) < 2.0:
s.validation_timeout_seconds = 2.0
changed = True
except (TypeError, ValueError):
s.validation_timeout_seconds = 12.0
changed = True
return s, changed
def normalize_listen_host(s: Settings) -> tuple[Settings, bool]:
"""Force loopback bind so LAN/DHCP IP changes (e.g. cloned VMs) never break the listener."""
if s.local_host == LISTEN_HOST:
@@ -103,7 +160,9 @@ def load_settings() -> Settings:
s.pinned_chain = []
if not hasattr(s, "manual_exit_proxy") or not isinstance(s.manual_exit_proxy, str):
s.manual_exit_proxy = ""
s, changed = normalize_listen_host(s)
s, ch1 = sanitize_settings(s)
s, ch2 = normalize_listen_host(s)
changed = ch1 or ch2
if changed:
try:
p.write_text(json.dumps(asdict(s), indent=2), encoding="utf-8")
@@ -113,6 +172,7 @@ def load_settings() -> Settings:
def save_settings(s: Settings) -> None:
s, _ = sanitize_settings(s)
s, _ = normalize_listen_host(s)
p = app_data_dir() / "settings.json"
p.write_text(json.dumps(asdict(s), indent=2), encoding="utf-8")

View File

@@ -22,9 +22,13 @@ GOST_RELEASE_ZIP = (
def _add_defender_exclusion(path: Path) -> None:
"""Add Windows Defender exclusion so GOST is not quarantined."""
try:
# ExclusionPath covers gost.exe under this folder; avoid invalid combined params on older builds.
folder = str(path.parent).replace("'", "''")
subprocess.run(
["powershell", "-NoProfile", "-NonInteractive", "-Command",
f"Add-MpPreference -ExclusionPath '{path.parent}' -ExclusionProcess 'gost.exe' -ErrorAction SilentlyContinue"],
[
"powershell", "-NoProfile", "-NonInteractive", "-Command",
f"Add-MpPreference -ExclusionPath '{folder}' -ErrorAction SilentlyContinue",
],
capture_output=True,
timeout=30,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),

View File

@@ -375,7 +375,7 @@ class ChainService:
async def _build_pool(self) -> list[str]:
"""Fetch and validate proxies. Runs blocking I/O in thread pool."""
s = self._settings
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
# Fetch all sources concurrently in thread pool (they are blocking)
async def _fetch_one(url: str) -> list[str]: