feat: 支持第三方节点订阅
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
"""第三方 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 "已隐藏订阅地址"
|
||||
Reference in New Issue
Block a user