mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-10-31 06:17:20 +00:00
internal/httpapi 用 //go:embed all:dist 嵌入前端,dist 是构建期产物且被 .gitignore 排除。干净 clone 上直接 go build 会报 "pattern all:dist: no matching files found"——这是基线既有状态,与本判据 要验证的东西无关,但会让判据在任意 checkout 上都红,失去可复现性。 现在若 dist 缺失就先 cd web 构建并拷回 internal/httpapi/dist(无 npm 时 给出明确的失败原因与手工步骤)。已实测:删掉 dist 后跑判据可自动补齐并全绿。
136 lines
6.7 KiB
Bash
Executable File
136 lines
6.7 KiB
Bash
Executable File
#!/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
|
||
}
|
||
|
||
# ---- 0. 前端嵌入产物 ----
|
||
# internal/httpapi 用 //go:embed all:dist 嵌入前端,而 dist 是构建期产物、
|
||
# 被 .gitignore 排除。干净 clone 上不先构建前端,go build 会直接报
|
||
# "pattern all:dist: no matching files found"——那是基线既有状态,与本判据
|
||
# 无关,所以这里先补齐,让判据在任何 checkout 上都能复现。
|
||
if [ ! -f internal/httpapi/dist/index.html ]; then
|
||
command -v npm >/dev/null 2>&1 \
|
||
|| fail "缺少前端构建产物 internal/httpapi/dist 且没有 npm;请在有 node 的环境跑,或先手工 cd web && npm ci && npm run build && cp -r dist ../internal/httpapi/dist"
|
||
( cd web \
|
||
&& { [ -d node_modules ] || npm ci --no-audit --no-fund >/dev/null 2>&1; } \
|
||
&& npm run build >/dev/null 2>&1 ) || fail "前端构建失败(cd web && npm run build)"
|
||
rm -rf internal/httpapi/dist && mkdir -p internal/httpapi/dist
|
||
cp -r web/dist/. internal/httpapi/dist/ || fail "拷贝前端产物失败"
|
||
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: 配置文件启动与口令不泄漏的判据全部通过"
|