# -*- coding: utf-8 -*- r'''让客户端能用"纯 IP + 明文 HTTP"的中转(新服务器就是这种)。 == 背景 == 2026-10-08 换了服务器:AWS Lightsail,**有公网 IP**(13.54.30.196), 中转直接监听 13245,**不再走 Cloudflare 隧道**。 好处一大堆(延迟从 1000ms 降到 1ms、不再有 7844 阻断、内存 4 倍), 但有个直接后果:**现在是明文 HTTP,没有 HTTPS 证书**。 而两个客户端原来都默认往 https 上走: 1. Android 的 RelayTransport:用户填 `13.54.30.196: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 记录指到 13.54.30.196, 然后 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://13.54.30.196: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 = ''' ''' MAN_NEW = ''' ''' # ---------------------------------------------------------------- 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())