diff --git a/lite/proxy_lite.py b/lite/proxy_lite.py index 233af7a..ce07798 100644 --- a/lite/proxy_lite.py +++ b/lite/proxy_lite.py @@ -895,16 +895,40 @@ def _nodes_sync_remote(): except Exception as e: log(f"节点配置在线更新失败,使用本地配置: {e}") +class _AuthError(Exception): + """订阅接口返回的错误:code 为 HTTP 状态码,str 为服务端返回的中文提示。""" + + def __init__(self, msg, code=0): + super().__init__(msg) + self.code = code + + +def _b64url(s: str) -> str: + """SSO 认证查询参数的 URL-safe Base64 编码(末尾 padding 省略)。""" + return base64.urlsafe_b64encode(str(s).encode("utf-8")).decode("ascii").rstrip("=") + + def _fetch_nodes_auth(url, user, password, timeout=10): - """带 Basic 认证拉取 nodes.json(参与 nginx 认证;直连,不经过系统代理)。""" + """用 SSO 账号拉取 nodes.json:user/password 按 URL-safe Base64 作为查询参数 + 拼接在 nodes_url 后(旧版 nginx auth_basic 已废弃);直连,不经过系统代理。 + 失败抛 _AuthError(服务端拒绝,str 为服务端中文原因)或 urllib 异常(网络问题)。""" import json import urllib.request + import urllib.error opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) - req = urllib.request.Request(url) - req.add_header("Authorization", "Basic " + base64.b64encode( - f"{user}:{password}".encode()).decode()) - with opener.open(req, timeout=timeout) as r: - return json.loads(r.read().decode("utf-8")) + sep = "&" if "?" in url else "?" + req_url = f"{url}{sep}user={_b64url(user)}&password={_b64url(password)}" + try: + with opener.open(req_url, timeout=timeout) as r: + return json.loads(r.read().decode("utf-8")) + except urllib.error.HTTPError as e: + # 401/403/429/5xx:服务端 body 是面向用户的中文原因,直接透出 + body = "" + try: + body = e.read().decode("utf-8", "replace").strip() + except Exception: + pass + raise _AuthError(body or f"请求被拒绝 (HTTP {e.code})", e.code) from None def _user_init_local(): @@ -922,7 +946,7 @@ def _user_init_local(): def _user_init_remote(base_url=None): """检查并更新 PAC 数据(后台线程): 直接拉取两个数据文件(内含 version/date),按版本比对后更新;失败用本地副本。 - 节点配置的拉取走登录框(nodes_url 需 nginx 认证,见 LoginDialog)。 + 节点配置的拉取走登录框(nodes_url 需 SSO 认证,见 LoginDialog)。 """ if _skip_update_check(): return # 设置跳过:不拉取也不打日志 @@ -1397,7 +1421,8 @@ class NodeLabel(QLabel): # ── 节点配置登录框 ───────────────────────────────────── class LoginDialog(QDialog): - """节点配置登录框:凭据参与 nodes_url 的 nginx 认证,成功后拉取并加密保存 nodes.json。""" + """节点配置登录框:SSO 账号(邮箱+密码,base64url 查询参数)认证 nodes_url, + 成功后拉取并加密保存 nodes.json。""" _done = pyqtSignal(bool, str) def __init__(self, parent=None): @@ -1454,7 +1479,7 @@ class LoginDialog(QDialog): form = QVBoxLayout() form.setSpacing(6) self.ed_user = QLineEdit() - self.ed_user.setPlaceholderText("登录名") + self.ed_user.setPlaceholderText("SSO 邮箱") form.addWidget(self.ed_user) self.ed_pass = QLineEdit() @@ -1509,7 +1534,7 @@ class LoginDialog(QDialog): threading.Thread(target=self._worker, args=(user, password), daemon=True).start() def _worker(self, user, password): - """后台登录:Basic 认证拉取 nodes.json,校验后加密写入用户数据目录。""" + """后台登录:SSO 认证拉取 nodes.json,校验后加密写入用户数据目录。""" try: import json url = str(_config_val("nodes_url", "") or "").strip() @@ -1523,12 +1548,13 @@ class LoginDialog(QDialog): text = json.dumps(data, ensure_ascii=False, indent=2) + "\n" _USER_DIR.mkdir(parents=True, exist_ok=True) (_USER_DIR / "nodes.json").write_bytes(_nodes_encrypt(text)) - _auth_save(user, password) # 记住凭据,之后每次启动自动同步节点 + _auth_save(user, password) # 记住 SSO 凭据,之后每次启动自动同步节点 self._done.emit(True, str(len(data))) + except _AuthError as e: + # 服务端拒绝:直接展示其中文原因(认证失败/未开通/限流等) + self._done.emit(False, str(e)) except Exception as e: - msg = ("认证失败,请检查用户名/密码" if getattr(e, "code", None) == 401 - else f"登录失败: {e}") - self._done.emit(False, msg) + self._done.emit(False, f"登录失败: {e}") def _on_done(self, ok, info): self.btn_ok.setEnabled(True)