mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-19 00:10:42 +00:00
搬到 Lightsail:nginx 反代 + TLS + 公共 DNS + 后台口令改由服务器持有
== 架构(最终形态)==
用户 --HTTPS--> Cloudflare CDN --HTTP/HTTPS--> nginx --+--> 127.0.0.1:13245
yk.xxmxxs.top 13.54.30.196
中转只绑 127.0.0.1,公网只能经 nginx 进来 —— 少一个暴露面
(实测公网直连 :13245 返回 000,符合预期)。
== 这一轮加的 ==
1. **nginx 反代**(_build/setup_nginx.py)
Cloudflare 只能回源到特定端口(80/8080/443/2053…),
中转跑的 13245 不在列表里,所以中间必须有一层。
配置要点:
/download/ proxy_buffering on
/v1/ proxy_read_timeout 180s + **proxy_buffering off**
(长轮询要挂 60 秒;关缓冲,否则要等整个响应完
才吐给客户端,那就不是长轮询了)
2. **源站 TLS 证书**(_build/setup_tls_origin.py)
Cloudflare 的 SSL 模式是 Full(strict),回源走 443,源站只有 80
-> 522。用 certbot 的 nginx 插件签了 Let's Encrypt 证书。
开着橙色云朵也能签:HTTP-01 挑战经 Cloudflare 回源到 80 能验证。
踩的坑:第一次用 example.com 当邮箱,Let's Encrypt 拒收保留域名。
3. **公共 DNS**(_build/fix_server_dns.py)
换域名之后 **服务器自己解析不了** yk.xxmxxs.top:
Lightsail 默认的内网解析器 172.26.0.2 还缓存着"不存在"(负缓存),
而 1.1.1.1 / 8.8.8.8 已经能解析。
后果不只是监测误报 —— 服务器上任何解析该域名的东西都会失败。
配上公共 DNS,内网那个降为后备(它对 AWS 自家服务更快)。
4. **监测与预案重写**(relay/monitor-lightsail.sh、rescue-lightsail.sh)
分两层探测,才能分清是哪一层坏的:
PRIMARY https://域名 用户实际走的
FALLBACK 127.0.0.1:13245 中转本身
PRIMARY 挂 + FALLBACK 好 -> edge 层 -> 动 nginx
两个都挂 -> core 层 -> 动中转
不区分的话只能瞎重启,而 edge 层的问题重启中转一万次也没用。
做法上也改了:不再"SSH 过去用嵌套 heredoc 现场打补丁"
(这一轮因此踩了五次转义/求值/百分号格式化的坑),
改成**本地写完整文件、整份上传**,能进 git 做版本管理。
5. **后台口令改由服务器持有**(_build/set_admin_password.py)
密码只写进 /opt/ykxmz-relay/data/admin_password.txt(600),
**不进代码、不进仓库**。本地脚本从环境变量或
_build/.admin_password(已 gitignore)读取。
这个仓库已经吃过一次"管理员密码硬编码进提交历史"的教训了。
6. **验证脚本**(_build/verify_admin_login.py)
第一版只看登录请求的状态码,结果"对的密码/错的密码/不带凭据"
**全是 302** —— 等于没测。改成看 cookie 再访问 /admin,
区分拿到的是登录页还是后台页。实测:
正确密码 -> cookie 1 条 -> 后台页
错误密码 -> cookie 0 条 -> 登录页
账号写错 -> cookie 0 条 -> 登录页
经 Cloudflare 域名 -> 后台页
== 其他修复 ==
* 下载响应有**两条** Cache-Control(nginx 和 relay 各发一条),
Cloudflare 遇到重复的缓存指令可能直接跳过缓存 -> 去掉 nginx 那条
* 监测探测地址跟着监听地址改(改入口必须回头看所有探测入口)
* curl 加 -L 跟跳转(certbot 加了 http->https 301)
* 旧服务器(随身 WiFi 那台)的服务全部停掉并取消自启,
但**没关机、没删文件** —— 保持可逆
全量回归 46 个阶段通过;594 个 Python 文件语法通过。
This commit is contained in:
+133
-45
@@ -1,77 +1,165 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''完整验证:登录 -> 后台页看到「抢救报告」-> 下载报告。
|
||||
r'''真正验证后台密码是否生效(不能只看 302)。
|
||||
|
||||
上一轮失败是因为我 curl 用错了接口(POST /login 只传 password),
|
||||
实际是 POST /admin/login 且要 username + password 两个字段。
|
||||
== 为什么要重做这个测试 ==
|
||||
|
||||
第一版是这样:
|
||||
|
||||
curl -X POST -d 'password=对的' /admin/login -> 302
|
||||
curl -X POST -d 'password=错的' /admin/login -> 302
|
||||
curl /admin -> 302
|
||||
|
||||
**三个都是 302** —— 说明状态码跟密码对不对**无关**,测试等于没测。
|
||||
(实际行为:密码对了 302 到后台并种 cookie;密码错了 302 回登录页。
|
||||
单看状态码区分不出来。)
|
||||
|
||||
== 正确的验法 ==
|
||||
|
||||
1. 正确密码 -> 存 cookie -> 带着 cookie 请求 /admin
|
||||
-> 应该拿到**后台页**(含「抢救报告」「下载区」)
|
||||
2. 错误密码 -> cookie 是空的 -> 请求 /admin
|
||||
-> 应该拿到**登录页**(含 password 输入框)
|
||||
3. 账号写错、密码对 -> 也应该进不去
|
||||
4. 完全不带凭据 -> 登录页
|
||||
5. 经 Cloudflare 的域名也走一遍
|
||||
|
||||
这样才能确定"密码真的改了,而且只认这个密码"。
|
||||
|
||||
== 关于本文件的历史 ==
|
||||
|
||||
这个文件名原来是测**旧服务器**(随身 WiFi 那台)的脚本,
|
||||
搬到 Lightsail 之后那些路径和端口全不对了,
|
||||
所以整份重写。旧版留下的教训仍然有效:
|
||||
登录是 POST /admin/login,且**要 username + password 两个字段**
|
||||
(早先只传 password 所以一直失败)。
|
||||
|
||||
== 又一个 % 格式化的坑 ==
|
||||
|
||||
远端命令里有 curl 的 `%{http_code}`。**绝不能用 % 格式化拼**,
|
||||
否则抛 "unsupported format character '{'"。
|
||||
这一轮我踩了五次 —— 凡带百分号的远端命令一律用 replace 拼。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "_build"))
|
||||
from devssh import run # noqa: E402
|
||||
from lssh import run # noqa: E402
|
||||
|
||||
REMOTE = r'''
|
||||
RELAY=/root/lanremote-relay
|
||||
B=http://127.0.0.1:8080
|
||||
USERNAME = "xmz7891"
|
||||
|
||||
echo "════ 0. 账号与密码 ════"
|
||||
grep -m1 '^ADMIN_USER' $RELAY/relay.py | sed 's/^/ /'
|
||||
echo " 密码文件长度: $(wc -c < $RELAY/data/admin_password.txt) 字节"
|
||||
|
||||
def _password() -> str:
|
||||
"""从外部读密码 —— **绝不写死在代码里**(这个文件会进公开仓库)。
|
||||
|
||||
2026-10-08 这个仓库已经吃过一次"管理员密码硬编码进提交历史"的教训,
|
||||
不能再犯。优先级:
|
||||
1. 环境变量 YKXMZ_ADMIN_PASSWORD
|
||||
2. _build/.admin_password(已 gitignore,一行密码)
|
||||
"""
|
||||
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")
|
||||
|
||||
|
||||
PASSWORD = _password()
|
||||
BASE = "http://127.0.0.1:13245"
|
||||
DOMAIN_URL = "https://yk.xxmxxs.top"
|
||||
|
||||
SCRIPT = r'''
|
||||
B=__BASE__
|
||||
D=__DOMAIN__
|
||||
U=__USER__
|
||||
P=__PASS__
|
||||
JAR=/tmp/ykxmz-cookie-test.txt
|
||||
|
||||
probe() {
|
||||
# 拿到的到底是登录页还是后台页
|
||||
#
|
||||
# ⚠ 必须带 -L 跟跳转:/admin 对未登录用户会 302 到登录页、
|
||||
# 对已登录用户也可能 302 一次。不带 -L 拿到的是空 body
|
||||
# (第一版就是栽在这里,全显示"长度 0")。
|
||||
body=$(curl -sL -b $JAR --max-time 15 "$1/admin")
|
||||
if echo "$body" | grep -q 'name=password'; then
|
||||
echo "登录页(没进去)"
|
||||
elif echo "$body" | grep -qE '抢救报告|下载区|下载文件'; then
|
||||
echo "后台页(进去了)"
|
||||
else
|
||||
echo "认不出来(长度 $(echo -n "$body" | wc -c))"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "════ 0. 账号与密码文件 ════"
|
||||
grep -m1 'ADMIN_USER_DEFAULT' /opt/ykxmz-relay/relay.py | sed 's/^/ /'
|
||||
printf ' 密码文件: '; ls -la /opt/ykxmz-relay/data/admin_password.txt \
|
||||
| awk '{print $1, $3":"$4, $5" 字节"}'
|
||||
|
||||
echo
|
||||
echo "════ 1. 造报告 ════"
|
||||
printf '这是一份演练报告。\n\n 判断:**本机是好的,问题在隧道或 Cloudflare 那一侧**。\n' \
|
||||
> "$RELAY/rescue_reports/19700101-000000-demo.txt"
|
||||
ls $RELAY/rescue_reports/ | sed 's/^/ /'
|
||||
echo "════ 1. 正确密码 ════"
|
||||
rm -f $JAR
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -c $JAR -X POST \
|
||||
-d "username=$U" -d "password=$P" "$B/admin/login")
|
||||
echo " 登录 HTTP $code 种下 cookie: $(grep -c lr_admin $JAR 2>/dev/null || echo 0) 条"
|
||||
echo " 访问 /admin -> $(probe $B)"
|
||||
|
||||
echo
|
||||
echo "════ 2. 登录(POST /admin/login,username + password)════"
|
||||
PASS=$(cat $RELAY/data/admin_password.txt 2>/dev/null)
|
||||
rm -f /tmp/ck3.txt
|
||||
curl -s -c /tmp/ck3.txt -o /dev/null -w ' HTTP %{http_code}\n' --max-time 10 \
|
||||
-d "username=" -d "password=$PASS" $B/admin/login
|
||||
echo " cookie 文件:"
|
||||
grep -c lr_admin /tmp/ck3.txt 2>/dev/null | sed 's/^/ 找到 lr_admin: /'
|
||||
echo "════ 2. 错误密码 ════"
|
||||
rm -f $JAR
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -c $JAR -X POST \
|
||||
-d "username=$U" -d "password=definitely-wrong-xyz" "$B/admin/login")
|
||||
echo " 登录 HTTP $code 种下 cookie: $(grep -c lr_admin $JAR 2>/dev/null || echo 0) 条"
|
||||
echo " 访问 /admin -> $(probe $B)"
|
||||
|
||||
echo
|
||||
echo "════ 3. 带登录拿后台页 ════"
|
||||
curl -s -b /tmp/ck3.txt -o /tmp/adm3.html -w ' HTTP %{http_code} 大小 %{size_download} 字节\n' \
|
||||
--max-time 10 $B/zhongzhuanzhan
|
||||
echo " 含「抢救报告」: $(grep -c '抢救报告' /tmp/adm3.html)"
|
||||
echo " 含演练报告文件名: $(grep -c '19700101-000000-demo.txt' /tmp/adm3.html)"
|
||||
echo " 含「下载区」: $(grep -c '下载区' /tmp/adm3.html)"
|
||||
echo "════ 3. 账号写错(密码对)════"
|
||||
rm -f $JAR
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -c $JAR -X POST \
|
||||
-d "username=someone-else" -d "password=$P" "$B/admin/login")
|
||||
echo " 登录 HTTP $code 种下 cookie: $(grep -c lr_admin $JAR 2>/dev/null || echo 0) 条"
|
||||
echo " 访问 /admin -> $(probe $B)"
|
||||
|
||||
echo
|
||||
echo "════ 4. 带登录下载报告 ════"
|
||||
curl -s -b /tmp/ck3.txt -o /tmp/rep3.txt -w ' HTTP %{http_code} 大小 %{size_download} 字节\n' \
|
||||
--max-time 10 "$B/rescue/19700101-000000-demo.txt"
|
||||
echo " 内容第一行: $(head -1 /tmp/rep3.txt)"
|
||||
echo " 缓存头: $(curl -s -b /tmp/ck3.txt -D- -o /dev/null --max-time 10 \
|
||||
"$B/rescue/19700101-000000-demo.txt" | grep -i 'cache-control' | tr -d '\r')"
|
||||
echo "════ 4. 完全不带凭据 ════"
|
||||
rm -f $JAR
|
||||
echo " 访问 /admin -> $(probe $B)"
|
||||
|
||||
echo
|
||||
echo "════ 5. 不带登录(应该 401)════"
|
||||
curl -s -o /dev/null -w ' HTTP %{http_code}\n' --max-time 10 \
|
||||
"$B/rescue/19700101-000000-demo.txt"
|
||||
echo "════ 5. 经 Cloudflare 域名(正确密码)════"
|
||||
rm -f $JAR
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -c $JAR -X POST \
|
||||
-d "username=$U" -d "password=$P" "$D/admin/login")
|
||||
echo " 登录 HTTP $code 种下 cookie: $(grep -c lr_admin $JAR 2>/dev/null || echo 0) 条"
|
||||
echo " 访问 /admin -> $(probe $D)"
|
||||
|
||||
echo
|
||||
echo "════ 6. X-Admin 头(脚本通道)也要能下 ════"
|
||||
curl -s -o /dev/null -w ' HTTP %{http_code}\n' --max-time 10 \
|
||||
-H "X-Admin: $PASS" "$B/rescue/19700101-000000-demo.txt"
|
||||
echo "════ 6. 密码有没有泄漏到代码里 ════"
|
||||
if grep -rq "$P" /opt/ykxmz-relay/*.py /opt/ykxmz-relay/*.sh 2>/dev/null; then
|
||||
echo " ✗ 代码/脚本里出现了明文密码"
|
||||
else
|
||||
echo " ✓ 代码和脚本里都没有明文密码"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "════ 清理 ════"
|
||||
rm -f "$RELAY/rescue_reports/19700101-000000-demo.txt" /tmp/ck3.txt /tmp/adm3.html /tmp/rep3.txt
|
||||
rm -f $JAR
|
||||
'''
|
||||
|
||||
|
||||
def main() -> int:
|
||||
out = run(REMOTE, timeout=200)
|
||||
text = out[1] if isinstance(out, tuple) else str(out)
|
||||
print(text)
|
||||
print("=" * 62)
|
||||
print(" 真正验证后台密码")
|
||||
print("=" * 62)
|
||||
out = run(SCRIPT.replace("__BASE__", BASE)
|
||||
.replace("__DOMAIN__", DOMAIN_URL)
|
||||
.replace("__USER__", USERNAME)
|
||||
.replace("__PASS__", PASSWORD), timeout=300, quiet=True)
|
||||
print(out[1] if isinstance(out, tuple) else out)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user