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 + 双连接 文档 - 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
271 lines
12 KiB
Python
271 lines
12 KiB
Python
# -*- 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())
|