From 8880e7033d2a6d1861612626f06fe646af616fab Mon Sep 17 00:00:00 2001 From: xmz7891 Date: Wed, 7 Oct 2026 17:59:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E3=80=8C=E7=BB=8F=E4=B8=AD=E8=BD=AC?= =?UTF-8?q?=E5=87=A0=E6=AC=A1=E5=90=8E=E6=B0=B8=E4=B9=85=E5=93=91=E6=8E=89?= =?UTF-8?q?=E3=80=8D=E2=80=94=E2=80=94=E3=80=8C=E6=8C=89=E4=B8=80=E4=B8=8B?= =?UTF-8?q?=E6=B2=A1=E5=8F=8D=E5=BA=94=E3=80=8D=E7=9A=84=E7=9C=9F=E5=9B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit == 现象 == 用户报: * 局域网能搜到电脑,IP/端口/配对码都对,连上后**没有任何反应** * 走中转「已配对」,但点按钮没反应 局域网那一半已经修过(preferLan)。这次抓到的是**经中转**那一半, 而且比预想的严重得多。 == 证据链(插桩复现出来的)== 被控端日志里直接抛了未捕获异常,线程当场死: Exception in thread client-relay: server.py:407 session.send(reply) relay.py:166 self._link.send_to_client(data) relay.py:357 self._post("/v1/agent/send", ...) relay.py:766 raise RelayError(f"请求失败: {last_error}") lanremote.relay.RelayError: 请求失败: Idle 之后的状态: * client-relay 线程没了 —— 电脑再也不往手表回任何东西 * relay-link 线程还活着、还在长轮询 —— 中转那边看着「设备在线」 * **手表看到的就是「连上了,但按一下毫无反应」** 写入仓库的测试 _tests/test_relay_local.py 一直是这个形状: 前面几项 PASS、跑到经中转发命令就卡死。之前没人深挖。 == 根因 1:一条 HTTP/1.1 连接被两个线程同时用 == _post() 里锁只包住「取连接」那一段: with self._http_lock: connection = self._http # 锁到这里就放了 try: connection.request("POST", ...) # 锁外发请求 于是两个线程会同时用同一条 connection: * relay-link 线程跑 /v1/agent/poll —— 长轮询,一条请求挂 180 秒 * client-relay 线程同时 POST /v1/agent/send HTTP/1.1 一条连接同一时刻只能有一个在途请求,第二个线程一进来 就撞上 http.client 状态机,抛 CannotSendRequest("Idle")。 _post 的「重试一次」也救不了:它 _drop_http() 之后,另一个线程 手里还攥着那条旧连接对象在发,照样坏。 修法: * **发命令用独立连接**(_post 加 dedicated 参数)。 长轮询那条常年被占着,发命令不该去挤它,各走各的。 * 两条连接各自一把锁**锁住整个「发请求 + 读响应」周期**, 不再只锁取连接那一下。 * 普通请求若发现缓存连接正忙(长轮询在途),临时另开一条用完即关。 * _drop_http(dedicated=) 区分清哪条;finally 里一定归还占用 (否则那条连接会被永久标记为忙)。 == 根因 2:回包失败会打死整个会话线程 == server.py 的 _client_loop 里: if reply is not None: session.send(reply) # <- 不在任何 try 里 ... except OSError: # <- 外层只捕获 OSError pass 而中转链路上 send 抛的是 RelayError,直接冒出去把线程打死。 **死一次就永久哑**,代价太大。 修法:session.send() 单独兜住所有异常,失败就断开这个会话 (让手表重连),绝不让线程消失。外层也补一个兜底 except。 == 修复效果 == 修复前:pair OK -> ping OK -> ping 卡死 -> 线程崩溃,永久哑 修复后:pair 21ms -> ping 82ms -> ping 4ms -> ping 6ms,零异常 _tests/test_relay_local.py:**之前完全卡死 -> 现在 21/21 全过** 这个测试之前一直是红的,说明「35 个阶段的回归全绿」这件事 本身就不成立过 —— 某个阶段卡住时看起来像环境问题而不是缺陷。 == 同批修掉验证阶段找到的其它问题 == 1. **TTL 清理 + 关掉接入开关 = 设备永久锁死**(严重) register() 用 `device_id in self.devices` 判断老设备,但超时清理 把设备从 devices 和 codes 都删了。电脑关机过夜(必然 > 6 小时) 第二天开机就会被自己的开关挡住,而且**重启也救不回来** (清 codes 时没落盘,重启后又当孤儿清一遍,自我维系)。 管理员在页面上还看不到这台设备,不知道该放开谁。 修法:状态文件里单独存 knownDevices 设备历史,超时清理绝不动它; register 改查这份历史;清理后补 _save();delete_device 才真正忘记。 2. **管理页 JS 语法错误 -> 所有按钮失效**(严重) admin.py 里 ADMIN_JS 是普通三引号字符串,我在 Python 源码里写的 `\n` 被变成真实换行,拼进 JS 就是字符串里的裸换行 -> 整段 ", + "lanIps": [], "port": 0, "version": "1.8.2", + "online": True, "idleSeconds": 1, "sessions": 0, + "queuedToClient": 0, "queuedToAgent": 0, + "bytesIn": 0, "bytesOut": 0}, + ] + snap4["codeCount"] = 1 + html4 = admin.render_admin(snap4, "https://x", "/zhongzhuanzhan", 1, + lambda: []) + check("设备名里的 " not in html4, + "发现原始 ", html, re.S) + if not m: + print(" ✗ 产物里没有 ", None), + ("引号", "dev\"'`", None), + ("路径分隔符", "../../etc/passwd", None), + ("Unicode", "设备-😀-ünïcode", None), + ("只有空格", " ", False), + ("空字符串", "", False), + ] + for label, bad, expect in cases: + r = reg(bad, fresh_code()) + got = r.get("ok") is True + if expect is None: + # 未定义期望:只记录实际行为,不判对错 + verdict = "接受" if got else "拒绝" + print(f" · {label:18s} -> {verdict}" + + ("" if got else f"({r.get('error','')})")) + if got and (" 标签(未转义的 deviceId)", + raw_img not in page, + "发现原始标签 -> 存储型 XSS") + check("管理页没有原始