mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 18:20:43 +00:00
用户报了三件事,一起处理。
== 一、发行版介绍里泄漏了服务器公网 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" 了,内容本身是对的。)
167 lines
6.8 KiB
Python
167 lines
6.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
r'''让客户端能用"纯 IP + 明文 HTTP"的中转(新服务器就是这种)。
|
||
|
||
== 背景 ==
|
||
|
||
2026-10-08 换了服务器:AWS Lightsail,**有公网 IP**(yk.xxmxxs.top),
|
||
中转直接监听 13245,**不再走 Cloudflare 隧道**。
|
||
|
||
好处一大堆(延迟从 1000ms 降到 1ms、不再有 7844 阻断、内存 4 倍),
|
||
但有个直接后果:**现在是明文 HTTP,没有 HTTPS 证书**。
|
||
|
||
而两个客户端原来都默认往 https 上走:
|
||
|
||
1. Android 的 RelayTransport:用户填 `yk.xxmxxs.top:13245`
|
||
(没写协议头)会被强制加上 `https://` -> 连不上(那台机器没证书)
|
||
|
||
2. Android 9+ **默认禁止明文 HTTP**。没有
|
||
`android:usesCleartextTraffic="true"` 的话,请求会被系统直接掐掉,
|
||
报的还是很含糊的 "Cleartext HTTP traffic not permitted"。
|
||
|
||
Windows 那边已经是对的:relay.py 的 _PRIVATE_HOST 正则里
|
||
`\d{1,3}(?:\.\d{1,3}){3}` 把**任意纯 IP(含公网)**都当"用明文",
|
||
所以裸 IP 会正确走 http://。
|
||
|
||
== 这个补丁做什么 ==
|
||
|
||
1. RelayTransport 的协议猜测改成和 Windows 一致:
|
||
纯 IP / localhost / *.local -> http://
|
||
域名 -> https://
|
||
|
||
2. AndroidManifest 加 usesCleartextTraffic="true"
|
||
|
||
3. 顺手清掉 windows/lanremote/relay.py 里一段**函数内死代码**
|
||
(_guess_scheme 的 return 之后还有一份一模一样的拷贝,
|
||
缩进仍属于函数体所以不报错,但看着很迷惑)
|
||
|
||
== 关于 HTTPS ==
|
||
|
||
明文 HTTP 意味着**同网络的人能看到流量**(虽然配对令牌、内容本来
|
||
就是加密的,但 URL、设备名这些会露)。真正的解法是上 HTTPS:
|
||
|
||
把域名 relay.xxmxxs.top 的 A 记录指到 yk.xxmxxs.top,
|
||
然后 certbot 签一张 Let's Encrypt 证书,confd 挂上。
|
||
|
||
那一步要用户动 DNS,所以先让明文能用(这个补丁),
|
||
HTTPS 等 DNS 指过来再做。
|
||
'''
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
JAVA = (ROOT / "android" / "src" / "com" / "lanremote" / "watch"
|
||
/ "RelayTransport.java")
|
||
MANIFEST = ROOT / "android" / "AndroidManifest.xml"
|
||
WINRELAY = ROOT / "windows" / "lanremote" / "relay.py"
|
||
|
||
|
||
def patch(path: Path, old: str, new: str, what: str, out: list) -> str:
|
||
raw = path.read_bytes()
|
||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||
text = raw.decode("utf-8-sig")
|
||
if old not in text:
|
||
out.append(("✗", what, "找不到锚点"))
|
||
return text
|
||
if text.count(old) != 1:
|
||
out.append(("!", what, "锚点 %d 处" % text.count(old)))
|
||
text = text.replace(old, new, 1)
|
||
path.write_bytes((b"\xef\xbb\xbf" if bom else b"")
|
||
+ text.encode("utf-8"))
|
||
out.append(("✓", what, ""))
|
||
return text
|
||
|
||
|
||
# ---------------------------------------------------------------- 1. Java
|
||
JAVA_OLD = ''' if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||
url = "https://" + url;
|
||
}'''
|
||
|
||
JAVA_NEW = ''' if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||
// 没写协议头时猜一个。规则要**和 Windows 端一致**
|
||
// (windows/lanremote/relay.py 的 _guess_scheme):
|
||
//
|
||
// IP / localhost / *.local -> http://
|
||
// 域名 -> https://
|
||
//
|
||
// 为什么纯 IP 走明文:IP 地址的中转基本都是自己搭的
|
||
// (局域网那台,或者像现在这样一台有公网 IP 的服务器),
|
||
// 上面往往没有证书。原来一律加 https:// 的结果是**永远连不上**,
|
||
// 而且报的是 SSL 错误,会把人带去查证书查半天。
|
||
//
|
||
// ⚠ 纯 IP 判断包含**公网 IP** —— 2026-10-08 换到 Lightsail
|
||
// 之后中转就是 http://yk.xxmxxs.top:13245,
|
||
// 只把 192.168/10./172.16 当私有的话这里会走错。
|
||
String hostOnly = url;
|
||
int slash = hostOnly.indexOf('/');
|
||
if (slash > 0) {
|
||
hostOnly = hostOnly.substring(0, slash);
|
||
}
|
||
int colon = hostOnly.indexOf(':');
|
||
if (colon > 0) {
|
||
hostOnly = hostOnly.substring(0, colon);
|
||
}
|
||
boolean plain = hostOnly.equalsIgnoreCase("localhost")
|
||
|| hostOnly.toLowerCase().endsWith(".local")
|
||
|| hostOnly.matches("\\\\d{1,3}(\\\\.\\\\d{1,3}){3}");
|
||
url = (plain ? "http://" : "https://") + url;
|
||
}'''
|
||
|
||
# ---------------------------------------------------------------- 2. 清单
|
||
MAN_OLD = ''' <uses-permission android:name="android.permission.INTERNET" />'''
|
||
|
||
MAN_NEW = ''' <!-- ⚠ 允许明文 HTTP。
|
||
中转可以是"自己搭的、没有证书"的服务:局域网那台,或者
|
||
2026-10-08 之后那台有公网 IP 的服务器(http://yk.xxmxxs.top:13245)。
|
||
Android 9+ 默认**禁止明文流量**,不加这一条的话请求会被系统
|
||
直接掐掉,日志里只有一句含糊的
|
||
"Cleartext HTTP traffic to ... not permitted"。
|
||
|
||
安全性说明:中转上跑的内容(配对令牌、画面、文件)本来就是
|
||
加密过的,明文只暴露 URL 和设备名。要更严的话后续上 HTTPS
|
||
(Let's Encrypt),那时可以把这一条去掉。 -->
|
||
<application android:usesCleartextTraffic="true" />
|
||
|
||
<uses-permission android:name="android.permission.INTERNET" />'''
|
||
|
||
# ---------------------------------------------------------------- 3. 死代码
|
||
DEAD_OLD = ''' host = address.split("/", 1)[0]
|
||
return "http://" if _PRIVATE_HOST.match(host) else "https://"
|
||
|
||
|
||
|
||
"""用户没写 http/https 时,猜一个。
|
||
|
||
IP 地址 / localhost -> http://
|
||
局域网里的中转服务是明文 HTTP。如果猜成 https,永远连不上,
|
||
而且报的是 SSL 错误,很容易误导人去查证书。
|
||
域名 -> https://
|
||
走公网必须加密(Cloudflare 隧道本来就是 https)。
|
||
"""
|
||
host = address.split("/", 1)[0]
|
||
return "http://" if _PRIVATE_HOST.match(host) else "https://"'''
|
||
|
||
DEAD_NEW = ''' host = address.split("/", 1)[0]
|
||
return "http://" if _PRIVATE_HOST.match(host) else "https://"'''
|
||
|
||
|
||
def main() -> int:
|
||
out: list = []
|
||
|
||
patch(JAVA, JAVA_OLD, JAVA_NEW, "RelayTransport 协议猜测改成和 Windows 一致",
|
||
out)
|
||
|
||
patch(MANIFEST, MAN_OLD, MAN_NEW, "AndroidManifest 允许明文 HTTP", out)
|
||
|
||
patch(WINRELAY, DEAD_OLD, DEAD_NEW, "清掉 relay.py 里的函数内死代码", out)
|
||
|
||
print(" ── 结果 ──")
|
||
for m, what, why in out:
|
||
print(" %s %s%s" % (m, what, (" —— " + why) if why else ""))
|
||
return 0 if not any(m == "✗" for m, _w, _y in out) else 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|