修复: - 节点认证方式升级为 SSO:user/password 按 URL-safe Base64 作为查询参数拼接在 config.json 的 nodes_url 后(旧 nginx auth_basic 已废弃),服务端返回的 401/403/429 等中文原因直接透出到登录框,登录框输入占位改为 SSO 邮箱
This commit is contained in:
+40
-14
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user