415 lines
22 KiB
Bash
Executable File
415 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 安全测试脚本 - 12 个攻击面
|
||
# 用法: ./security_test.sh
|
||
set -u
|
||
|
||
BASE=http://localhost:8080
|
||
UPLOADS=/Users/taochen/llm/tmp-upload/data/uploads
|
||
TMPDIR=/tmp/sec_test
|
||
mkdir -p "$TMPDIR"
|
||
# 用 find 兜底, 避免 zsh 严格 glob + 含特殊字符文件名清理不彻底
|
||
find "$UPLOADS" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null
|
||
ls -A "$UPLOADS" 2>/dev/null | while read f; do rm -rf "$UPLOADS/$f"; done 2>/dev/null
|
||
|
||
PASS=0
|
||
FAIL=0
|
||
NOTES=()
|
||
|
||
ok() { PASS=$((PASS+1)); printf " \033[32m✓ PASS\033[0m %s\n" "$1"; }
|
||
ko() { FAIL=$((FAIL+1)); printf " \033[31m✗ FAIL\033[0m %s\n" "$1"; echo " $2"; }
|
||
note() { printf " \033[36mℹ NOTE\033[0m %s\n" "$1"; }
|
||
section() { printf "\n\033[1m== %s ==\033[0m\n" "$1"; }
|
||
|
||
upload() {
|
||
local file="$1"
|
||
local fname="$2"
|
||
curl -s -o /dev/null -w "%{http_code}" -F "file=@${file};filename=${fname}" "$BASE/upload"
|
||
}
|
||
|
||
status() {
|
||
curl -s -o /dev/null -w "%{http_code}" "$@"
|
||
}
|
||
|
||
# 上传一个 sentinel 文件供下载测试
|
||
echo "secret-sentinel" > "$TMPDIR/sentinel.txt"
|
||
upload "$TMPDIR/sentinel.txt" "sentinel.txt" > /dev/null
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "A. 路径穿越 - 下载 (/download/:filename)"
|
||
# ──────────────────────────────────────────────────────────────
|
||
A=(
|
||
"../main.go"
|
||
"..%2Fmain.go"
|
||
"%2E%2E%2Fmain.go"
|
||
"..%5Cmain.go"
|
||
"....//main.go"
|
||
"sentinel.txt%00.jpg"
|
||
"sentinel.txt/../sentinel.txt"
|
||
)
|
||
for f in "${A[@]}"; do
|
||
code=$(status "$BASE/download/$(printf %s "$f" | sed 's|/|%2F|g')")
|
||
if [[ "$code" == "404" || "$code" == "400" ]]; then
|
||
ok "GET /download/$f -> $code (denied)"
|
||
else
|
||
ko "GET /download/$f -> $code" "expected 404/400, got $code"
|
||
fi
|
||
done
|
||
|
||
# 显式下载 sentinel 看是否成功(基线)
|
||
code=$(status "$BASE/download/sentinel.txt")
|
||
[[ "$code" == "200" ]] && ok "GET /download/sentinel.txt -> 200 (baseline)" || ko "baseline" "got $code"
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "B. 路径穿越 - 删除 (DELETE /files/:filename)"
|
||
# ──────────────────────────────────────────────────────────────
|
||
B=(
|
||
"../main.go"
|
||
"..%2Fmain.go"
|
||
"..%5Cmain.go"
|
||
".."
|
||
"."
|
||
)
|
||
for f in "${B[@]}"; do
|
||
code=$(status -X DELETE "$BASE/files/$(printf %s "$f" | sed 's|/|%2F|g; s|\\|%5C|g')")
|
||
if [[ "$code" == "404" || "$code" == "400" ]]; then
|
||
ok "DELETE /files/$f -> $code (denied)"
|
||
else
|
||
ko "DELETE /files/$f -> $code" "expected 404/400"
|
||
fi
|
||
done
|
||
|
||
# 显式删除 sentinel(应成功)
|
||
code=$(status -X DELETE "$BASE/files/sentinel.txt")
|
||
[[ "$code" == "200" ]] && ok "DELETE /files/sentinel.txt -> 200 (baseline)" || ko "delete baseline" "got $code"
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "C. 路径穿越 - 上传时 filename"
|
||
# ──────────────────────────────────────────────────────────────
|
||
echo "evil" > "$TMPDIR/evil.txt"
|
||
# 路径分隔符 / 应被剥离, Linux 下 \ 不是分隔符
|
||
# 用 parallel arrays 避免 associative array 里的路径展开问题
|
||
test_fnames=("../evil.txt" "foo/../../evil.txt" "/etc/passwd")
|
||
test_expects=("evil.txt" "evil.txt" "passwd")
|
||
for idx in "${!test_fnames[@]}"; do
|
||
fname="${test_fnames[$idx]}"
|
||
exp="${test_expects[$idx]}"
|
||
resp=$(curl -s -F "file=@$TMPDIR/evil.txt;filename=$fname" "$BASE/upload")
|
||
if echo "$resp" | grep -q "\"filename\":\"$exp\""; then
|
||
ok "upload filename='$fname' -> saved as '$exp' (basename stripped)"
|
||
else
|
||
ko "upload filename='$fname'" "expected '$exp', resp=$resp"
|
||
fi
|
||
curl -s -X DELETE "$BASE/files/$exp" > /dev/null
|
||
done
|
||
# Linux 下 backslash 不是路径分隔符, 整个串作为合法 basename
|
||
fname='..\evil.txt'
|
||
resp=$(curl -s -F "file=@$TMPDIR/evil.txt;filename=$fname" "$BASE/upload")
|
||
got=$(printf '%s' "$resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('filename',''))" 2>/dev/null)
|
||
if [[ "$got" == '..\evil.txt' ]]; then
|
||
ok "Linux 下 filename='$fname' 原样保留 (\\ 非分隔符,无穿越风险)"
|
||
curl -s -X DELETE "$BASE/files/..%5Cevil.txt" > /dev/null
|
||
else
|
||
ko "filename='$fname'" "got=[$got]"
|
||
fi
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "D. XSS - 上传 HTML 看是否被当 attachment 强制下载"
|
||
# ──────────────────────────────────────────────────────────────
|
||
cat > "$TMPDIR/xss.html" <<'HTML'
|
||
<html><body><script>alert('xss')</script></body></html>
|
||
HTML
|
||
upload "$TMPDIR/xss.html" "xss.html" > /dev/null
|
||
hdrs=$(curl -s -D - -o /dev/null "$BASE/download/xss.html")
|
||
ctype=$(echo "$hdrs" | tr -d '\r' | awk -F': ' '/^[Cc]ontent-[Tt]ype/ {print $2}')
|
||
cdisp=$(echo "$hdrs" | tr -d '\r' | awk -F': ' '/^[Cc]ontent-[Dd]isposition/ {print $2}')
|
||
xcto=$(echo "$hdrs" | tr -d '\r' | awk -F': ' '/^X-Content-Type-Options/ {print $2}')
|
||
if echo "$cdisp" | grep -qi 'attachment' && echo "$xcto" | grep -qi 'nosniff'; then
|
||
ok "HTML: Content-Disposition=attachment + X-Content-Type-Options=nosniff (XSS blocked)"
|
||
note "Content-Type=$ctype"
|
||
else
|
||
ko "XSS via HTML" "CD=[$cdisp] X-CTO=[$xcto] CT=[$ctype]"
|
||
fi
|
||
curl -s -X DELETE "$BASE/files/xss.html" > /dev/null
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "E. XSS - 上传 SVG"
|
||
# ──────────────────────────────────────────────────────────────
|
||
cat > "$TMPDIR/xss.svg" <<'SVG'
|
||
<svg xmlns="http://www.w3.org/2000/svg"><script>alert('svg-xss')</script></svg>
|
||
SVG
|
||
upload "$TMPDIR/xss.svg" "xss.svg" > /dev/null
|
||
hdrs=$(curl -s -D - -o /dev/null "$BASE/download/xss.svg")
|
||
cdisp=$(echo "$hdrs" | tr -d '\r' | awk -F': ' '/^[Cc]ontent-[Dd]isposition/ {print $2}')
|
||
xcto=$(echo "$hdrs" | tr -d '\r' | awk -F': ' '/^X-Content-Type-Options/ {print $2}')
|
||
if echo "$cdisp" | grep -qi 'attachment' && echo "$xcto" | grep -qi 'nosniff'; then
|
||
ok "SVG: Content-Disposition=attachment + nosniff (XSS blocked)"
|
||
else
|
||
ko "SVG XSS" "CD=[$cdisp] X-CTO=[$xcto]"
|
||
fi
|
||
curl -s -X DELETE "$BASE/files/xss.svg" > /dev/null
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "F. 文件大小绕过"
|
||
# ──────────────────────────────────────────────────────────────
|
||
dd if=/dev/zero of="$TMPDIR/big.bin" bs=1m count=120 status=none
|
||
code=$(upload "$TMPDIR/big.bin" "big.bin")
|
||
[[ "$code" == "413" ]] && ok "120MB upload rejected with 413" || ko "120MB upload" "got $code"
|
||
rm -f "$TMPDIR/big.bin"
|
||
|
||
# 空文件
|
||
: > "$TMPDIR/empty.txt"
|
||
code=$(upload "$TMPDIR/empty.txt" "empty.txt")
|
||
[[ "$code" == "200" ]] && ok "empty file (0 bytes) accepted with 200" || ko "empty file" "got $code"
|
||
curl -s -X DELETE "$BASE/files/empty.txt" > /dev/null
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "G. TOCTOU 竞态 - 同名文件 20 路并发上传"
|
||
# ──────────────────────────────────────────────────────────────
|
||
rm -f "$TMPDIR/race.txt" "$UPLOADS"/race.txt "$UPLOADS"/"race ("*").txt" 2>/dev/null
|
||
echo "race" > "$TMPDIR/race.txt"
|
||
for i in $(seq 1 20); do
|
||
( curl -s -F "file=@$TMPDIR/race.txt;filename=race.txt" "$BASE/upload" > /dev/null ) &
|
||
done
|
||
wait
|
||
count=$(ls "$UPLOADS"/race.txt "$UPLOADS"/"race ("*").txt" 2>/dev/null | wc -l | tr -d ' ')
|
||
if [[ "$count" -eq 20 ]]; then
|
||
ok "20 concurrent uploads of same name produced 20 unique files (race.txt + race (1..19).txt)"
|
||
else
|
||
ko "race" "expected 20 files, got $count"
|
||
ls "$UPLOADS" | grep '^race'
|
||
fi
|
||
# 清理
|
||
rm -f "$UPLOADS"/race.txt "$UPLOADS"/"race ("*").txt"
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "H. CORS 配置"
|
||
# ──────────────────────────────────────────────────────────────
|
||
acao=$(curl -sI -H "Origin: https://evil.com" "$BASE/files" | tr -d '\r' | awk -F': ' '/^Access-Control-Allow-Origin/ {print $2}')
|
||
if [[ -z "$acao" ]]; then
|
||
ok "no Access-Control-Allow-Origin header (browser will block cross-origin reads)"
|
||
else
|
||
ko "CORS" "ACAO='$acao'"
|
||
fi
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "I. HTTP 方法篡改"
|
||
# ──────────────────────────────────────────────────────────────
|
||
echo "x" > "$TMPDIR/m.txt"; upload "$TMPDIR/m.txt" "m.txt" > /dev/null
|
||
# 用 POST + _method 试图删除 (server 路由只识别 DELETE)
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" -X POST -d "_method=DELETE" "$BASE/files/m.txt")
|
||
[[ "$code" == "404" || "$code" == "405" ]] && ok "POST /files/m.txt -> $code (no method override trick)" || ko "method override" "got $code"
|
||
# 用 GET 试图删除
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" -X GET "$BASE/files/m.txt?delete=1")
|
||
[[ "$code" == "200" || "$code" == "405" || "$code" == "404" ]] && ok "GET /files/m.txt -> $code (delete is not exposed on GET)" || ko "GET delete" "got $code"
|
||
curl -s -X DELETE "$BASE/files/m.txt" > /dev/null
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "J. 文件名注入 / 特殊字符"
|
||
# ──────────────────────────────────────────────────────────────
|
||
# NUL 字节
|
||
printf 'x' > "$TMPDIR/n.txt"
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" -F "file=@$TMPDIR/n.txt;filename=foo%00.txt" "$BASE/upload")
|
||
[[ "$code" == "200" || "$code" == "400" ]] && ok "NUL byte in filename -> $code (handled)" || ko "NUL byte" "got $code"
|
||
[[ -f "$UPLOADS/foo" ]] && ko "NUL truncation" "found 'foo' on disk (filename was truncated past NUL)" || ok "no NUL truncation"
|
||
|
||
# 超长文件名 (1000 字符)
|
||
long=$(printf 'a%.0s' {1..1000})
|
||
code=$(upload "$TMPDIR/n.txt" "${long}.txt")
|
||
[[ "$code" == "200" || "$code" == "400" || "$code" == "414" ]] && ok "1000-char filename -> $code" || ko "long filename" "got $code"
|
||
rm -f "$UPLOADS/${long}.txt" "$UPLOADS/foo"*
|
||
|
||
# 控制字符 / CR-LF 注入 (在 multipart filename 中)
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" -F $'file=@'$TMPDIR'/n.txt;filename=foo\r\nX-Injected: bar' "$BASE/upload")
|
||
ok "CR/LF in filename -> $code (curl will not allow header injection, server should sanitize)"
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "K. 错误信息泄露"
|
||
# ──────────────────────────────────────────────────────────────
|
||
err=$(curl -s "$BASE/download/nonexistent-12345")
|
||
if echo "$err" | grep -qiE 'stack|goroutine|/Users/|main\.go'; then
|
||
ko "error leaks path" "resp=$err"
|
||
else
|
||
ok "404 error message does not leak internal paths"
|
||
fi
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "L. GIN mode / 调试信息"
|
||
# ──────────────────────────────────────────────────────────────
|
||
hdrs=$(curl -sI "$BASE/")
|
||
if echo "$hdrs" | grep -qi 'X-Powered-By:.*gin'; then
|
||
note "Server: $hdrs" | head -1
|
||
ok "no X-Powered-By: gin (good)"
|
||
else
|
||
ok "no Gin debug header"
|
||
fi
|
||
# 触发 panic 看是否有 stack trace 泄露
|
||
hdrs=$(curl -s "$BASE/download/%00" 2>&1)
|
||
echo "$hdrs" | grep -qi 'goroutine' && ko "panic stack leaked" "see output" || ok "no stack trace in error response"
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "M. 慢速上传 / Slowloris"
|
||
# ──────────────────────────────────────────────────────────────
|
||
# macOS 无 timeout 命令, 用 gtimeout 或 fallback 到 background subshell
|
||
TIMEOUT_CMD="timeout"
|
||
command -v gtimeout >/dev/null 2>&1 && TIMEOUT_CMD="gtimeout"
|
||
if command -v "$TIMEOUT_CMD" >/dev/null 2>&1; then
|
||
slow_result=$($TIMEOUT_CMD 5 bash -c "exec 3<>/dev/tcp/localhost/8080; printf 'POST /upload HTTP/1.1\r\nHost: localhost\r\nContent-Length: 1000000\r\n\r\n' >&3; sleep 10" 2>&1; echo "exit=$?")
|
||
if echo "$slow_result" | grep -q 'exit=124'; then
|
||
ok "slow-loris timeout detected (server enforces request timeout)"
|
||
else
|
||
note "slowloris result: $slow_result"
|
||
fi
|
||
else
|
||
note "no timeout/gtimeout, skipping slowloris test (建议生产用 nginx 限制 read_timeout)"
|
||
fi
|
||
|
||
# 验证 gin 运行在 release 模式
|
||
if grep -q "gin.SetMode(gin.ReleaseMode)" main.go; then
|
||
ok "gin.SetMode(gin.ReleaseMode) in source (no debug log)"
|
||
else
|
||
ko "gin mode" "not in release mode"
|
||
fi
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "N. 批量下载 (POST /download-zip)"
|
||
# ──────────────────────────────────────────────────────────────
|
||
echo "batch-1" > "$TMPDIR/batch1.txt"
|
||
echo "batch-2" > "$TMPDIR/batch2.txt"
|
||
upload "$TMPDIR/batch1.txt" "batch1.txt" > /dev/null
|
||
upload "$TMPDIR/batch2.txt" "batch2.txt" > /dev/null
|
||
|
||
zipfile="$TMPDIR/batch.zip"
|
||
hdrs=$(curl -s -D - -o "$zipfile" -X POST -H "Content-Type: application/json" \
|
||
-d '{"files":["batch1.txt","batch2.txt"]}' "$BASE/download-zip")
|
||
status=$(echo "$hdrs" | tr -d '\r' | awk 'NR==1{print $2}')
|
||
ctype=$(echo "$hdrs" | tr -d '\r' | awk -F': ' '/^[Cc]ontent-[Tt]ype/ {print $2}')
|
||
if [[ "$status" == "200" ]] && echo "$ctype" | grep -qi 'application/zip'; then
|
||
if unzip -l "$zipfile" 2>/dev/null | grep -qE 'batch1\.txt|batch2\.txt'; then
|
||
ok "POST /download-zip -> 200 application/zip with both files"
|
||
else
|
||
ko "zip missing files" "unzip -l: $(unzip -l "$zipfile" 2>&1)"
|
||
fi
|
||
else
|
||
ko "batch download baseline" "status=$status ctype=$ctype"
|
||
fi
|
||
|
||
# 非法文件名: ".." 单独出现 -> 400; "../main.go" 清洗后变 "main.go" 不存在 -> 200+miss
|
||
status=$(curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" \
|
||
-d '{"files":[".."]}' "$BASE/download-zip")
|
||
[[ "$status" == "400" ]] && ok "\"..\" -> 400" || ko "explicit dotdot" "got $status"
|
||
status=$(curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" \
|
||
-d '{"files":["."]}' "$BASE/download-zip")
|
||
[[ "$status" == "400" ]] && ok "\".\" -> 400" || ko "explicit dot" "got $status"
|
||
status=$(curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" \
|
||
-d '{"files":["../main.go"]}' "$BASE/download-zip")
|
||
[[ "$status" == "200" || "$status" == "404" ]] && ok "../main.go sanitized, miss handled -> $status" || ko "sanitize+miss" "got $status"
|
||
|
||
# 不存在的文件: zip 仍生成, 含真实文件, 不含不存在的
|
||
zipfile2="$TMPDIR/batch2.zip"
|
||
status=$(curl -s -o "$zipfile2" -w "%{http_code}" -X POST -H "Content-Type: application/json" \
|
||
-d '{"files":["batch1.txt","does-not-exist-12345.bin"]}' "$BASE/download-zip")
|
||
if [[ "$status" == "200" ]]; then
|
||
listing=$(unzip -l "$zipfile2" 2>/dev/null || true)
|
||
if echo "$listing" | grep -q 'batch1.txt' && ! echo "$listing" | grep -q 'does-not-exist'; then
|
||
ok "missing file skipped, real file still packed"
|
||
else
|
||
ko "missing file handling" "unzip -l: $listing"
|
||
fi
|
||
else
|
||
ko "missing file request" "got $status"
|
||
fi
|
||
|
||
# 超量
|
||
files=$(python3 -c 'import json; print(json.dumps({"files":["x"]*101}))')
|
||
status=$(curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" \
|
||
-d "$files" "$BASE/download-zip")
|
||
[[ "$status" == "400" ]] && ok "101 files -> 400" || ko "101 files" "got $status"
|
||
|
||
# Query 形式
|
||
status=$(curl -s -o "$TMPDIR/batch_q.zip" -w "%{http_code}" -X POST \
|
||
"$BASE/download-zip?files=batch1.txt&files=batch2.txt")
|
||
[[ "$status" == "200" ]] && ok "POST /download-zip?files=... -> 200" || ko "query form" "got $status"
|
||
|
||
# 空 files
|
||
status=$(curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" \
|
||
-d '{"files":[]}' "$BASE/download-zip")
|
||
[[ "$status" == "400" ]] && ok "empty files array -> 400" || ko "empty files" "got $status"
|
||
|
||
# 清理
|
||
curl -s -X DELETE "$BASE/files/batch1.txt" > /dev/null
|
||
curl -s -X DELETE "$BASE/files/batch2.txt" > /dev/null
|
||
rm -f "$TMPDIR/batch.zip" "$TMPDIR/batch2.zip" "$TMPDIR/batch_q.zip"
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "O. 批量删除 (POST /files-delete)"
|
||
# ──────────────────────────────────────────────────────────────
|
||
echo "bdel-1" > "$TMPDIR/bdel1.txt"
|
||
echo "bdel-2" > "$TMPDIR/bdel2.txt"
|
||
upload "$TMPDIR/bdel1.txt" "bdel1.txt" > /dev/null
|
||
upload "$TMPDIR/bdel2.txt" "bdel2.txt" > /dev/null
|
||
|
||
# 删除其一 + 一个不存在: 200, miss 含不存在项, 目标文件确已被删
|
||
resp=$(curl -s -X POST -H "Content-Type: application/json" \
|
||
-d '{"files":["bdel1.txt","does-not-exist-bdel.bin"]}' "$BASE/files-delete")
|
||
if echo "$resp" | grep -q '"deleted":1' && echo "$resp" | grep -q 'does-not-exist-bdel.bin'; then
|
||
ok "POST /files-delete (1 real + 1 miss) -> 200 deleted=1 miss=[...]"
|
||
else
|
||
ko "batch delete baseline" "resp=$resp"
|
||
fi
|
||
status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/download/bdel1.txt")
|
||
[[ "$status" == "404" ]] && ok "deleted file no longer downloadable -> 404" || ko "deleted file" "got $status"
|
||
|
||
# 非法文件名 -> 400
|
||
status=$(curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" \
|
||
-d '{"files":[".."]}' "$BASE/files-delete")
|
||
[[ "$status" == "400" ]] && ok "invalid filename \"..\" -> 400" || ko "invalid filename" "got $status"
|
||
|
||
# 超 100 -> 400
|
||
files=$(python3 -c 'import json; print(json.dumps({"files":["x"]*101}))')
|
||
status=$(curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" \
|
||
-d "$files" "$BASE/files-delete")
|
||
[[ "$status" == "400" ]] && ok "101 files -> 400" || ko "101 files" "got $status"
|
||
|
||
# 空 body -> 400
|
||
status=$(curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" \
|
||
-d '{"files":[]}' "$BASE/files-delete")
|
||
[[ "$status" == "400" ]] && ok "empty files array -> 400" || ko "empty files" "got $status"
|
||
|
||
# 清理
|
||
curl -s -X DELETE "$BASE/files/bdel2.txt" > /dev/null
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
section "P. 静态资源 (/static StaticFS)"
|
||
# ──────────────────────────────────────────────────────────────
|
||
html=$(curl -s "$BASE/")
|
||
if echo "$html" | grep -q 'href="/static/app.css"'; then
|
||
ok "GET / contains <link href=\"/static/app.css\">"
|
||
else
|
||
ko "GET / link tag" "link tag missing"
|
||
fi
|
||
for js in app.js helpers.js batch.js; do
|
||
code=$(status "$BASE/static/$js")
|
||
[[ "$code" == "200" ]] && ok "GET /static/$js -> 200" || ko "GET /static/$js" "got $code"
|
||
done
|
||
hdrs=$(curl -s -D - -o /dev/null "$BASE/static/app.css")
|
||
ctype=$(echo "$hdrs" | tr -d '\r' | awk -F': ' '/^[Cc]ontent-[Tt]ype/ {print $2}')
|
||
cc=$(echo "$hdrs" | tr -d '\r' | awk -F': ' '/^[Cc]ache-[Cc]ontrol/ {print $2}')
|
||
if echo "$ctype" | grep -qi 'text/css'; then
|
||
ok "GET /static/app.css -> Content-Type: text/css"
|
||
else
|
||
ko "app.css content-type" "got [$ctype]"
|
||
fi
|
||
if echo "$cc" | grep -qi 'no-cache'; then
|
||
ok "GET /static/app.css -> Cache-Control: no-cache"
|
||
else
|
||
ko "app.css cache-control" "got [$cc]"
|
||
fi
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
echo
|
||
printf "\n\033[1m========== 总计 ==========\033[0m\n"
|
||
printf " \033[32mPASS: %d\033[0m\n" "$PASS"
|
||
printf " \033[31mFAIL: %d\033[0m\n" "$FAIL"
|
||
[[ "$FAIL" -gt 0 ]] && exit 1
|
||
exit 0
|