两处都是「看起来成功、实际给了错误信息/留下凭据」的问题。
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。
278 lines
12 KiB
Bash
Executable File
278 lines
12 KiB
Bash
Executable File
#!/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
|