mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 14:50:45 +00:00
被控端 - 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%) - 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败) - 每分钟回头找局域网,自动切回快 400 倍的路径 - 画面心跳帧:静止时省流量,客户端仍知道流是活的 - 锁屏时给出可操作的提示 手表端 - 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException - 连接错误翻译成人话(证书/时间/解析/超时分别提示) - 经中转连上后用服务器报的地址刷新已配对记录 - 新增局域网连接页面、服务器延迟检测 服务器端 - 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502) - 网络调优脚本 net-tune.sh(开机自动应用) - 管理页:批量删除、网络体检、下载区 - 隧道改 HTTP/2 + 双连接 文档 - 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
302 lines
11 KiB
Python
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())
|