"""第三方 RRPP Client JSON 订阅的下载、校验与展示辅助。""" import json import urllib.request from urllib.parse import urlsplit, urlunsplit def fetch_nodes(url: str, timeout=15): """直连读取一个第三方订阅,避免首次导入受系统代理残留影响。""" opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) request = urllib.request.Request( url, headers={"Accept": "application/json", "User-Agent": "RRPP-Lite"}, ) with opener.open(request, timeout=timeout) as response: return validate_nodes(json.loads(response.read().decode("utf-8-sig"))) def validate_nodes(data): """校验 RRPP Client 节点数组,返回可安全写入本地缓存的副本。""" if not isinstance(data, list) or not data: raise ValueError("订阅应为非空的 JSON 节点数组") result = [] for index, node in enumerate(data, 1): if not isinstance(node, dict): raise ValueError(f"第 {index} 项不是节点对象") domain = str(node.get("domain") or "").strip() method = (node.get("methods") or {}).get("http") or {} port = method.get("port", node.get("port")) try: port = int(port) except (TypeError, ValueError): port = 0 if not domain: raise ValueError(f"第 {index} 项缺少 domain") if not 1 <= port <= 65535: raise ValueError(f"第 {index} 项的 HTTP 代理端口无效") result.append(dict(node)) return result def mask_url(url: str) -> str: """用于 UI 展示,隐藏查询参数和 URL 用户信息中的敏感令牌。""" try: parts = urlsplit(str(url)) host = parts.hostname or "" if parts.port: host = f"{host}:{parts.port}" if parts.username: host = "***@" + host return urlunsplit((parts.scheme, host, parts.path, "…" if parts.query else "", "")) except Exception: return "已隐藏订阅地址"