Files
webui4frpc/scripts/criteria/config-startup.sh
JianFeeeee f6c877b736 test(criteria): 收紧失败语义判据——只认「没进入监听」,并改用空闲端口
复核发现「坏 JSON 静默忽略」这条变异没被咬住:原判据只看退出码,而错误串
里本来就含 invalid JSON、端口又固定在 17699,实现把 Fatal 改成打日志后仍
可能因端口冲突拿到非零退出码,于是被误判为"已正确报错"。

- 新增 assert_died_before_listen:只要日志里出现 "listening on",就判失败。
  配置非法时进程必须启动前退出,不能带着默认口令继续提供服务。
- 所有启动分支改用 free_port 分配空闲端口——判据不能依赖宿主机上恰好没有
  进程占用固定端口(默认 7500 上通常就有生产实例)。
- 顺带断言配置文件里的 addr 真的生效(必须在该端口上监听),避免"进程活着"
  被当成"配置生效"。
2026-10-23 09:17:09 +08:00

121 lines
5.7 KiB
Bash
Executable File
Raw 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.

#!/bin/bash
# 判据:webui4frpc 支持从配置文件启动,且口令不泄漏。
#
# 覆盖三件事:
# 1. go build ./... 与 go test ./... 全绿(含配置文件优先级的回归测试);
# 2. 优先级 显式 flag > 配置文件 > 内置默认值 —— 未显式传的 flag 不得用
# 默认值压过配置文件;
# 3. 用配置文件(file: 引用口令)启动后,明文口令既不出现在进程命令行,
# 也不出现在启动日志;坏 JSON 必须非零退出,文件缺失必须能正常启动。
#
# 退出码 0 = 通过。
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT" || { echo "FAIL: 无法进入仓库根 $ROOT"; exit 1; }
WORK="$(mktemp -d)"
PIDS=()
cleanup() { for p in "${PIDS[@]:-}"; do kill "$p" 2>/dev/null; done; wait 2>/dev/null; rm -rf "$WORK"; }
trap cleanup EXIT
fail() { echo "FAIL: $*"; exit 1; }
# 取一个空闲端口。判据里必须用随机空闲端口:默认 127.0.0.1:7500 上通常已有
# 生产实例在跑,端口冲突造成的非零退出会被误判成"配置错误已正确报错"。
free_port() {
python3 -c 'import socket
s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()'
}
# 断言:进程在配置文件非法时"没进入监听"就必须死。
# 只检查退出码不够——错误串里本来就含 "invalid JSON",实现把 Fatal 改成
# 打日志后继续跑,退出码可能因为端口冲突等原因仍非零。真正的判据是它不能再
# 继续提供服务,所以出现 listening 日志即判失败。
assert_died_before_listen() {
local log="$1" why="$2"
if grep -q 'listening on' "$log"; then
cat "$log"
fail "$why:配置错误被静默忽略后仍进入监听(应启动前就退出)"
fi
}
# ---- 1. 构建与全量测试 ----
go build ./... || fail "go build ./..."
go test ./... > "$WORK/test.log" 2>&1 || { cat "$WORK/test.log"; fail "go test ./..."; }
# ---- 2. 优先级回归测试必须存在且通过 ----
# 这是整个改动最容易做错的一点:Go 的 flag 默认值会无条件覆盖配置文件。
go test ./internal/appconfig/ -count=1 \
-run 'TestResolveFileBeatsFlagDefaults|TestResolveExplicitFlagBeatsFile|TestLoadRejectsUnknownKey|TestSummaryRedactsSecrets' \
> "$WORK/appconfig.log" 2>&1 || { cat "$WORK/appconfig.log"; fail "appconfig 优先级/脱敏回归测试未通过"; }
# ---- 3. 端到端:配置文件启动,口令不落到命令行与日志 ----
BIN="$WORK/w4f"
go build -o "$BIN" ./cmd/webui4frpc || fail "构建二进制失败"
SECRET="CRIT_SECRET_${RANDOM}_9f3a"
printf '%s\n' "$SECRET" > "$WORK/pw"
chmod 600 "$WORK/pw"
PORT="$(free_port)"
python3 -c '
import json, sys
t, addr = sys.argv[1], sys.argv[2]
json.dump({"addr": addr, "user": "crituser",
"password": "file:" + t + "/pw", "workdir": t + "/data"},
open(t + "/cfg.json", "w"))
' "$WORK" "127.0.0.1:$PORT"
# 注意:-config 只是路径,口令来自 file: 引用,命令行本身不含秘密。
"$BIN" -config "$WORK/cfg.json" > "$WORK/run.log" 2>&1 &
P=$!
PIDS+=("$P")
sleep 3
kill -0 "$P" 2>/dev/null || { cat "$WORK/run.log"; fail "进程未能从配置文件启动"; }
if tr '\0' ' ' < "/proc/$P/cmdline" | grep -q -- "$SECRET"; then
fail "明文口令出现在进程命令行(ps 可见)"
fi
if grep -q -- "$SECRET" "$WORK/run.log"; then
cat "$WORK/run.log"
fail "明文口令出现在启动日志"
fi
grep -q "listening on 127.0.0.1:$PORT" "$WORK/run.log" \
|| { cat "$WORK/run.log"; fail "配置文件里的 addr 未生效(未在 $PORT 上监听)"; }
kill "$P" 2>/dev/null; wait "$P" 2>/dev/null
# ---- 4. 文件存在但 JSON 非法 -> 响亮报错退出 ----
# 用 timeout 兜底:若实现退化成"只记日志继续启动",进程会一直跑而不是报错,
# 这里必须把它判成失败,不能把自己挂死。
printf '{"addr":' > "$WORK/bad.json"
timeout 10 "$BIN" -config "$WORK/bad.json" \
-addr "127.0.0.1:$(free_port)" -workdir "$WORK/d_bad" > "$WORK/bad.log" 2>&1
rc=$?
[ "$rc" -eq 124 ] && { cat "$WORK/bad.log"; fail "配置文件 JSON 非法时进程没有退出(静默忽略),判据要求响亮报错"; }
[ "$rc" -eq 0 ] && { cat "$WORK/bad.log"; fail "配置文件 JSON 非法时应非零退出,实际正常退出"; }
assert_died_before_listen "$WORK/bad.log" "JSON 非法"
grep -qi 'invalid JSON' "$WORK/bad.log" || { cat "$WORK/bad.log"; fail "非法 JSON 的报错信息不明确"; }
# ---- 5. 未知键(拼写错误)-> 报错退出,不得静默回落到默认口令 ----
printf '{"passwd":"x"}' > "$WORK/typo.json"
timeout 10 "$BIN" -config "$WORK/typo.json" \
-addr "127.0.0.1:$(free_port)" -workdir "$WORK/d_typo" > "$WORK/typo.log" 2>&1
rc=$?
[ "$rc" -eq 124 ] && { cat "$WORK/typo.log"; fail "未知键被静默忽略(进程继续运行),拼错的 password 会回落到默认值"; }
[ "$rc" -eq 0 ] && { cat "$WORK/typo.log"; fail "未知键应被拒绝(否则拼错的 password 会静默使用默认值)"; }
assert_died_before_listen "$WORK/typo.log" "未知键"
# ---- 6. 配置文件缺失 -> 不算错误,回落到纯 flag 行为 ----
"$BIN" -config "$WORK/none.json" -addr "127.0.0.1:$(free_port)" -workdir "$WORK/d2" > "$WORK/miss.log" 2>&1 &
P2=$!
PIDS+=("$P2")
sleep 3
kill -0 "$P2" 2>/dev/null || { cat "$WORK/miss.log"; fail "配置文件缺失时应回落到纯 flag 行为并正常启动"; }
kill "$P2" 2>/dev/null; wait "$P2" 2>/dev/null
# ---- 7. 交付的单元/示例配置不得携带明文口令 ----
if grep -rn -- '-password' packaging/common/webui4frpc.service \
packaging/common/config.json scripts/release.sh 2>/dev/null \
| grep -v 'never as -password'; then
fail "打包产物(单元/示例配置/分发模板)仍含命令行口令"
fi
echo "OK: 配置文件启动与口令不泄漏的判据全部通过"