修复: - 节点认证方式升级为 SSO:user/password 按 URL-safe Base64 作为查询参数拼接在 config.json 的 nodes_url 后(旧 nginx auth_basic 已废弃),服务端返回的 401/403/429 等中文原因直接透出到登录框,登录框输入占位改为 SSO 邮箱
This commit is contained in:
+39
-13
@@ -895,16 +895,40 @@ def _nodes_sync_remote():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f"节点配置在线更新失败,使用本地配置: {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):
|
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 json
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||||
req = urllib.request.Request(url)
|
sep = "&" if "?" in url else "?"
|
||||||
req.add_header("Authorization", "Basic " + base64.b64encode(
|
req_url = f"{url}{sep}user={_b64url(user)}&password={_b64url(password)}"
|
||||||
f"{user}:{password}".encode()).decode())
|
try:
|
||||||
with opener.open(req, timeout=timeout) as r:
|
with opener.open(req_url, timeout=timeout) as r:
|
||||||
return json.loads(r.read().decode("utf-8"))
|
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():
|
def _user_init_local():
|
||||||
@@ -922,7 +946,7 @@ def _user_init_local():
|
|||||||
def _user_init_remote(base_url=None):
|
def _user_init_remote(base_url=None):
|
||||||
"""检查并更新 PAC 数据(后台线程):
|
"""检查并更新 PAC 数据(后台线程):
|
||||||
直接拉取两个数据文件(内含 version/date),按版本比对后更新;失败用本地副本。
|
直接拉取两个数据文件(内含 version/date),按版本比对后更新;失败用本地副本。
|
||||||
节点配置的拉取走登录框(nodes_url 需 nginx 认证,见 LoginDialog)。
|
节点配置的拉取走登录框(nodes_url 需 SSO 认证,见 LoginDialog)。
|
||||||
"""
|
"""
|
||||||
if _skip_update_check():
|
if _skip_update_check():
|
||||||
return # 设置跳过:不拉取也不打日志
|
return # 设置跳过:不拉取也不打日志
|
||||||
@@ -1397,7 +1421,8 @@ class NodeLabel(QLabel):
|
|||||||
# ── 节点配置登录框 ─────────────────────────────────────
|
# ── 节点配置登录框 ─────────────────────────────────────
|
||||||
|
|
||||||
class LoginDialog(QDialog):
|
class LoginDialog(QDialog):
|
||||||
"""节点配置登录框:凭据参与 nodes_url 的 nginx 认证,成功后拉取并加密保存 nodes.json。"""
|
"""节点配置登录框:SSO 账号(邮箱+密码,base64url 查询参数)认证 nodes_url,
|
||||||
|
成功后拉取并加密保存 nodes.json。"""
|
||||||
_done = pyqtSignal(bool, str)
|
_done = pyqtSignal(bool, str)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
@@ -1454,7 +1479,7 @@ class LoginDialog(QDialog):
|
|||||||
form = QVBoxLayout()
|
form = QVBoxLayout()
|
||||||
form.setSpacing(6)
|
form.setSpacing(6)
|
||||||
self.ed_user = QLineEdit()
|
self.ed_user = QLineEdit()
|
||||||
self.ed_user.setPlaceholderText("登录名")
|
self.ed_user.setPlaceholderText("SSO 邮箱")
|
||||||
form.addWidget(self.ed_user)
|
form.addWidget(self.ed_user)
|
||||||
|
|
||||||
self.ed_pass = QLineEdit()
|
self.ed_pass = QLineEdit()
|
||||||
@@ -1509,7 +1534,7 @@ class LoginDialog(QDialog):
|
|||||||
threading.Thread(target=self._worker, args=(user, password), daemon=True).start()
|
threading.Thread(target=self._worker, args=(user, password), daemon=True).start()
|
||||||
|
|
||||||
def _worker(self, user, password):
|
def _worker(self, user, password):
|
||||||
"""后台登录:Basic 认证拉取 nodes.json,校验后加密写入用户数据目录。"""
|
"""后台登录:SSO 认证拉取 nodes.json,校验后加密写入用户数据目录。"""
|
||||||
try:
|
try:
|
||||||
import json
|
import json
|
||||||
url = str(_config_val("nodes_url", "") or "").strip()
|
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"
|
text = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
|
||||||
_USER_DIR.mkdir(parents=True, exist_ok=True)
|
_USER_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
(_USER_DIR / "nodes.json").write_bytes(_nodes_encrypt(text))
|
(_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)))
|
self._done.emit(True, str(len(data)))
|
||||||
|
except _AuthError as e:
|
||||||
|
# 服务端拒绝:直接展示其中文原因(认证失败/未开通/限流等)
|
||||||
|
self._done.emit(False, str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = ("认证失败,请检查用户名/密码" if getattr(e, "code", None) == 401
|
self._done.emit(False, f"登录失败: {e}")
|
||||||
else f"登录失败: {e}")
|
|
||||||
self._done.emit(False, msg)
|
|
||||||
|
|
||||||
def _on_done(self, ok, info):
|
def _on_done(self, ok, info):
|
||||||
self.btn_ok.setEnabled(True)
|
self.btn_ok.setEnabled(True)
|
||||||
|
|||||||
Reference in New Issue
Block a user