# -*- 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())