#!/bin/bash # 真机验收:静态资源传输层(1.0.9) # # 验的是「浏览器实际收到什么」,不是源码里写了什么: # ① 带指纹的 app.js → Cache-Control: immutable 长缓存 # ② 第二次请求 If-None-Match → 304,且**零字节响应体** # ③ 无指纹的 /app.js → 仍是 no-cache # ④ Accept-Encoding: br 但目标机没装 brotli → **必须退到 gzip** # (这是本轮最大的风险点,退化就是 583KB 裸传) # ⑤ 页脚版本号已注入,不再是 __APP_VERSION__ 字面量 # ⑥ index.html 的 ETag 里含版本号 # ⑦ logo 是 ,页脚有版本号元素 set -u BASE="http://127.0.0.1:8080" WEB="http://127.0.0.1:8080" fail=0 ck() { if [ "$2" = "1" ]; then echo " ✓ $1"; else echo " ✗ $1"; fail=$((fail+1)); fi; } echo "=== 1. 探测面板是否在跑 ===" # ⚠️ **不要在这里试登录**。密码猜错有锁定计数(实测错误返回 # 「还可尝试 3 次」),验收脚本反复试会把用户的账号锁死 —— # 而且脚本里硬编码密码这个做法本身就不该保留。 # 本脚本要验的东西全是公开静态资源 + 免鉴权的 /api/openapi,不需要登录。 CODE=$(curl -sk -m 5 -o /dev/null -w '%{http_code}' "$BASE/index.html") echo " GET /index.html → $CODE" [ "$CODE" = "200" ] || { echo "面板没起来($CODE),后续无意义"; exit 1; } echo echo "=== 2. 取 app.js 的指纹(从 index.html 里解析)===" IDX=$(curl -sk -m 5 "$BASE/index.html") # ⚠️ 不能用 sed 抽 /app.js?v=... —— 问号是 sed 的选项分隔符, # 会报「s 的未知选项」,然后指纹解析静默失败、退回测无指纹 URL, # 于是「immutable 长缓存」那条必然变红 —— 假红比不测更坏。 APPV=$(printf '%s' "$IDX" | grep -o '/app\.js?v=[0-9]*' | head -1) [ -z "$APPV" ] && APPV="/app.js" echo " 指纹 URL = $APPV" echo echo "=== 3. 带指纹请求 → 应为 immutable 长缓存 ===" H1=$(curl -sk -m 10 -D - -o /dev/null "$BASE$APPV") CC=$(printf '%s' "$H1" | sed -n 's/^[Cc]ache-[Cc]ontrol:[[:space:]]*//p' | tr -d '\r') echo " Cache-Control: $CC" [ -z "$CC" ] && CC=$(printf '%s' "$H1" | grep -i '^cache-control:' | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r') printf '%s' "$CC" | grep -q 'immutable' && ck "带指纹 → immutable 长缓存" 1 || ck "带指纹 → immutable 长缓存(实际: $CC)" 0 printf '%s' "$CC" | grep -q 'max-age=31536000' && ck "max-age 是一年" 1 || ck "max-age 是一年" 0 ETAG=$(printf '%s' "$H1" | grep -i '^etag:' | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r') [ -n "$ETAG" ] && ck "有 ETag($ETAG)" 1 || ck "有 ETag" 0 echo echo "=== 4. 传输量(br / gzip / 无压缩 各一次)===" SZ_BR=$(curl -sk -m 20 -H 'Accept-Encoding: br, gzip' -o /dev/null -w '%{size_download}' "$BASE$APPV") ENC_BR=$(curl -sk -m 20 -D - -o /dev/null -H 'Accept-Encoding: br, gzip' "$BASE$APPV" | grep -i '^content-encoding:' | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r') SZ_GZ=$(curl -sk -m 20 -H 'Accept-Encoding: gzip' -o /dev/null -w '%{size_download}' "$BASE$APPV") ENC_GZ=$(curl -sk -m 20 -D - -o /dev/null -H 'Accept-Encoding: gzip' "$BASE$APPV" | grep -i '^content-encoding:' | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r') SZ_RAW=$(curl -sk -m 20 -H 'Accept-Encoding:' -o /dev/null -w '%{size_download}' "$BASE$APPV") SRC=$(stat -c %s /opt/drouter/web/app.js) echo " 源文件 $SRC 字节" echo " Accept-Encoding: br,gzip → $SZ_BR 字节(Content-Encoding: ${ENC_BR:-无})" echo " Accept-Encoding: gzip → $SZ_GZ 字节(Content-Encoding: ${ENC_GZ:-无})" echo " 不带编码 → $SZ_RAW 字节" # 关键:目标机没装 python3-brotli,br 必须降级到 gzip, # 绝不能「要 br 就发原文」—— 那样等于 583KB 裸传,比优化前更慢。 if [ -n "$ENC_BR" ]; then ck "带 br 时有编码(实际 ${ENC_BR})" 1 if [ "$ENC_BR" = "gzip" ]; then ck "br 不可用时已降级到 gzip(本机无 python3-brotli,属预期)" 1 elif [ "$ENC_BR" = "br" ]; then echo " (本机装了 brotli,用的是 br)" fi else SZ_NOGZ=$(curl -sk -m 20 -H 'Accept-Encoding: br' -o /dev/null -w '%{size_download}' "$BASE$APPV") echo " ⚠ br 无编码且不支持 gzip → $SZ_NOGZ 字节裸传" ck "br 不可用且无 gzip 兜底时会裸传(应为 0,本机应装 gzip 支持)" 0 fi [ "$SZ_GZ" -lt "$SRC" ] && ck "gzip 确实压小了($SZ_GZ < $SRC)" 1 || ck "gzip 确实压小了" 0 [ "$SZ_RAW" = "$SRC" ] && ck "不带编码时原样输出($SZ_RAW = 源文件大小)" 1 || ck "不带编码时原样输出" 0 echo echo "=== 5. 304 协商(第二次打开应当零传输)===" # 304 响应没有 body,curl **不会创建** -o 指定的文件 → stat 会报 # 「没有那个文件或目录」,进而让 [: -lt ] 拿到空值。 # 用 %{size_download} 让 curl 自己报字节数,不依赖文件是否存在。 rm -f /tmp/_304body read -r CODE BODY <<<"$(curl -sk -m 10 -o /tmp/_304body -w '%{http_code} %{size_download}' \ -H "If-None-Match: $ETAG" "$BASE$APPV")" BODY=${BODY:-0} echo " 状态码 $CODE,响应体 $BODY 字节" [ "$CODE" = "304" ] && ck "If-None-Match 命中 → 304" 1 || ck "If-None-Match 命中 → 304(实际 $CODE)" 0 [ "$BODY" -lt 100 ] && ck "304 不回传响应体" 1 || ck "304 不回传响应体(实际 $BODY 字节)" 0 rm -f /tmp/_304body echo echo "=== 6. 无指纹请求 → 必须仍是 no-cache ===" CC2=$(curl -sk -m 10 -D - -o /dev/null "$BASE/app.js" | grep -i '^cache-control:' | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r') echo " Cache-Control: $CC2" printf '%s' "$CC2" | grep -q 'no-cache' && ck "无指纹 → no-cache(否则部署后拿到旧文件)" 1 || ck "无指纹 → no-cache(实际: $CC2)" 0 printf '%s' "$CC2" | grep -q 'immutable' && ck "⚠ 无指纹却给了 immutable(会导致用户卡在旧版本)" 0 || ck "无指纹没有误给 immutable" 1 echo echo "=== 7. Vary 头(代理正确性)===" V=$(curl -sk -m 10 -D - -o /dev/null -H 'Accept-Encoding: gzip' "$BASE$APPV" | grep -ci '^vary:.*Accept-Encoding') [ "$V" -ge 1 ] && ck "压缩响应带 Vary: Accept-Encoding" 1 || ck "压缩响应带 Vary: Accept-Encoding" 0 echo echo "=== 8. 版本号注入 ===" if printf '%s' "$IDX" | grep -q '__APP_VERSION__'; then ck "页脚版本号已注入(不应残留占位符)" 0 else ck "页脚版本号已注入(无占位符残留)" 1 fi VER=$(printf '%s' "$IDX" | sed -n 's/.*id="app-ver"[^>]*>\(v[0-9][^<]*\)<.*/\1/p' | head -1) [ -z "$VER" ] && VER=$(printf '%s' "$IDX" | grep -o 'v[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?' | tail -1) echo " 页面显示版本号 = ${VER:-(未找到)}" [ -n "$VER" ] && ck "页脚有版本号" 1 || ck "页脚有版本号" 0 VFILE=$(cat /opt/drouter/VERSION 2>/dev/null | tr -d ' \t\r\n') echo " /opt/drouter/VERSION = ${VFILE:-(不存在)}" [ -n "$VFILE" ] && ck "VERSION 文件已部署" 1 || ck "VERSION 文件已部署" 0 if [ -n "$VER" ] && [ -n "$VFILE" ]; then [ "v$VFILE" = "$VER" ] && ck "页面版本号与 VERSION 文件一致" 1 || ck "页面版本号与 VERSION 文件一致(页面 $VER vs 文件 v$VFILE)" 0 fi echo echo "=== 9. index.html 的 ETag 含版本号 ===" IE=$(curl -sk -m 10 -D - -o /dev/null "$BASE/index.html" | grep -i '^etag:' | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r') echo " index.html ETag = $IE" if [ -n "$VFILE" ]; then printf '%s' "$IE" | grep -q "$VFILE" && ck "ETag 里含版本号(改版本即换 ETag)" 1 || ck "ETag 里含版本号" 0 fi echo echo "=== 10. 两个 UI 元素 ===" printf '%s' "$IDX" | grep -q 'id="brand-home"' && ck "左上角 logo 是可点击元素" 1 || ck "左上角 logo 是可点击元素" 0 printf '%s' "$IDX" | grep -q '(键盘/读屏可用)" 1 || ck "logo 用 " 0 printf '%s' "$IDX" | grep -q 'title="返回系统概览"' && ck "logo 有可访问名称" 1 || ck "logo 有可访问名称" 0 printf '%s' "$IDX" | grep -q 'id="app-ver"' && ck "页脚有版本号容器" 1 || ck "页脚有版本号容器" 0 echo echo "=== 11. openapi 的 version 与页面一致(/api/openapi 免鉴权)===" API_VER=$(curl -sk -m 5 "$BASE/api/openapi" | grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/') echo " openapi version = ${API_VER:-(未取到)}" [ -n "$API_VER" ] && ck "openapi 可免鉴权访问且能取到 version" 1 || ck "openapi version 取不到" 0 if [ -n "$VFILE" ] && [ -n "$API_VER" ]; then [ "$API_VER" = "$VFILE" ] && ck "openapi version 与 VERSION 文件一致(都是 $VFILE)" 1 \ || ck "openapi version 与 VERSION 一致($API_VER vs $VFILE)" 0 fi echo if [ "$fail" -eq 0 ]; then echo "=== 全部通过 ==="; else echo "=== $fail 项失败 ==="; fi exit $fail