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:
Dr Jones
2026-04-16 00:27:39 -07:00
parent 0316b1befc
commit 214d2d2ff7
9 changed files with 495 additions and 89 deletions

View File

@@ -3,9 +3,22 @@ from __future__ import annotations
import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
from urllib.parse import quote, unquote, urlparse, urlunparse
from .paths import app_data_dir
def _host_port_for_url(host: str, port: int | None) -> str:
"""``host`` and optional ``port`` as used in proxy URLs (bracket IPv6 literals)."""
if not host:
return ""
if ":" in host and "." not in host:
inner = f"[{host}]"
else:
inner = host
return f"{inner}:{port}" if port else inner
# Always bind the local forwarder to loopback. VM clones and NIC/IP changes (DHCP)
# do not affect 127.0.0.1; avoid storing LAN IPs or hostnames for the listener.
LISTEN_HOST = "127.0.0.1"
@@ -21,6 +34,69 @@ def normalize_proxy_url(raw: str) -> str:
return t.rstrip("/")
def split_proxy_for_edit(raw: str) -> tuple[str, str, str]:
"""Split stored proxy URL into (url_without_credentials, username, password).
Used to populate fixed-exit / form fields. Credentials are percent-decoded.
"""
t = normalize_proxy_url((raw or "").strip())
if not t:
return "", "", ""
p = urlparse(t)
if not p.hostname:
return t, "", ""
host = p.hostname
port = p.port
scheme = p.scheme or "http"
base = f"{scheme}://{_host_port_for_url(host, port)}"
u = unquote(p.username) if p.username else ""
pw = unquote(p.password) if p.password else ""
return base, u, pw
def merge_proxy_credentials(raw_url: str, username: str = "", password: str = "") -> str:
"""Build a single proxy URL: host/port from ``raw_url`` (any embedded auth ignored) plus optional auth.
User and password are URL-encoded for ``@`` and ``:`` inside values.
"""
base = normalize_proxy_url((raw_url or "").strip())
if not base:
return ""
p = urlparse(base)
if not p.hostname:
return base
host = p.hostname
port = p.port
scheme = p.scheme or "http"
user = (username or "").strip()
pw = (password or "").strip()
if not user and not pw:
# Leave credentials embedded in URL when form fields are blank (paste user:pass@host).
return base
hp = _host_port_for_url(host, port)
netloc = f"{quote(user, safe='')}:{quote(pw, safe='')}@{hp}"
return urlunparse((scheme, netloc, "", "", "", "")).rstrip("/")
def redact_proxy_url(url: str) -> str:
"""Same URL shape for logs/UI, with credentials replaced by ``***``."""
t = (url or "").strip()
if not t:
return ""
if "://" not in t:
t = "http://" + t
p = urlparse(t.rstrip("/"))
if not p.hostname:
return t
has_auth = bool(p.username) or bool(p.password)
if not has_auth:
return t.rstrip("/")
scheme = p.scheme or "http"
hp = _host_port_for_url(p.hostname, p.port)
netloc = f"***:***@{hp}"
return urlunparse((scheme, netloc, "", "", "", "")).rstrip("/")
OBFUSCATION_MODES = ["auto", "http_only", "socks5_only", "random_mix"]
OBFUSCATION_LABELS = {
"auto": "Auto (best available)",