fix: sanitize settings JSON, asyncio get_running_loop, Defender exclusion, _btn cleanup, ASCII Save label
Made-with: Cursor
This commit is contained in:
@@ -111,7 +111,9 @@ def main() -> None:
|
|||||||
# HELPERS
|
# HELPERS
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
def _btn(parent: Any, label: str, cmd: Any, w: int = 80, h: int = 28, **kw: Any) -> ctk.CTkButton:
|
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))
|
font = kw.pop("font", (FONT, 11))
|
||||||
fg_color = kw.pop("fg_color", ACCENT)
|
fg_color = kw.pop("fg_color", ACCENT)
|
||||||
hover_color = kw.pop("hover_color", ACCENT2)
|
hover_color = kw.pop("hover_color", ACCENT2)
|
||||||
@@ -669,7 +671,7 @@ def main() -> None:
|
|||||||
).pack(side="left")
|
).pack(side="left")
|
||||||
|
|
||||||
ctk.CTkFrame(sf_outer, fg_color="transparent", height=4).pack()
|
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,
|
lambda: _save_settings(verbose=True), w=220, h=36,
|
||||||
font=(FONT, 13)).pack(anchor="w", padx=6, pady=8)
|
font=(FONT, 13)).pack(anchor="w", padx=6, pady=8)
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,63 @@ class Settings:
|
|||||||
return app_data_dir() / "settings.json"
|
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]:
|
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."""
|
"""Force loopback bind so LAN/DHCP IP changes (e.g. cloned VMs) never break the listener."""
|
||||||
if s.local_host == LISTEN_HOST:
|
if s.local_host == LISTEN_HOST:
|
||||||
@@ -103,7 +160,9 @@ def load_settings() -> Settings:
|
|||||||
s.pinned_chain = []
|
s.pinned_chain = []
|
||||||
if not hasattr(s, "manual_exit_proxy") or not isinstance(s.manual_exit_proxy, str):
|
if not hasattr(s, "manual_exit_proxy") or not isinstance(s.manual_exit_proxy, str):
|
||||||
s.manual_exit_proxy = ""
|
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:
|
if changed:
|
||||||
try:
|
try:
|
||||||
p.write_text(json.dumps(asdict(s), indent=2), encoding="utf-8")
|
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:
|
def save_settings(s: Settings) -> None:
|
||||||
|
s, _ = sanitize_settings(s)
|
||||||
s, _ = normalize_listen_host(s)
|
s, _ = normalize_listen_host(s)
|
||||||
p = app_data_dir() / "settings.json"
|
p = app_data_dir() / "settings.json"
|
||||||
p.write_text(json.dumps(asdict(s), indent=2), encoding="utf-8")
|
p.write_text(json.dumps(asdict(s), indent=2), encoding="utf-8")
|
||||||
|
|||||||
@@ -22,9 +22,13 @@ GOST_RELEASE_ZIP = (
|
|||||||
def _add_defender_exclusion(path: Path) -> None:
|
def _add_defender_exclusion(path: Path) -> None:
|
||||||
"""Add Windows Defender exclusion so GOST is not quarantined."""
|
"""Add Windows Defender exclusion so GOST is not quarantined."""
|
||||||
try:
|
try:
|
||||||
|
# ExclusionPath covers gost.exe under this folder; avoid invalid combined params on older builds.
|
||||||
|
folder = str(path.parent).replace("'", "''")
|
||||||
subprocess.run(
|
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,
|
capture_output=True,
|
||||||
timeout=30,
|
timeout=30,
|
||||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ class ChainService:
|
|||||||
async def _build_pool(self) -> list[str]:
|
async def _build_pool(self) -> list[str]:
|
||||||
"""Fetch and validate proxies. Runs blocking I/O in thread pool."""
|
"""Fetch and validate proxies. Runs blocking I/O in thread pool."""
|
||||||
s = self._settings
|
s = self._settings
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
# Fetch all sources concurrently in thread pool (they are blocking)
|
# Fetch all sources concurrently in thread pool (they are blocking)
|
||||||
async def _fetch_one(url: str) -> list[str]:
|
async def _fetch_one(url: str) -> list[str]:
|
||||||
|
|||||||
Reference in New Issue
Block a user