Files
ykxmz/_build/fix_pc_bugs.py
T
xmz7891 b705216e4a ykxmz 1.5.0锛氫慨澶嶆墜琛ㄧ TLS 璇佷功鏍¢獙 + MTU 淇缃戠粶绋冲畾鎬?
琚帶绔?- 淇 MTU 闂锛氬す MSS + 璺緞 MTU 鎺㈡祴锛堝叕缃戝彲鐢ㄧ巼 48% -> 100%锛?- 澶嶇敤 HTTP 杩炴帴锛堝厤鎺夋瘡娆℃柊寤?TLS 鐨?30% 鎻℃墜澶辫触锛?- 姣忓垎閽熷洖澶存壘灞€鍩熺綉锛岃嚜鍔ㄥ垏鍥炲揩 400 鍊嶇殑璺緞
- 鐢婚潰蹇冭烦甯э細闈欐鏃剁渷娴侀噺锛屽鎴风浠嶇煡閬撴祦鏄椿鐨?- 閿佸睆鏃剁粰鍑哄彲鎿嶄綔鐨勬彁绀?
鎵嬭〃绔?- 鍐呯疆 GTS Root R4 / GlobalSign Root CA锛屼慨 SSLHandshakeException
- 杩炴帴閿欒缈昏瘧鎴愪汉璇濓紙璇佷功/鏃堕棿/瑙f瀽/瓒呮椂鍒嗗埆鎻愮ず锛?- 缁忎腑杞繛涓婂悗鐢ㄦ湇鍔″櫒鎶ョ殑鍦板潃鍒锋柊宸查厤瀵硅褰?- 鏂板灞€鍩熺綉杩炴帴椤甸潰銆佹湇鍔″櫒寤惰繜妫€娴?
鏈嶅姟鍣ㄧ
- 鏀寔 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())