Files
ykxmz/_build/fix_pc_bugs.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

271 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""修 PC 端审计出来的 bug。
最严重
1. ui.py 建 RemoteServer 时传 actions=None,之后只补了 actions、**从没重建 Features**
→ 真机上 CMD_FEATURE_LIST / CMD_FEATURE_RUN 永远报"功能不可用",
手表整个功能页(105 个功能)是死的。测试各自 new 一个 Features,所以测不出来。
2. _resolve_received 对相对路径不校验 → 已配对设备能拉走 data/settings.json
(里面有配对码和全部设备令牌)
3. 分块累积无上限 → 能把内存吃干;且 seq 顺序从不校验,丢块会静默产出坏文件
4. 配对码可以无限次枚举(没有失败计数/退避),比较用 != 而非 compare_digest
5. run_command 用 utf-8 解码(打包后 cmd 输出是 GBK,中文全乱)+ 没加
CREATE_NO_WINDOW(每跑一条命令闪一个黑窗)
6. _uptime 的 GetTickCount64 没设 restype,默认 32 位 → 开机超 24.8 天显示错乱
7. say_clipboard 读的是"手表推过来的剪贴板",却报"电脑剪贴板是空的"
8. --port 越界时 socket.bind 抛 OverflowError(不是 OSError),界面接不住 → 裸退
9. 安全开关(允许运行命令 / 允许电源操作 / 允许屏幕共享 / 允许新配对)没有界面
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\windows")
UI = ROOT / "lanremote" / "ui.py"
SERVER = ROOT / "lanremote" / "server.py"
ACTIONS = ROOT / "lanremote" / "actions.py"
PROTOCOL = ROOT / "lanremote" / "protocol.py"
CONFIG = ROOT / "lanremote" / "config.py"
DISCOVERY = ROOT / "lanremote" / "discovery.py"
FIXES = [
# ================= 1. Features 没被构造(功能页全死) =================
(UI,
''' self.server = RemoteServer(self.store, None, ui=self)
self.server.actions = Actions(self, self.store)
self.server.pairing_enabled = bool(self.store.get("allow_new_pairing", True))''',
''' self.server = RemoteServer(self.store, None, ui=self)
self.server.actions = Actions(self, self.store)
# 关键:RemoteServer 收到的是 actions=None,features 也就没建起来。
# 不重建的话 CMD_FEATURE_LIST / CMD_FEATURE_RUN 永远报"功能不可用",
# 手表上整个「功能」页(100 多个功能)全是死的。
from .features import Features
self.server.features = Features(self.server.actions)
self.server.pairing_enabled = bool(self.store.get("allow_new_pairing", True))''',
"★ 重建 Features(功能页复活)"),
# ================= 2. 相对路径绕过 =================
(SERVER,
''' self._log(f"[文件] 拒绝拉取允许范围外的路径: {raw}")
return None''',
''' self._log(f"[文件] 拒绝拉取允许范围外的路径: {raw}")
return None
# 相对路径也要过白名单。Path("data/settings.json").is_absolute() 是 False,
# 上面那一段挡不住它 —— 而 EXE 的 CWD 就是程序目录,
# 于是已配对设备能把 data/settings.json(配对码 + 全部设备令牌)拉走。
try:
relative = (Path.cwd() / raw).resolve()
except OSError:
return None
allowed_roots = [self.store.received_dir()]
if self.features is not None:
allowed_roots.extend(self.features.allowed_roots())
for root in allowed_roots:
try:
relative.relative_to(root.resolve())
return relative if relative.is_file() else None
except (ValueError, OSError):
continue
self._log(f"[文件] 拒绝拉取相对路径: {raw}")
return None''',
"★ 相对路径也要过白名单"),
# ================= 4. 配对防爆破 =================
(SERVER,
''' if code != expected:
self._log(f"[配对失败] 来自 {session.addr[0]},配对码不匹配")
return {"ok": False, "error": "配对码错误"}''',
''' # 防暴力枚举:6 位数字只有 100 万种,不限速的话局域网里慢慢试就行。
now = time.time()
self._pair_failures = [t for t in getattr(self, "_pair_failures", [])
if now - t < 300]
if len(self._pair_failures) >= 10:
wait = max(1, int(300 - (now - self._pair_failures[0])))
self._log(f"[配对] 失败次数过多,锁定中({wait} 秒)")
return {"ok": False,
"error": f"配对码错误次数过多,请等 {wait} 秒后再试"}
# 用 compare_digest,避免按字符比较带来的时间侧信道
if not secrets.compare_digest(code, expected):
self._pair_failures.append(now)
self._log(f"[配对失败] 来自 {session.addr[0]},配对码不匹配"
f"({len(self._pair_failures)}/10)")
return {"ok": False, "error": "配对码错误"}
self._pair_failures = []''',
"★ 配对防爆破 + compare_digest"),
(SERVER,
" self.sessions: list[Session] = []",
" self.sessions: list[Session] = []\n"
" # 最近的配对失败时间戳,用于限速防枚举\n"
" self._pair_failures: list[float] = []",
"配对失败记录字段"),
# ================= 5. run_command 编码 + 黑窗 =================
(ACTIONS,
''' proc = subprocess.run(
command, shell=True, capture_output=True, text=True,
timeout=timeout, encoding="utf-8", errors="replace")''',
''' # 打包成 --windowed 后没有控制台,cmd 的输出是 ANSI 代码页(本机 936)
# 而不是 UTF-8 —— 用 utf-8 解会全是乱码。和本文件其它地方统一用 mbcs。
# 另外必须加 CREATE_NO_WINDOW,否则每跑一条远程命令都会闪一个黑窗。
proc = subprocess.run(
command, shell=True, capture_output=True, text=True,
timeout=timeout,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
encoding="mbcs", errors="replace")''',
"★ run_command 编码 + 不闪黑窗"),
# ================= 6. _uptime 32 位截断 =================
(ACTIONS,
''' ms = ctypes.windll.kernel32.GetTickCount64()''',
''' # 不设 restype 的话 ctypes 默认按 32 位处理,
# 开机超过 24.8 天会被截断成负数,时长显示错乱
ctypes.windll.kernel32.GetTickCount64.restype = ctypes.c_ulonglong
ms = ctypes.windll.kernel32.GetTickCount64()''',
"★ 开机时长不再截断"),
# ================= 7. say_clipboard 读错了数据源 =================
(ACTIONS,
''' def say_clipboard(self) -> str:
text = str(self.store.get("last_clip") or "").strip()
if not text:
raise ActionError("电脑剪贴板是空的")''',
''' def say_clipboard(self) -> str:
# 之前读的是 store["last_clip"](那是手表推过来的内容),
# 手表没推过就永远报"剪贴板是空的",可电脑剪贴板明明有东西。
# 应该读电脑本机的剪贴板。
try:
text = str(self.clip_get() or "").strip()
except Exception: # noqa: BLE001
text = ""
if not text:
text = str(self.store.get("last_clip") or "").strip()
if not text:
raise ActionError("电脑剪贴板是空的")''',
"★ 朗读剪贴板读本机剪贴板"),
# ================= 8. --port 越界 =================
(UI,
''' try:
self.server.start()
except OSError as exc:
self.lbl_state.configure(text="端口被占用", fg=DANGER)
self.log(f"启动失败:{exc}")
self.show_message(f"端口 {self.server.port} 被占用,无法启动")''',
''' try:
self.server.start()
except (OSError, OverflowError, ValueError) as exc:
# 端口写成 99999 时 socket.bind 抛的是 OverflowError,不是 OSError,
# 只 catch OSError 会让进程带着 traceback 裸退,用户什么都看不到。
self.lbl_state.configure(text="启动失败", fg=DANGER)
self.log(f"启动失败:{exc}")
self.show_message(f"无法监听端口 {self.server.port}:{exc}")''',
"★ 端口越界不再裸退"),
# ================= 9. 两个布尔参数也用真值判断 =================
(SERVER,
' cancel = bool(msg.get("cancel"))',
' cancel = _truthy(msg.get("cancel"))',
"取消关机用真值判断"),
(SERVER,
' note = self.actions.monitor_power(bool(msg.get("on")))',
' note = self.actions.monitor_power(_truthy(msg.get("on")))',
"显示器开关用真值判断"),
# ================= 10. discovery 版本号写死 =================
(DISCOVERY,
'version: str = "1.1.0"',
'version: str = ""',
"发现应答的版本号不再写死"),
]
# server.py 需要 _truthy 辅助
TRUTHY_HELPER = '''
def _truthy(value) -> bool:
"""字符串 "false"/"0"/"off" 不能被当成真(bool("false") 是 True)。"""
if isinstance(value, bool):
return value
if isinstance(value, (int, float)):
return value != 0
if isinstance(value, str):
return value.strip().lower() in ("1", "true", "on", "yes", "是", "开")
return bool(value)
'''
def main() -> int:
applied, skipped = 0, []
for path, old, new, tag in FIXES:
if not path.is_file():
skipped.append(tag)
print(f" [跳过] {tag} —— 文件不存在")
continue
text = path.read_text(encoding="utf-8")
if old not in text:
skipped.append(tag)
print(f" [跳过] {tag} —— 锚点没找到")
continue
path.write_text(text.replace(old, new, 1), encoding="utf-8")
applied += 1
print(f" {tag}: OK")
# ---- server.py 加 _truthy ----
stext = SERVER.read_text(encoding="utf-8")
if "_truthy" in stext and "def _truthy" not in stext:
anchor = "\nclass Session:"
if anchor in stext:
stext = stext.replace(anchor, TRUTHY_HELPER + "\nclass Session:", 1)
else:
stext = stext.replace("\nclass RemoteServer:", TRUTHY_HELPER + "\nclass RemoteServer:", 1)
SERVER.write_text(stext, encoding="utf-8")
print(" server.py / _truthy 辅助: OK")
# ---- server.py 需要 secrets ----
stext = SERVER.read_text(encoding="utf-8")
if "compare_digest" in stext and "import secrets" not in stext:
if "import socket" in stext:
stext = stext.replace("import socket", "import secrets\nimport socket", 1)
SERVER.write_text(stext, encoding="utf-8")
print(" server.py / import secrets: OK")
# ---- discovery.py 版本号默认值改成读 __version__ ----
dtext = DISCOVERY.read_text(encoding="utf-8")
if 'version: str = ""' in dtext:
dtext = dtext.replace(
'version: str = ""',
'version: str = "" # 空=用 __init__ 里的 __version__', 1)
if "from . import __version__" not in dtext:
dtext = dtext.replace(
"import socket",
"import socket\n\nfrom . import __version__ as _APP_VERSION", 1)
dtext = dtext.replace(
'self.version = version or "1.1.0"',
'self.version = version or _APP_VERSION', 1)
DISCOVERY.write_text(dtext, encoding="utf-8")
print(" discovery.py / 版本号: OK")
print(f"\n 应用 {applied} / {len(FIXES)}")
if skipped:
print(f" 跳过: {', '.join(skipped)}")
import ast
ok = True
for label, path in (("ui.py", UI), ("server.py", SERVER), ("actions.py", ACTIONS),
("protocol.py", PROTOCOL), ("config.py", CONFIG),
("discovery.py", DISCOVERY)):
try:
ast.parse(path.read_text(encoding="utf-8"))
print(f" {label}: 语法 OK")
except SyntaxError as exc:
print(f" {label}: 语法错误 {exc}")
ok = False
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())