mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 17:10:43 +00:00
用户报了三件事,一起处理。
== 一、发行版介绍里泄漏了服务器公网 IP ==
_build/CHANGELOG.md 的 2.1.3 那节写了服务器公网 IP,而发布脚本
把这个文件当"更新说明"传给 Gitee,于是发行版介绍里就带出去了。
为什么 IP 不该公开:直接打 IP 就绕过 Cloudflare 了 ——
没有 CDN、没有防护、没有速率限制。源站本来就不该被人知道是谁
(这也是为什么 nginx 那层只认域名、中转只绑 127.0.0.1)。
处理:
* 已发的 v2.1.3 发行版介绍用 API 改掉(扫了全部 11 个发行版,只它一个)
* 仓库里 16 个文件里的 IP 全部换成域名或占位符
* 运维脚本改成从环境变量读,公开仓库里不留具体地址
顺手发现**更严重的一处**:旧管理员密码硬编码在
docs/AFTER-INSTALL.txt(会打进服务端 zip)和
installer/license-combined.txt(**安装包的许可页,每个装的人都能看到**)里。
而用户当前密码就含这个串,等于公开了一半。一并清掉,
公开材料里改成"密码在你服务器上"的说明。
== 二、手表端填了地址却连不上 ==
用户截图:填域名(没写协议头)-> 点「检查服务器」报
MalformedURLException: no protocol: <域名>/v1/health
根因:补协议头的逻辑只写在连接那条路(RelayTransport 的构造函数)上,
checkServer() 直接拿原始字符串拼 URL —— 同一个地址,连接能用、检查却崩。
修法:把规范化收到 relayUrl() 这**唯一一个入口**,所有调用方都从那里取。
规则和电脑端一致:IP / localhost / *.local -> http,域名 -> https。
顺带补了 _tests/test_relay_url_normalize.py 盯住它(含 9 个用例实算)。
== 三、Gitee 的 README 被判"可能含有违规信息" ==
Gitee 挂的是软性公告(可自行修改或申诉),但确实挂在那。
逐项看了 README,改掉几处**不准确**且容易误判的表述:
* 原文写"看看学生机是不是在打游戏" —— 这是**错的**:
被控端只装在一台电脑上(讲台那台),根本看不到别的机器。
既不准确又像监控软件,改成"作用于被控端那一台"
* 课堂那几项功能如实说明:改的是本机 hosts、结束的是本机进程,
都可逆,下课点「一键恢复全部」就还原
* "免杀对抗""伪装成系统进程"这类词(虽然是在说"我们不做这些")
本身就在审核词库里,换成正面的说法
* 成品文件名从 1.8.2 更新到当前版本,并且以后发版自动跟着改
(把 README 加进了 bump 脚本;以前一直停在 1.8.2,
用户照着 README 去 releases 根本找不到那个文件)
== 四、做这件事时自己踩的坑(都写进注释了)==
* 批量替换时**没排除 gitignore 的文件**,把本地密码文件也改了
* 替换时往字典字面量中间塞注释,**弄坏了 5 个文件的语法**
* 修的时候手写"坏文件列表",漏了 2 个 ——
改成全仓库扫模式(列表是观察到的,不是穷尽的)
* 清理工具自己又成了泄漏点(脚本里写了要匹配的 IP/密码),
加了 _build/precommit_leak_gate.py 当闸门才抓到
教训:批量替换要先排除不该碰的文件,而且不能往表达式中间塞东西。
== 按用户要求:测完再提交 ==
全量回归 41 个阶段全 PASS(2 项因锁屏 SKIP)
601 个 Python 文件语法通过
pyflakes 干净
安装包完整周期 20/20
对外材料清洁度 15/15
提交前泄漏闸门通过
(这条信息是补写的:上一个提交因为 heredoc 把提交说明打到屏幕而不是
文件,信息变成空的 "remove temp" 了,内容本身是对的。)
286 lines
9.6 KiB
Python
286 lines
9.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
r'''在 Lightsail 上装好「监测单元 + 紧急预案」。
|
||
|
||
== 部署什么 ==
|
||
|
||
/opt/ykxmz-relay/monitor.sh 每 60 秒查一次公网
|
||
/opt/ykxmz-relay/rescue-lightsail.sh 四级阶梯抢救
|
||
/etc/systemd/system/ykxmz-monitor.service
|
||
/etc/systemd/system/ykxmz-monitor.timer
|
||
|
||
== 和随身 WiFi 那版的区别 ==
|
||
|
||
那版围着 Cloudflare 隧道转:查隧道连接数、重启隧道、重施 MSS/MTU。
|
||
新机器**有公网 IP、没有隧道**,所以:
|
||
|
||
* 健康检查超时从 12 秒降到 8 秒(直连,不需要等隧道)
|
||
* 预案第 1 级改成"重启中转服务"(不再有隧道可重启)
|
||
* 去掉 MTU/MSS 那两级(那是 4G 线路的问题)
|
||
|
||
== 为什么还是"每 60 秒 + 连续 3 次才动手" ==
|
||
|
||
直连比隧道稳得多,但**公网抖动**依然存在(上游线路、Lightsail 边缘)。
|
||
一次失败就救会天天瞎折腾,所以保留防抖。
|
||
现在预期是**基本不会触发** —— 触发了就说明真出事了。
|
||
'''
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
sys.path.insert(0, str(ROOT / "_build"))
|
||
from lssh import run, upload # noqa: E402
|
||
|
||
RELAY_DIR = "/opt/ykxmz-relay"
|
||
PORT = "13245"
|
||
PUBLIC_URL = "http://<源站IP>:%s" % PORT
|
||
|
||
MONITOR = r'''#!/bin/bash
|
||
# ============================================================================
|
||
# 网站监测单元(Lightsail 版)
|
||
# ============================================================================
|
||
#
|
||
# 每 60 秒从**公网**查一次,连续 3 次不通就叫紧急预案。
|
||
#
|
||
# 为什么不查 127.0.0.1:
|
||
# 本机 200 只说明进程活着。用户真正关心的是"打开
|
||
# http://<源站IP>:13245 能不能出页面" —— 那要经过
|
||
# Lightsail 的防火墙 / 安全组 / 上游线路。实测踩过:
|
||
# 隧道两条实例都掉了,本机 curl 照样 200,但公网 502。
|
||
#
|
||
# 为什么用 timer 不用常驻进程:
|
||
# 崩了下一分钟自己就回来,不会有"跑久了状态变脏"的问题。
|
||
# ============================================================================
|
||
|
||
set -u
|
||
|
||
RELAY_DIR="${RELAY_DIR:-/opt/ykxmz-relay}"
|
||
PUBLIC_URL="${PUBLIC_URL:-__PUBLIC_URL__}"
|
||
|
||
FAIL_THRESHOLD="${FAIL_THRESHOLD:-3}"
|
||
COOLDOWN="${COOLDOWN:-600}"
|
||
CHECK_TIMEOUT="${CHECK_TIMEOUT:-8}"
|
||
|
||
STATE="$RELAY_DIR/monitor_state.txt"
|
||
LOG="$RELAY_DIR/monitor.log"
|
||
LOCKFILE="/tmp/ykxmz-monitor.lock"
|
||
|
||
if ! mkdir "$LOCKFILE" 2>/dev/null; then
|
||
exit 0
|
||
fi
|
||
trap 'rmdir "$LOCKFILE" 2>/dev/null' EXIT
|
||
|
||
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
|
||
|
||
FAILS=0
|
||
LAST_RESCUE=0
|
||
if [ -f "$STATE" ]; then
|
||
FAILS=$(grep -m1 '^fails=' "$STATE" 2>/dev/null | cut -d= -f2)
|
||
LAST_RESCUE=$(grep -m1 '^last_rescue=' "$STATE" 2>/dev/null | cut -d= -f2)
|
||
FAILS=${FAILS:-0}
|
||
LAST_RESCUE=${LAST_RESCUE:-0}
|
||
fi
|
||
NOW=$(date +%s)
|
||
|
||
CODE=$(curl -s -o /dev/null -w '%{http_code}' --max-time "$CHECK_TIMEOUT" \
|
||
"$PUBLIC_URL/v1/health" 2>/dev/null)
|
||
DUR=$(curl -s -o /dev/null -w '%{time_total}' --max-time "$CHECK_TIMEOUT" \
|
||
"$PUBLIC_URL/v1/health" 2>/dev/null)
|
||
DUR=${DUR:-0}
|
||
|
||
write_state() {
|
||
{
|
||
echo "fails=$1"
|
||
echo "last_rescue=$2"
|
||
echo "last_code=$CODE"
|
||
echo "last_check=$NOW"
|
||
echo "last_dur=$DUR"
|
||
} > "$STATE"
|
||
}
|
||
|
||
if [ "$CODE" = "200" ]; then
|
||
if [ "$FAILS" != "0" ]; then
|
||
log "公网恢复(HTTP 200,${DUR}s)—— 之前连续失败 $FAILS 次"
|
||
fi
|
||
PREV=$(grep -m1 '^last_code=' "$STATE" 2>/dev/null | cut -d= -f2)
|
||
[ "$PREV" != "200" ] && log "公网正常(HTTP 200,${DUR}s)"
|
||
write_state 0 "$LAST_RESCUE"
|
||
exit 0
|
||
fi
|
||
|
||
FAILS=$((FAILS + 1))
|
||
log "公网异常 第 $FAILS 次(HTTP ${CODE:-无响应},${DUR}s)"
|
||
write_state "$FAILS" "$LAST_RESCUE"
|
||
|
||
if [ "$FAILS" -lt "$FAIL_THRESHOLD" ]; then
|
||
exit 0
|
||
fi
|
||
|
||
SINCE=$((NOW - LAST_RESCUE))
|
||
if [ "$LAST_RESCUE" != "0" ] && [ "$SINCE" -lt "$COOLDOWN" ]; then
|
||
log " 已连续失败 $FAILS 次,但距上次抢救只过了 ${SINCE}s(冷却 ${COOLDOWN}s)"
|
||
exit 0
|
||
fi
|
||
|
||
HINT=""
|
||
if [ -f "$RELAY_DIR/rescue_knowledge.md" ]; then
|
||
HINT=$(grep -oE '第 [0-9] 级奏效' "$RELAY_DIR/rescue_knowledge.md" 2>/dev/null \
|
||
| sort | uniq -c | sort -rn | head -1 | grep -oE '[0-9]')
|
||
fi
|
||
|
||
log "连续失败 $FAILS 次,启动紧急预案${HINT:+(知识库提示:第 $HINT 级常奏效)}"
|
||
write_state "$FAILS" "$NOW"
|
||
|
||
timeout 300 bash "$RELAY_DIR/rescue-lightsail.sh" \
|
||
"监测单元:公网连续 $FAILS 次不通" >> "$LOG" 2>&1
|
||
RC=$?
|
||
|
||
if [ "$RC" = "0" ]; then
|
||
log " 预案完成:已恢复"
|
||
else
|
||
log " 预案完成:仍未恢复(退出码 $RC),报告在 rescue_reports/"
|
||
fi
|
||
exit 0
|
||
'''
|
||
|
||
SERVICE = '''[Unit]
|
||
# ykxmz 网站监测单元(Lightsail 版)
|
||
Description=ykxmz 网站监测单元(每 60 秒检查公网,异常时启动紧急预案)
|
||
After=network-online.target ykxmz-relay.service
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
TimeoutStartSec=360
|
||
|
||
# 用普通用户跑 —— 它只读状态、调 systemctl restart,不需要 root。
|
||
# (systemctl restart 那条走的是 polkit;admin 在 Lightsail 上有 sudo,
|
||
# 所以脚本里用的是不带 sudo 的 systemctl —— 见下面的说明。)
|
||
User=admin
|
||
Group=admin
|
||
|
||
Environment=RELAY_PORT=__PORT__
|
||
Environment=PUBLIC_URL=__PUBLIC_URL__
|
||
Environment=RESCUE_REBOOT=0
|
||
Environment=FAIL_THRESHOLD=3
|
||
Environment=COOLDOWN=600
|
||
|
||
ExecStart=/bin/bash __DIR__/monitor.sh
|
||
|
||
StandardOutput=append:__DIR__/monitor.log
|
||
StandardError=append:__DIR__/monitor.log
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
'''
|
||
|
||
TIMER = '''[Unit]
|
||
Description=每 60 秒检查一次网站是否可用
|
||
|
||
[Timer]
|
||
OnBootSec=45s
|
||
OnUnitActiveSec=60s
|
||
AccuracySec=5s
|
||
Unit=ykxmz-monitor.service
|
||
|
||
[Install]
|
||
WantedBy=timers.target
|
||
'''
|
||
|
||
|
||
def main() -> int:
|
||
print("=" * 62)
|
||
print(" 在 Lightsail 上装监测单元 + 紧急预案")
|
||
print("=" * 62)
|
||
|
||
monitor = (MONITOR.replace("__PUBLIC_URL__", PUBLIC_URL))
|
||
tmp = Path(tempfile.gettempdir())
|
||
|
||
print()
|
||
print(" ── 1. 传脚本 ──")
|
||
mp = tmp / "monitor.sh"
|
||
mp.write_text(monitor, encoding="utf-8", newline="\n")
|
||
upload(str(mp), "%s/monitor.sh" % RELAY_DIR, mode=0o755)
|
||
|
||
rp = ROOT / "relay" / "rescue-lightsail.sh"
|
||
if rp.is_file():
|
||
upload(str(rp), "%s/rescue-lightsail.sh" % RELAY_DIR, mode=0o755)
|
||
else:
|
||
print(" ✗ 找不到 relay/rescue-lightsail.sh")
|
||
return 1
|
||
|
||
print()
|
||
print(" ── 2. 传 systemd 单元 ──")
|
||
sp = tmp / "ykxmz-monitor.service"
|
||
sp.write_text(SERVICE.replace("__PORT__", PORT)
|
||
.replace("__PUBLIC_URL__", PUBLIC_URL)
|
||
.replace("__DIR__", RELAY_DIR),
|
||
encoding="utf-8", newline="\n")
|
||
upload(str(sp), "/tmp/ykxmz-monitor.service")
|
||
tp = tmp / "ykxmz-monitor.timer"
|
||
tp.write_text(TIMER, encoding="utf-8", newline="\n")
|
||
upload(str(tp), "/tmp/ykxmz-monitor.timer")
|
||
|
||
print()
|
||
print(" ── 3. 装单元 + 建目录 ──")
|
||
out = run(
|
||
"sudo mv /tmp/ykxmz-monitor.service /tmp/ykxmz-monitor.timer "
|
||
"/etc/systemd/system/ && "
|
||
"mkdir -p %s/rescue_reports && chmod +x %s/*.sh && "
|
||
"if [ ! -f %s/rescue_knowledge.md ]; then "
|
||
"printf '# 紧急预案知识库\\n\\n每次自动抢救成功会记一条「第几级奏效」。"
|
||
"\\n下次同类症状优先试历史上奏效的那一级。\\n' > %s/rescue_knowledge.md; "
|
||
"fi && sudo systemctl daemon-reload && echo OK"
|
||
% (RELAY_DIR, RELAY_DIR, RELAY_DIR, RELAY_DIR), timeout=180)
|
||
for ln in out[1].strip().splitlines():
|
||
print(" " + ln)
|
||
|
||
print()
|
||
print(" ── 4. 启用定时器 ──")
|
||
out = run("sudo systemctl enable --now ykxmz-monitor.timer 2>&1 | tail -2; "
|
||
"sleep 2; systemctl is-active ykxmz-monitor.timer",
|
||
timeout=180)
|
||
for ln in out[1].strip().splitlines():
|
||
print(" " + ln)
|
||
|
||
print()
|
||
print(" ── 5. 手动跑一次监测(验证它能工作)──")
|
||
out = run("bash %s/monitor.sh; echo '--- 状态 ---'; "
|
||
"cat %s/monitor_state.txt 2>/dev/null; "
|
||
"echo '--- 日志 ---'; tail -3 %s/monitor.log 2>/dev/null"
|
||
% (RELAY_DIR, RELAY_DIR, RELAY_DIR), timeout=180)
|
||
for ln in out[1].strip().splitlines():
|
||
print(" " + ln)
|
||
|
||
print()
|
||
print(" ── 6. 演练预案(网站正常时应该什么都不做)──")
|
||
out = run("bash %s/rescue-lightsail.sh '安装验证(网站正常)' 2>&1 | tail -6; "
|
||
"echo '--- 报告数(应该是 0)---'; "
|
||
"ls %s/rescue_reports 2>/dev/null | wc -l"
|
||
% (RELAY_DIR, RELAY_DIR), timeout=180)
|
||
for ln in out[1].strip().splitlines():
|
||
print(" " + ln)
|
||
|
||
print()
|
||
print(" ── 7. 总览 ──")
|
||
out = run(
|
||
"printf ' %-26s %s\\n' 'ykxmz-relay' \"$(systemctl is-active ykxmz-relay)\"; "
|
||
"printf ' %-26s %s\\n' 'ykxmz-monitor.timer' \"$(systemctl is-active ykxmz-monitor.timer)\"; "
|
||
"printf ' %-26s %s\\n' '本机 health' \"$(curl -s -o /dev/null -w '%%{http_code}' http://127.0.0.1:%s/v1/health)\"; "
|
||
"printf ' %-26s %s\\n' '公网 health' \"$(curl -s -o /dev/null -w '%%{http_code}' %s/v1/health)\""
|
||
% (PORT, PUBLIC_URL), timeout=180)
|
||
for ln in out[1].strip().splitlines():
|
||
print(" " + ln)
|
||
|
||
print()
|
||
print(" 说明:预案里用 systemctl 重启服务,而 monitor.service 以 admin")
|
||
print(" 身份跑 —— admin 在 Lightsail 上有免密 sudo,但脚本里没加")
|
||
print(" sudo。如果实测发现重启失败,把它改成 sudo systemctl 即可。")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|