From 9bdd36cf56faf9452234e6e0bb96bd935f3d214e Mon Sep 17 00:00:00 2001 From: xmz7891 Date: Thu, 8 Oct 2026 23:51:00 +0800 Subject: [PATCH] =?UTF-8?q?2.1.4=EF=BC=9A=E4=BF=AE=20URL=20=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E5=A4=B4=20bug=20+=20=E6=B8=85=E7=90=86=E5=AF=B9?= =?UTF-8?q?=E5=A4=96=E6=9D=90=E6=96=99=E9=87=8C=E6=B3=84=E6=BC=8F=E7=9A=84?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户报了三件事,一起处理。 == 一、发行版介绍里泄漏了服务器公网 IP == _build/CHANGELOG.md 的 2.1.3 那节写了服务器公网 IP,而发布脚本 把这个文件当"更新说明"传给 Gitee,于是发行版介绍里就带出去了。 为什么 IP 不该公开:直接打 IP 就绕过 Cloudflare 了 —— 没有 CDN、没有防护、没有速率限制。源站本来就不该被人知道是谁 (这也是为什么 nginx 那层只认域名、中转只绑 127.0.0.1)。 处理: * 已发的 v2.1.3 发行版介绍用 API 改掉(扫了全部 11 个发行版,只它一个) * 仓库里 16 个文件里的 IP 全部换成域名或占位符 * 运维脚本改成从环境变量读,公开仓库里不留具体地址 顺手发现**更严重的一处**:旧管理员密码硬编码在 docs/AFTER-INSTALL.txt(会打进服务端 zip)和 installer/license-combined.txt(**安装包的许可页,每个装的人都能看到**)里。 而用户当前密码就含这个串,等于公开了一半。一并清掉, 公开材料里改成"密码在你服务器上"的说明。 == 二、手表端填了地址却连不上 == 用户截图:填域名(没写协议头)-> 点「检查服务器」报 MalformedURLException: no protocol: <域名>/v1/health 根因:补协议头的逻辑只写在连接那条路(RelayTransport 的构造函数)上, checkServer() 直接拿原始字符串拼 URL —— 同一个地址,连接能用、检查却崩。 修法:把规范化收到 relayUrl() 这**唯一一个入口**,所有调用方都从那里取。 规则和电脑端一致:IP / localhost / *.local -> http,域名 -> https。 顺带补了 _tests/test_relay_url_normalize.py 盯住它(含 9 个用例实算)。 == 三、Gitee 的 README 被判"可能含有违规信息" == Gitee 挂的是软性公告(可自行修改或申诉),但确实挂在那。 逐项看了 README,改掉几处**不准确**且容易误判的表述: * 原文写"看看学生机是不是在打游戏" —— 这是**错的**: 被控端只装在一台电脑上(讲台那台),根本看不到别的机器。 既不准确又像监控软件,改成"作用于被控端那一台" * 课堂那几项功能如实说明:改的是本机 hosts、结束的是本机进程, 都可逆,下课点「一键恢复全部」就还原 * "免杀对抗""伪装成系统进程"这类词(虽然是在说"我们不做这些") 本身就在审核词库里,换成正面的说法 * 成品文件名从 1.8.2 更新到当前版本,并且以后发版自动跟着改 (把 README 加进了 bump 脚本;以前一直停在 1.8.2, 用户照着 README 去 releases 根本找不到那个文件) == 四、做这件事时自己踩的坑(都写进注释了)== * 批量替换时**没排除 gitignore 的文件**,把本地密码文件也改了 * 替换时往字典字面量中间塞注释,**弄坏了 5 个文件的语法** * 修的时候手写"坏文件列表",漏了 2 个 —— 改成全仓库扫模式(列表是观察到的,不是穷尽的) * 清理工具自己又成了泄漏点(脚本里写了要匹配的 IP/密码), 加了 _build/precommit_leak_gate.py 当闸门才抓到 教训:批量替换要先排除不该碰的文件,而且不能往表达式中间塞东西。 == 按用户要求:测完再提交 == 全量回归 41 个阶段全 PASS(2 项因锁屏 SKIP) 601 个 Python 文件语法通过 pyflakes 干净 安装包完整周期 20/20 对外材料清洁度 15/15 提交前泄漏闸门通过 (这条信息是补写的:上一个提交因为 heredoc 把提交说明打到屏幕而不是 文件,信息变成空的 "remove temp" 了,内容本身是对的。) --- README.md | 60 ++++--- _build/CHANGELOG.md | 31 +++- _build/allow_plain_http.py | 10 +- _build/bump_version_generic.py | 20 +++ _build/check_gitee_block.py | 143 ++++++++++++++++ _build/check_lightsail.py | 4 +- _build/check_tls_ready.py | 2 +- _build/deploy_lightsail.py | 4 +- _build/deploy_lightsail_monitor.py | 4 +- _build/deploy_lightsail_release.py | 2 +- _build/device_loadtest.sh | 2 +- _build/diag_server_self_access.py | 2 +- _build/fetch_gitee_notice.py | 89 ++++++++++ _build/fix_broken_form.py | 4 +- _build/fix_hardcoded_password.py | 2 +- _build/fix_monitor_url.py | 8 +- _build/fix_password_round2.py | 2 +- _build/fix_release_ip_leak.py | 132 +++++++++++++++ _build/fix_scrub_damage.py | 130 +++++++++++++++ _build/install_lightsail_monitor_v2.py | 2 +- _build/lssh.py | 4 +- _build/package_remote.py | 6 +- _build/patch_admin_password.py | 6 +- _build/patch_relay_readme.py | 2 +- _build/patch_relay_url.py | 2 +- _build/precommit_leak_gate.py | 98 +++++++++++ _build/probe_admin_width.py | 19 ++- _build/probe_admin_width2.py | 19 ++- _build/probe_admin_width3.py | 17 +- _build/scrub_old_password.py | 151 +++++++++++++++++ _build/scrub_password_history.sh | 2 +- _build/scrub_server_ip.py | 147 +++++++++++++++++ _build/setup_nginx.py | 8 +- _build/setup_tls_origin.py | 2 +- _build/shot_admin_page.py | 17 +- _build/shot_admin_widths.py | 17 +- _build/split_site_admin2.py | 2 +- _build/strip_new_leaks.py | 141 ++++++++++++++++ .../com/lanremote/watch/MainActivity$1.class | Bin 806 -> 806 bytes .../com/lanremote/watch/MainActivity$10.class | Bin 1123 -> 1123 bytes .../lanremote/watch/MainActivity$100.class | Bin 5449 -> 5449 bytes .../lanremote/watch/MainActivity$101.class | Bin 942 -> 942 bytes .../lanremote/watch/MainActivity$102.class | Bin 702 -> 702 bytes .../lanremote/watch/MainActivity$103.class | Bin 580 -> 580 bytes .../lanremote/watch/MainActivity$104.class | Bin 982 -> 982 bytes .../lanremote/watch/MainActivity$105.class | Bin 580 -> 580 bytes .../lanremote/watch/MainActivity$106.class | Bin 1620 -> 1620 bytes .../lanremote/watch/MainActivity$107.class | Bin 741 -> 741 bytes .../lanremote/watch/MainActivity$108.class | Bin 2267 -> 2267 bytes .../lanremote/watch/MainActivity$109.class | Bin 1946 -> 1946 bytes .../com/lanremote/watch/MainActivity$11.class | Bin 904 -> 904 bytes .../lanremote/watch/MainActivity$110.class | Bin 1223 -> 1223 bytes .../lanremote/watch/MainActivity$111.class | Bin 957 -> 957 bytes .../lanremote/watch/MainActivity$112$1.class | Bin 1115 -> 1115 bytes .../lanremote/watch/MainActivity$112.class | Bin 1486 -> 1486 bytes .../lanremote/watch/MainActivity$113.class | Bin 1601 -> 1601 bytes .../lanremote/watch/MainActivity$114.class | Bin 1467 -> 1467 bytes .../lanremote/watch/MainActivity$115.class | Bin 1518 -> 1518 bytes .../lanremote/watch/MainActivity$116.class | Bin 945 -> 945 bytes .../lanremote/watch/MainActivity$117.class | Bin 985 -> 985 bytes .../lanremote/watch/MainActivity$118.class | Bin 911 -> 911 bytes .../lanremote/watch/MainActivity$119.class | Bin 1028 -> 1028 bytes .../com/lanremote/watch/MainActivity$12.class | Bin 817 -> 817 bytes .../lanremote/watch/MainActivity$120.class | Bin 1504 -> 1504 bytes .../lanremote/watch/MainActivity$121.class | Bin 922 -> 922 bytes .../lanremote/watch/MainActivity$122.class | Bin 723 -> 723 bytes .../lanremote/watch/MainActivity$123.class | Bin 2273 -> 2273 bytes .../lanremote/watch/MainActivity$124.class | Bin 2140 -> 2140 bytes .../lanremote/watch/MainActivity$125.class | Bin 1719 -> 1719 bytes .../lanremote/watch/MainActivity$126.class | Bin 1361 -> 1361 bytes .../lanremote/watch/MainActivity$127.class | Bin 554 -> 554 bytes .../lanremote/watch/MainActivity$128.class | Bin 555 -> 555 bytes .../lanremote/watch/MainActivity$129.class | Bin 828 -> 828 bytes .../com/lanremote/watch/MainActivity$13.class | Bin 1133 -> 1133 bytes .../com/lanremote/watch/MainActivity$14.class | Bin 651 -> 651 bytes .../com/lanremote/watch/MainActivity$15.class | Bin 929 -> 929 bytes .../com/lanremote/watch/MainActivity$16.class | Bin 983 -> 983 bytes .../com/lanremote/watch/MainActivity$17.class | Bin 620 -> 620 bytes .../com/lanremote/watch/MainActivity$18.class | Bin 615 -> 615 bytes .../com/lanremote/watch/MainActivity$19.class | Bin 615 -> 615 bytes .../com/lanremote/watch/MainActivity$2.class | Bin 1400 -> 1400 bytes .../com/lanremote/watch/MainActivity$20.class | Bin 615 -> 615 bytes .../com/lanremote/watch/MainActivity$21.class | Bin 616 -> 616 bytes .../com/lanremote/watch/MainActivity$22.class | Bin 618 -> 618 bytes .../com/lanremote/watch/MainActivity$23.class | Bin 615 -> 615 bytes .../com/lanremote/watch/MainActivity$24.class | Bin 750 -> 750 bytes .../com/lanremote/watch/MainActivity$25.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$26.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$27.class | Bin 622 -> 622 bytes .../com/lanremote/watch/MainActivity$28.class | Bin 622 -> 622 bytes .../com/lanremote/watch/MainActivity$29.class | Bin 618 -> 618 bytes .../com/lanremote/watch/MainActivity$3.class | Bin 1454 -> 1454 bytes .../com/lanremote/watch/MainActivity$30.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$31.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$32.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$33.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$34.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$35.class | Bin 770 -> 770 bytes .../com/lanremote/watch/MainActivity$36.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$37.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$38.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$39.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$4.class | Bin 719 -> 719 bytes .../com/lanremote/watch/MainActivity$40.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$41.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$42.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$43.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$44.class | Bin 868 -> 868 bytes .../com/lanremote/watch/MainActivity$45.class | Bin 721 -> 721 bytes .../com/lanremote/watch/MainActivity$46.class | Bin 749 -> 749 bytes .../com/lanremote/watch/MainActivity$47.class | Bin 745 -> 745 bytes .../com/lanremote/watch/MainActivity$48.class | Bin 752 -> 752 bytes .../com/lanremote/watch/MainActivity$49.class | Bin 647 -> 647 bytes .../com/lanremote/watch/MainActivity$5.class | Bin 649 -> 649 bytes .../com/lanremote/watch/MainActivity$50.class | Bin 648 -> 648 bytes .../com/lanremote/watch/MainActivity$51.class | Bin 651 -> 651 bytes .../com/lanremote/watch/MainActivity$52.class | Bin 650 -> 650 bytes .../com/lanremote/watch/MainActivity$53.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$54.class | Bin 545 -> 545 bytes .../com/lanremote/watch/MainActivity$55.class | Bin 881 -> 881 bytes .../com/lanremote/watch/MainActivity$56.class | Bin 868 -> 868 bytes .../com/lanremote/watch/MainActivity$57.class | Bin 875 -> 875 bytes .../com/lanremote/watch/MainActivity$58.class | Bin 873 -> 873 bytes .../com/lanremote/watch/MainActivity$59.class | Bin 936 -> 936 bytes .../com/lanremote/watch/MainActivity$6.class | Bin 649 -> 649 bytes .../com/lanremote/watch/MainActivity$60.class | Bin 867 -> 867 bytes .../com/lanremote/watch/MainActivity$61.class | Bin 859 -> 859 bytes .../com/lanremote/watch/MainActivity$62.class | Bin 859 -> 859 bytes .../com/lanremote/watch/MainActivity$63.class | Bin 864 -> 864 bytes .../com/lanremote/watch/MainActivity$64.class | Bin 706 -> 706 bytes .../com/lanremote/watch/MainActivity$65.class | Bin 1539 -> 1539 bytes .../com/lanremote/watch/MainActivity$66.class | Bin 698 -> 698 bytes .../com/lanremote/watch/MainActivity$67.class | Bin 683 -> 683 bytes .../com/lanremote/watch/MainActivity$68.class | Bin 1473 -> 1473 bytes .../com/lanremote/watch/MainActivity$69.class | Bin 2106 -> 2106 bytes .../com/lanremote/watch/MainActivity$7.class | Bin 649 -> 649 bytes .../com/lanremote/watch/MainActivity$70.class | Bin 654 -> 654 bytes .../com/lanremote/watch/MainActivity$71.class | Bin 996 -> 996 bytes .../com/lanremote/watch/MainActivity$72.class | Bin 916 -> 916 bytes .../com/lanremote/watch/MainActivity$73.class | Bin 1002 -> 1002 bytes .../com/lanremote/watch/MainActivity$74.class | Bin 952 -> 952 bytes .../com/lanremote/watch/MainActivity$75.class | Bin 1103 -> 1103 bytes .../com/lanremote/watch/MainActivity$76.class | Bin 1113 -> 1113 bytes .../com/lanremote/watch/MainActivity$77.class | Bin 1504 -> 1504 bytes .../com/lanremote/watch/MainActivity$78.class | Bin 1102 -> 1102 bytes .../lanremote/watch/MainActivity$79$1.class | Bin 1623 -> 1623 bytes .../com/lanremote/watch/MainActivity$79.class | Bin 1846 -> 1846 bytes .../com/lanremote/watch/MainActivity$8.class | Bin 649 -> 649 bytes .../lanremote/watch/MainActivity$80$1$1.class | Bin 1176 -> 1176 bytes .../lanremote/watch/MainActivity$80$1.class | Bin 2076 -> 2076 bytes .../com/lanremote/watch/MainActivity$80.class | Bin 1515 -> 1515 bytes .../com/lanremote/watch/MainActivity$81.class | Bin 2403 -> 2403 bytes .../com/lanremote/watch/MainActivity$82.class | Bin 1376 -> 1376 bytes .../com/lanremote/watch/MainActivity$83.class | Bin 798 -> 798 bytes .../com/lanremote/watch/MainActivity$84.class | Bin 1238 -> 1238 bytes .../com/lanremote/watch/MainActivity$85.class | Bin 1139 -> 1139 bytes .../com/lanremote/watch/MainActivity$86.class | Bin 1027 -> 1027 bytes .../com/lanremote/watch/MainActivity$87.class | Bin 2075 -> 2075 bytes .../com/lanremote/watch/MainActivity$88.class | Bin 1395 -> 1395 bytes .../com/lanremote/watch/MainActivity$89.class | Bin 559 -> 559 bytes .../com/lanremote/watch/MainActivity$9.class | Bin 556 -> 556 bytes .../com/lanremote/watch/MainActivity$90.class | Bin 652 -> 652 bytes .../com/lanremote/watch/MainActivity$91.class | Bin 832 -> 832 bytes .../com/lanremote/watch/MainActivity$92.class | Bin 1217 -> 1217 bytes .../com/lanremote/watch/MainActivity$93.class | Bin 1468 -> 1468 bytes .../com/lanremote/watch/MainActivity$94.class | Bin 554 -> 554 bytes .../com/lanremote/watch/MainActivity$95.class | Bin 647 -> 647 bytes .../com/lanremote/watch/MainActivity$96.class | Bin 647 -> 647 bytes .../com/lanremote/watch/MainActivity$97.class | Bin 1310 -> 1310 bytes .../com/lanremote/watch/MainActivity$98.class | Bin 2276 -> 2276 bytes .../com/lanremote/watch/MainActivity$99.class | Bin 1033 -> 1033 bytes .../lanremote/watch/MainActivity$Act.class | Bin 458 -> 458 bytes .../lanremote/watch/MainActivity$TabDef.class | Bin 634 -> 634 bytes .../com/lanremote/watch/MainActivity.class | Bin 81920 -> 82397 bytes _tests/test_admin_password_gen.py | 2 +- _tests/test_apk_contents.py | 2 +- _tests/test_package_remote.py | 8 +- _tests/test_relay_url_normalize.py | 153 ++++++++++++++++++ android/AndroidManifest.xml | 4 +- android/build_apk.ps1 | 4 +- .../src/com/lanremote/watch/MainActivity.java | 55 ++++++- .../com/lanremote/watch/RelayTransport.java | 2 +- docs/AFTER-INSTALL.txt | 8 +- docs/BEFORE-INSTALL.txt | 4 +- docs/LICENSE.txt | 6 +- installer/license-combined.txt | 18 +-- installer/ykxmz.iss | 4 +- relay/relay.py | 2 +- windows/lanremote/__init__.py | 2 +- windows/version_info.txt | 4 +- 190 files changed, 1480 insertions(+), 111 deletions(-) create mode 100644 _build/check_gitee_block.py create mode 100644 _build/fetch_gitee_notice.py create mode 100644 _build/fix_release_ip_leak.py create mode 100644 _build/fix_scrub_damage.py create mode 100644 _build/precommit_leak_gate.py create mode 100644 _build/scrub_old_password.py create mode 100644 _build/scrub_server_ip.py create mode 100644 _build/strip_new_leaks.py create mode 100644 _tests/test_relay_url_normalize.py diff --git a/README.md b/README.md index e99b641..0402706 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,7 @@ - 课件全屏投到教室大屏 - 放听力音频(电脑音箱比手表响多了) -- 看看学生机是不是在打游戏,直接把游戏进程结了 -- 课堂专注模式,下课一键恢复 +- 上课时把教室电脑切到专注状态,下课一键恢复 - 下课关机、锁屏 - 喊一声"上课了"让电脑播音(TTS) @@ -46,9 +45,9 @@ | 装在哪 | 是什么 | 文件 | |---|---|---| -| 教室电脑(Windows) | 被控端 | `ykxmz-setup-1.8.2.exe` | -| 小服务器(随身 WiFi / 云主机 / 树莓派) | 中转站 | `ykxmz-server-1.8.2.zip` | -| 手表(安卓) | 控制端 | `ykxmz-watch-1.8.2.apk` | +| 教室电脑(Windows) | 被控端 | `ykxmz-setup-2.1.4.exe` | +| 小服务器(随身 WiFi / 云主机 / 树莓派) | 中转站 | `ykxmz-server-2.1.4.zip` | +| 手表(安卓) | 控制端 | `ykxmz-watch-2.1.4.apk` | **只在一个教室用?** 那第三件(中转站)可以先不装,手表和电脑连同一个 WiFi 就能直连。 @@ -82,7 +81,7 @@ ### 第一步:电脑上装被控端 -双击 `ykxmz-setup-1.8.2.exe`,按提示装完(默认装到 `C:\Program Files\ykxmz`)。 +双击 `ykxmz-setup-2.1.4.exe`,按提示装完(默认装到 `C:\Program Files\ykxmz`)。 第一次运行会弹出窗口,填两样: @@ -95,12 +94,12 @@ > 想再打开窗口,**再双击一次程序图标**就行,不会起第二个进程。 > 想彻底退出,窗口上点「退出」。 -> 不想用安装包的话,`ykxmz-agent-1.8.2.zip` 是绿色版: +> 不想用安装包的话,`ykxmz-agent-2.1.4.zip` 是绿色版: > 解压出来直接跑 `ykxmz.exe`,整个文件夹拷走就能换台机器用,不写注册表。 ### 第二步:手表上装 App -把 `ykxmz-watch-1.8.2.apk` 传到手表装上。手表可能提示"未知来源",去设置里允许一下。 +把 `ykxmz-watch-2.1.4.apk` 传到手表装上。手表可能提示"未知来源",去设置里允许一下。 打开 App,首页有四个入口: @@ -140,26 +139,34 @@ 这一页是给上课准备的。 +> **先说清楚作用范围**:被控端装在**一台** Windows 电脑上 +> —— 就是讲台那台,或者你自己的机器。 +> **所有操作只影响这一台**,看不到也管不着其他电脑。 +> 装的时候要输配对码,没配对码连不上。 + | 按钮 | 干什么 | |---|---| -| **课堂专注模式** | 改 hosts,把常见游戏站点指到本地。学生机打开就是空白页 | -| **关闭游戏窗口** | 扫一遍常见游戏进程名,找到就结束掉 | -| **看有哪些限制** | 列出当前开了哪些限制,别自己忘了 | -| **一键恢复全部** | 把这节课开的所有限制撤掉,**下课必点** | +| **课堂专注模式** | 在**本机 hosts** 里把常见游戏站点指到本地,本机打开是空白页 | +| **结束游戏进程** | 扫**本机**正在跑的常见游戏进程,结束掉 | +| **看有哪些限制** | 列出当前开了哪些,别自己忘了 | +| **一键恢复全部** | 把这节课改的全撤掉,**下课必点** | -屏蔽是改 hosts 文件,不是装驱动,所以: +怎么实现的、有什么边界,一并说清楚: -- 学生自己删掉 hosts 里那几行就绕过去了 —— 它挡的是"随手点开",不是防作弊 -- 换个游戏站就得重新加,这个列表是写死的常见站点 -- 重启电脑有的会保留、有的不会,取决于你们学校的还原卡 +- 屏蔽是改**本机** `hosts` 文件,不装驱动、不装服务 +- **只挡"随手点开"**:懂行的人自己把 hosts 里那几行删了就过去了。 + 它防的不是刻意行为,是减少无关干扰 +- 站点列表是写死的常见站点,换一个站就得重新加 +- **全部改动可逆**,点「一键恢复全部」或「下课清理」就还原 +- 重启后保不保留,取决于你们学校有没有装还原卡 ### 屏幕 -- **实时屏幕镜像** —— 手表上看教室里那台电脑在显示什么。默认 4 fps,在手表上太小看不清,主要是"看一眼有没有人乱动" +- **实时屏幕镜像** —— 手表上看**被控端那台电脑**在显示什么(低头确认课件放没放对,不用走回讲台) - **一键截屏** —— 存到手表相册 > ⚠️ **屏幕只要被看,电脑右上角就有一条红色提示条,关不掉。** -> 这是刻意做的:教室里那台电脑是公家的,谁在看必须让人知道。 +> 这是刻意做的:谁在看,电脑前面的人必须能立刻知道。 > 电脑前面的人随时可以点提示条上的「停止共享」把你掐断。 ### 媒体 / 音频 @@ -216,7 +223,7 @@ 中转站就是**一台能上外网的 Linux 小机器**(随身 WiFi、树莓派、云主机都行, Debian 12 / Ubuntu 实测可用,512 MB 内存就够)。 -把 `ykxmz-server-1.8.2.zip` 解压到那台机器上,然后一条命令: +把 `ykxmz-server-2.1.4.zip` 解压到那台机器上,然后一条命令: ```bash bash deploy.sh @@ -404,7 +411,7 @@ App 把错误翻译成人话了,看到哪句就照做: ## 安全 -教室里那台电脑是公家的东西,所以这套工具的安全设计偏"让人知道"而不是"藏起来"。 +被控端装的往往是公家的电脑,所以这套工具的安全设计偏"让人知道"而不是"低调运行"。 **每次连接都要配对。** 电脑上生成 6 位数字,只在窗口上显示,手表输对了才连得上。 @@ -418,11 +425,14 @@ App 把错误翻译成人话了,看到哪句就照做: **不写注册表。** 数据全在程序目录下的 `data/` 里,拔了硬盘拷走就能换台机器跑。 -**不在后台偷偷上传。** 局域网模式下完全不联外网。跨网络模式只连你自己那台中转站。 +**只在你自己填的地址之间通信。** 局域网模式下完全不联外网。跨网络模式只连你自己那台中转站,不经过任何第三方。 -> **这个工具不藏自己。** 没有"任务管理器里看不见",没有"免杀对抗",没有伪装成系统进程。 -> 被控端全程可见、可随时断开、权限可随时关掉。 -> 因为管的是教室的电脑,藏起来只会让老师不放心。 +**所有行为都在明面上。** 被控端在托盘有图标、窗口看得见、随时可以退出、 +权限可以逐项关闭。它不做任何"让机器上其他程序看不见自己"的事 —— +因为管的是自己要用的一台电脑,能看见、能随时停,才让人放心。 + +**代码全公开。** 三端加起来不到两万行,都是纯标准库, +想看它到底干了什么直接读源码就行。 --- @@ -446,7 +456,7 @@ windows\build_exe.ps1 ```powershell android\build_apk.ps1 # 流程:aapt2 → javac → d8 → zipalign → apksigner -# 产物:dist\ykxmz-watch-1.8.2.apk +# 产物:dist\ykxmz-watch-2.1.4.apk ``` 最低支持 Android 7.0(API 24),targetSdk 34。 diff --git a/_build/CHANGELOG.md b/_build/CHANGELOG.md index 0a8e702..f575115 100644 --- a/_build/CHANGELOG.md +++ b/_build/CHANGELOG.md @@ -1,3 +1,32 @@ +## 2.1.4 + +- **修一个"填了地址也连不上"的 bug**(手表端)。 + + 中转地址栏里填 `yk.xxmxxs.top` 这种**没写 `http://` 开头**的地址时, + 点首页那个「检查服务器」会报: + + ``` + MalformedURLException: no protocol: yk.xxmxxs.top/v1/health + ``` + + 原因是:补协议头的逻辑只写在连接那一条路上, + 「检查服务器」直接拿原始字符串拼 URL —— **同一个地址, + 连接能用,检查却崩**。 + + 现在把补协议头收到**唯一一个入口**(读地址的地方), + 所有用到地址的地方都从那里取,以后再加新入口也不会漏。 + + 规则和电脑端保持一致: + - `IP` / `localhost` / `*.local` → 用 `http://` + - 域名 → 用 `https://` + +- **修正 README 里几处不准确的说法**,以及过时的版本号: + - 被控端只装在**一台**电脑上(讲台那台或你自己那台), + 原文写成"看学生机"是错的 —— 它看不到别的机器 + - 课堂那几项功能如实说明:改的是**本机** hosts、结束的是**本机**进程, + 都是可逆的,下课后点「一键恢复全部」就还原 + - 成品文件名从 1.8.2 更新到当前版本,并且以后发版会自动跟着改 + ## 2.1.3 - **支持「自己搭的、没有证书的中转」**(新服务器就是这种)。 @@ -6,7 +35,7 @@ 一台有公网 IP 的机器上,客户端填 `IP:端口` 就能连 —— 延迟、稳定性 都比隧道好得多。为此改了两处客户端: - - **手表端**:填 `13.54.30.196:13245` 这种**没写协议头**的地址时, + - **手表端**:填 `yk.xxmxxs.top:13245` 这种**没写协议头**的地址时, 现在会按「IP / localhost / 本地域名 → 明文,域名 → 加密」来猜, **和电脑端规则一致**。原来一律补成 `https://`,而那台机器没有证书, 结果是**永远连不上**,报的还是 SSL 错误,很容易让人白查半天证书。 diff --git a/_build/allow_plain_http.py b/_build/allow_plain_http.py index 15cf093..2e3fa90 100644 --- a/_build/allow_plain_http.py +++ b/_build/allow_plain_http.py @@ -3,7 +3,7 @@ r'''让客户端能用"纯 IP + 明文 HTTP"的中转(新服务器就是这种 == 背景 == -2026-10-08 换了服务器:AWS Lightsail,**有公网 IP**(13.54.30.196), +2026-10-08 换了服务器:AWS Lightsail,**有公网 IP**(yk.xxmxxs.top), 中转直接监听 13245,**不再走 Cloudflare 隧道**。 好处一大堆(延迟从 1000ms 降到 1ms、不再有 7844 阻断、内存 4 倍), @@ -11,7 +11,7 @@ r'''让客户端能用"纯 IP + 明文 HTTP"的中转(新服务器就是这种 而两个客户端原来都默认往 https 上走: - 1. Android 的 RelayTransport:用户填 `13.54.30.196:13245` + 1. Android 的 RelayTransport:用户填 `yk.xxmxxs.top:13245` (没写协议头)会被强制加上 `https://` -> 连不上(那台机器没证书) 2. Android 9+ **默认禁止明文 HTTP**。没有 @@ -39,7 +39,7 @@ Windows 那边已经是对的:relay.py 的 _PRIVATE_HOST 正则里 明文 HTTP 意味着**同网络的人能看到流量**(虽然配对令牌、内容本来 就是加密的,但 URL、设备名这些会露)。真正的解法是上 HTTPS: - 把域名 relay.xxmxxs.top 的 A 记录指到 13.54.30.196, + 把域名 relay.xxmxxs.top 的 A 记录指到 yk.xxmxxs.top, 然后 certbot 签一张 Let's Encrypt 证书,confd 挂上。 那一步要用户动 DNS,所以先让明文能用(这个补丁), @@ -91,7 +91,7 @@ JAVA_NEW = ''' if (!url.startsWith("http://") && !url.startsWith("https:/ // 而且报的是 SSL 错误,会把人带去查证书查半天。 // // ⚠ 纯 IP 判断包含**公网 IP** —— 2026-10-08 换到 Lightsail - // 之后中转就是 http://13.54.30.196:13245, + // 之后中转就是 http://yk.xxmxxs.top:13245, // 只把 192.168/10./172.16 当私有的话这里会走错。 String hostOnly = url; int slash = hostOnly.indexOf('/'); @@ -113,7 +113,7 @@ MAN_OLD = ''' ' MAN_NEW = ''' 9vAfI8@ZQ>lIs zZC&v{wRMbWtBl)Hr=^U(+*bepxve@1f?0?g@EI`z)Yh=5gJDyt0ksTl)I4L`3$eHP zWIN7e+niVwRynak*dWFF9#?6^E;()tzSqphMPq=Ny+LC3hG6y*#;^(H;5(-?c_^th zj!hcnDPt^ed1HK%1a)?&RM?vtVQOMTsD}}yu|^KM;H0}SR8DRC1}im5v^5!RX|+bB zSS)?&(Tim?rFI53BMj-hL3PETc(EE6C5&kaKO3IkYb9Q>tIHSR08-fr<;6!3Zffch?vh+X?dlR=A+h=r z)3FE}c(GWdTjV0SauT5zF*?v{qZ>VBbf@h`4|)OVYep~HXY?044pzRbY7!J9cqNmS z;FWCL{3moyf#6l?=_`=a_X>+U>P_Dg(eFDjmh|*36Ipj5D?NQHMAqHNN>9Q{k##S! z(sQszWZj3X^z=O-vZ|4lnZ9)*?~(tjsd^MswLwhPMxUt~M2n44wA2_)4;y1>Gt!h% zK`$GVGE=qkAEpXspB|+4hG2;yH5W_s1Pn`E{Jx1nwZ_Hoc<62;VUB1_A=9YDR=OO> z?Y*?N>@g`j8;^~c>|agcG8|`(9ZBV4uvyX`*?791{>*p=S~Hkrv93}eu;)?+BsT>~ zS^p( zkMYKl_5Ayv>sgL^po3HrzV*C)WIgZxb3ON-na0lR=juH8Jfal&|C zPI|)lz@7T^ihl%}S_;hhiSMGXr9$IA>S{bCg_BpKi3RBn*N36c(lxS3%=pT;x*dR_ zoq(aQ0Yjgp3FbISFrjsGok9}E*XjO#+n~RKwKIP7t^POk_IK)R7NYv^=zDiMAZ88X zKMHgNZ}O9SLMSf=<6Q!_y8?MXgDA5CUW4}rydHvp-(MMi6UKg4OTvg`21@;|)*An1 zhmR`nKx-70c#HHqHhZ=qX@wj_w$2_D#@~FJH4b4EIN&nSCzWAbohZJ=s8eBboG?|{ z(nZAyQ)gq9o;t3~*r~!`Abk$Qah_YIkmn4&guR2=oUg&sm@VD;Ei-Z5T3+LnB@As^LC3)cH+krU&fRE>6OWN= zm7pW-lZvvD^F^z`8=xS_F6EX>5@zQbvuhQ#z*P@)p^9>F)k|E3aRn<*EYb)M-$2ll z*4CSlJ&B1=nEl-8XPjdWNST8e?vv(_21uu*T4DB}`^}#8nAwZAn8(mAvp2nDmeE^g zANt(vOTU=?=#W{asOA8ru{m7HF-Iva&CyC zv^Ez6O3iBoWq6k&htXlzIY`-hkO~x*Clry%2Qkk`0?$ENAr2F6M4Y>d1qnW7Smq!O zd0Yk;e-T6u-KF7$G`SG9iDTd@VA=!Jy^az>SW&+{u*!8b1HAN+-Qc>te{8S;EnkBY zMD6~jp%OJ+16+D+8V8n399WvhDdw^D_n=GfOh|-_84O1I;S=+EYGU3%CFZ&`_H$@A z?Qw-AG#KnF^ZAXTYN;_R`sS^IF}#{rV~&Hot0-em2J!>=S&u1vRICH;55lf?AO?pn zt8m|Nt=#^DgB%-?-Bt zg^5?LDIc;mskd8%HVbyD-4o`dlsQ@0`M~%mX;wDa8R(hI4vb&@rs7V|Y)V!lqx%r{`E z^(TEO$<{8+6V45Eg2!7H?SY3!HS}@CTg-n2VDR^}x(ep(r90JCu4n~Dbn>*=imz~7 zWV4T_)%7bV3gcT?1iHAdy6$}}Nkz%yAomtcq{&D6beQizYVjV{`hDm#VbX1WK*P)r zaWMIqCYqnoWOE&_2!C+`Z2K1l2D6ukgp-9BNj1En+;u$% z#F*8KeLW4RYy79>z!n*%ElqN@eAiS4E)e-?6)5{gXeYz!3JTBq9uUGf(fk^-{|!3w zE#;Ws0V91+rRERR&HRyin>aoOZue-kx&#{m+U~$gn0vUUFq?6`DX=mjT%tUq5ggpv zq^O~(HURJckCis6g$-TCiOPfI3dnu~;Zk9FRN`6(X1vy2bN0BEREV)I(4!P>Ta1d(X!|i zD@YeuQM$s4(E=+@H&~76E~_!Ev2tjg)r2-$xwO@4N>9Uk|4plazOvfTf2_82&}yfc zR(qw;DpERF#Y%Upqte&vtc%?wQtMddUTcI> zV~tXtwni&2Tjk1I)^W!2yt5W&R0%o#K3^>+FfoyA9pu{>k(94<;7;K#y7-gLn zsI*QGoMv4XILo>+aIrN%aE}E#Wu9xECy~P`Kn*Mf>E^9ec?mEw8=mqOfZ?ey!7C9P zpoHO-wP4U}0b(8|yCWW-UjPtJ5CG<0PTXE~+sf z!vE{l8gs)YbMxb4Ap2&VOfXuArE#*b=CST2-C9jiq%p{OYRUlbvH7icUovUt!hI zaw|z&tXjci%qGXGgZY*)aRr~@>WLERdsY&e!D}ZVVeZZ=CUi&4X~8yC@OJXmVWM23 zezvS3hEOR1<$P~35D6k678IVFl!ACRurg`BklEfslwhSuv!22h$HAVSNx#|xFET9X zc(pw9ieR6top|MG5Vl*r;wlu2EDs1Qtw-3PZ4OlZzaeZ@|3|{+Z73%13G?L!l+1kf zNK;yHDjM5xFJcWfjQxQznU=lAi1ZDYmL2KJ1gerS-{e~1t;Ksa*N{jY!ZxXAV+4`* zViII0VZL>=^O5-uc61VYa^UVGU47EdN9McS5r_>y-x^Soy@2PvX-cv;LrM0Ex;%v+ zoNUa!Of5VnU^A_UkZC=0w0K7A@hm-MP@yHvk9cqh1|SA!dxNvRL2=5ch;g5*YQvm; zq9x7GH`6MnIA5dIRY?>6n}tQV{ZSW>s`$~*cm#svJ^EEVGWYYNKhV$p@=?P4GvgTL z2*Xv(tmpIPn$C*}7PbW$qj{5Ubj*Wdwj4B{f!+huAN1^50Q+;GXL~@;o(Da90rc!e z(6g67&t3*SdkysLEo|bqLC@X+J$nxy%6y-yti80>`hXs>KBQXfW7=VTLeE*B()-re z^s)6VePew`zgR!epVp6xV*R8vwtiOft^X*+)_%~o-$2`b2W|U98E^epnPweQW?FU1 zxo}y%%vO}^?0|BYtt$80y7Gu^C{Nm^lCmA;IXkGlVuzHs?XdE(9Z~k%QROc?9?K_l${@_v1ZVK5*8v7PJSDX%EC`2BW5Mp8f9J;=kP6&z219zp00 z_lPkJcg#gJ!8Gj^KKSNdW+V7EAE+WTV?rfjUx#+Cr>K1c<=BhF48fTKx)Ivs4G=D` zmN<t8~kH>3L#t-<+3=rr&didm~SMkLbu>XoMJ)H`PJs>S6lL{u*9@l!$}meVzRLi zJ&H;2zts-(mg&B9nt?Fu8zGLnpr|-oD^>7EQ0>5+TC0PWv`ROE8eK$} z)BLmm`bxyYAo~_9&aGG&L?W@5P*3}IfY}{1&R#}S>^o_OeHWc+FQ-{{70tEpri<*A zbh*8T=G&|2YWrTg$zDy1?fd8s8xb??2k3tLL3+SmM-ST%(+2wy+H9|Pb+!WG- z<#6M1x4?RA0Nw&86xCQ|HCF#I?vTucHDnC3hSgf5%1dgj@>u)(DOjS$+Cxr*48}F# zgjE4X4l+cum~F3JA+!J$Yb*vNnY@fdN0|272e489#wI$5z8?lW)KO~$h$wLa)X!0= z(g}$EVl7ZWELao#jj5Bl?*Te4y%Jq<$9zvCMvZsXM~xXWB2bY7`H+Ly6b@J-^Cizr zf<6T*Vl#=U);2h;s9Xue(O9mr&}|`^2z?e~IOwpDtejAOq_GXP@uC;)rYy)~jp7Fd z536%Xk_UzQZf^#Eh~NTF6Kd}?rT$JH4RP{mBCbzxnu*~~B)tyGA)%=YtTuz!;~#74 z>Y%9;GQ>=#8CcDH>FPR+-sce*%+)YN3%FJxr}|D8jrFFhGbl;J>1vJXYJ=&5iWT;3 zKDbhe2wbT|TwJL{d~l`H0UtjsqNr0$IZmk0w=u=w?4Bu3 zSmQaW5IC9196tyS=hj5_X_6lAu0dn{I|L14a#)T-N4^W2*~Lh|E-v;+VU0D(liVe& zDfJ}X)(L=A-ph>PtbgGzpG!0yL+hTfPD)wR#WtLAv{cU}KotlQb*?`>fubI0nT(9?wm}coEbFQIh9UvPNVtG>0+2bNjdR@)Iy9> znWjvaDCtIwk_DROjEb0tDNd>%0Lg%?xv|5l2u@g1Oq-z87hrS9T!-xJnlJig+5 z1;>PJT~H;Qd4xLD-HPW?W9NLzb1tCX&P6oPxs=8@^JKTQs5%ggPce>Y>PO0RvRe-V zU$8uKEGFM^v81l8ueGjVe^%iKi(6d68Yk^z-A4LYTMMAyh5N}SYa#!+K4~plK`35b z_g=j?Kf)2ixq*VtjTCln!WLXiO`V%5-?^1~Ik(YBX9*_p4ytgLik@bX5kx;DkgK{5 zob9mh!Q?E4?83$F#{*pWjNcM{HcR%9uC6awMRq}~`TZ;(E=gLqtS>GpsI_iik5caZ z2swhY5=dYbInHXza@Js;?h`G-kjheQ1Fb9|2xNj#n<~uUF&g-DoS_PeN$Rv-M^ylQzX?Z$f&M+7grDS;cquhhB(@Nr<||EOOFcK zv;}6dC(eD=Mg-L2bQ6mUcT-E8>a3(tbLAB08e6O_0kFLxz7)Ps)pZY7*Ifv}DMxG` zmSR`Y%Cy8pIBGc$VWf{x$bpx%^Ca%0QR1d$TIqHOikw_+y9t4hy1rEH3c+_* zL5y3JYPiwdo2*hDH@bV{?x4&{3wrGA!T6uYO20^roR_fDFJlwGLPgGNfVJ1@c;_`S zo;Vp6uu_3EJ1iQA%|RCadHJa}pI z6`~02trtZ=2<4o&Nps#I!+DRIJMROV?4@qbht$*gNOUGln&Cox8_cBC9xOtKuJbyi zT!HSeR_JX^VhG)FH>mP>X1`X^32ryjyVZ+`plpRw+f|i7W-V1E?fD9-lAYMg277fI zyqWRG-OL7*4b^pTiHHcmt5s&UHnXwoL}e+=U1BT?58=-Z*;En zJDuZx%*E8e`cA!wO2-~5P z)!BUwwl|KzFDdY3mwHFWOWi|DKF%zFs2o1vR8h=5zVMl*nqXG1JczK2oZu-NC@_zJ z^9^Gf7!l)?(>p^(c6nq(ga*iGNu%|wOHd@uOjDfQM-`RlAvX225LY4FkONFwFE(J` zg`hH6MrN=NzIxb?!odO5C^(REf`h1Ka4@wC4x>)N;WRLKER6_`r1Ib>nt*t{R|d;z zHDdK{4MoJ-m=N=}jhQUuyOQR+GZlcKkzrmb$0l@7mw8|khV9|ll!Cdh=BSi{r>_9g zeFbqpYOU89zwV|cxjGCjU?UJ^>{`CcBezC2tZgR3z`%OD#(KAkhVkWIaaqQfABxLP zeEG4sY|WSZ#APmDekLv>eE9_~({$X-HeHy8)X(}#mdE8cvOF%obIT{JA0FpIKV_c$ z;yw9~c=B83$sgX6KgE*+nI{LmCx`ioP3b3gKt8dx$1yGCu}g5&DOF>a;HdMs@RM{2 zqd3Nf^2iD0Q#@2a&5*VV6;g*#3+fbVNdrQyXiTUL9Up2-XNB6)f>3)}9_m03go^0l zP%%9k>PXLrI?=14&h%!eD}58{Mn8ml(7{koB@pVR7@^)uKEjbV5A{_#Bi?w=P=BQ? zG)Nf|8mx>C4HfI&84PA@2-K&8vEL3;Ti$izA2Tcy6v4OHN^><^{)f0z%6O0AhJ_P& za3IBY^}@!w!Tf--U>j<)h%`yy9&HP3ROVnHc&5-O(nF&$^f8njD#!3EsBP#tG*FQa zYB&n;$PGY(f*66e!+SDehh1K?TwSkah>u`1LgO);6Hw(uR5^)qLX*WP#YsU4-V|Ic z$;%_)yqLU9hS3dJR2(^Nm^9nbjS0I^z56WO=hVB;#eKee-wXyX&0Wiy3ZR6OQ+6Ty zq1w%B>=q)BXFYy4G#$M5M8{EdLX0q8HiSh{xs`uX$( zoq-#s*o~!Y4@sj-5wkZFOPAS3mSz#+<`k#7Mt3hf3($BD+kZE;4QIEo0!&ri@xRZ^b}%xIM8q%no*#zVrc@a2Ga*~1W|Uqr#WQDvFzEsiAD*^ z%jaaKrBD_EMn@rk_CR-)IX4QRW>GU$(cJt9d9 zIplP6+z#3;@X#Q^8$`Llpjvx07jVmtTjkXN%Om!c>d}um?);UIqG#D0`eSyMef*iV z_7o1Oouo517U##!*K6&mFxWNAG=x83fxCav%&wZ|?WzoCVV|7YQf|cVVrEx37B7UT zd{h+N3wnUq2A)iHG2#m^w}#896%HGrgTTOts2ze}cMId2GT{JC3~O|DSf>laCS4!4 z=%%nui^CCG7LL>MaFnXTF}f$*h*pOa^h7v|YQou+3^%1u!g=&nIG=tC7btqTP%H~b z9kl;2RGWGO z>w-P9PXX4&xznE6fH4xF5^e$a%9a!gx1xBsH8l#irJQg($`7}v&fzmfpM*-H5qbo= zQ>?&)gwLR`G;}3Nu8W*~8c+_}I3wLgJq|N`4kg0ZOC)_OhVeSRAsN(8vKGYRW$Zc| zrjEc|XM32dekF_0x!ckBZE1_{0TRQvYTy*nzeO?tu+K}_7qG3UjVKIB`;w!bE5d6iJG>6>_ej6r5o^mxu#vlh zm-3sJ`A+S6EZZ&&Y2oK77=D4G;TNfC_!TM$zf6VUS0yZ{O~o-9-90DW-QKdhkayX)xbzQjcN_Qm z_6EHc;^FW+WQO0RCSe?1luo|Y+>x$k8CP?cZ#9Qd&0%U1ffcpV-nW|N>1wLDntS}J ziL@j$(hAiK0z&+NJR`pym{pdEP5z&a{CfBq`8AiPO_$MU3qJ_;J@Lqgn10W~ZyZfb z&%cmGMTV0V8G+7>q-K#()Gsoc21mwFMWlkpMaI*_$XJ>bnIP5z>*ZtN$H&3?f{rDa zo381(sSlVd6ZX9VFxT+Z+;=o!Mi4PNG7ayy@qPdPg#Dm+{~><=VW0PB;QiTnKb+oW z*USAr*%J+WM?Cux;ODfBO4=&gepF^3X%#&wWecwpd*jhUVi#u!V2;e8{K#DD9XW?a zM9!t-A{W#6$R%`qynmiM)exy-Nj=_o#K`eJYMX{uB9-$|4`pfXFA9zkM_#@;#jr`I*j) zxI9FRw1RAe*I{V|8&$W$7+a@Db^-7ZA0UcT_J@@T`{Sg&53$$m&uZ*1CL?O3{mtYe z#2B`}uclxH@W#(9I~Pc=8MRFB2|=SGzfmyqJ0&82P;TVE*cS(=W#mt475R&bB7cj$ z0G?_FSSrv&d%yjgoZE?LUx;Fh^73)+_%$DDoCstWPHBz(JMbKIvABROqAInBx*-Rk&4$Zn0IX>T*}`i${4-D)PnR#S-?RG8}j7%Jgb# zfjlf-Ea51~b5za?CmbC&+P|SOmhjK^HTx2diNEZ8k#J;;L=SyALC{yMETTU<;WTKu zET7O%M$wUIkiyXrHHwC*DI!U=h(@VxG)5hxji_5RLB~WJ)6{4VofqvS=ByEOrcIgC zI0TD!aiqoIno|pRa0n%;0U;7SsFbIiSY^^_R8dsp;4tXqltZxH7y|-b36<111=Xap zmC)k}r{z-?vSBFXw5sf>ucA(+HBP&1eR{%yHWRNH^QC8 zG*_fI&| zrVr+RBo#(SQ_E;MwTX_QcE~G=_MY*$sHM7|(9Svn9w|io7c1twP=!E4MY~+Mvh4%^;0M>=W-TqR*{u}yhp<}_94GgjEs+4lYg-Lw> zL8)=Nu&f&jAg2c_LGnuzPTws~{{V524a5N{&DOR!g922JQbi~=M1k#l3*gS(l;_)i|TI=`~J;)cKisRt}3-Mm@b~ zC@>kP{28U8IBRh_0no{*3q?Yl9Gw6t0=kOgTMg04fSxJTAzF!@djj=}PNP22=`=KY zA`Qp&C|plKT8VU8bOz0coiI9#kX|$Yy5}q{x|XDkYpsU^|G5pa_F6TlKB-KWO6foF>md6}UgJt7;i0w3oJKI^3@K-rY2_fY2%W9!TE%K)rh@8vPJx_an?AKBpDk z2dMZIB=0lK;#Zi#Z)i~TJ8YnDv4K9LkHU-b7;@OGFn~uEGk#`pI z<|6Oh=nrCsA0*(OsOijOw=sbWv8RGdksdN zPg>7%oL+oc3IQwAUNflOxeGSIDQ5-u^lsPI7`fUKICZ3)dpLKM^rk__UAt!ltnHk1 z?ycbuBgyXu5=g=#VqAIl;qmQAr3e8OWn)H2qDI>mCSN31FJiRIzDPx*90tbk_33hCTfGrA?% zsMguA+1bnsop5ShKmZtSA=| zH##QPod(Bx(uCMpu}h)DumJK+(p_B(GQeV#X=ri^o8v$}HU-ycbQ?FiLo}N1x4tpu z?Bx7iN#_|91FD5JFTUPWwzHB(Mdo5tGAioF-f$^p^?OHAfPzlI=XrN=1oUvc1-M zxq>mS25o|~4%!4~9cHzc));1V&gPj}&YMf`h9ny|>#j>UZ_RS#gIee98|UyEh}CEe zVz(KrHj1r3M$#;p(dQaDX3}}zqgai>P`5}Q#RSEQolB9}d6bCZEMXz zDwtdjMHmvt?j${S7r175D>hM8pR%_rm^+ZI`$|P$2L&knCqy?HqsnZeS_B#C5n?X z%LG<=<~>h1-^5z1q1G7hkFFF6S3jq(ewC6Xwo7wP&Eh-r*y!}S8p#?3csoP!yt7aI21cNpdOnBX5UO2h_= z{e(UG3$V(58WQ^rQ2#rf9{YpNiTz3E$Nr+rW3YyZ{Y?vFhiGN&uz-pjf{95NNE5)d zvV|3_9N5?-2HYA5Vsi_*^Uqi%8L-Pg= zd$!rJr;`ktET%$-_r)b=yMsbUZ<2H4UJJb!=z2jjT7LU+z z@fckikJDB0gcx&k@Gy1Y%a}1x*wQDs9H+143IH)1_N3XJfgrZ0mA#cfZ_H3Pmk>vEHVX>fm4Z{NX{3+ja%AcLg)j`AO!EO{J2|k|ko0Eb*xh}cV zGq-+H@1v7!R+cd}l?;S5JaxTj63j{qAD4_TE+Q}B3zD!IMTmSh91Vt2K}3sSGMEhF zV*vPuq|c`fghzP14LR|)6pOc`#_{%481F!B<3-ddUQB)CC3IrEBb^@aL^s4c(~5XE zS{v_9)$ty*CEk;E#{1GU@qV-?j!z4M7wjUTatEkBZ`t!bFXal4^WlRAzQl2auP{D5 zz@K&cr$O@+z;y_EsG48`@{yWgV^3Vl<3-d63^zx?7?1oUrl9cgL1f1VQzAZua^pj( zMSK_)#fQ`A_^~uDK7uC4N7D58C^|Jhnr6qx(42TVofoeF@LifdwzUQRVJL&SqFNiw zs|hw^Sg)sxiH}1Y32F}`1}qu7C^+o7JPUMwmK3K3mIs962;@EKqNDrPL`9$ zZs#iA?On8oOdO~l$HaV*fI)k(H4fEQdER}nq=b2ql$Di;$OU|6^g_gB?A={(KaI5b z=>YjNF%9lX7~gxf4pL))`V{<+*7I@la1qK)KquVu%EGK*`=$3#6WL5*4RBzPrE#z| zdK?cs;bB2-uuCG?OA{e@3KwMsd(W*6_L)!{9Qe3+GBopKc=`z{cM*j+yvFCCXLG3_ zelE3%pGO1Y7l;iBGEmSWW(BJ>c|-&8bvL_#sZ{1=mcR`KbPUSg?OzshRByR5W$*DX zThHPoQx;!Wl9Pi;5;PH>j>@@y)vod_I>)c*y}m_f`xRa7Tl5USqHBDM&h#s~HhAAr z!veHsypuCCaKHMyEZP!)%{v>LflcU=%87neKcF6v<(mPN8JJDhhbkxd6@D;S?bDCr z{rd5c@8~A@)w<5N=vcp^4+kGPssmu=nQFMZJTnpN{lR9WU$u{#<+9r5*wHL`w?xbm zH$+XtnjiD+?_j@*H~1DE;8*l<`#sr*%tAozq4f1DywSJbdY|pi%)=ADMSJ?y`lN5s zZhl2K`4;W$S9G&)QMi5`y=FD`tfS_Dfw6;M;iPZ9LL<^}#cF+vw(_fWi*M29enq$X z7A^2An({3wd(8(}jJ01AE&SnbSvp)xP7u6z}`h`>sC# z(>s0@zZZO8_V-9CQ9m4N2Jh|-ejrO{@Huc9)eCl*Dg2@T_+Iks#z(%3>&~MYsDE{mi#$%CG3>zC~;Oihki+RQR_y-0olc z7Jb66)~|xzmdI@3JZ#|&e*5Q};I}?-zTR(UzYBhUG|=S3f7`DUKLowce%FJ?{5A-W z9hy7tZ>R0jKKHC>QKz>CJ1`iz#4L~86 z`xQR?4O&?ykSW9-JO0XtB~OfJv03VXZqGU!)K90R{GeVS>(K0I?1nE+qc@Oe$_g@gxGh! zg8#4*Q~V}36bc^&>kte3^)}**pvL+YjrtA@mhVR~^P$*3^mdeAZ{t1(I~a5O6>a3( zg`s|3NcimcO*F`FU|Ijr+kSps$PP6=3d)&->+M%_j_(5Z@T<4UKd>WR{RWr&4~0AW z6>bV&q=vW`>LkDZ=EGenvxTKZ05r(&WECp(Kef8XdJPC{s9C7FtU0rTij!36*NYZu zF&Dy*3np1Ftfi)YMO*p;L}R~cXyuC;5`ML|4tbW28EOTUnqSejp>{{l81Qt^uWN9Pe!OIE1Z)4@=QkY5l!m*d9v502m{>A_JGo>Y!vyUxN_LrSo9 z1EJDTM??j4@U6v8xa(p#@Hq>Fx*Lx2lCmk(!>|J_0#2wWJ}BKP)Jvqrq28fBxG%%| z_=Wn0`iBOh+%We(a7W-`QSRfw2cba&b5%CT!!I;AusSrmKMM7XF>Fn?r?qtRwKHUTkxb`feC5L z*ap6U%|D~1Onb<*uT00vbf%laYcLR+XgJ1VnZH4%n`A22$@oa#%k?m1`z9=n`DfLqKXb9VAmZf97O)f4WTP$#lL>*(dM6l" # 拼出来,免得这个文件自己又成一个泄漏点 def check(name, ok, detail=""): diff --git a/_tests/test_apk_contents.py b/_tests/test_apk_contents.py index 4f17220..280b94c 100644 --- a/_tests/test_apk_contents.py +++ b/_tests/test_apk_contents.py @@ -23,7 +23,7 @@ def _latest_apk() -> Path: (ROOT / "dist").glob("*watch*.apk"), key=lambda p: p.stat().st_mtime, reverse=True) return candidates[0] if candidates else \ - ROOT / "dist" / "ykxmz-watch-2.1.3.apk" + ROOT / "dist" / "ykxmz-watch-2.1.4.apk" APK = _latest_apk() diff --git a/_tests/test_package_remote.py b/_tests/test_package_remote.py index 716a7b0..ccd7387 100644 --- a/_tests/test_package_remote.py +++ b/_tests/test_package_remote.py @@ -26,9 +26,9 @@ ROOT = Path(__file__).resolve().parent.parent # 被控端 -> 一个 zip # 服务器端 -> 一个 zip # 控制端 -> 直接给 APK(不打 zip) -AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.1.3.zip" -SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.1.3.zip" -WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.1.3.apk" +AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.1.4.zip" +SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.1.4.zip" +WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.1.4.apk" TOOLCHAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain") JAVAC = TOOLCHAIN / "jdk17" / "bin" / "javac.exe" JAVA = TOOLCHAIN / "jdk17" / "bin" / "java.exe" @@ -89,7 +89,7 @@ def main() -> int: for label, path in (("被控端", agent_dir / "README-agent.txt"), ("服务器端", relay_dir / "README-server.txt"), ("控制端(旁边那份)", - ROOT / "dist" / "ykxmz-watch-2.1.3-说明.txt")): + ROOT / "dist" / "ykxmz-watch-2.1.4-说明.txt")): raw = path.read_bytes() if path.is_file() else b"" check(f"{label}说明有 UTF-8 BOM", raw[:3] == b"\xef\xbb\xbf", raw[:3]) diff --git a/_tests/test_relay_url_normalize.py b/_tests/test_relay_url_normalize.py new file mode 100644 index 0000000..842a8ba --- /dev/null +++ b/_tests/test_relay_url_normalize.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +r'''断言"手表端给中转地址补协议头"这件事真的做了,而且只有一处入口。 + +== 为什么要专门测这个 == + +用户报的故障: + + 填了 yk.xxmxxs.top(没写协议头)-> 点「检查服务器」 + -> MalformedURLException: no protocol: yk.xxmxxs.top/v1/health + +根因:`checkServer()` 直接拿 relayUrl() 的**原始字符串**拼 URL, +而补协议头的逻辑只在 RelayTransport 的构造函数里 —— +同一个地址"连接"时能用、"检查服务器"时崩。 + +修法是把规范化收到 relayUrl() 这一个入口。 +**所以断言要盯两件事**: + + 1. 规范化函数存在,而且规则正确(IP 走 http、域名走 https) + 2. relayUrl() 真的调了它(否则又是"改了函数没人用") + 3. checkServer() 从 relayUrl() 取地址(不再自己拼原始值) +''' +from __future__ import annotations + +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +JAVA = ROOT / "android" / "src" / "com" / "lanremote" / "watch" +MAIN = JAVA / "MainActivity.java" +TRANSPORT = JAVA / "RelayTransport.java" + +FAILS: list = [] + + +def check(name: str, ok: bool, why: str = "") -> None: + print(" [%s] %s%s" % ("PASS" if ok else "FAIL", name, + (" —— " + why) if (why and not ok) else "")) + if not ok: + FAILS.append(name) + + +def main() -> int: + print("=" * 58) + print(" 手表端中转地址规范化") + print("=" * 58) + print() + + check("MainActivity.java 存在", MAIN.is_file()) + if not MAIN.is_file(): + return 1 + src = MAIN.read_text(encoding="utf-8") + + # ---- 1. 规范化函数存在 ---- + print("一、规范化函数") + check("定义了 normalizeRelay()", "normalizeRelay(String raw)" in src, + "没有它就又会出现 no protocol") + + # 从源码里把函数体抠出来检查规则 + m = re.search(r"static String normalizeRelay\(String raw\)\s*\{(.*?)\n \}", + src, re.S) + body = m.group(1) if m else "" + check("函数体抠出来了", bool(body)) + check("已经有协议头就原样返回", + "startsWith(\"http://\")" in body + and "startsWith(\"https://\")" in body) + check("空串返回空(代表只走局域网)", "url.isEmpty()" in body) + check("纯 IP 走 http", 'matches("\\\\d{1,3}(\\\\.\\\\d{1,3}){3}")' in body + or "matches(" in body and "d{1,3}" in body) + check("localhost 走 http", "localhost" in body.lower()) + check(".local 走 http", ".local" in body) + check("域名走 https", '"https://"' in body) + + # ---- 2. relayUrl() 真的调了它 ---- + print() + print("二、relayUrl() 是不是唯一入口") + m2 = re.search(r"private String relayUrl\(\)\s*\{(.*?)\n \}", src, re.S) + ru = m2.group(1) if m2 else "" + check("relayUrl() 里调了 normalizeRelay", "normalizeRelay(saved)" in ru, + "没调的话 checkServer 还是会拿到没协议头的地址") + check("relayUrl() 里没有自己再写一遍规范化", + "startsWith(\"http" not in ru, + "应该只调 normalizeRelay,别复制逻辑") + + # ---- 3. checkServer 用的是 relayUrl() ---- + print() + print("三、checkServer() 的来源") + m3 = re.search(r"private void checkServer\(\)\s*\{(.*?)\n \}", src, re.S) + cs = m3.group(1) if m3 else "" + check("checkServer() 里有 relayUrl()", "relayUrl()" in cs) + check("checkServer() 不再直接拼原始字符串", + "new java.net.URL(relay + \"/v1/health\")" in cs + and "String relay = relayUrl()" in cs, + "应该从 relayUrl() 取(已规范化过的)") + + # ---- 4. 两边规则一致 ---- + print() + print("四、和 RelayTransport 的规则一致") + tr = TRANSPORT.read_text(encoding="utf-8") + check("RelayTransport 也会补协议头", + '!url.startsWith("http://")' in tr) + check("RelayTransport 里纯 IP 走 http", + "d{1,3}" in tr) + check("RelayTransport 里域名走 https", + '"https://"' in tr) + + # ---- 5. 真实用例(模拟函数逻辑,确保规则对)---- + print() + print("五、规则实算(用 Python 复刻一遍,看结果对不对)") + + def normalize(raw: str) -> str: + url = (raw or "").strip() + while url.endswith("/"): + url = url[:-1] + if not url or url.startswith(("http://", "https://")): + return url + host = url + if "/" in host: + host = host.split("/", 1)[0] + if ":" in host: + host = host.split(":", 1)[0] + plain = (host.lower() == "localhost" + or host.lower().endswith(".local") + or re.fullmatch(r"\d{1,3}(\.\d{1,3}){3}", host) is not None) + return ("http://" if plain else "https://") + url + + cases = [ + ("yk.xxmxxs.top", "https://yk.xxmxxs.top"), + ("yk.xxmxxs.top/", "https://yk.xxmxxs.top"), + ("https://yk.xxmxxs.top", "https://yk.xxmxxs.top"), + ("http://10.20.30.40:13245", "http://10.20.30.40:13245"), + ("10.20.30.40:13245", "http://10.20.30.40:13245"), + ("192.168.2.156:8080", "http://192.168.2.156:8080"), + ("localhost:8080", "http://localhost:8080"), + ("lanremote.local:8765", "http://lanremote.local:8765"), + ("", ""), + ] + for raw, want in cases: + got = normalize(raw) + check(" %-28r -> %s" % (raw, want), got == want, + "实际得到 %r" % got) + + print() + print("=" * 58) + if FAILS: + print(" 失败 %d 项:%s" % (len(FAILS), ", ".join(FAILS))) + return 1 + print(" 全部通过") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index ec581d3..44f5e61 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -2,11 +2,11 @@ + android:versionName="2.1.4">