mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-19 08:20:44 +00:00
2.1.4:修 URL 协议头 bug + 清理对外材料里泄漏的服务器信息
用户报了三件事,一起处理。
== 一、发行版介绍里泄漏了服务器公网 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" 了,内容本身是对的。)
This commit is contained in:
+30
-1
@@ -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 错误,很容易让人白查半天证书。
|
||||
|
||||
@@ -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 = ''' <uses-permission android:name="android.permission.INTERNET" />'
|
||||
|
||||
MAN_NEW = ''' <!-- ⚠ 允许明文 HTTP。
|
||||
中转可以是"自己搭的、没有证书"的服务:局域网那台,或者
|
||||
2026-10-08 之后那台有公网 IP 的服务器(http://13.54.30.196:13245)。
|
||||
2026-10-08 之后那台有公网 IP 的服务器(http://yk.xxmxxs.top:13245)。
|
||||
Android 9+ 默认**禁止明文流量**,不加这一条的话请求会被系统
|
||||
直接掐掉,日志里只有一句含糊的
|
||||
"Cleartext HTTP traffic to ... not permitted"。
|
||||
|
||||
@@ -146,6 +146,26 @@ def main() -> int:
|
||||
print(f" docs/{path.name}")
|
||||
changed += 1
|
||||
|
||||
# ---- README 里的成品文件名 ----
|
||||
#
|
||||
# README 里有 8 处写着 `ykxmz-setup-1.8.2.exe` 这类成品名。
|
||||
# 以前没管它,结果一直停在 1.8.2 —— 用户照着 README 去 releases
|
||||
# 找 1.8.2 的包,当然找不到(最新已经是 2.x 了)。
|
||||
#
|
||||
# 这里用"任意数字版本号 -> 新版本"的正则,而不是精确替换 OLD:
|
||||
# README 可能因为漏过几次 bump 而停在更老的版本上,
|
||||
# 精确匹配 OLD 那时候就换不掉了。
|
||||
readme = ROOT / "README.md"
|
||||
if readme.is_file():
|
||||
text = readme.read_text(encoding="utf-8")
|
||||
new_text = re.sub(
|
||||
r"(ykxmz-(?:setup|server|watch|agent)-)\d+\.\d+\.\d+",
|
||||
lambda m: m.group(1) + NEW, text)
|
||||
if new_text != text:
|
||||
readme.write_text(new_text, encoding="utf-8", newline="\n")
|
||||
print(" README.md")
|
||||
changed += 1
|
||||
|
||||
# ---- 会断言成品名的测试 ----
|
||||
for name in ("test_package_remote.py", "test_apk_contents.py"):
|
||||
path = ROOT / "_tests" / name
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''查 Gitee 到底为什么判违规,以及 README 里哪些词可能触发。
|
||||
|
||||
== 已知 ==
|
||||
|
||||
https://gitee.com/xmz7891/ykxmz/raw/main/README.md -> HTTP 451
|
||||
Unavailable For Legal Reasons
|
||||
|
||||
451 是"应法律要求不可用"。在国内平台上,这个通常是**内容审核拦的**。
|
||||
|
||||
== 要分清两种情况 ==
|
||||
|
||||
A. 整个仓库被限制(那 API 读仓库信息也会失败/受限)
|
||||
B. 只有 README 这个文件被拦(网页能开、raw 不行)
|
||||
|
||||
== 同时扫一下 README 里可能触发审核的词 ==
|
||||
|
||||
国内平台的关键词审核对这几类比较敏感:
|
||||
|
||||
* 翻墙 / 代理 / VPN / 科学上网
|
||||
* 挖矿 / 矿机 / 算力 / 虚拟货币
|
||||
* 破解 / 破解版 / 盗版 / 激活工具
|
||||
* 外挂 / 作弊
|
||||
* 监控 / 偷拍 / 窃听 / 追踪("监控"在教育场景容易误判)
|
||||
* 游戏相关的"封号""加速器"
|
||||
* 二维码 / 收款 / 打赏
|
||||
|
||||
这份 README 是"教室手表控电脑",正常。但要确认没混进上面这些。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
GITEE = "https://gitee.com/xmz7891/ykxmz"
|
||||
|
||||
RISKY = [
|
||||
"翻墙", "代理", "VPN", "vpn", "科学上网", "梯子", "加速器",
|
||||
"挖矿", "矿机", "算力", "比特币", "虚拟货币", "以太坊",
|
||||
"破解", "盗版", "激活工具", "注册机", "keygen", "crack",
|
||||
"外挂", "作弊", "开挂",
|
||||
"监控", "偷拍", "窃听", "追踪", "定位",
|
||||
"打赏", "收款", "二维码收款",
|
||||
"荷官", "博彩", "赌博",
|
||||
]
|
||||
|
||||
|
||||
def get(url: str, ua: str = "Mozilla/5.0") -> tuple:
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("User-Agent", ua)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return r.status, r.read().decode("utf-8", "replace")
|
||||
except urllib.error.HTTPError as exc:
|
||||
return exc.code, ""
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return "失败", str(exc)[:60]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=" * 62)
|
||||
print(" 查 Gitee 为什么 451 + 扫 README 可疑词")
|
||||
print("=" * 62)
|
||||
|
||||
print()
|
||||
print(" ── 1. 各入口的状态码 ──")
|
||||
for label, url in [
|
||||
("仓库主页", GITEE),
|
||||
("raw README", GITEE + "/raw/main/README.md"),
|
||||
("raw LICENSE", GITEE + "/raw/main/LICENSE"),
|
||||
("网页版 README", GITEE + "/blob/main/README.md"),
|
||||
("发行版页", GITEE + "/releases"),
|
||||
]:
|
||||
code, body = get(url)
|
||||
note = ""
|
||||
if isinstance(code, int):
|
||||
if code == 200:
|
||||
note = "正常"
|
||||
elif code == 451:
|
||||
note = "**被法律原因拦了**"
|
||||
elif code == 404:
|
||||
note = "不存在"
|
||||
print(" %-16s HTTP %-6s %s" % (label, code, note))
|
||||
|
||||
print()
|
||||
print(" ── 2. 用 Gitee API 看仓库(API 走的是另一条路)──")
|
||||
token_file = ROOT / "_build" / ".gitee_token"
|
||||
if token_file.is_file():
|
||||
token = token_file.read_text(encoding="utf-8").strip()
|
||||
code, body = get(
|
||||
"https://gitee.com/api/v5/repos/xmz7891/ykxmz"
|
||||
"?access_token=%s" % token)
|
||||
print(" API 仓库信息 HTTP %s" % code)
|
||||
if code == 200:
|
||||
try:
|
||||
d = json.loads(body)
|
||||
print(" 名称:%s" % d.get("name"))
|
||||
print(" 是否私有:%s" % d.get("private"))
|
||||
print(" 最近推送:%s" % d.get("pushed_at"))
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
print()
|
||||
print(" ── 3. 扫 README 里的可疑词 ──")
|
||||
rm = ROOT / "README.md"
|
||||
text = rm.read_text(encoding="utf-8")
|
||||
lines = text.splitlines()
|
||||
hits = 0
|
||||
for word in RISKY:
|
||||
for i, line in enumerate(lines, 1):
|
||||
if word in line:
|
||||
hits += 1
|
||||
print(" %-10s 第 %d 行: %s"
|
||||
% (word, i, line.strip()[:60]))
|
||||
if not hits:
|
||||
print(" ✅ 没扫到这些词")
|
||||
|
||||
print()
|
||||
print(" ── 4. README 里的链接(外链也可能被拦)──")
|
||||
urls = re.findall(r"https?://[^\s\)\]\"'<>]+", text)
|
||||
seen = []
|
||||
for u in urls:
|
||||
host = re.sub(r"^https?://", "", u).split("/")[0]
|
||||
if host not in seen:
|
||||
seen.append(host)
|
||||
for h in seen:
|
||||
print(" %s" % h)
|
||||
|
||||
print()
|
||||
print(" ── 5. 结论提示 ──")
|
||||
print(" 如果只有 raw 是 451、主页 200,那就是**单个文件被拦**,")
|
||||
print(" 通常改掉触发的措辞就能恢复。")
|
||||
print(" 如果主页也 451,那是**整个仓库被限制**,得走 Gitee 申诉。")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -28,8 +28,8 @@ RELAY_DIR = "/opt/ykxmz-relay"
|
||||
# IP:80 —— 绕过 Cloudflare,直连 nginx(域名没配好时靠它)
|
||||
# :13245 —— **应该不通**(中转已只绑 127.0.0.1,不再对公网)
|
||||
DOMAIN_URL = "http://%s" % DOMAIN
|
||||
NGINX_URL = "http://13.54.30.196:80"
|
||||
DIRECT_URL = "http://13.54.30.196:%s" % PORT
|
||||
NGINX_URL = "http://<源站IP>:80"
|
||||
DIRECT_URL = "http://<源站IP>:%s" % PORT
|
||||
|
||||
REMOTE = r'''
|
||||
R=__DIR__
|
||||
|
||||
@@ -22,7 +22,7 @@ sys.path.insert(0, str(ROOT / "_build"))
|
||||
from lssh import run # noqa: E402
|
||||
|
||||
DOMAIN = "yk.xxmxxs.top"
|
||||
IP = "13.54.30.196"
|
||||
IP = "<源站IP>"
|
||||
|
||||
|
||||
def tcp(host: str, port: int, timeout: float = 8.0) -> bool:
|
||||
|
||||
@@ -88,7 +88,7 @@ WantedBy=multi-user.target
|
||||
|
||||
def main() -> int:
|
||||
print("=" * 62)
|
||||
print(" 部署到 Lightsail(13.54.30.196),端口 %s" % PORT)
|
||||
print(" 部署到 Lightsail(<源站IP>),端口 %s" % PORT)
|
||||
print("=" * 62)
|
||||
|
||||
print()
|
||||
@@ -167,7 +167,7 @@ def main() -> int:
|
||||
|
||||
print()
|
||||
print(" 提示:防火墙要先放行 TCP %s(用户来开)。" % PORT)
|
||||
print(" 之后公网地址就是 http://13.54.30.196:%s/" % PORT)
|
||||
print(" 之后公网地址就是 http://<源站IP>:%s/" % PORT)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ from lssh import run, upload # noqa: E402
|
||||
|
||||
RELAY_DIR = "/opt/ykxmz-relay"
|
||||
PORT = "13245"
|
||||
PUBLIC_URL = "http://13.54.30.196:%s" % PORT
|
||||
PUBLIC_URL = "http://<源站IP>:%s" % PORT
|
||||
|
||||
MONITOR = r'''#!/bin/bash
|
||||
# ============================================================================
|
||||
@@ -46,7 +46,7 @@ MONITOR = r'''#!/bin/bash
|
||||
#
|
||||
# 为什么不查 127.0.0.1:
|
||||
# 本机 200 只说明进程活着。用户真正关心的是"打开
|
||||
# http://13.54.30.196:13245 能不能出页面" —— 那要经过
|
||||
# http://<源站IP>:13245 能不能出页面" —— 那要经过
|
||||
# Lightsail 的防火墙 / 安全组 / 上游线路。实测踩过:
|
||||
# 隧道两条实例都掉了,本机 curl 照样 200,但公网 502。
|
||||
#
|
||||
|
||||
@@ -24,7 +24,7 @@ from lssh import run, upload # noqa: E402
|
||||
|
||||
RELAY_DIR = "/opt/ykxmz-relay"
|
||||
PORT = "13245"
|
||||
PUBLIC = "http://13.54.30.196:%s" % PORT
|
||||
PUBLIC = "http://<源站IP>:%s" % PORT
|
||||
|
||||
CODE = ["relay.py", "admin.py", "landing.py", "showcase.py"]
|
||||
ARTIFACTS = ["ykxmz-setup-{v}.exe", "ykxmz-watch-{v}.apk",
|
||||
|
||||
@@ -96,7 +96,7 @@ PY
|
||||
echo
|
||||
echo " 收尾:清掉负载测试造的设备和会话"
|
||||
curl -s -o /dev/null --max-time 8 -X POST "$BASE/v1/admin/forget" \
|
||||
-H 'X-Admin: 120830' -H 'Content-Type: application/json' \
|
||||
-H 'X-Admin: <改从环境变量读>' -H 'Content-Type: application/json' \
|
||||
-d '{"code":"112233"}'
|
||||
fi
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ done
|
||||
echo
|
||||
echo "════ 6. 对比:直连自己的 nginx ════"
|
||||
printf ' %-30s %s\n' "127.0.0.1:443(带 SNI)" "$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 --resolve $D:443:127.0.0.1 https://$D/v1/health)"
|
||||
printf ' %-30s %s\n' "公网 IP:443" "$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 --resolve $D:443:13.54.30.196 https://$D/v1/health)"
|
||||
printf ' %-30s %s\n' "公网 IP:443" "$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 --resolve $D:443:<源站IP> https://$D/v1/health)"
|
||||
'''
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''抓 Gitee 在 README 页面上挂的违规公告原文。
|
||||
|
||||
== 为什么要抓原文 ==
|
||||
|
||||
网页版 README 页里出现了:
|
||||
「违规信息」2 次
|
||||
「内容可能」2 次
|
||||
「违规」2 次
|
||||
「举报」10 次
|
||||
|
||||
但 raw 接口是 451。光看关键词猜没用 —— 要把 Gitee 的**原话**读出来,
|
||||
才知道它到底判的是哪一条,才能对症改。
|
||||
|
||||
== 抓什么 ==
|
||||
|
||||
把「违规信息」「内容可能」附近的 HTML 片段剥掉标签打出来。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
URL = "https://gitee.com/xmz7891/ykxmz/blob/main/README.md"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
req = urllib.request.Request(URL)
|
||||
req.add_header("User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 Chrome/120 Safari/537.36")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
page = r.read().decode("utf-8", "replace")
|
||||
except urllib.error.HTTPError as exc:
|
||||
print(" ✗ HTTP %s" % exc.code)
|
||||
return 1
|
||||
|
||||
print(" 页面 %d 字节" % len(page))
|
||||
|
||||
# 去掉 script/style
|
||||
body = re.sub(r"<script.*?</script>", " ", page, flags=re.S | re.I)
|
||||
body = re.sub(r"<style.*?</style>", " ", body, flags=re.S | re.I)
|
||||
|
||||
print()
|
||||
print("=" * 62)
|
||||
print(" 「违规」/「内容可能」附近的原文")
|
||||
print("=" * 62)
|
||||
for kw in ("违规信息", "内容可能", "违规"):
|
||||
for m in re.finditer(kw, body):
|
||||
start = max(0, m.start() - 500)
|
||||
end = min(len(body), m.end() + 500)
|
||||
chunk = body[start:end]
|
||||
chunk = re.sub(r"<[^>]+>", " ", chunk)
|
||||
chunk = html.unescape(chunk)
|
||||
chunk = re.sub(r"\s+", " ", chunk).strip()
|
||||
if len(chunk) > 30:
|
||||
print()
|
||||
print(" 【%s】" % kw)
|
||||
print(" ...%s..." % chunk)
|
||||
break # 每个词只看第一条,免得太长
|
||||
|
||||
print()
|
||||
print("=" * 62)
|
||||
print(" 页面里所有提到「审核 / 举报 / 法律」的短句")
|
||||
print("=" * 62)
|
||||
for kw in ("审核", "举报", "法律", "不可用"):
|
||||
seen = set()
|
||||
for m in re.finditer(kw, body):
|
||||
start = max(0, m.start() - 120)
|
||||
end = min(len(body), m.end() + 120)
|
||||
chunk = re.sub(r"<[^>]+>", " ", body[start:end])
|
||||
chunk = html.unescape(chunk)
|
||||
chunk = re.sub(r"\s+", " ", chunk).strip()
|
||||
# 只留信息量够、而且不是重复的
|
||||
key = chunk[:40]
|
||||
if len(chunk) > 25 and key not in seen:
|
||||
seen.add(key)
|
||||
print(" 【%s】%s" % (kw, chunk[:180]))
|
||||
if len(seen) >= 2:
|
||||
break
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -2,10 +2,10 @@
|
||||
r'''修上一步弄坏的登录表单。
|
||||
|
||||
上一步的正则插账号时把字典插坏了:
|
||||
urlencode({"username": "xmz7891", {"password": "120830"})
|
||||
urlencode({"username": "xmz7891", {"password": "" # 见下方注释:不再写死})
|
||||
^ 多了个左括号
|
||||
应该是:
|
||||
urlencode({"username": "xmz7891", "password": "120830"})
|
||||
urlencode({"username": "xmz7891", "password": "" # 见下方注释:不再写死})
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
OLD = "REVA" + "edu120830" # 拼出来,本脚本自己不含明文
|
||||
OLD = "REVA" + "edu<改从环境变量读>" # 拼出来,本脚本自己不含明文
|
||||
|
||||
TARGETS = [
|
||||
ROOT / "_tests" / "test_relay_admin_login.py",
|
||||
|
||||
@@ -47,7 +47,7 @@ from lssh import run # noqa: E402
|
||||
|
||||
RELAY_DIR = "/opt/ykxmz-relay"
|
||||
DOMAIN = "yk.xxmxxs.top"
|
||||
FALLBACK = "http://13.54.30.196:80"
|
||||
FALLBACK = "http://<源站IP>:80"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -58,7 +58,7 @@ def main() -> int:
|
||||
print()
|
||||
print(" ── 1. 改 monitor.sh 的健康检查(域名优先,IP 兜底)──")
|
||||
# 把 PUBLIC_URL 的默认值换掉,并把 is_healthy 改成"两个都试"
|
||||
old_url = 'PUBLIC_URL="${PUBLIC_URL:-http://13.54.30.196:13245}"'
|
||||
old_url = 'PUBLIC_URL="${PUBLIC_URL:-http://<源站IP>:13245}"'
|
||||
new_url = (
|
||||
'# 探测地址:域名优先(用户实际走的路),IP:80 兜底\n'
|
||||
'# (域名 DNS 还没指过来时用它,否则监测会一直误报)\n'
|
||||
@@ -151,10 +151,10 @@ def main() -> int:
|
||||
'http://%s/v1/health)"; '
|
||||
'printf " %-28s %s\\n" "IP:80 (兜底)" '
|
||||
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 '
|
||||
'http://13.54.30.196:80/v1/health)"; '
|
||||
'http://<源站IP>:80/v1/health)"; '
|
||||
'printf " %-28s %s\\n" "IP:13245 (已关闭)" '
|
||||
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 '
|
||||
'http://13.54.30.196:13245/v1/health)"' % DOMAIN,
|
||||
'http://<源站IP>:13245/v1/health)"' % DOMAIN,
|
||||
timeout=120, quiet=True)
|
||||
for ln in out[1].strip().splitlines():
|
||||
print(ln)
|
||||
|
||||
@@ -20,7 +20,7 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
OLD = "REVA" + "edu120830"
|
||||
OLD = "REVA" + "edu<改从环境变量读>"
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''撤掉 Gitee 发行版介绍里泄漏的公网 IP。
|
||||
|
||||
== 用户报的 ==
|
||||
|
||||
"那个发行版的介绍里,服务器公网 IP 地址暴露出去了"
|
||||
|
||||
来源:发布脚本(gitee_release.py / release_all.py)把
|
||||
_build/CHANGELOG.md 当"更新说明"传给 Gitee 建发行版,
|
||||
而 CHANGELOG 的 2.1.3 那节里写了 <源站IP>。
|
||||
|
||||
工作区那边已经清了(_build/scrub_server_ip.py),
|
||||
但**已经发出去的发行版介绍还在 Gitee 上**,得用 API 改回去。
|
||||
|
||||
== 接口 ==
|
||||
|
||||
PATCH /api/v5/repos/{owner}/{repo}/releases/{id}
|
||||
参数:access_token + body(新的发行版介绍)
|
||||
|
||||
== 顺带检查所有发行版 ==
|
||||
|
||||
不只 v2.1.3 —— 从 2.1.0 起我就在"把 CHANGELOG 当更新说明",
|
||||
而 2.1.0/2.1.1 那几节也提到过服务器。全部扫一遍、有 IP 的都改掉。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
IP = "<源站IP>"
|
||||
DOMAIN = "yk.xxmxxs.top"
|
||||
OWNER_REPO = "xmz7891/ykxmz"
|
||||
|
||||
|
||||
def api(url: str, data: bytes = None, method: str = None) -> tuple:
|
||||
req = urllib.request.Request(url, data=data, method=method)
|
||||
req.add_header("User-Agent", "ykxmz-release-fix")
|
||||
if data is not None:
|
||||
req.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
return r.status, json.loads(r.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as exc:
|
||||
return exc.code, exc.read().decode("utf-8", "replace")[:300]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
token_file = ROOT / "_build" / ".gitee_token"
|
||||
if not token_file.is_file():
|
||||
print(" ✗ 找不到 _build/.gitee_token")
|
||||
return 1
|
||||
token = token_file.read_text(encoding="utf-8").strip()
|
||||
|
||||
print("=" * 62)
|
||||
print(" 撤掉 Gitee 发行版介绍里的公网 IP")
|
||||
print("=" * 62)
|
||||
|
||||
print()
|
||||
print(" ── 1. 列出所有发行版 ──")
|
||||
code, data = api(
|
||||
"https://gitee.com/api/v5/repos/%s/releases"
|
||||
"?access_token=%s&per_page=60" % (OWNER_REPO, token))
|
||||
if code != 200:
|
||||
print(" ✗ 列不出来: %s" % data)
|
||||
return 1
|
||||
print(" 共 %d 个发行版" % len(data))
|
||||
|
||||
print()
|
||||
print(" ── 2. 逐个检查 body,有 IP 的改掉 ──")
|
||||
fixed = 0
|
||||
for rel in data:
|
||||
tag = rel.get("tag_name", "?")
|
||||
rid = rel.get("id")
|
||||
body = rel.get("body") or ""
|
||||
if IP not in body:
|
||||
print(" %-10s 干净" % tag)
|
||||
continue
|
||||
|
||||
n = body.count(IP)
|
||||
new_body = body.replace(IP, DOMAIN)
|
||||
print(" %-10s ⚠ 有 %d 处 -> 改掉" % (tag, n))
|
||||
|
||||
# ⚠ 两个坑:
|
||||
#
|
||||
# 1. 必须用 **JSON**。第一版 urlencode 拼了 data,但 api()
|
||||
# 见 data 非空就声明 Content-Type: application/json ——
|
||||
# 内容和声明不一致,Gitee 回 400(一页 HTML 错误页)。
|
||||
#
|
||||
# 2. **tag_name 和 name 必须一起传**,即使不改它们。
|
||||
# 漏了会报 {"messages":["tag_name is missing",
|
||||
# "name is missing"]}。
|
||||
# (PATCH 语义上应该允许只改 body,但 Gitee 的实现要求
|
||||
# 把标识字段带上 —— 那就带上。)
|
||||
payload = json.dumps({
|
||||
"access_token": token,
|
||||
"tag_name": tag,
|
||||
"name": rel.get("name") or tag,
|
||||
"body": new_body,
|
||||
}).encode("utf-8")
|
||||
code2, resp = api(
|
||||
"https://gitee.com/api/v5/repos/%s/releases/%s"
|
||||
% (OWNER_REPO, rid), data=payload, method="PATCH")
|
||||
if code2 == 200:
|
||||
print(" ✓ 已更新")
|
||||
fixed += 1
|
||||
else:
|
||||
print(" ✗ 更新失败 (%s): %s" % (code2, resp))
|
||||
|
||||
print()
|
||||
print(" ── 3. 复查 ──")
|
||||
code, data = api(
|
||||
"https://gitee.com/api/v5/repos/%s/releases"
|
||||
"?access_token=%s&per_page=60" % (OWNER_REPO, token))
|
||||
still = [r.get("tag_name") for r in data
|
||||
if IP in (r.get("body") or "")]
|
||||
if still:
|
||||
print(" ⚠ 还有: %s" % ", ".join(still))
|
||||
else:
|
||||
print(" ✅ 所有发行版介绍里都没有这个 IP 了")
|
||||
|
||||
print()
|
||||
print(" 改了 %d 个发行版" % fixed)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''修我自己 scrub 时弄坏的 3 个文件。
|
||||
|
||||
== 怎么坏的 ==
|
||||
|
||||
scrub_old_password.py 里对这个模式:
|
||||
|
||||
{"password": "<六位数字>"}
|
||||
|
||||
做了个"聪明"的替换:
|
||||
|
||||
{"password": "" # 见下方注释:不再写死}
|
||||
|
||||
—— 把 `"<六位数字>"` 换成了带注释的东西,**塞进了字典字面量中间**,
|
||||
于是 `{` 永远不闭合,三个文件全语法错。
|
||||
|
||||
教训:**批量替换不能往表达式中间塞注释**。
|
||||
这次改成把整行替换掉,并在文件顶部加一个读密码的辅助函数。
|
||||
|
||||
(同一批被改的其它文件只是字符串内容变了,语法没坏,
|
||||
这里只修真正语法出错的三个。)
|
||||
|
||||
== 顺带 ==
|
||||
|
||||
这个脚本本身也演示了"怎么在运维脚本里拿密码而不写死":
|
||||
从环境变量或 _build/.admin_password 读(后者已 gitignore)。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
BROKEN_LINE = re.compile(
|
||||
r'^(\s*)form = urllib\.parse\.urlencode\(\{"password": ""'
|
||||
r'\s*# 见下方注释:不再写死\}\)\.encode\(\)\s*$')
|
||||
|
||||
# ⚠ 不要手写文件列表。
|
||||
#
|
||||
# 第一版我列了三个文件名(照语法检查的输出抄的),
|
||||
# 结果一跑发现还有两个同样的坏文件没修 —— 列表是**观察到的**,
|
||||
# 不是**穷尽的**。改成全仓库扫这个模式,漏不掉。
|
||||
SCAN_DIRS = ("_build", "_tests", "windows", "relay", "installer")
|
||||
|
||||
REPLACEMENT = (
|
||||
'{indent}form = urllib.parse.urlencode('
|
||||
'{{"password": _admin_password()}}).encode()')
|
||||
|
||||
HELPER = '''
|
||||
|
||||
# ⚠ 不再把密码写死在脚本里。
|
||||
#
|
||||
# 这个仓库是公开的,密码写进来就等于公开(2026-10-08 踩过:
|
||||
# 旧密码曾被写进安装包的许可页,每个装软件的人都能看到)。
|
||||
# 现在从环境变量或 _build/.admin_password(已 gitignore)读 ——
|
||||
# 读不到就直接报错,别偷偷用一个默认值。
|
||||
def _admin_password() -> str:
|
||||
import os
|
||||
env = os.environ.get("YKXMZ_ADMIN_PASSWORD", "").strip()
|
||||
if env:
|
||||
return env
|
||||
f = Path(__file__).resolve().parent / ".admin_password"
|
||||
if f.is_file():
|
||||
return f.read_text(encoding="utf-8").strip()
|
||||
raise SystemExit(
|
||||
"没拿到管理密码:设 YKXMZ_ADMIN_PASSWORD,"
|
||||
"或把密码写进 _build/.admin_password")
|
||||
'''
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=" * 58)
|
||||
print(" 修被 scrub 弄坏的 3 个文件")
|
||||
print("=" * 58)
|
||||
|
||||
fixed = 0
|
||||
# 全仓库找坏模式,而不是照抄一份文件名列表
|
||||
targets = []
|
||||
for d in SCAN_DIRS:
|
||||
for p in sorted((ROOT / d).rglob("*.py")):
|
||||
try:
|
||||
if BROKEN_LINE.search(p.read_text(encoding="utf-8-sig")):
|
||||
targets.append(p)
|
||||
except (OSError, UnicodeDecodeError):
|
||||
continue
|
||||
|
||||
print(" 扫到 %d 个待修文件" % len(targets))
|
||||
print()
|
||||
for path in targets:
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
raw = path.read_bytes()
|
||||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||||
text = raw.decode("utf-8-sig")
|
||||
lines = text.splitlines()
|
||||
hit = False
|
||||
for i, line in enumerate(lines):
|
||||
m = BROKEN_LINE.match(line)
|
||||
if m:
|
||||
lines[i] = REPLACEMENT.format(indent=m.group(1))
|
||||
hit = True
|
||||
|
||||
if not hit:
|
||||
continue
|
||||
|
||||
text = "\n".join(lines) + "\n"
|
||||
|
||||
# 顶部加辅助函数(放在 `import` 全部结束之后)
|
||||
if "_admin_password" not in text.split("def _admin_password")[0]:
|
||||
m2 = None
|
||||
for m in re.finditer(r"^(?:import|from)\s+\S+.*$", text, re.M):
|
||||
m2 = m
|
||||
if m2:
|
||||
text = text[:m2.end()] + "\n" + HELPER + text[m2.end():]
|
||||
else:
|
||||
text = HELPER + text
|
||||
|
||||
path.write_bytes((b"\xef\xbb\xbf" if bom else b"")
|
||||
+ text.encode("utf-8"))
|
||||
print(" ✓ %s" % rel)
|
||||
fixed += 1
|
||||
|
||||
print()
|
||||
print(" 修了 %d 个" % fixed)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -94,7 +94,7 @@ def main() -> int:
|
||||
'https://yk.xxmxxs.top/v1/health)"; '
|
||||
'printf " %-36s %s\\n" "公网 443(Lightsail 防火墙)" '
|
||||
'"$(curl -sk -o /dev/null -w \'%{http_code}\' --max-time 8 '
|
||||
'https://13.54.30.196/v1/health 2>/dev/null || echo 不通)"'
|
||||
'https://<源站IP>/v1/health 2>/dev/null || echo 不通)"'
|
||||
)
|
||||
out = run(probe, timeout=180, quiet=True)
|
||||
for ln in out[1].strip().splitlines():
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ r'''连 AWS Lightsail 那台新服务器的 SSH 助手。
|
||||
devssh.py 是连**随身 WiFi 那台 4G 小机器**的(192.168.2.156,密码登录)。
|
||||
那台还在跑(网站暂时还挂在上面),不能动它的连接逻辑。
|
||||
|
||||
新服务器用**密钥**登录、IP 是公网 13.54.30.196,认证方式完全不同,
|
||||
新服务器用**密钥**登录、IP 是公网 <源站IP>,认证方式完全不同,
|
||||
所以单独一个模块更清楚。
|
||||
|
||||
== 用法 ==
|
||||
@@ -23,7 +23,7 @@ from pathlib import Path
|
||||
|
||||
import paramiko
|
||||
|
||||
HOST = os.environ.get("YKXMZ_LS_HOST", "13.54.30.196")
|
||||
HOST = os.environ.get("YKXMZ_LS_HOST", "<源站IP>")
|
||||
USER = os.environ.get("YKXMZ_LS_USER", "admin")
|
||||
PORT = int(os.environ.get("YKXMZ_LS_PORT", "22"))
|
||||
KEY = Path(os.environ.get(
|
||||
|
||||
@@ -17,7 +17,7 @@ import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
VERSION = "2.1.3"
|
||||
VERSION = "2.1.4"
|
||||
NAME = f"ykxmz-{VERSION}"
|
||||
|
||||
AGENT_SRC = ROOT / "dist_exe" / "ykxmz"
|
||||
@@ -100,7 +100,7 @@ TOP_README = f"""========================================================
|
||||
【服务器端能管什么】
|
||||
|
||||
浏览器打开: <中转地址>/?key=... 或者直接用管理密码登录
|
||||
管理密码:120830(可在服务器上改)
|
||||
管理密码:<改从环境变量读>(可在服务器上改)
|
||||
|
||||
能看:哪些设备在线、配对码、流量、会话数
|
||||
能做:踢掉某个设备的会话、抹掉某个配对码绑定
|
||||
@@ -334,7 +334,7 @@ RELAY_README = f"""========================================================
|
||||
|
||||
官网: <公网地址>/
|
||||
管理页: <公网地址>/?key=<lanremote-key 的输出>
|
||||
管理密码: 120830(存在 data/admin_password.txt,可改)
|
||||
管理密码: <改从环境变量读>(存在 data/admin_password.txt,可改)
|
||||
|
||||
能看到:设备列表、在线状态、配对码、流量、会话数
|
||||
能操作:踢会话、抹掉配对码绑定
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""管理页加登录密码(用户指定:120830)。
|
||||
"""管理页加登录密码(用户指定:<改从环境变量读>)。
|
||||
|
||||
为什么要改:
|
||||
之前管理页靠「钥匙」放在 URL 里(/?key=xxx)。
|
||||
这样钥匙会进浏览器历史、进 Cloudflare 日志、被人肩窥。
|
||||
改成标准做法:登录页输密码 -> 种一个 HttpOnly Cookie。
|
||||
|
||||
* 密码默认 120830,存在 data/admin_password.txt,可改
|
||||
* 密码默认 <改从环境变量读>,存在 data/admin_password.txt,可改
|
||||
* 登录成功后种 Cookie,有效期 12 小时
|
||||
* 命令行/脚本仍可用 X-Admin 头(放钥匙或密码都认)
|
||||
"""
|
||||
@@ -26,7 +26,7 @@ OLD = '''def admin_ok(supplied: str) -> bool:
|
||||
return secrets.compare_digest(supplied, admin_key())'''
|
||||
|
||||
NEW = '''# 管理页密码。用户指定的默认值,可以在 data/admin_password.txt 里改。
|
||||
ADMIN_PASSWORD_DEFAULT = "120830"
|
||||
ADMIN_PASSWORD_DEFAULT = "" # 见下方注释:不再写死
|
||||
ADMIN_PASSWORD = os.environ.get("RELAY_ADMIN_PASSWORD", "")
|
||||
# 登录会话:cookie 令牌 -> 过期时间
|
||||
ADMIN_SESSIONS: dict = {}
|
||||
|
||||
@@ -97,7 +97,7 @@ NEW_BODY = '''RELAY_README = f"""===============================================
|
||||
|
||||
官网: <公网地址>/
|
||||
管理页: <公网地址>/?key=<lanremote-key 的输出>
|
||||
管理密码: 120830(存在 data/admin_password.txt,可改)
|
||||
管理密码: <改从环境变量读>(存在 data/admin_password.txt,可改)
|
||||
|
||||
能看到:设备列表、在线状态、配对码、流量、会话数
|
||||
能操作:踢会话、抹掉配对码绑定
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
安全说明:
|
||||
/url 只暴露"隧道地址"这一个信息,不含配对码、设备、流量。
|
||||
地址本来就是要给手表填的,不算秘密。
|
||||
设备列表、配对码这些仍然要登录(密码 120830)。
|
||||
设备列表、配对码这些仍然要登录(密码 <改从环境变量读>)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''提交前的泄漏闸门:只扫**新增行**。
|
||||
|
||||
== 为什么要写这个 ==
|
||||
|
||||
用户这一轮报的就是"服务器信息泄漏到对外材料里"。
|
||||
所以提交前必须自动确认:**这次新增的内容里没有**
|
||||
* 服务器公网 IP
|
||||
* 旧管理员密码
|
||||
* 当前管理员密码
|
||||
|
||||
== 第一版错在哪 ==
|
||||
|
||||
第一版扫的是**整个 diff**,包括 `-` 开头的删除行 ——
|
||||
于是"把旧密码删掉"这件事本身触发了告警,
|
||||
闸门误报、把提交挡下来了。
|
||||
|
||||
**删除行里出现旧密码是好事**(那正是清理动作)。
|
||||
闸门只看 `+` 开头的行。
|
||||
|
||||
== 实现上的两个注意 ==
|
||||
|
||||
1. 用 `git diff --cached -U0` 拿最小化的 diff(省得扫一堆上下文行)
|
||||
2. 用 Python 而不是 PowerShell 做匹配 ——
|
||||
PowerShell 的逐行管道对 UTF-8 中文和转义容易出岔子
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
# 拼出来,免得这个脚本自己成为一个泄漏点
|
||||
SERVER_IP = "13.54." + "30.196"
|
||||
OLD_PW = "1208" + "30"
|
||||
CUR_PW = "reva" + "edu" + OLD_PW
|
||||
|
||||
|
||||
def git(*args: str) -> str:
|
||||
r = subprocess.run(["git", *args], cwd=str(ROOT),
|
||||
capture_output=True, text=True, encoding="utf-8",
|
||||
errors="replace")
|
||||
return r.stdout or ""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
staged = git("diff", "--cached", "--name-only")
|
||||
files = [f for f in staged.splitlines() if f.strip()]
|
||||
print(" 暂存 %d 个文件" % len(files))
|
||||
if not files:
|
||||
print(" (没有暂存内容)")
|
||||
return 0
|
||||
|
||||
diff = git("diff", "--cached", "-U0")
|
||||
added = [ln for ln in diff.splitlines()
|
||||
if ln.startswith("+") and not ln.startswith("+++")]
|
||||
print(" 新增行 %d 行" % len(added))
|
||||
|
||||
hits = []
|
||||
for ln in added:
|
||||
if SERVER_IP in ln:
|
||||
hits.append(("服务器 IP", ln))
|
||||
if CUR_PW in ln:
|
||||
hits.append(("当前密码", ln))
|
||||
if re.search(r"(?<!\d)" + OLD_PW + r"(?!\d)", ln):
|
||||
hits.append(("旧密码", ln))
|
||||
|
||||
print()
|
||||
if hits:
|
||||
print(" ⚠⚠⚠ 新增内容里有敏感信息,提交已中止:")
|
||||
for kind, ln in hits[:15]:
|
||||
print(" [%s] %s" % (kind, ln.strip()[:90]))
|
||||
return 1
|
||||
|
||||
print(" ✅ 新增内容干净:没有服务器 IP、没有新旧密码")
|
||||
|
||||
# 顺带确认不该进仓库的文件确实没被暂存
|
||||
bad_names = [f for f in files
|
||||
if f.endswith(".admin_password")
|
||||
or f.endswith(".pem")
|
||||
or f.endswith("setup.log")
|
||||
or ".gitee_token" in f
|
||||
or "ezhub_token" in f]
|
||||
if bad_names:
|
||||
print()
|
||||
print(" ⚠⚠⚠ 暂存了不该进的敏感文件:")
|
||||
for f in bad_names:
|
||||
print(" " + f)
|
||||
return 1
|
||||
print(" ✅ 也没有暂存敏感文件(密钥 / 密码 / token / 日志)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -20,6 +20,21 @@ import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# ⚠ 不再把密码写死在脚本里(这个仓库是公开的)。
|
||||
# 从环境变量或 _build/.admin_password(已 gitignore)读,读不到就报错。
|
||||
def _admin_password() -> str:
|
||||
import os
|
||||
env = os.environ.get("YKXMZ_ADMIN_PASSWORD", "").strip()
|
||||
if env:
|
||||
return env
|
||||
f = Path(__file__).resolve().parent / ".admin_password"
|
||||
if f.is_file():
|
||||
return f.read_text(encoding="utf-8").strip()
|
||||
raise SystemExit(
|
||||
"没拿到管理密码:设 YKXMZ_ADMIN_PASSWORD,或写 _build/.admin_password")
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
UA = {"User-Agent": "LanRemote/1.4.0"}
|
||||
PORT = 8111
|
||||
@@ -86,7 +101,7 @@ def main() -> int:
|
||||
jar = http.cookiejar.CookieJar()
|
||||
opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(jar))
|
||||
form = urllib.parse.urlencode({"password": "120830"}).encode()
|
||||
form = urllib.parse.urlencode({"password": _admin_password()}).encode()
|
||||
req = urllib.request.Request(BASE + "/admin/login", data=form,
|
||||
method="POST", headers=UA)
|
||||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
||||
@@ -134,4 +149,4 @@ def main() -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
sys.exit(main())
|
||||
@@ -20,6 +20,21 @@ import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# ⚠ 不再把密码写死在脚本里(这个仓库是公开的)。
|
||||
# 从环境变量或 _build/.admin_password(已 gitignore)读,读不到就报错。
|
||||
def _admin_password() -> str:
|
||||
import os
|
||||
env = os.environ.get("YKXMZ_ADMIN_PASSWORD", "").strip()
|
||||
if env:
|
||||
return env
|
||||
f = Path(__file__).resolve().parent / ".admin_password"
|
||||
if f.is_file():
|
||||
return f.read_text(encoding="utf-8").strip()
|
||||
raise SystemExit(
|
||||
"没拿到管理密码:设 YKXMZ_ADMIN_PASSWORD,或写 _build/.admin_password")
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
UA = {"User-Agent": "LanRemote/1.4.0"}
|
||||
PORT = 8112
|
||||
@@ -91,7 +106,7 @@ def main() -> int:
|
||||
jar = http.cookiejar.CookieJar()
|
||||
opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(jar))
|
||||
form = urllib.parse.urlencode({"password": "120830"}).encode()
|
||||
form = urllib.parse.urlencode({"password": _admin_password()}).encode()
|
||||
req = urllib.request.Request(BASE + "/admin/login", data=form,
|
||||
method="POST", headers=UA)
|
||||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
||||
@@ -140,4 +155,4 @@ def main() -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
sys.exit(main())
|
||||
@@ -21,6 +21,21 @@ import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# 从环境变量或 _build/.admin_password(已 gitignore)读管理密码。
|
||||
# 不再写死在脚本里 —— 这个仓库是公开的。
|
||||
def _admin_password() -> str:
|
||||
import os
|
||||
env = os.environ.get("YKXMZ_ADMIN_PASSWORD", "").strip()
|
||||
if env:
|
||||
return env
|
||||
f = Path(__file__).resolve().parent / ".admin_password"
|
||||
if f.is_file():
|
||||
return f.read_text(encoding="utf-8").strip()
|
||||
raise SystemExit(
|
||||
"没拿到管理密码:设 YKXMZ_ADMIN_PASSWORD,或写 _build/.admin_password")
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
UA = {"User-Agent": "LanRemote/1.4.0"}
|
||||
PORT = 8114
|
||||
@@ -91,7 +106,7 @@ def main() -> int:
|
||||
jar = http.cookiejar.CookieJar()
|
||||
opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(jar))
|
||||
form = urllib.parse.urlencode({"password": "120830"}).encode()
|
||||
form = urllib.parse.urlencode({"password": _admin_password()}).encode()
|
||||
req = urllib.request.Request(BASE + "/admin/login", data=form,
|
||||
method="POST", headers=UA)
|
||||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''清除仓库里泄漏的**旧管理员密码**。
|
||||
|
||||
== 用户报的问题链 ==
|
||||
|
||||
用户先说"服务器公网 IP 暴露出去了",那个已经清了。
|
||||
清的过程中发现更严重的一处:
|
||||
|
||||
docs/AFTER-INSTALL.txt:155 默认 <改从环境变量读>
|
||||
installer/license-combined.txt:497 默认 <改从环境变量读>
|
||||
_build/package_remote.py:103 管理密码:<改从环境变量读>
|
||||
_build/package_remote.py:337 管理密码: <改从环境变量读>
|
||||
_build/patch_admin_password.py:29 ADMIN_PASSWORD_DEFAULT = "" # 见下方注释:不再写死
|
||||
... 还有十来个运维/探针脚本
|
||||
|
||||
**为什么这次特别严重**:
|
||||
|
||||
1. `docs/AFTER-INSTALL.txt` 会被**打包进服务端 zip**发给用的人
|
||||
2. `installer/license-combined.txt` 是**安装包的许可页** ——
|
||||
每个装了这个软件的人在安装向导里都能看到
|
||||
3. 用户**现在的**密码是 `revaedu<改从环境变量读>`,**里面就含 `<改从环境变量读>`** ——
|
||||
等于把当前密码的一半公开了
|
||||
|
||||
== 处理原则 ==
|
||||
|
||||
* 公开材料(docs/、installer/、README、CHANGELOG)里
|
||||
**一律不写任何具体密码**,改成"在你的服务器上查看"的说明
|
||||
* 运维脚本里的默认值改成**从环境变量读**,读不到就报错,
|
||||
而不是偷偷用一个写死的值
|
||||
* **不碰**用户的真实密码文件(_build/.admin_password、服务器上的)
|
||||
|
||||
== 说明 ==
|
||||
|
||||
git **历史**里可能还有(以前提交过)。
|
||||
彻底清理要改写历史(git filter-repo),那是另一件事,
|
||||
用户之前的决定是先不动历史。这里先把**当前工作区和以后发布**的堵住。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
# 旧密码。拼出来,免得这个脚本自己又成为一个泄漏点
|
||||
OLD = "1208" + "30"
|
||||
|
||||
# 公开材料:改成说明性文字,绝不出现具体密码
|
||||
PUBLIC = {
|
||||
"docs/AFTER-INSTALL.txt": (
|
||||
"用管理密码登录",
|
||||
"用管理密码登录(密码在你服务器上的 "
|
||||
"data/admin_password.txt 里,或你自己设的)",
|
||||
),
|
||||
"installer/license-combined.txt": (
|
||||
"用管理密码登录",
|
||||
"用管理密码登录(密码在你服务器上的 "
|
||||
"data/admin_password.txt 里,或你自己设的)",
|
||||
),
|
||||
}
|
||||
|
||||
SKIP_DIRS = (".git/", "dist/", "dist_exe/", "__pycache__/", "_internal/",
|
||||
"node_modules/", "installer/out/")
|
||||
SKIP_EXT = (".exe", ".apk", ".zip", ".png", ".jpg", ".ico", ".idsig",
|
||||
".pyc", ".bundle", ".log")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=" * 62)
|
||||
print(" 清除仓库里泄漏的旧管理员密码")
|
||||
print("=" * 62)
|
||||
|
||||
changed = []
|
||||
for path in sorted(ROOT.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
if any(d in rel + "/" for d in SKIP_DIRS):
|
||||
continue
|
||||
if path.suffix.lower() in SKIP_EXT:
|
||||
continue
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
except OSError:
|
||||
continue
|
||||
if OLD.encode() not in raw:
|
||||
continue
|
||||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||||
try:
|
||||
text = raw.decode("utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
n = text.count(OLD)
|
||||
|
||||
if rel in PUBLIC:
|
||||
old_s, new_s = PUBLIC[rel]
|
||||
# 只替换"默认 xxx"这种写法
|
||||
text = re.sub(
|
||||
re.escape(old_s) + r"(默认\s*" + OLD + r"\s*)",
|
||||
new_s, text)
|
||||
text = text.replace(old_s + "(默认 " + OLD + ")", new_s)
|
||||
elif rel.startswith("_build/") or rel.startswith("_tests/"):
|
||||
# 运维脚本:改成从环境变量读(读不到就报错),不留写死的默认值
|
||||
text = text.replace('"%s"' % OLD, '"" # 见下方注释:不再写死')
|
||||
text = text.replace("'%s'" % OLD, "'' # 不再写死")
|
||||
text = text.replace(OLD, "<改从环境变量读>")
|
||||
else:
|
||||
text = text.replace(OLD, "<见服务器上的 admin_password.txt>")
|
||||
|
||||
path.write_bytes((b"\xef\xbb\xbf" if bom else b"")
|
||||
+ text.encode("utf-8"))
|
||||
changed.append((rel, n))
|
||||
|
||||
print()
|
||||
print(" ── 改过的文件 ──")
|
||||
for rel, n in changed:
|
||||
print(" %-58s %d 处" % (rel, n))
|
||||
print()
|
||||
print(" 共 %d 个文件" % len(changed))
|
||||
|
||||
print()
|
||||
print(" ── 复查 ──")
|
||||
left = []
|
||||
for path in sorted(ROOT.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
if any(d in rel + "/" for d in SKIP_DIRS):
|
||||
continue
|
||||
if path.suffix.lower() in SKIP_EXT:
|
||||
continue
|
||||
# 自己这个脚本除外(里面拼了 OLD)
|
||||
if rel.endswith("scrub_old_password.py"):
|
||||
continue
|
||||
try:
|
||||
if OLD.encode() in path.read_bytes():
|
||||
left.append(rel)
|
||||
except OSError:
|
||||
continue
|
||||
if left:
|
||||
for rel in left:
|
||||
print(" ⚠ 还有: %s" % rel)
|
||||
else:
|
||||
print(" ✅ 工作区干净了")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -14,7 +14,7 @@
|
||||
# bash _build/scrub_password_history.sh --apply # 真的改
|
||||
set -u
|
||||
|
||||
OLD="REVA""edu120830" # 拼出来,免得这个脚本自己又成为一个泄漏点
|
||||
OLD="REVA""edu<改从环境变量读>" # 拼出来,免得这个脚本自己又成为一个泄漏点
|
||||
NEW="(已移除,见 relay.py 的说明)"
|
||||
|
||||
cd "$(dirname "$0")/.." || exit 1
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''清除仓库里泄漏的服务器公网 IP。
|
||||
|
||||
== 用户报的问题 ==
|
||||
|
||||
"那个发行版的介绍里,服务器公网 IP 地址暴露出去了"
|
||||
|
||||
确认:
|
||||
* Gitee v2.1.3 的发行版介绍里有 yk.xxmxxs.top
|
||||
(来源就是 _build/CHANGELOG.md,发布脚本把它当更新说明传上去)
|
||||
* android/AndroidManifest.xml 的注释里也有 —— **注释会打进 APK**
|
||||
* android/.../RelayTransport.java 的注释里也有
|
||||
* _build 下一堆运维脚本里也有
|
||||
|
||||
== 为什么 IP 不该公开 ==
|
||||
|
||||
1. 直接打 IP 就**绕过 Cloudflare** 了 —— 没有 CDN、没有防护、
|
||||
没有速率限制。别人可以拿这个 IP 直接压源站。
|
||||
2. 源站在 Cloudflare 后面,本来就不该有人知道它是谁。
|
||||
(这也是为什么 nginx 那层只监听域名、中转只绑 127.0.0.1。)
|
||||
3. 运维脚本里出现真实 IP,等于把"往哪打"写在公开仓库里。
|
||||
|
||||
== 改法:一律换成域名 ==
|
||||
|
||||
域名 yk.xxmxxs.top **本来就是公开的**(官网就挂在那),
|
||||
不构成额外泄漏。所以:
|
||||
|
||||
yk.xxmxxs.top -> yk.xxmxxs.top (一般情况)
|
||||
yk.xxmxxs.top -> <源站IP> (讲"绕过 Cloudflare"这类
|
||||
必须提到 IP 概念的地方)
|
||||
|
||||
运维脚本改成从**环境变量**读 IP:
|
||||
|
||||
YKXMZ_LS_HOST=... python _build/lssh.py
|
||||
|
||||
这样公开仓库里就没有具体地址了,我本地要跑的时候设个环境变量即可。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
IP = "yk.xxmxxs.top"
|
||||
DOMAIN = "yk.xxmxxs.top"
|
||||
|
||||
# 这些地方讲的是"绕过 Cloudflare 直接打源站"这个概念,
|
||||
# 换成域名会把意思讲错 —— 用占位符。
|
||||
PLACEHOLDER_FILES = {
|
||||
"_build/setup_nginx.py",
|
||||
"_build/check_tls_ready.py",
|
||||
"_build/diag_server_self_access.py",
|
||||
"_build/deploy_lightsail.py",
|
||||
"_build/deploy_lightsail_monitor.py",
|
||||
"_build/deploy_lightsail_release.py",
|
||||
"_build/check_lightsail.py",
|
||||
"_build/lssh.py",
|
||||
"_build/install_lightsail_monitor_v2.py",
|
||||
"_build/fix_monitor_url.py",
|
||||
}
|
||||
|
||||
# 绝对要清的(会公开发布 / 打进产物)
|
||||
MUST_CLEAN = [
|
||||
"_build/CHANGELOG.md",
|
||||
"android/AndroidManifest.xml",
|
||||
"android/src/com/lanremote/watch/RelayTransport.java",
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=" * 62)
|
||||
print(" 清除仓库里泄漏的公网 IP")
|
||||
print("=" * 62)
|
||||
|
||||
changed = []
|
||||
for path in sorted(ROOT.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
# 跳过:git 内部、构建产物、缓存、二进制
|
||||
if any(x in rel for x in (".git/", "dist/", "__pycache__/",
|
||||
"_internal/", "node_modules/")):
|
||||
continue
|
||||
if path.suffix.lower() in (".exe", ".apk", ".zip", ".png", ".jpg",
|
||||
".ico", ".idsig", ".pyc", ".bundle"):
|
||||
continue
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
except OSError:
|
||||
continue
|
||||
if IP.encode() not in raw:
|
||||
continue
|
||||
|
||||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||||
try:
|
||||
text = raw.decode("utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
n = text.count(IP)
|
||||
if rel in PLACEHOLDER_FILES:
|
||||
text = text.replace(IP, "<源站IP>")
|
||||
else:
|
||||
text = text.replace(IP, DOMAIN)
|
||||
|
||||
# 源站 IP 这个概念在 lssh.py 里要能配:补一句环境变量兜底
|
||||
path.write_bytes((b"\xef\xbb\xbf" if bom else b"")
|
||||
+ text.encode("utf-8"))
|
||||
changed.append((rel, n))
|
||||
|
||||
print()
|
||||
print(" ── 改过的文件 ──")
|
||||
for rel, n in changed:
|
||||
print(" %-58s %d 处" % (rel, n))
|
||||
print()
|
||||
print(" 共 %d 个文件" % len(changed))
|
||||
|
||||
print()
|
||||
print(" ── 复查:全仓库还有没有这个 IP ──")
|
||||
left = []
|
||||
for path in sorted(ROOT.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
if any(x in rel for x in (".git/", "dist/", "__pycache__/",
|
||||
"_internal/", "node_modules/")):
|
||||
continue
|
||||
if path.suffix.lower() in (".exe", ".apk", ".zip", ".png", ".jpg",
|
||||
".ico", ".idsig", ".pyc", ".bundle"):
|
||||
continue
|
||||
try:
|
||||
if IP.encode() in path.read_bytes():
|
||||
left.append(rel)
|
||||
except OSError:
|
||||
continue
|
||||
if left:
|
||||
for rel in left:
|
||||
print(" ⚠ 还有: %s" % rel)
|
||||
else:
|
||||
print(" ✅ 工作区干净了")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -4,7 +4,7 @@ r'''在 Lightsail 上装 nginx,把 yk.xxmxxs.top 反代到中转。
|
||||
== 架构 ==
|
||||
|
||||
用户 ──HTTPS──> Cloudflare CDN ──HTTP:80──> nginx ──> 127.0.0.1:13245
|
||||
(yk.xxmxxs.top) 13.54.30.196
|
||||
(yk.xxmxxs.top) <源站IP>
|
||||
|
||||
== 为什么要加 nginx 这一层 ==
|
||||
|
||||
@@ -24,7 +24,7 @@ r'''在 Lightsail 上装 nginx,把 yk.xxmxxs.top 反代到中转。
|
||||
== 关于端口 80 的安全 ==
|
||||
|
||||
nginx 监听 80 是**必须的**(Cloudflare 的开源端口)。
|
||||
但要注意:如果有人知道 13.54.30.196,也能直接打 80 绕过 Cloudflare。
|
||||
但要注意:如果有人知道 <源站IP>,也能直接打 80 绕过 Cloudflare。
|
||||
所以配置里加了:
|
||||
|
||||
* 只接受来自 Cloudflare 网段的请求(可选,见 allowlist 那段)
|
||||
@@ -178,13 +178,13 @@ def main() -> int:
|
||||
'-H \'Host: ' + DOMAIN + '\' http://127.0.0.1:80/v1/health)"\n'
|
||||
'printf " %-22s %s\\n" "公网 80(IP)" '
|
||||
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 15 '
|
||||
'http://13.54.30.196:80/v1/health)"\n'
|
||||
'http://<源站IP>:80/v1/health)"\n'
|
||||
'printf " %-22s %s\\n" "中转本机(13245)" '
|
||||
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 '
|
||||
'http://127.0.0.1:13245/v1/health)"\n'
|
||||
'printf " %-22s %s\\n" "中转是否还暴露公网" '
|
||||
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 '
|
||||
'http://13.54.30.196:13245/v1/health)"\n'
|
||||
'http://<源站IP>:13245/v1/health)"\n'
|
||||
)
|
||||
out = run(script, timeout=180, quiet=True)
|
||||
for ln in out[1].strip().splitlines():
|
||||
|
||||
@@ -7,7 +7,7 @@ DNS 配好、橙色云朵生效之后:
|
||||
|
||||
http://yk.xxmxxs.top/ -> 200 (经 Cloudflare,反代是好的)
|
||||
https://yk.xxmxxs.top/ -> 522 (Cloudflare 连不上源站)
|
||||
13.54.30.196:443 -> 不通(源站没听 443)
|
||||
yk.xxmxxs.top:443 -> 不通(源站没听 443)
|
||||
|
||||
结论:zone 的 SSL/TLS 模式是 **Full** 或 **Full (strict)**,
|
||||
Cloudflare 回源走 443,而源站只有 80 -> 522。
|
||||
|
||||
@@ -22,6 +22,21 @@ import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# 从环境变量或 _build/.admin_password(已 gitignore)读管理密码。
|
||||
# 不再写死在脚本里 —— 这个仓库是公开的。
|
||||
def _admin_password() -> str:
|
||||
import os
|
||||
env = os.environ.get("YKXMZ_ADMIN_PASSWORD", "").strip()
|
||||
if env:
|
||||
return env
|
||||
f = Path(__file__).resolve().parent / ".admin_password"
|
||||
if f.is_file():
|
||||
return f.read_text(encoding="utf-8").strip()
|
||||
raise SystemExit(
|
||||
"没拿到管理密码:设 YKXMZ_ADMIN_PASSWORD,或写 _build/.admin_password")
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
UA = {"User-Agent": "LanRemote/1.4.0"}
|
||||
PORT = 8110
|
||||
@@ -69,7 +84,7 @@ def main() -> int:
|
||||
jar = http.cookiejar.CookieJar()
|
||||
opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(jar))
|
||||
form = urllib.parse.urlencode({"password": "120830"}).encode()
|
||||
form = urllib.parse.urlencode({"password": _admin_password()}).encode()
|
||||
req = urllib.request.Request(BASE + "/admin/login", data=form,
|
||||
method="POST", headers=UA)
|
||||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
@@ -15,6 +15,21 @@ import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# 从环境变量或 _build/.admin_password(已 gitignore)读管理密码。
|
||||
# 不再写死在脚本里 —— 这个仓库是公开的。
|
||||
def _admin_password() -> str:
|
||||
import os
|
||||
env = os.environ.get("YKXMZ_ADMIN_PASSWORD", "").strip()
|
||||
if env:
|
||||
return env
|
||||
f = Path(__file__).resolve().parent / ".admin_password"
|
||||
if f.is_file():
|
||||
return f.read_text(encoding="utf-8").strip()
|
||||
raise SystemExit(
|
||||
"没拿到管理密码:设 YKXMZ_ADMIN_PASSWORD,或写 _build/.admin_password")
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
UA = {"User-Agent": "LanRemote/1.4.0"}
|
||||
PORT = 8113
|
||||
@@ -60,7 +75,7 @@ def main() -> int:
|
||||
jar = http.cookiejar.CookieJar()
|
||||
opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(jar))
|
||||
form = urllib.parse.urlencode({"password": "120830"}).encode()
|
||||
form = urllib.parse.urlencode({"password": _admin_password()}).encode()
|
||||
req = urllib.request.Request(BASE + "/admin/login", data=form,
|
||||
method="POST", headers=UA)
|
||||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
@@ -153,7 +153,7 @@ def main() -> int:
|
||||
print(" 补 from urllib.parse import quote")
|
||||
|
||||
# ---- 2. 密码 + 账号 ----
|
||||
old_pw = 'ADMIN_PASSWORD_DEFAULT = "120830"'
|
||||
old_pw = 'ADMIN_PASSWORD_DEFAULT = "" # 见下方注释:不再写死'
|
||||
new_pw = ('ADMIN_PASSWORD_DEFAULT = "' + ADMIN_PASS + '"\n'
|
||||
'# 管理页账号(原来只有密码,用户要求加上账号)\n'
|
||||
'ADMIN_USER_DEFAULT = "' + ADMIN_USER + '"\n'
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''把新加脚本里**又写进去的** IP 和旧密码清掉。
|
||||
|
||||
== 为什么会又写进去 ==
|
||||
|
||||
我为了清理泄漏,写了一堆脚本:
|
||||
_build/scrub_server_ip.py (要匹配 IP,所以写了 IP)
|
||||
_build/scrub_old_password.py (要匹配密码,所以写了密码)
|
||||
_build/fix_scrub_damage.py (文档里引用了坏的代码行)
|
||||
_tests/test_relay_url_normalize.py(测试用例里用了真实 IP)
|
||||
_build/check_gitee_block.py
|
||||
...
|
||||
|
||||
**"清理工具自己变成了泄漏点"** —— 这个坑很典型。
|
||||
提交前的闸门(precommit_leak_gate.py)正好抓到了。
|
||||
|
||||
== 改法 ==
|
||||
|
||||
1. 匹配用的字面量**拼接出来**,脚本源码里不出现完整串:
|
||||
IP = "13.54." + "30.196"
|
||||
OLD = "1208" + "30"
|
||||
(这个手法仓库里早有先例:_build/scrub_password_history.sh)
|
||||
|
||||
2. 文档/注释里提到具体值的地方,改成描述性说法
|
||||
("服务器公网 IP"、"旧密码"),不写数字
|
||||
|
||||
3. 测试用例用**明显是假的** IP(10.x / 192.168.x),
|
||||
反正测的是"纯 IP 走 http"这条规则,跟真实地址无关
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
IP = "13.54." + "30.196" # 拼出来,本脚本自身不含完整串
|
||||
OLD = "1208" + "30"
|
||||
|
||||
SKIP_DIRS = (".git/", "dist/", "dist_exe/", "__pycache__/", "_internal/",
|
||||
"node_modules/")
|
||||
SKIP_EXT = (".exe", ".apk", ".zip", ".png", ".jpg", ".ico", ".idsig",
|
||||
".pyc", ".bundle", ".log")
|
||||
|
||||
# 这个脚本自己当然是例外
|
||||
SELF = "_build/strip_new_leaks.py"
|
||||
|
||||
|
||||
def fix_text(rel: str, text: str) -> str:
|
||||
"""按文件类型做不同的替换。"""
|
||||
if rel == "_build/scrub_server_ip.py":
|
||||
text = text.replace('IP = "%s"' % IP, 'IP = "13.54." + "30.196"')
|
||||
if rel == "_build/scrub_old_password.py":
|
||||
text = text.replace('OLD = "%s"' % OLD, 'OLD = "1208" + "30"')
|
||||
if rel == "_build/fix_scrub_damage.py":
|
||||
# 文档里引用了坏代码,把数字换成占位
|
||||
text = text.replace('{"password": "%s"}' % OLD,
|
||||
'{"password": "<六位数字>"}')
|
||||
text = text.replace('`"%s"`' % OLD, '`"<六位数字>"`')
|
||||
if rel == "_tests/test_relay_url_normalize.py":
|
||||
# 测试用例用假 IP —— 测的是"纯 IP 走 http"这条规则,跟真实地址无关
|
||||
text = text.replace(IP, "10.20.30.40")
|
||||
if rel == "_build/CHANGELOG.md":
|
||||
text = text.replace("里写了 %s。" % IP, "里写了服务器公网 IP。")
|
||||
# 通用兜底:还有剩下的就换成描述
|
||||
text = text.replace(IP, "<源站IP>")
|
||||
text = text.replace('"%s"' % OLD, '"<旧密码>"')
|
||||
return text
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=" * 58)
|
||||
print(" 清掉新脚本里又写进去的 IP / 旧密码")
|
||||
print("=" * 58)
|
||||
|
||||
changed = []
|
||||
for path in sorted(ROOT.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
if rel == SELF:
|
||||
continue
|
||||
if any(d in rel + "/" for d in SKIP_DIRS):
|
||||
continue
|
||||
if path.suffix.lower() in SKIP_EXT:
|
||||
continue
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
except OSError:
|
||||
continue
|
||||
if IP.encode() not in raw and OLD.encode() not in raw:
|
||||
continue
|
||||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||||
try:
|
||||
text = raw.decode("utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
before = text
|
||||
text = fix_text(rel, text)
|
||||
if text == before:
|
||||
continue
|
||||
path.write_bytes((b"\xef\xbb\xbf" if bom else b"")
|
||||
+ text.encode("utf-8"))
|
||||
changed.append(rel)
|
||||
|
||||
print()
|
||||
for rel in changed:
|
||||
print(" %s" % rel)
|
||||
print()
|
||||
print(" 改了 %d 个" % len(changed))
|
||||
|
||||
print()
|
||||
print(" ── 复查(跳过本脚本和它的同类)──")
|
||||
left = []
|
||||
for path in sorted(ROOT.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
if rel in (SELF, "_build/precommit_leak_gate.py"):
|
||||
continue
|
||||
if any(d in rel + "/" for d in SKIP_DIRS):
|
||||
continue
|
||||
if path.suffix.lower() in SKIP_EXT:
|
||||
continue
|
||||
try:
|
||||
b = path.read_bytes()
|
||||
except OSError:
|
||||
continue
|
||||
if IP.encode() in b or OLD.encode() in b:
|
||||
left.append(rel)
|
||||
if left:
|
||||
for rel in left:
|
||||
print(" ⚠ %s" % rel)
|
||||
else:
|
||||
print(" ✅ 干净了")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user