Files
HomeDash/deploy/zd-host_test.sh
JianFeeeee aae3fb7065 fix(deploy): 历史条数不再假报 0,明文令牌的 /tmp env 文件必删
两处都是「看起来成功、实际给了错误信息/留下凭据」的问题。

A. 历史条数假数据(行为变化)
   旧逻辑:ADMIN_TOK=$(grep ... || true) 取不到时仍发一个空的 `Bearer `
   请求;第 1 步把 history-server 改成 fail-closed 后它必然回 401 JSON,
   而 `grep -o '"url"' | wc -l` 对错误响应数出 0,再被 HIST=${HIST:-0}
   兜一层,于是对外打印「历史条数 0」—— 一个假数据,看起来像历史被清空。
   现在抽成 human_history_count(),三条路径分开:
     · 未配置 NAVI_ADMIN_TOKEN → 「未知(未配置…跳过探测)」,不发任何请求
     · 非 200(401/502/连不上的 000) → 「未知(HTTP xxx)」,不数 body
     · 只有 200 才数 '"url"' 出现次数
   ⚠️ 行为变化:token 取不到时历史条数显示「未知」而不是「0」;
   真实的 0 条(200 + 空列表)仍然显示 0。仍旧绝不阻断升级。

B. 明文令牌的 env 文件残留
   /tmp/navi-host-<stamp>.env 含 NAVI_ADMIN_TOKEN / NAVI_SYNC_TOKEN 明文,
   chmod 600 只防别的用户读,不防它一直躺在 /tmp。现在:
     · 统一为 $ENV_FILE(含 .t / .new 中间文件),不再散落字面量路径
     · 新增 cleanup_env_file(),同时删三个文件
     · 注册 trap ... EXIT(任何 exit 都删)+ fail() 回滚路径显式调用
       + 成功收尾显式调用(三处)
   删它不影响已起容器:docker run 已在启动时把值注入容器 env。

顺带确认(未改动,仅补注释):重置门户口令后打印一次 $NEWPW 是有意设计
——那是管理员唯一一次拿到它的机会(数据卷只存哈希),注释已写明
「只打印这一次」,且不写日志文件、不进任何 env。

新增 deploy/zd-host_test.sh(bash deploy/zd-host_test.sh,退出码 0=全绿):
1. bash -n 语法检查;
2. 静态护栏:轮换只打印 ${#VAR} 长度、无 set -x、无 HIST=${HIST:-0}、
   无「grep url | wc -l || echo 0」旧写法、必须校验 HTTP 状态码、
   env 文件在 fail() 与 EXIT trap 与成功路径上都有删除、中间文件也删;
3. 用 BEGIN/END 标记把 human_history_count 原样抠出来,配 stub curl 单测
   空 token / 401 / 502 / 连接失败 / 200 三条 / 200 空列表 六种情形,
   并断言空 token 时根本没发请求、有 token 时带了 Authorization 头。

反向验证:把历史条数换回旧写法并删掉三处 env 文件清理后,
测试从 26 通过 / 0 失败变成 10 通过 / 16 失败(列出「仍有 HIST=${HIST:-0}」
「fail() 回滚路径没有删 env 文件」「空 token 时仍然调用了 curl」等)。
另外用第 1 步的真实 history-server + 模拟两层鉴权的反代做了端到端复核,
旧写法在 401/连接失败下都报 0,新写法分别报「未知(HTTP 401)」「未知(HTTP 000)」,
200 + 3 条报 3,200 + 空列表报 0。
2026-10-22 23:25:26 +08:00

278 lines
12 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# deploy/zd-host.sh 的静态护栏 + 逻辑单测。
#
# 为什么需要它:zd-host.sh 是「一跑就动线上」的部署脚本,没法在 CI 里真跑。
# 于是把可验证的部分拆成两层:
# ① 静态护栏:对脚本正文做断言(令牌只打印长度、没有假数据路径、env 文件必删)
# ② 逻辑抽取:把 human_history_count 那段函数原样抠出来,用 stub curl 单测
#
# 用法:bash deploy/zd-host_test.sh (退出码 0 = 全绿)
# 依赖:bash / grep / sed / awk / mktemp,无网络、无 docker。
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TARGET="$HERE/zd-host.sh"
PASS=0
FAIL=0
ok() { PASS=$((PASS+1)); printf ' \033[32m✓\033[0m %s\n' "$1"; }
bad() { FAIL=$((FAIL+1)); printf ' \033[31m✗\033[0m %s\n' "$1"; [ -n "${2:-}" ] && printf ' %s\n' "$2"; }
head_() { printf '\n== %s ==\n' "$1"; }
[ -f "$TARGET" ] || { echo "找不到 $TARGET" >&2; exit 1; }
# ────────────────────────────────────────────────────────────
head_ "1. 语法检查(bash -n)"
# ────────────────────────────────────────────────────────────
if bash -n "$TARGET" 2>/tmp/zdnsyn.$$; then
ok "bash -n deploy/zd-host.sh 通过"
else
bad "bash -n 失败" "$(cat /tmp/zdnsyn.$$)"
fi
rm -f /tmp/zdnsyn.$$
# ────────────────────────────────────────────────────────────
head_ "2a. 令牌轮换只打印长度,不打印值"
# ────────────────────────────────────────────────────────────
# 轮换段必须用 ${#VAR}(长度)形式打印
len_prints=$(grep -c '新值长度 \${#NAVI_ROTATE_' "$TARGET" || true)
if [ "${len_prints:-0}" -ge 2 ]; then
ok "两处轮换都只打印长度(${len_prints} 处)"
else
bad "令牌轮换的长度打印少于 2 处(实际 ${len_prints:-0})"
fi
# 不得出现「echo ... $NAVI_ROTATE_xxx 的值本身」这种写法:
# 即 echo 行里引用变量却没有用 ${#VAR} 取长度。
# 例外:`echo "NAVI_ADMIN_TOKEN=..." >> "$ENV_FILE.t"` 是**写进 env 文件**(容器启动要用),
# 不是打到 stdout/日志,因此排除带 `>>` 重定向的行。
leak=$(grep -nE '^[^#]*echo[^#]*\$\{?NAVI_ROTATE_(ADMIN|SYNC)_TOKEN' "$TARGET" \
| grep -v '\${#NAVI_ROTATE_' \
| grep -v '>>' || true)
if [ -z "$leak" ]; then
ok "没有 echo 直接打印 NAVI_ROTATE_*_TOKEN 的值"
else
bad "发现可能打印令牌值的 echo 行" "$leak"
fi
# 全脚本不得出现 set -x(会把 env/变量回显到日志)
if grep -qE '^[[:space:]]*set[[:space:]]+-[a-z]*x' "$TARGET"; then
bad "脚本里出现 set -x,会把变量值回显到日志"
else
ok "没有 set -x"
fi
# ────────────────────────────────────────────────────────────
head_ "2b. ADMIN_TOK 缺失时不得打印「历史条数 0」"
# ────────────────────────────────────────────────────────────
if grep -q 'if \[ -z "\$tok" \]' "$TARGET"; then
ok "human_history_count 里有 token 为空的分支"
else
bad "human_history_count 缺少「token 为空」分支"
fi
if grep -q '未知(未配置 NAVI_ADMIN_TOKEN,跳过探测)' "$TARGET"; then
ok "空 token 时输出「未知(…跳过探测)」"
else
bad "空 token 分支没有输出「未知」文案"
fi
# 假的 0 从哪来:旧写法 `| grep -o '"url"' | wc -l || echo 0` + `HIST=${HIST:-0}`。
if grep -qE 'HIST=\$\{HIST:-0\}' "$TARGET"; then
bad "仍有 HIST=\${HIST:-0} 兜底,会把「取不到」变成「0 条」"
else
ok "没有 HIST=\${HIST:-0} 假数据兜底"
fi
if grep -nE 'grep -o .\"url\". \| wc -l \|\| echo 0' "$TARGET" >/dev/null 2>&1; then
bad "仍有「grep url | wc -l || echo 0」的历史条数写法"
else
ok "没有「grep url | wc -l || echo 0」的旧写法"
fi
# 必须校验 HTTP 状态码(非 200 不数 body)
if grep -q "code.*!= \"200\"" "$TARGET"; then
ok "探测时校验 HTTP 状态码,非 200 不数 body"
else
bad "探测没有校验 HTTP 状态码"
fi
if grep -q "未知(HTTP " "$TARGET"; then
ok "非 200 时输出「未知(HTTP xxx)」"
else
bad "非 200 时没有「未知(HTTP xxx)」输出"
fi
# ────────────────────────────────────────────────────────────
head_ "2c. 明文令牌的 env 文件在成功与失败路径都必须删除"
# ────────────────────────────────────────────────────────────
fail_body=$(awk '/^fail\(\) \{/,/^}/' "$TARGET")
if printf '%s' "$fail_body" | grep -q 'cleanup_env_file'; then
ok "fail() 回滚路径会调 cleanup_env_file"
else
bad "fail() 回滚路径没有删 env 文件"
fi
if grep -qE '^\s*trap cleanup_env_file EXIT' "$TARGET"; then
ok "设置了 EXIT trap(异常退出也会删)"
else
bad "没有 EXIT trap 兜底删除 env 文件"
fi
# 成功路径:在 rm -f "$REMOTE_TAR" 附近也要有一次显式 cleanup
if grep -qE '^cleanup_env_file' "$TARGET"; then
ok "成功收尾路径显式调 cleanup_env_file"
else
bad "成功收尾路径没有显式删 env 文件"
fi
if grep -q 'rm -f "\$ENV_FILE" "\$ENV_FILE.t" "\$ENV_FILE.new"' "$TARGET"; then
ok "cleanup 同时删 .t / .new 中间文件"
else
bad "cleanup 漏删中间文件(.t / .new 也含明文令牌)"
fi
# ────────────────────────────────────────────────────────────
head_ "3. human_history_count 逻辑单测(stub curl)"
# ────────────────────────────────────────────────────────────
TMPD=$(mktemp -d)
trap 'rm -rf "$TMPD"' EXIT
# 抽取函数体(由脚本里的 BEGIN/END 标记界定;没标记说明被人删了标记,直接失败)
awk '/^# >>> human_history_count BEGIN/,/^# <<< human_history_count END/' "$TARGET" > "$TMPD/func.sh"
if [ -s "$TMPD/func.sh" ]; then
ok "成功抽取 human_history_count 函数"
else
bad "抽取 human_history_count 失败(BEGIN/END 标记缺失?)"
fi
# stub curl:用环境变量控制行为,并记录「是否被调用」
mkdir -p "$TMPD/bin"
cat > "$TMPD/bin/curl" <<'STUB'
#!/usr/bin/env bash
# 只实现被 human_history_count 用到的那几个参数:-s -m N -o FILE -w '%{http_code}' -H ...
out=""
while [ $# -gt 0 ]; do
case "$1" in
-o) out="$2"; shift 2 ;;
-H|-m) shift 2 ;;
-w) shift 2 ;;
-*) shift ;;
*) shift ;;
esac
done
echo "called" >> "${STUB_CALLS:-/dev/null}"
case "${STUB_MODE:-ok}" in
ok) [ -n "$out" ] && printf '%s' "${STUB_BODY:-}" > "$out"; printf '200'; exit 0 ;;
401) [ -n "$out" ] && printf '%s' '{"error":"invalid or missing token"}' > "$out"; printf '401'; exit 0 ;;
502) [ -n "$out" ] && printf '%s' '<html>502 Bad Gateway</html>' > "$out"; printf '502'; exit 0 ;;
conn) # 连不上:curl 会打印 000 并返回非 0
[ -n "$out" ] && : > "$out"; printf '000'; exit 7 ;;
esac
exit 0
STUB
chmod +x "$TMPD/bin/curl"
run_hist() { # $1=token ; 其余由 STUB_* 控制
PATH="$TMPD/bin:$PATH" STUB_CALLS="$TMPD/calls" \
bash -c 'set -uo pipefail; . "$1"; human_history_count "$2"' _ "$TMPD/func.sh" "$1"
}
BODY3='{"items":[{"url":"https://a.example.com/"},{"url":"https://b.example.com/"},{"url":"https://c.example.com/"}],"total":3}'
# 3.1 空 token → 跳过探测,输出「未知」,且**不能**调用 curl(更不能打印 0)
rm -f "$TMPD/calls"
got=$(STUB_MODE=ok STUB_BODY="$BODY3" run_hist "" )
if printf '%s' "$got" | grep -q '未知' && ! printf '%s' "$got" | grep -qx '0'; then
ok "空 token → 「未知」而不是 0(输出: $got)"
else
bad "空 token 时输出了非预期内容" "got=[$got]"
fi
if [ ! -f "$TMPD/calls" ]; then
ok "空 token 时完全没有发请求(没带空 Bearer 打过去)"
else
bad "空 token 时仍然调用了 curl"
fi
# 3.2 有 token 但接口 401 → 「未知(HTTP 401)」,不是 0
got=$(STUB_MODE=401 run_hist "some-admin-token")
if printf '%s' "$got" | grep -q '未知(HTTP 401' ; then
ok "401 → 「未知(HTTP 401)」(输出: $got)"
else
bad "401 时没有输出「未知(HTTP 401)」" "got=[$got]"
fi
if printf '%s' "$got" | grep -qx '0'; then
bad "401 时把响应体当数据数出了 0 条"
else
ok "401 时不会数 body"
fi
# 3.3 502(body 是 HTML)→ 「未知(HTTP 502)」,不得把 HTML 当数据
got=$(STUB_MODE=502 run_hist "some-admin-token")
if printf '%s' "$got" | grep -q '未知(HTTP 502'; then
ok "502 → 「未知(HTTP 502)」"
else
bad "502 时没有输出「未知(HTTP 502)」" "got=[$got]"
fi
# 3.4 连不上(curl 打印 000 且返回非 0)→ 「未知(HTTP 000)」,且提示里不出现重复的 000
got=$(STUB_MODE=conn run_hist "some-admin-token")
if printf '%s' "$got" | grep -q '未知(HTTP 000'; then
ok "连接失败 → 「未知(HTTP 000)」(输出: $got)"
else
bad "连接失败时输出不符合预期" "got=[$got]"
fi
if printf '%s' "$got" | grep -qE '000[[:space:]]*000'; then
bad "状态码被拼成了两个 000" "got=[$got]"
else
ok "状态码已归一化(没有重复 000)"
fi
# 3.5 200 且 3 条 → 输出 3
got=$(STUB_MODE=ok STUB_BODY="$BODY3" run_hist "some-admin-token")
if [ "$got" = "3" ]; then
ok "200 + 3 条 → 输出 3"
else
bad "200 时条数不对" "got=[$got] want=[3]"
fi
# 3.6 200 但 0 条 → 输出 0(这是**真实**的 0,必须能显示出来)
got=$(STUB_MODE=ok STUB_BODY='{"items":[],"total":0}' run_hist "some-admin-token")
if [ "$got" = "0" ]; then
ok "200 + 空列表 → 输出 0(真实的 0 仍然显示)"
else
bad "真实 0 条时输出不对" "got=[$got] want=[0]"
fi
# 3.7 有 token 时确实带上了 Authorization 头(用 stub 校验参数)
cat > "$TMPD/bin/curl" <<'STUB2'
#!/usr/bin/env bash
args="$*"
echo "$args" >> "$STUB_ARGS"
out=""
while [ $# -gt 0 ]; do
case "$1" in
-o) out="$2"; shift 2 ;;
*) shift ;;
esac
done
[ -n "$out" ] && printf '{"items":[{"url":"x"}]}' > "$out"
printf '200'
STUB2
chmod +x "$TMPD/bin/curl"
rm -f "$TMPD/args"
PATH="$TMPD/bin:$PATH" STUB_ARGS="$TMPD/args" \
bash -c 'set -uo pipefail; . "$1"; human_history_count "tok-abc"' _ "$TMPD/func.sh" >/dev/null
if grep -q 'Authorization: Bearer tok-abc' "$TMPD/args"; then
ok "有 token 时请求带 Authorization: Bearer"
else
bad "请求没带 Authorization 头" "$(cat "$TMPD/args" 2>/dev/null)"
fi
if grep -q '/admin/api/history' "$TMPD/args"; then
ok "探测打的是 /admin/api/history(不是根 /api/)"
else
bad "探测地址不对" "$(cat "$TMPD/args" 2>/dev/null)"
fi
# ────────────────────────────────────────────────────────────
printf '\n== 结果 ==\n通过 %d,失败 %d\n' "$PASS" "$FAIL"
[ "$FAIL" -eq 0 ] || exit 1