重构:模块化 RRPP Lite 并更新入口
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""连通性探测:用 test/nodes.json 的真实节点,走 _tls_start 转发逻辑实测。
|
||||
|
||||
仅需标准库;不启动 GUI(main 的 GUI 在 __main__ 保护下)。
|
||||
用法: python3 test/probe_nodes.py [JP US ...] 不带参数测全部
|
||||
"""
|
||||
import os, sys, time, json, ssl, socket, urllib.request, urllib.error
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
LITE = HERE.parent
|
||||
sys.path.insert(0, str(HERE)) # PyQt5 桩
|
||||
sys.path.insert(0, str(LITE))
|
||||
|
||||
for k in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "all_proxy", "ALL_PROXY"):
|
||||
os.environ.pop(k, None)
|
||||
# 不能设 NO_PROXY=*:urllib 会把 127.0.0.1 也当直连,显式 ProxyHandler 被绕过
|
||||
os.environ["NO_PROXY"] = ""
|
||||
|
||||
import main as pp
|
||||
|
||||
NODES = json.loads((HERE / "nodes.json").read_text("utf-8"))
|
||||
want = [a.upper() for a in sys.argv[1:]]
|
||||
if want:
|
||||
NODES = [n for n in NODES if str(n.get("country", "")).upper() in want]
|
||||
|
||||
TIMEOUT = 12
|
||||
|
||||
|
||||
def free_port():
|
||||
s = socket.socket(); s.bind(("127.0.0.1", 0))
|
||||
p = s.getsockname()[1]; s.close(); return p
|
||||
|
||||
|
||||
def http_method(node):
|
||||
return (node.get("methods") or {}).get("http") or {}
|
||||
|
||||
|
||||
def as_flat_node(node):
|
||||
"""把 nodes.json 的 methods.http 结构摊平成代码 _node_val() 期望的形状。
|
||||
仅用于探测:暴露「代码读不到真实节点字段」这一问题。"""
|
||||
m = http_method(node)
|
||||
return dict(node, port=int(m.get("port")), username=m.get("username", ""),
|
||||
password=m.get("password", ""))
|
||||
|
||||
|
||||
def probe_connect(host, port, sni=None, connect_timeout=8):
|
||||
"""直连探测:DNS -> TCP -> TLS -> 发 CONNECT 127.0.0.1:9 看上游代理响应。"""
|
||||
res = {"dns": None, "tcp": None, "tls": None, "proxy": None, "err": None}
|
||||
t0 = time.time()
|
||||
try:
|
||||
infos = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
|
||||
res["dns"] = sorted({i[4][0] for i in infos})
|
||||
except Exception as e:
|
||||
res["err"] = f"DNS: {e}"
|
||||
return res
|
||||
addr = res["dns"][0]
|
||||
af = socket.AF_INET6 if ":" in addr else socket.AF_INET
|
||||
try:
|
||||
s = socket.socket(af, socket.SOCK_STREAM)
|
||||
s.settimeout(connect_timeout)
|
||||
s.connect((addr, port))
|
||||
res["tcp"] = round(time.time() - t0, 2)
|
||||
except Exception as e:
|
||||
res["err"] = f"TCP: {e}"
|
||||
return res
|
||||
try:
|
||||
ctx = ssl.create_default_context()
|
||||
s = ctx.wrap_socket(s, server_hostname=sni or host)
|
||||
res["tls"] = s.version()
|
||||
except Exception as e:
|
||||
res["err"] = f"TLS: {e}"
|
||||
try: s.close()
|
||||
except Exception: pass
|
||||
return res
|
||||
try:
|
||||
s.sendall(b"CONNECT 127.0.0.1:9 HTTP/1.1\r\nHost: 127.0.0.1:9\r\n\r\n")
|
||||
res["proxy"] = s.recv(200).split(b"\r\n", 1)[0].decode("latin-1", "replace")
|
||||
except Exception as e:
|
||||
res["err"] = f"CONNECT: {e}"
|
||||
try: s.close()
|
||||
except Exception: pass
|
||||
return res
|
||||
|
||||
|
||||
def probe_via_forwarder(node):
|
||||
"""真实转发器链路:_tls_start -> 经 127.0.0.1 代理请求,取出口 IP 与归属地。"""
|
||||
m = http_method(node)
|
||||
if not m:
|
||||
return "(该节点无 http 方法,跳过)"
|
||||
old_node, old_port = pp._current_node, pp._TLS_LOCAL_PORT
|
||||
port = free_port()
|
||||
try:
|
||||
# _proxy_auth_basic() 内部走 _current_node(),固定成本次待测节点
|
||||
pp._current_node = lambda: node
|
||||
pp._TLS_LOCAL_PORT = port
|
||||
pp._tls_stop()
|
||||
pp._tls_start(node["domain"], int(m["port"]), use_tls=True,
|
||||
bypass_lan=False, smart=False, quiet=True)
|
||||
ph = urllib.request.ProxyHandler({"http": f"127.0.0.1:{port}",
|
||||
"https": f"127.0.0.1:{port}"})
|
||||
op = urllib.request.build_opener(ph)
|
||||
t0 = time.time()
|
||||
# 明文 HTTP 代理(走 GET 绝对 URI)与 CONNECT 隧道各测一条
|
||||
with op.open("http://ip-api.com/json/?fields=query,country,regionName,isp",
|
||||
timeout=TIMEOUT) as r:
|
||||
info = json.loads(r.read(512).decode("utf-8", "replace"))
|
||||
with op.open("https://ifconfig.me/ip", timeout=TIMEOUT) as r:
|
||||
https_ip = r.read(64).decode().strip()
|
||||
dt = round(time.time() - t0, 2)
|
||||
return (f"OK {dt}s 出口IP={info.get('query')} "
|
||||
f"[{info.get('country')} {info.get('regionName')} / {info.get('isp')}] "
|
||||
f"HTTPS隧道出口={https_ip}")
|
||||
except urllib.error.HTTPError as e:
|
||||
return f"HTTP {e.code}: {e.read(120).decode('utf-8', 'replace').strip()[:100]}"
|
||||
except Exception as e:
|
||||
return f"失败: {type(e).__name__}: {e}"
|
||||
finally:
|
||||
pp._tls_stop()
|
||||
pp._current_node, pp._TLS_LOCAL_PORT = old_node, old_port
|
||||
|
||||
|
||||
print(f"节点数: {len(NODES)}")
|
||||
n0 = NODES[0]
|
||||
print(f"代码 _endpoint_domain()={pp._endpoint_domain()!r} "
|
||||
f"_endpoint_port()={pp._endpoint_port()!r} _endpoint_country()={pp._endpoint_country()!r}")
|
||||
print(f"代码 _proxy_auth_basic()={pp._proxy_auth_basic()!r}")
|
||||
print(f"nodes.json[0] 顶层键 = {list(n0.keys())}")
|
||||
print(f"nodes.json[0] domain={n0.get('domain')!r} methods.http.port="
|
||||
f"{http_method(n0).get('port')!r}")
|
||||
print()
|
||||
|
||||
for n in NODES:
|
||||
c = n.get("country", "?")
|
||||
m = http_method(n)
|
||||
print("=" * 78)
|
||||
print(f"[{c}] {n.get('domain')}:{m.get('port')} provider={n.get('provider')} "
|
||||
f"traffic={n.get('traffic')} level={n.get('available_level')}")
|
||||
if not m:
|
||||
print(" 该节点无 http 方法,跳过")
|
||||
continue
|
||||
r = probe_connect(n["domain"], int(m["port"]))
|
||||
print(f" DNS : {r['dns']}")
|
||||
print(f" TCP : {r['tcp']}s" if r["tcp"] is not None else " TCP : -")
|
||||
print(f" TLS : {r['tls']}" if r.get("tls") else " TLS : -")
|
||||
print(f" CONNECT: {r['proxy']}" if r.get("proxy") else " CONNECT: -")
|
||||
if r.get("err"):
|
||||
print(f" 错误 : {r['err'].splitlines()[0][:200]}")
|
||||
print(f" 经转发器: {probe_via_forwarder(n)}")
|
||||
print(f" 经转发器(摊平节点): {probe_via_forwarder(as_flat_node(n))}")
|
||||
print()
|
||||
Reference in New Issue
Block a user