Files
ykxmz/_tests/test_relay_local.py
T
xmz7891 b62ac865ea ykxmz 1.5.0:修复手表端 TLS 证书校验 + MTU 修复网络稳定性
被控端
- 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%)
- 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败)
- 每分钟回头找局域网,自动切回快 400 倍的路径
- 画面心跳帧:静止时省流量,客户端仍知道流是活的
- 锁屏时给出可操作的提示

手表端
- 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException
- 连接错误翻译成人话(证书/时间/解析/超时分别提示)
- 经中转连上后用服务器报的地址刷新已配对记录
- 新增局域网连接页面、服务器延迟检测

服务器端
- 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502)
- 网络调优脚本 net-tune.sh(开机自动应用)
- 管理页:批量删除、网络体检、下载区
- 隧道改 HTTP/2 + 双连接

文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
2026-10-06 19:01:05 +08:00

302 lines
11 KiB
Python

# -*- coding: utf-8 -*-
"""本地验证:被控端经中转给手表提供服务(不碰隧道)。
把一个中转服务器跑在本地,被控端主动连它,再模拟手表走同样的接口。
数据流:
手表 -> relay -> RelaySocket -> Session -> 命令处理 -> RelaySocket
-> relay -> 手表
这一版中途不再用同步 subprocess.call(上一版草稿就是这么错的)。
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request
from pathlib import Path
# Cloudflare 的 Browser Integrity Check 会拦 Python 默认 UA
# (返回 403 + error code 1010),所以请求都要带上这个。
_UA = {"User-Agent": "LanRemote/1.4.0"}
ROOT = Path(__file__).resolve().parent.parent
WINDOWS = ROOT / "windows"
RELAY_PORT = 8090
AGENT_PORT = 8899
RELAY_URL = f"http://127.0.0.1:{RELAY_PORT}"
PAIR_CODE = "135790"
passed = 0
failed = 0
def check(name, ok, detail=""):
global passed, failed
if ok:
passed += 1
print(" [PASS] " + name)
else:
failed += 1
print(" [FAIL] " + name + " " + str(detail))
def relay_post(path, body=b"", headers=None, timeout=40.0):
req = urllib.request.Request(RELAY_URL + path, data=body, method="POST", headers=_UA)
for name, value in (headers or {}).items():
req.add_header(name, value)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.status, resp.read()
except urllib.error.HTTPError as exc:
return exc.code, exc.read()
def relay_get(path, timeout=10.0):
try:
_req = urllib.request.Request(RELAY_URL + path, headers=_UA)
with urllib.request.urlopen(_req, timeout=timeout) as resp:
return resp.status, resp.read()
except urllib.error.HTTPError as exc:
return exc.code, exc.read()
class Watch:
"""模拟手表:经中转和被控端说协议。"""
def __init__(self, session: str):
self.session = session
self.buf = b""
def send(self, obj: dict) -> None:
payload = (json.dumps(obj, ensure_ascii=False) + "\n").encode("utf-8")
status, _ = relay_post("/v1/client/send", payload,
{"X-Session": self.session})
if status != 200:
raise RuntimeError(f"发送失败 {status}")
def read_reply(self, timeout=40.0):
deadline = time.time() + timeout
while time.time() < deadline:
while b"\n" in self.buf:
line, _, self.buf = self.buf.partition(b"\n")
if not line.strip():
continue
message = json.loads(line.decode("utf-8"))
if "event" in message:
continue
return message
status, body = relay_post("/v1/client/poll", b"",
{"X-Session": self.session},
timeout=max(5.0, deadline - time.time()))
if status != 200:
time.sleep(0.5)
continue
self.buf += body
raise TimeoutError("等回复超时")
def main() -> int:
agent_dir = ROOT / "windows"
real_data = agent_dir / "data"
backup = agent_dir / "data_bk_relaytest"
if backup.exists():
shutil.rmtree(backup, ignore_errors=True)
had_data = real_data.exists()
if had_data:
shutil.move(str(real_data), str(backup))
relay_data = Path(tempfile.mkdtemp(prefix="relaytest_"))
relay_proc = agent_proc = None
try:
# ---------------- 1. 配置被控端 ----------------
print("一、准备被控端")
real_data.mkdir(parents=True, exist_ok=True)
sys.path.insert(0, str(WINDOWS))
from lanremote.config import Store
store = Store(real_data)
store.set_pairing_code(PAIR_CODE)
store.set("setup_done", True)
store.set("allow_new_pairing", True)
store.set("close_pair_after_first", False)
store.set("server_name", "本地测试电脑")
store.set("relay_url", RELAY_URL)
store.set("port", AGENT_PORT)
check("配对码已设", store.pairing_code() == PAIR_CODE)
check("中转地址已设", store.get("relay_url") == RELAY_URL)
# ---------------- 2. 起中转 ----------------
print()
print("二、起本地中转服务器")
env = dict(os.environ)
env.update({
"RELAY_PORT": str(RELAY_PORT),
"RELAY_BIND": "127.0.0.1",
"RELAY_DATA": str(relay_data),
"RELAY_POLL": "8",
"PYTHONIOENCODING": "utf-8",
"PYTHONUNBUFFERED": "1",
})
log = open(relay_data / "relay.log", "wb")
relay_proc = subprocess.Popen(
[sys.executable, "-u", str(ROOT / "relay" / "relay.py")],
env=env, stdout=log, stderr=subprocess.STDOUT)
for _ in range(40):
time.sleep(0.5)
try:
status, _ = relay_get("/v1/health", timeout=2)
if status == 200:
break
except Exception: # noqa: BLE001
continue
status, body = relay_get("/v1/health")
check("中转已启动", status == 200, status)
# ---------------- 3. 起被控端 ----------------
print()
print("三、起被控端(它会主动连中转)")
agent_log = open(relay_data / "agent.log", "wb")
agent_proc = subprocess.Popen(
[sys.executable, "-u", str(WINDOWS / "run_agent.py"),
"--silent", "--port", str(AGENT_PORT)],
cwd=str(WINDOWS), stdout=agent_log, stderr=subprocess.STDOUT)
registered = False
device = {}
for _ in range(60):
time.sleep(1.0)
status, body = relay_get("/v1/health")
try:
snap = json.loads(body)
except Exception: # noqa: BLE001
continue
for item in snap.get("devices", []):
if item.get("online"):
device = item
registered = True
break
if registered:
break
check("被控端登记到中转并在线", registered, str(device)[:160])
if not registered:
print(" 被控端日志:")
print((relay_data / "agent.log").read_text(
encoding="utf-8", errors="replace")[-1000:])
return 1
check("中转看到的配对码正确", device.get("code") == PAIR_CODE,
device.get("code"))
check("中转看到设备名", device.get("name") == "本地测试电脑",
device.get("name"))
# ---------------- 4. 手表接入 ----------------
print()
print("四、模拟手表接入")
status, body = relay_post("/v1/client/attach", json.dumps({
"code": PAIR_CODE, "name": "测试手表"}).encode("utf-8"))
attach = json.loads(body)
check("接入成功", attach.get("ok") is True, str(attach)[:140])
session = attach.get("session", "")
check("看到被控端名字", attach.get("deviceName") == "本地测试电脑",
attach.get("deviceName"))
watch = Watch(session)
# ---------------- 5. 走协议 ----------------
print()
print("五、经中转发协议命令(这才是真验证)")
# ping 需要鉴权,先发会被正确拒绝 —— 正好验证鉴权仍然生效
watch.send({"cmd": "ping"})
reply = watch.read_reply()
check("未配对时 ping 被拒(鉴权仍生效)",
reply.get("ok") is False, str(reply)[:120])
watch.send({"cmd": "pair", "deviceId": "watch-local-test",
"deviceName": "测试手表", "code": PAIR_CODE})
reply = watch.read_reply()
check("配对成功(经中转)", reply.get("ok") is True, str(reply)[:160])
token = reply.get("token", "")
check("拿到设备令牌", len(token) == 32, len(token))
check("拿到被控端名字", reply.get("serverName") == "本地测试电脑",
reply.get("serverName"))
watch.send({"cmd": "ping"})
reply = watch.read_reply()
check("配对后 ping 正常", reply.get("ok") is True, str(reply)[:120])
watch.send({"cmd": "feature_list"})
reply = watch.read_reply()
features = reply.get("features") or []
check("功能列表经中转返回", reply.get("ok") is True
and len(features) > 100, f"{len(features)} 个")
watch.send({"cmd": "feature_run", "name": "world_clock",
"params": {}})
reply = watch.read_reply()
check("执行功能经中转成功", reply.get("ok") is True,
str(reply)[:120])
check("功能真的有输出", "北京" in str(reply.get("note", "")),
str(reply.get("note"))[:60])
watch.send({"cmd": "info"})
reply = watch.read_reply()
check("info 经中转返回", reply.get("ok") is True, str(reply)[:100])
# ---------------- 6. 会话切换 ----------------
print()
print("六、换一个手表(中转应该踢掉旧的,被控端应认出新会话)")
status, body = relay_post("/v1/client/attach", json.dumps({
"code": PAIR_CODE, "name": "第二块手表"}).encode("utf-8"))
second = json.loads(body)
check("第二个手表能接入", second.get("ok") is True, str(second)[:120])
watch2 = Watch(second.get("session", ""))
watch2.send({"cmd": "auth", "deviceId": "watch-local-test",
"token": token})
reply = watch2.read_reply()
check("新会话能用已保存的令牌直接登录", reply.get("ok") is True,
str(reply)[:140])
watch2.send({"cmd": "ping"})
reply = watch2.read_reply()
check("新会话 ping 正常", reply.get("ok") is True, str(reply)[:120])
status, body = relay_get("/v1/health")
snap = json.loads(body)
check("中转上只剩一个会话", snap.get("sessions") == 1,
snap.get("sessions"))
print()
print("=" * 66)
print(f"通过 {passed} 项,失败 {failed} 项")
return 0 if failed == 0 else 1
finally:
for process in (agent_proc, relay_proc):
if process is not None:
try:
process.terminate()
process.wait(timeout=8)
except Exception: # noqa: BLE001
try:
process.kill()
except Exception: # noqa: BLE001
pass
time.sleep(1)
if real_data.exists():
shutil.rmtree(real_data, ignore_errors=True)
if had_data and backup.exists():
shutil.move(str(backup), str(real_data))
shutil.rmtree(relay_data, ignore_errors=True)
if __name__ == "__main__":
sys.exit(main())