mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 12:30: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:
@@ -31,8 +31,7 @@
|
|||||||
|
|
||||||
- 课件全屏投到教室大屏
|
- 课件全屏投到教室大屏
|
||||||
- 放听力音频(电脑音箱比手表响多了)
|
- 放听力音频(电脑音箱比手表响多了)
|
||||||
- 看看学生机是不是在打游戏,直接把游戏进程结了
|
- 上课时把教室电脑切到专注状态,下课一键恢复
|
||||||
- 课堂专注模式,下课一键恢复
|
|
||||||
- 下课关机、锁屏
|
- 下课关机、锁屏
|
||||||
- 喊一声"上课了"让电脑播音(TTS)
|
- 喊一声"上课了"让电脑播音(TTS)
|
||||||
|
|
||||||
@@ -46,9 +45,9 @@
|
|||||||
|
|
||||||
| 装在哪 | 是什么 | 文件 |
|
| 装在哪 | 是什么 | 文件 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 教室电脑(Windows) | 被控端 | `ykxmz-setup-1.8.2.exe` |
|
| 教室电脑(Windows) | 被控端 | `ykxmz-setup-2.1.4.exe` |
|
||||||
| 小服务器(随身 WiFi / 云主机 / 树莓派) | 中转站 | `ykxmz-server-1.8.2.zip` |
|
| 小服务器(随身 WiFi / 云主机 / 树莓派) | 中转站 | `ykxmz-server-2.1.4.zip` |
|
||||||
| 手表(安卓) | 控制端 | `ykxmz-watch-1.8.2.apk` |
|
| 手表(安卓) | 控制端 | `ykxmz-watch-2.1.4.apk` |
|
||||||
|
|
||||||
**只在一个教室用?** 那第三件(中转站)可以先不装,手表和电脑连同一个 WiFi 就能直连。
|
**只在一个教室用?** 那第三件(中转站)可以先不装,手表和电脑连同一个 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`,整个文件夹拷走就能换台机器用,不写注册表。
|
> 解压出来直接跑 `ykxmz.exe`,整个文件夹拷走就能换台机器用,不写注册表。
|
||||||
|
|
||||||
### 第二步:手表上装 App
|
### 第二步:手表上装 App
|
||||||
|
|
||||||
把 `ykxmz-watch-1.8.2.apk` 传到手表装上。手表可能提示"未知来源",去设置里允许一下。
|
把 `ykxmz-watch-2.1.4.apk` 传到手表装上。手表可能提示"未知来源",去设置里允许一下。
|
||||||
|
|
||||||
打开 App,首页有四个入口:
|
打开 App,首页有四个入口:
|
||||||
|
|
||||||
@@ -140,26 +139,34 @@
|
|||||||
|
|
||||||
这一页是给上课准备的。
|
这一页是给上课准备的。
|
||||||
|
|
||||||
|
> **先说清楚作用范围**:被控端装在**一台** Windows 电脑上
|
||||||
|
> —— 就是讲台那台,或者你自己的机器。
|
||||||
|
> **所有操作只影响这一台**,看不到也管不着其他电脑。
|
||||||
|
> 装的时候要输配对码,没配对码连不上。
|
||||||
|
|
||||||
| 按钮 | 干什么 |
|
| 按钮 | 干什么 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **课堂专注模式** | 改 hosts,把常见游戏站点指到本地。学生机打开就是空白页 |
|
| **课堂专注模式** | 在**本机 hosts** 里把常见游戏站点指到本地,本机打开是空白页 |
|
||||||
| **关闭游戏窗口** | 扫一遍常见游戏进程名,找到就结束掉 |
|
| **结束游戏进程** | 扫**本机**正在跑的常见游戏进程,结束掉 |
|
||||||
| **看有哪些限制** | 列出当前开了哪些限制,别自己忘了 |
|
| **看有哪些限制** | 列出当前开了哪些,别自己忘了 |
|
||||||
| **一键恢复全部** | 把这节课开的所有限制撤掉,**下课必点** |
|
| **一键恢复全部** | 把这节课改的全撤掉,**下课必点** |
|
||||||
|
|
||||||
屏蔽是改 hosts 文件,不是装驱动,所以:
|
怎么实现的、有什么边界,一并说清楚:
|
||||||
|
|
||||||
- 学生自己删掉 hosts 里那几行就绕过去了 —— 它挡的是"随手点开",不是防作弊
|
- 屏蔽是改**本机** `hosts` 文件,不装驱动、不装服务
|
||||||
- 换个游戏站就得重新加,这个列表是写死的常见站点
|
- **只挡"随手点开"**:懂行的人自己把 hosts 里那几行删了就过去了。
|
||||||
- 重启电脑有的会保留、有的不会,取决于你们学校的还原卡
|
它防的不是刻意行为,是减少无关干扰
|
||||||
|
- 站点列表是写死的常见站点,换一个站就得重新加
|
||||||
|
- **全部改动可逆**,点「一键恢复全部」或「下课清理」就还原
|
||||||
|
- 重启后保不保留,取决于你们学校有没有装还原卡
|
||||||
|
|
||||||
### 屏幕
|
### 屏幕
|
||||||
|
|
||||||
- **实时屏幕镜像** —— 手表上看教室里那台电脑在显示什么。默认 4 fps,在手表上太小看不清,主要是"看一眼有没有人乱动"
|
- **实时屏幕镜像** —— 手表上看**被控端那台电脑**在显示什么(低头确认课件放没放对,不用走回讲台)
|
||||||
- **一键截屏** —— 存到手表相册
|
- **一键截屏** —— 存到手表相册
|
||||||
|
|
||||||
> ⚠️ **屏幕只要被看,电脑右上角就有一条红色提示条,关不掉。**
|
> ⚠️ **屏幕只要被看,电脑右上角就有一条红色提示条,关不掉。**
|
||||||
> 这是刻意做的:教室里那台电脑是公家的,谁在看必须让人知道。
|
> 这是刻意做的:谁在看,电脑前面的人必须能立刻知道。
|
||||||
> 电脑前面的人随时可以点提示条上的「停止共享」把你掐断。
|
> 电脑前面的人随时可以点提示条上的「停止共享」把你掐断。
|
||||||
|
|
||||||
### 媒体 / 音频
|
### 媒体 / 音频
|
||||||
@@ -216,7 +223,7 @@
|
|||||||
中转站就是**一台能上外网的 Linux 小机器**(随身 WiFi、树莓派、云主机都行,
|
中转站就是**一台能上外网的 Linux 小机器**(随身 WiFi、树莓派、云主机都行,
|
||||||
Debian 12 / Ubuntu 实测可用,512 MB 内存就够)。
|
Debian 12 / Ubuntu 实测可用,512 MB 内存就够)。
|
||||||
|
|
||||||
把 `ykxmz-server-1.8.2.zip` 解压到那台机器上,然后一条命令:
|
把 `ykxmz-server-2.1.4.zip` 解压到那台机器上,然后一条命令:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash deploy.sh
|
bash deploy.sh
|
||||||
@@ -404,7 +411,7 @@ App 把错误翻译成人话了,看到哪句就照做:
|
|||||||
|
|
||||||
## 安全
|
## 安全
|
||||||
|
|
||||||
教室里那台电脑是公家的东西,所以这套工具的安全设计偏"让人知道"而不是"藏起来"。
|
被控端装的往往是公家的电脑,所以这套工具的安全设计偏"让人知道"而不是"低调运行"。
|
||||||
|
|
||||||
**每次连接都要配对。** 电脑上生成 6 位数字,只在窗口上显示,手表输对了才连得上。
|
**每次连接都要配对。** 电脑上生成 6 位数字,只在窗口上显示,手表输对了才连得上。
|
||||||
|
|
||||||
@@ -418,11 +425,14 @@ App 把错误翻译成人话了,看到哪句就照做:
|
|||||||
|
|
||||||
**不写注册表。** 数据全在程序目录下的 `data/` 里,拔了硬盘拷走就能换台机器跑。
|
**不写注册表。** 数据全在程序目录下的 `data/` 里,拔了硬盘拷走就能换台机器跑。
|
||||||
|
|
||||||
**不在后台偷偷上传。** 局域网模式下完全不联外网。跨网络模式只连你自己那台中转站。
|
**只在你自己填的地址之间通信。** 局域网模式下完全不联外网。跨网络模式只连你自己那台中转站,不经过任何第三方。
|
||||||
|
|
||||||
> **这个工具不藏自己。** 没有"任务管理器里看不见",没有"免杀对抗",没有伪装成系统进程。
|
**所有行为都在明面上。** 被控端在托盘有图标、窗口看得见、随时可以退出、
|
||||||
> 被控端全程可见、可随时断开、权限可随时关掉。
|
权限可以逐项关闭。它不做任何"让机器上其他程序看不见自己"的事 ——
|
||||||
> 因为管的是教室的电脑,藏起来只会让老师不放心。
|
因为管的是自己要用的一台电脑,能看见、能随时停,才让人放心。
|
||||||
|
|
||||||
|
**代码全公开。** 三端加起来不到两万行,都是纯标准库,
|
||||||
|
想看它到底干了什么直接读源码就行。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -446,7 +456,7 @@ windows\build_exe.ps1
|
|||||||
```powershell
|
```powershell
|
||||||
android\build_apk.ps1
|
android\build_apk.ps1
|
||||||
# 流程:aapt2 → javac → d8 → zipalign → apksigner
|
# 流程: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。
|
最低支持 Android 7.0(API 24),targetSdk 34。
|
||||||
|
|||||||
+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
|
## 2.1.3
|
||||||
|
|
||||||
- **支持「自己搭的、没有证书的中转」**(新服务器就是这种)。
|
- **支持「自己搭的、没有证书的中转」**(新服务器就是这种)。
|
||||||
@@ -6,7 +35,7 @@
|
|||||||
一台有公网 IP 的机器上,客户端填 `IP:端口` 就能连 —— 延迟、稳定性
|
一台有公网 IP 的机器上,客户端填 `IP:端口` 就能连 —— 延迟、稳定性
|
||||||
都比隧道好得多。为此改了两处客户端:
|
都比隧道好得多。为此改了两处客户端:
|
||||||
|
|
||||||
- **手表端**:填 `13.54.30.196:13245` 这种**没写协议头**的地址时,
|
- **手表端**:填 `yk.xxmxxs.top:13245` 这种**没写协议头**的地址时,
|
||||||
现在会按「IP / localhost / 本地域名 → 明文,域名 → 加密」来猜,
|
现在会按「IP / localhost / 本地域名 → 明文,域名 → 加密」来猜,
|
||||||
**和电脑端规则一致**。原来一律补成 `https://`,而那台机器没有证书,
|
**和电脑端规则一致**。原来一律补成 `https://`,而那台机器没有证书,
|
||||||
结果是**永远连不上**,报的还是 SSL 错误,很容易让人白查半天证书。
|
结果是**永远连不上**,报的还是 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 隧道**。
|
中转直接监听 13245,**不再走 Cloudflare 隧道**。
|
||||||
|
|
||||||
好处一大堆(延迟从 1000ms 降到 1ms、不再有 7844 阻断、内存 4 倍),
|
好处一大堆(延迟从 1000ms 降到 1ms、不再有 7844 阻断、内存 4 倍),
|
||||||
@@ -11,7 +11,7 @@ r'''让客户端能用"纯 IP + 明文 HTTP"的中转(新服务器就是这种
|
|||||||
|
|
||||||
而两个客户端原来都默认往 https 上走:
|
而两个客户端原来都默认往 https 上走:
|
||||||
|
|
||||||
1. Android 的 RelayTransport:用户填 `13.54.30.196:13245`
|
1. Android 的 RelayTransport:用户填 `yk.xxmxxs.top:13245`
|
||||||
(没写协议头)会被强制加上 `https://` -> 连不上(那台机器没证书)
|
(没写协议头)会被强制加上 `https://` -> 连不上(那台机器没证书)
|
||||||
|
|
||||||
2. Android 9+ **默认禁止明文 HTTP**。没有
|
2. Android 9+ **默认禁止明文 HTTP**。没有
|
||||||
@@ -39,7 +39,7 @@ Windows 那边已经是对的:relay.py 的 _PRIVATE_HOST 正则里
|
|||||||
明文 HTTP 意味着**同网络的人能看到流量**(虽然配对令牌、内容本来
|
明文 HTTP 意味着**同网络的人能看到流量**(虽然配对令牌、内容本来
|
||||||
就是加密的,但 URL、设备名这些会露)。真正的解法是上 HTTPS:
|
就是加密的,但 URL、设备名这些会露)。真正的解法是上 HTTPS:
|
||||||
|
|
||||||
把域名 relay.xxmxxs.top 的 A 记录指到 13.54.30.196,
|
把域名 relay.xxmxxs.top 的 A 记录指到 yk.xxmxxs.top,
|
||||||
然后 certbot 签一张 Let's Encrypt 证书,confd 挂上。
|
然后 certbot 签一张 Let's Encrypt 证书,confd 挂上。
|
||||||
|
|
||||||
那一步要用户动 DNS,所以先让明文能用(这个补丁),
|
那一步要用户动 DNS,所以先让明文能用(这个补丁),
|
||||||
@@ -91,7 +91,7 @@ JAVA_NEW = ''' if (!url.startsWith("http://") && !url.startsWith("https:/
|
|||||||
// 而且报的是 SSL 错误,会把人带去查证书查半天。
|
// 而且报的是 SSL 错误,会把人带去查证书查半天。
|
||||||
//
|
//
|
||||||
// ⚠ 纯 IP 判断包含**公网 IP** —— 2026-10-08 换到 Lightsail
|
// ⚠ 纯 IP 判断包含**公网 IP** —— 2026-10-08 换到 Lightsail
|
||||||
// 之后中转就是 http://13.54.30.196:13245,
|
// 之后中转就是 http://yk.xxmxxs.top:13245,
|
||||||
// 只把 192.168/10./172.16 当私有的话这里会走错。
|
// 只把 192.168/10./172.16 当私有的话这里会走错。
|
||||||
String hostOnly = url;
|
String hostOnly = url;
|
||||||
int slash = hostOnly.indexOf('/');
|
int slash = hostOnly.indexOf('/');
|
||||||
@@ -113,7 +113,7 @@ MAN_OLD = ''' <uses-permission android:name="android.permission.INTERNET" />'
|
|||||||
|
|
||||||
MAN_NEW = ''' <!-- ⚠ 允许明文 HTTP。
|
MAN_NEW = ''' <!-- ⚠ 允许明文 HTTP。
|
||||||
中转可以是"自己搭的、没有证书"的服务:局域网那台,或者
|
中转可以是"自己搭的、没有证书"的服务:局域网那台,或者
|
||||||
2026-10-08 之后那台有公网 IP 的服务器(http://13.54.30.196:13245)。
|
2026-10-08 之后那台有公网 IP 的服务器(http://yk.xxmxxs.top:13245)。
|
||||||
Android 9+ 默认**禁止明文流量**,不加这一条的话请求会被系统
|
Android 9+ 默认**禁止明文流量**,不加这一条的话请求会被系统
|
||||||
直接掐掉,日志里只有一句含糊的
|
直接掐掉,日志里只有一句含糊的
|
||||||
"Cleartext HTTP traffic to ... not permitted"。
|
"Cleartext HTTP traffic to ... not permitted"。
|
||||||
|
|||||||
@@ -146,6 +146,26 @@ def main() -> int:
|
|||||||
print(f" docs/{path.name}")
|
print(f" docs/{path.name}")
|
||||||
changed += 1
|
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"):
|
for name in ("test_package_remote.py", "test_apk_contents.py"):
|
||||||
path = ROOT / "_tests" / name
|
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(域名没配好时靠它)
|
# IP:80 —— 绕过 Cloudflare,直连 nginx(域名没配好时靠它)
|
||||||
# :13245 —— **应该不通**(中转已只绑 127.0.0.1,不再对公网)
|
# :13245 —— **应该不通**(中转已只绑 127.0.0.1,不再对公网)
|
||||||
DOMAIN_URL = "http://%s" % DOMAIN
|
DOMAIN_URL = "http://%s" % DOMAIN
|
||||||
NGINX_URL = "http://13.54.30.196:80"
|
NGINX_URL = "http://<源站IP>:80"
|
||||||
DIRECT_URL = "http://13.54.30.196:%s" % PORT
|
DIRECT_URL = "http://<源站IP>:%s" % PORT
|
||||||
|
|
||||||
REMOTE = r'''
|
REMOTE = r'''
|
||||||
R=__DIR__
|
R=__DIR__
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ sys.path.insert(0, str(ROOT / "_build"))
|
|||||||
from lssh import run # noqa: E402
|
from lssh import run # noqa: E402
|
||||||
|
|
||||||
DOMAIN = "yk.xxmxxs.top"
|
DOMAIN = "yk.xxmxxs.top"
|
||||||
IP = "13.54.30.196"
|
IP = "<源站IP>"
|
||||||
|
|
||||||
|
|
||||||
def tcp(host: str, port: int, timeout: float = 8.0) -> bool:
|
def tcp(host: str, port: int, timeout: float = 8.0) -> bool:
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ WantedBy=multi-user.target
|
|||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
print("=" * 62)
|
print("=" * 62)
|
||||||
print(" 部署到 Lightsail(13.54.30.196),端口 %s" % PORT)
|
print(" 部署到 Lightsail(<源站IP>),端口 %s" % PORT)
|
||||||
print("=" * 62)
|
print("=" * 62)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
@@ -167,7 +167,7 @@ def main() -> int:
|
|||||||
|
|
||||||
print()
|
print()
|
||||||
print(" 提示:防火墙要先放行 TCP %s(用户来开)。" % PORT)
|
print(" 提示:防火墙要先放行 TCP %s(用户来开)。" % PORT)
|
||||||
print(" 之后公网地址就是 http://13.54.30.196:%s/" % PORT)
|
print(" 之后公网地址就是 http://<源站IP>:%s/" % PORT)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from lssh import run, upload # noqa: E402
|
|||||||
|
|
||||||
RELAY_DIR = "/opt/ykxmz-relay"
|
RELAY_DIR = "/opt/ykxmz-relay"
|
||||||
PORT = "13245"
|
PORT = "13245"
|
||||||
PUBLIC_URL = "http://13.54.30.196:%s" % PORT
|
PUBLIC_URL = "http://<源站IP>:%s" % PORT
|
||||||
|
|
||||||
MONITOR = r'''#!/bin/bash
|
MONITOR = r'''#!/bin/bash
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -46,7 +46,7 @@ MONITOR = r'''#!/bin/bash
|
|||||||
#
|
#
|
||||||
# 为什么不查 127.0.0.1:
|
# 为什么不查 127.0.0.1:
|
||||||
# 本机 200 只说明进程活着。用户真正关心的是"打开
|
# 本机 200 只说明进程活着。用户真正关心的是"打开
|
||||||
# http://13.54.30.196:13245 能不能出页面" —— 那要经过
|
# http://<源站IP>:13245 能不能出页面" —— 那要经过
|
||||||
# Lightsail 的防火墙 / 安全组 / 上游线路。实测踩过:
|
# Lightsail 的防火墙 / 安全组 / 上游线路。实测踩过:
|
||||||
# 隧道两条实例都掉了,本机 curl 照样 200,但公网 502。
|
# 隧道两条实例都掉了,本机 curl 照样 200,但公网 502。
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from lssh import run, upload # noqa: E402
|
|||||||
|
|
||||||
RELAY_DIR = "/opt/ykxmz-relay"
|
RELAY_DIR = "/opt/ykxmz-relay"
|
||||||
PORT = "13245"
|
PORT = "13245"
|
||||||
PUBLIC = "http://13.54.30.196:%s" % PORT
|
PUBLIC = "http://<源站IP>:%s" % PORT
|
||||||
|
|
||||||
CODE = ["relay.py", "admin.py", "landing.py", "showcase.py"]
|
CODE = ["relay.py", "admin.py", "landing.py", "showcase.py"]
|
||||||
ARTIFACTS = ["ykxmz-setup-{v}.exe", "ykxmz-watch-{v}.apk",
|
ARTIFACTS = ["ykxmz-setup-{v}.exe", "ykxmz-watch-{v}.apk",
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ PY
|
|||||||
echo
|
echo
|
||||||
echo " 收尾:清掉负载测试造的设备和会话"
|
echo " 收尾:清掉负载测试造的设备和会话"
|
||||||
curl -s -o /dev/null --max-time 8 -X POST "$BASE/v1/admin/forget" \
|
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"}'
|
-d '{"code":"112233"}'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ done
|
|||||||
echo
|
echo
|
||||||
echo "════ 6. 对比:直连自己的 nginx ════"
|
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' "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'''修上一步弄坏的登录表单。
|
r'''修上一步弄坏的登录表单。
|
||||||
|
|
||||||
上一步的正则插账号时把字典插坏了:
|
上一步的正则插账号时把字典插坏了:
|
||||||
urlencode({"username": "xmz7891", {"password": "120830"})
|
urlencode({"username": "xmz7891", {"password": "" # 见下方注释:不再写死})
|
||||||
^ 多了个左括号
|
^ 多了个左括号
|
||||||
应该是:
|
应该是:
|
||||||
urlencode({"username": "xmz7891", "password": "120830"})
|
urlencode({"username": "xmz7891", "password": "" # 见下方注释:不再写死})
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
OLD = "REVA" + "edu120830" # 拼出来,本脚本自己不含明文
|
OLD = "REVA" + "edu<改从环境变量读>" # 拼出来,本脚本自己不含明文
|
||||||
|
|
||||||
TARGETS = [
|
TARGETS = [
|
||||||
ROOT / "_tests" / "test_relay_admin_login.py",
|
ROOT / "_tests" / "test_relay_admin_login.py",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ from lssh import run # noqa: E402
|
|||||||
|
|
||||||
RELAY_DIR = "/opt/ykxmz-relay"
|
RELAY_DIR = "/opt/ykxmz-relay"
|
||||||
DOMAIN = "yk.xxmxxs.top"
|
DOMAIN = "yk.xxmxxs.top"
|
||||||
FALLBACK = "http://13.54.30.196:80"
|
FALLBACK = "http://<源站IP>:80"
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
@@ -58,7 +58,7 @@ def main() -> int:
|
|||||||
print()
|
print()
|
||||||
print(" ── 1. 改 monitor.sh 的健康检查(域名优先,IP 兜底)──")
|
print(" ── 1. 改 monitor.sh 的健康检查(域名优先,IP 兜底)──")
|
||||||
# 把 PUBLIC_URL 的默认值换掉,并把 is_healthy 改成"两个都试"
|
# 把 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 = (
|
new_url = (
|
||||||
'# 探测地址:域名优先(用户实际走的路),IP:80 兜底\n'
|
'# 探测地址:域名优先(用户实际走的路),IP:80 兜底\n'
|
||||||
'# (域名 DNS 还没指过来时用它,否则监测会一直误报)\n'
|
'# (域名 DNS 还没指过来时用它,否则监测会一直误报)\n'
|
||||||
@@ -151,10 +151,10 @@ def main() -> int:
|
|||||||
'http://%s/v1/health)"; '
|
'http://%s/v1/health)"; '
|
||||||
'printf " %-28s %s\\n" "IP:80 (兜底)" '
|
'printf " %-28s %s\\n" "IP:80 (兜底)" '
|
||||||
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 '
|
'"$(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 (已关闭)" '
|
'printf " %-28s %s\\n" "IP:13245 (已关闭)" '
|
||||||
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 '
|
'"$(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)
|
timeout=120, quiet=True)
|
||||||
for ln in out[1].strip().splitlines():
|
for ln in out[1].strip().splitlines():
|
||||||
print(ln)
|
print(ln)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
OLD = "REVA" + "edu120830"
|
OLD = "REVA" + "edu<改从环境变量读>"
|
||||||
|
|
||||||
passed = 0
|
passed = 0
|
||||||
failed = 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)"; '
|
'https://yk.xxmxxs.top/v1/health)"; '
|
||||||
'printf " %-36s %s\\n" "公网 443(Lightsail 防火墙)" '
|
'printf " %-36s %s\\n" "公网 443(Lightsail 防火墙)" '
|
||||||
'"$(curl -sk -o /dev/null -w \'%{http_code}\' --max-time 8 '
|
'"$(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)
|
out = run(probe, timeout=180, quiet=True)
|
||||||
for ln in out[1].strip().splitlines():
|
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,密码登录)。
|
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
|
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")
|
USER = os.environ.get("YKXMZ_LS_USER", "admin")
|
||||||
PORT = int(os.environ.get("YKXMZ_LS_PORT", "22"))
|
PORT = int(os.environ.get("YKXMZ_LS_PORT", "22"))
|
||||||
KEY = Path(os.environ.get(
|
KEY = Path(os.environ.get(
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import zipfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||||
VERSION = "2.1.3"
|
VERSION = "2.1.4"
|
||||||
NAME = f"ykxmz-{VERSION}"
|
NAME = f"ykxmz-{VERSION}"
|
||||||
|
|
||||||
AGENT_SRC = ROOT / "dist_exe" / "ykxmz"
|
AGENT_SRC = ROOT / "dist_exe" / "ykxmz"
|
||||||
@@ -100,7 +100,7 @@ TOP_README = f"""========================================================
|
|||||||
【服务器端能管什么】
|
【服务器端能管什么】
|
||||||
|
|
||||||
浏览器打开: <中转地址>/?key=... 或者直接用管理密码登录
|
浏览器打开: <中转地址>/?key=... 或者直接用管理密码登录
|
||||||
管理密码:120830(可在服务器上改)
|
管理密码:<改从环境变量读>(可在服务器上改)
|
||||||
|
|
||||||
能看:哪些设备在线、配对码、流量、会话数
|
能看:哪些设备在线、配对码、流量、会话数
|
||||||
能做:踢掉某个设备的会话、抹掉某个配对码绑定
|
能做:踢掉某个设备的会话、抹掉某个配对码绑定
|
||||||
@@ -334,7 +334,7 @@ RELAY_README = f"""========================================================
|
|||||||
|
|
||||||
官网: <公网地址>/
|
官网: <公网地址>/
|
||||||
管理页: <公网地址>/?key=<lanremote-key 的输出>
|
管理页: <公网地址>/?key=<lanremote-key 的输出>
|
||||||
管理密码: 120830(存在 data/admin_password.txt,可改)
|
管理密码: <改从环境变量读>(存在 data/admin_password.txt,可改)
|
||||||
|
|
||||||
能看到:设备列表、在线状态、配对码、流量、会话数
|
能看到:设备列表、在线状态、配对码、流量、会话数
|
||||||
能操作:踢会话、抹掉配对码绑定
|
能操作:踢会话、抹掉配对码绑定
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""管理页加登录密码(用户指定:120830)。
|
"""管理页加登录密码(用户指定:<改从环境变量读>)。
|
||||||
|
|
||||||
为什么要改:
|
为什么要改:
|
||||||
之前管理页靠「钥匙」放在 URL 里(/?key=xxx)。
|
之前管理页靠「钥匙」放在 URL 里(/?key=xxx)。
|
||||||
这样钥匙会进浏览器历史、进 Cloudflare 日志、被人肩窥。
|
这样钥匙会进浏览器历史、进 Cloudflare 日志、被人肩窥。
|
||||||
改成标准做法:登录页输密码 -> 种一个 HttpOnly Cookie。
|
改成标准做法:登录页输密码 -> 种一个 HttpOnly Cookie。
|
||||||
|
|
||||||
* 密码默认 120830,存在 data/admin_password.txt,可改
|
* 密码默认 <改从环境变量读>,存在 data/admin_password.txt,可改
|
||||||
* 登录成功后种 Cookie,有效期 12 小时
|
* 登录成功后种 Cookie,有效期 12 小时
|
||||||
* 命令行/脚本仍可用 X-Admin 头(放钥匙或密码都认)
|
* 命令行/脚本仍可用 X-Admin 头(放钥匙或密码都认)
|
||||||
"""
|
"""
|
||||||
@@ -26,7 +26,7 @@ OLD = '''def admin_ok(supplied: str) -> bool:
|
|||||||
return secrets.compare_digest(supplied, admin_key())'''
|
return secrets.compare_digest(supplied, admin_key())'''
|
||||||
|
|
||||||
NEW = '''# 管理页密码。用户指定的默认值,可以在 data/admin_password.txt 里改。
|
NEW = '''# 管理页密码。用户指定的默认值,可以在 data/admin_password.txt 里改。
|
||||||
ADMIN_PASSWORD_DEFAULT = "120830"
|
ADMIN_PASSWORD_DEFAULT = "" # 见下方注释:不再写死
|
||||||
ADMIN_PASSWORD = os.environ.get("RELAY_ADMIN_PASSWORD", "")
|
ADMIN_PASSWORD = os.environ.get("RELAY_ADMIN_PASSWORD", "")
|
||||||
# 登录会话:cookie 令牌 -> 过期时间
|
# 登录会话:cookie 令牌 -> 过期时间
|
||||||
ADMIN_SESSIONS: dict = {}
|
ADMIN_SESSIONS: dict = {}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ NEW_BODY = '''RELAY_README = f"""===============================================
|
|||||||
|
|
||||||
官网: <公网地址>/
|
官网: <公网地址>/
|
||||||
管理页: <公网地址>/?key=<lanremote-key 的输出>
|
管理页: <公网地址>/?key=<lanremote-key 的输出>
|
||||||
管理密码: 120830(存在 data/admin_password.txt,可改)
|
管理密码: <改从环境变量读>(存在 data/admin_password.txt,可改)
|
||||||
|
|
||||||
能看到:设备列表、在线状态、配对码、流量、会话数
|
能看到:设备列表、在线状态、配对码、流量、会话数
|
||||||
能操作:踢会话、抹掉配对码绑定
|
能操作:踢会话、抹掉配对码绑定
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
安全说明:
|
安全说明:
|
||||||
/url 只暴露"隧道地址"这一个信息,不含配对码、设备、流量。
|
/url 只暴露"隧道地址"这一个信息,不含配对码、设备、流量。
|
||||||
地址本来就是要给手表填的,不算秘密。
|
地址本来就是要给手表填的,不算秘密。
|
||||||
设备列表、配对码这些仍然要登录(密码 120830)。
|
设备列表、配对码这些仍然要登录(密码 <改从环境变量读>)。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
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
|
import urllib.request
|
||||||
from pathlib import Path
|
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
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
UA = {"User-Agent": "LanRemote/1.4.0"}
|
UA = {"User-Agent": "LanRemote/1.4.0"}
|
||||||
PORT = 8111
|
PORT = 8111
|
||||||
@@ -86,7 +101,7 @@ def main() -> int:
|
|||||||
jar = http.cookiejar.CookieJar()
|
jar = http.cookiejar.CookieJar()
|
||||||
opener = urllib.request.build_opener(
|
opener = urllib.request.build_opener(
|
||||||
urllib.request.HTTPCookieProcessor(jar))
|
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,
|
req = urllib.request.Request(BASE + "/admin/login", data=form,
|
||||||
method="POST", headers=UA)
|
method="POST", headers=UA)
|
||||||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
@@ -134,4 +149,4 @@ def main() -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
@@ -20,6 +20,21 @@ import urllib.parse
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
from pathlib import Path
|
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
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
UA = {"User-Agent": "LanRemote/1.4.0"}
|
UA = {"User-Agent": "LanRemote/1.4.0"}
|
||||||
PORT = 8112
|
PORT = 8112
|
||||||
@@ -91,7 +106,7 @@ def main() -> int:
|
|||||||
jar = http.cookiejar.CookieJar()
|
jar = http.cookiejar.CookieJar()
|
||||||
opener = urllib.request.build_opener(
|
opener = urllib.request.build_opener(
|
||||||
urllib.request.HTTPCookieProcessor(jar))
|
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,
|
req = urllib.request.Request(BASE + "/admin/login", data=form,
|
||||||
method="POST", headers=UA)
|
method="POST", headers=UA)
|
||||||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
@@ -140,4 +155,4 @@ def main() -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
@@ -21,6 +21,21 @@ import urllib.parse
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
from pathlib import Path
|
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
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
UA = {"User-Agent": "LanRemote/1.4.0"}
|
UA = {"User-Agent": "LanRemote/1.4.0"}
|
||||||
PORT = 8114
|
PORT = 8114
|
||||||
@@ -91,7 +106,7 @@ def main() -> int:
|
|||||||
jar = http.cookiejar.CookieJar()
|
jar = http.cookiejar.CookieJar()
|
||||||
opener = urllib.request.build_opener(
|
opener = urllib.request.build_opener(
|
||||||
urllib.request.HTTPCookieProcessor(jar))
|
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,
|
req = urllib.request.Request(BASE + "/admin/login", data=form,
|
||||||
method="POST", headers=UA)
|
method="POST", headers=UA)
|
||||||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
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 # 真的改
|
# bash _build/scrub_password_history.sh --apply # 真的改
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
OLD="REVA""edu120830" # 拼出来,免得这个脚本自己又成为一个泄漏点
|
OLD="REVA""edu<改从环境变量读>" # 拼出来,免得这个脚本自己又成为一个泄漏点
|
||||||
NEW="(已移除,见 relay.py 的说明)"
|
NEW="(已移除,见 relay.py 的说明)"
|
||||||
|
|
||||||
cd "$(dirname "$0")/.." || exit 1
|
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
|
用户 ──HTTPS──> Cloudflare CDN ──HTTP:80──> nginx ──> 127.0.0.1:13245
|
||||||
(yk.xxmxxs.top) 13.54.30.196
|
(yk.xxmxxs.top) <源站IP>
|
||||||
|
|
||||||
== 为什么要加 nginx 这一层 ==
|
== 为什么要加 nginx 这一层 ==
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ r'''在 Lightsail 上装 nginx,把 yk.xxmxxs.top 反代到中转。
|
|||||||
== 关于端口 80 的安全 ==
|
== 关于端口 80 的安全 ==
|
||||||
|
|
||||||
nginx 监听 80 是**必须的**(Cloudflare 的开源端口)。
|
nginx 监听 80 是**必须的**(Cloudflare 的开源端口)。
|
||||||
但要注意:如果有人知道 13.54.30.196,也能直接打 80 绕过 Cloudflare。
|
但要注意:如果有人知道 <源站IP>,也能直接打 80 绕过 Cloudflare。
|
||||||
所以配置里加了:
|
所以配置里加了:
|
||||||
|
|
||||||
* 只接受来自 Cloudflare 网段的请求(可选,见 allowlist 那段)
|
* 只接受来自 Cloudflare 网段的请求(可选,见 allowlist 那段)
|
||||||
@@ -178,13 +178,13 @@ def main() -> int:
|
|||||||
'-H \'Host: ' + DOMAIN + '\' http://127.0.0.1:80/v1/health)"\n'
|
'-H \'Host: ' + DOMAIN + '\' http://127.0.0.1:80/v1/health)"\n'
|
||||||
'printf " %-22s %s\\n" "公网 80(IP)" '
|
'printf " %-22s %s\\n" "公网 80(IP)" '
|
||||||
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 15 '
|
'"$(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)" '
|
'printf " %-22s %s\\n" "中转本机(13245)" '
|
||||||
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 '
|
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 '
|
||||||
'http://127.0.0.1:13245/v1/health)"\n'
|
'http://127.0.0.1:13245/v1/health)"\n'
|
||||||
'printf " %-22s %s\\n" "中转是否还暴露公网" '
|
'printf " %-22s %s\\n" "中转是否还暴露公网" '
|
||||||
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 '
|
'"$(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)
|
out = run(script, timeout=180, quiet=True)
|
||||||
for ln in out[1].strip().splitlines():
|
for ln in out[1].strip().splitlines():
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ DNS 配好、橙色云朵生效之后:
|
|||||||
|
|
||||||
http://yk.xxmxxs.top/ -> 200 (经 Cloudflare,反代是好的)
|
http://yk.xxmxxs.top/ -> 200 (经 Cloudflare,反代是好的)
|
||||||
https://yk.xxmxxs.top/ -> 522 (Cloudflare 连不上源站)
|
https://yk.xxmxxs.top/ -> 522 (Cloudflare 连不上源站)
|
||||||
13.54.30.196:443 -> 不通(源站没听 443)
|
yk.xxmxxs.top:443 -> 不通(源站没听 443)
|
||||||
|
|
||||||
结论:zone 的 SSL/TLS 模式是 **Full** 或 **Full (strict)**,
|
结论:zone 的 SSL/TLS 模式是 **Full** 或 **Full (strict)**,
|
||||||
Cloudflare 回源走 443,而源站只有 80 -> 522。
|
Cloudflare 回源走 443,而源站只有 80 -> 522。
|
||||||
|
|||||||
@@ -22,6 +22,21 @@ import urllib.parse
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
from pathlib import Path
|
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
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
UA = {"User-Agent": "LanRemote/1.4.0"}
|
UA = {"User-Agent": "LanRemote/1.4.0"}
|
||||||
PORT = 8110
|
PORT = 8110
|
||||||
@@ -69,7 +84,7 @@ def main() -> int:
|
|||||||
jar = http.cookiejar.CookieJar()
|
jar = http.cookiejar.CookieJar()
|
||||||
opener = urllib.request.build_opener(
|
opener = urllib.request.build_opener(
|
||||||
urllib.request.HTTPCookieProcessor(jar))
|
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,
|
req = urllib.request.Request(BASE + "/admin/login", data=form,
|
||||||
method="POST", headers=UA)
|
method="POST", headers=UA)
|
||||||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|||||||
@@ -15,6 +15,21 @@ import urllib.parse
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
from pathlib import Path
|
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
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
UA = {"User-Agent": "LanRemote/1.4.0"}
|
UA = {"User-Agent": "LanRemote/1.4.0"}
|
||||||
PORT = 8113
|
PORT = 8113
|
||||||
@@ -60,7 +75,7 @@ def main() -> int:
|
|||||||
jar = http.cookiejar.CookieJar()
|
jar = http.cookiejar.CookieJar()
|
||||||
opener = urllib.request.build_opener(
|
opener = urllib.request.build_opener(
|
||||||
urllib.request.HTTPCookieProcessor(jar))
|
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,
|
req = urllib.request.Request(BASE + "/admin/login", data=form,
|
||||||
method="POST", headers=UA)
|
method="POST", headers=UA)
|
||||||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ def main() -> int:
|
|||||||
print(" 补 from urllib.parse import quote")
|
print(" 补 from urllib.parse import quote")
|
||||||
|
|
||||||
# ---- 2. 密码 + 账号 ----
|
# ---- 2. 密码 + 账号 ----
|
||||||
old_pw = 'ADMIN_PASSWORD_DEFAULT = "120830"'
|
old_pw = 'ADMIN_PASSWORD_DEFAULT = "" # 见下方注释:不再写死'
|
||||||
new_pw = ('ADMIN_PASSWORD_DEFAULT = "' + ADMIN_PASS + '"\n'
|
new_pw = ('ADMIN_PASSWORD_DEFAULT = "' + ADMIN_PASS + '"\n'
|
||||||
'# 管理页账号(原来只有密码,用户要求加上账号)\n'
|
'# 管理页账号(原来只有密码,用户要求加上账号)\n'
|
||||||
'ADMIN_USER_DEFAULT = "' + ADMIN_USER + '"\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())
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user