- 新增 middleware.c/middleware.h:注册表 + 链式执行机制 - 内置 CORS 中间件:支持预检请求、跨域头 - 内置 Basic Auth 中间件:HTTP 基础认证 - 内置 Rate Limit 中间件:基于 IP 的秒级限流 - 修复 config_merge 参数不匹配(新增 has_cors_enabled/has_auth_user/has_auth_pass/has_rate_limit) - 修复 server_destroy 与 main() 双重释放字符串指针 - 修复 main.c 启动段错误(access_log_path 未初始化) - 修复 rate_limit 哈希函数包含端口导致同一 IP 不同 bucket 的问题 - 修复 rate_limit 中间件 user_data 悬空指针(改为 ctx->mw_config) - 修复 401/429 响应 Content-Length 与 body 长度不匹配导致 curl 挂起 - 修复集成测试服务器启动循环:使用 nc 代替 curl,避免触发限流计数 - 修复集成测试 kill_server:使用 kill -9 确保进程退出,并检查端口释放 - 单元测试 18/18 通过,集成测试 66/66 通过
847 lines
24 KiB
Bash
Executable File
847 lines
24 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# integration_test.sh — Cocoon 集成测试套件
|
||
#
|
||
# 依赖: curl, pgrep, sleep
|
||
# 用法: ./tests/integration_test.sh [server_binary] [doc_root]
|
||
#
|
||
|
||
set -euo pipefail
|
||
|
||
SERVER="${1:-./cocoon}"
|
||
ROOT="${2:-./tests/fixtures}"
|
||
HOST="localhost:9999"
|
||
BASE="http://${HOST}"
|
||
PASS=0
|
||
FAIL=0
|
||
TMPDIR=$(mktemp -d)
|
||
trap 'rm -rf "$TMPDIR"; kill_server' EXIT
|
||
|
||
kill_server() {
|
||
local pids
|
||
pids=$(pgrep -f "cocoon.*-p 9999" 2>/dev/null || true)
|
||
if [[ -n "$pids" ]]; then
|
||
echo "$pids" | xargs kill -9 2>/dev/null || true
|
||
fi
|
||
pids=$(pgrep -f "cocoon.*--cert" 2>/dev/null || true)
|
||
if [[ -n "$pids" ]]; then
|
||
echo "$pids" | xargs kill -9 2>/dev/null || true
|
||
fi
|
||
sleep 0.5
|
||
# 确保端口已释放
|
||
for i in {1..20}; do
|
||
if ! ss -tlnp 2>/dev/null | grep -q ':9999'; then
|
||
break
|
||
fi
|
||
sleep 0.1
|
||
done
|
||
}
|
||
|
||
# 启动服务器
|
||
start_server() {
|
||
echo "[test] 启动服务器: $SERVER -r $ROOT -p 9999"
|
||
$SERVER -r "$ROOT" -p 9999 -l debug > "$TMPDIR/server.log" 2>&1 &
|
||
local pid=$!
|
||
for i in {1..30}; do
|
||
if nc -z localhost 9999 2>/dev/null; then
|
||
echo "[test] 服务器就绪 (PID $pid)"
|
||
return 0
|
||
fi
|
||
sleep 0.1
|
||
done
|
||
echo "[test] 服务器启动失败"
|
||
cat "$TMPDIR/server.log"
|
||
exit 1
|
||
}
|
||
|
||
# 断言辅助函数
|
||
pass() {
|
||
PASS=$((PASS + 1))
|
||
}
|
||
|
||
fail() {
|
||
FAIL=$((FAIL + 1))
|
||
}
|
||
|
||
assert_status() {
|
||
local url="$1"
|
||
local expect="$2"
|
||
local desc="${3:-$url}"
|
||
local status
|
||
status=$(curl -s -o /dev/null -w "%{http_code}" "$url")
|
||
if [[ "$status" == "$expect" ]]; then
|
||
echo " ✓ $desc — HTTP $status"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 期望 HTTP $expect, 实际 HTTP $status"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_status_with_header() {
|
||
local header="$1"
|
||
local url="$2"
|
||
local expect="$3"
|
||
local desc="${4:-$url}"
|
||
local status
|
||
status=$(curl -s -o /dev/null -w "%{http_code}" -H "$header" "$url")
|
||
if [[ "$status" == "$expect" ]]; then
|
||
echo " ✓ $desc — HTTP $status"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 期望 HTTP $expect, 实际 HTTP $status"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_head_status() {
|
||
local url="$1"
|
||
local expect="$2"
|
||
local desc="${3:-$url}"
|
||
local status
|
||
status=$(curl -s -o /dev/null -w "%{http_code}" -I "$url")
|
||
if [[ "$status" == "$expect" ]]; then
|
||
echo " ✓ $desc — HTTP $status"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 期望 HTTP $expect, 实际 HTTP $status"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_head_no_body() {
|
||
local url="$1"
|
||
local desc="${2:-$url}"
|
||
local len
|
||
len=$(curl -s -o /dev/null -w "%{size_download}" -I "$url")
|
||
if [[ "$len" == "0" ]]; then
|
||
echo " ✓ $desc — 无响应体"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 期望无响应体, 实际 $len 字节"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_header_contains() {
|
||
local url="$1"
|
||
local header="$2"
|
||
local expect="$3"
|
||
local desc="${4:-$url}"
|
||
local value
|
||
value=$(curl -sI "$url" | grep -i "^$header:" | tr -d '\r' || true)
|
||
if echo "$value" | grep -qi "$expect"; then
|
||
echo " ✓ $desc — $header 包含 '$expect'"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — $header 期望包含 '$expect', 实际: $value"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_header_contains_with_header() {
|
||
local req_header="$1"
|
||
local url="$2"
|
||
local header="$3"
|
||
local expect="$4"
|
||
local desc="${5:-$url}"
|
||
local value
|
||
value=$(curl -sI -H "$req_header" "$url" | grep -i "^$header:" | tr -d '\r' || true)
|
||
if echo "$value" | grep -qi "$expect"; then
|
||
echo " ✓ $desc — $header 包含 '$expect'"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — $header 期望包含 '$expect', 实际: $value"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_body_contains() {
|
||
local url="$1"
|
||
local expect="$2"
|
||
local desc="${3:-$url}"
|
||
local body
|
||
body=$(curl -s -k --compressed "$url")
|
||
if echo "$body" | grep -q "$expect"; then
|
||
echo " ✓ $desc — 响应体包含 '$expect'"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 响应体期望包含 '$expect'"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_brotli() {
|
||
local url="$1"
|
||
local desc="${2:-$url}"
|
||
local encoding
|
||
encoding=$(curl -s -D - -o /dev/null -H "Accept-Encoding: br" "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||
if echo "$encoding" | grep -qi "br"; then
|
||
echo " ✓ $desc — 返回 brotli 压缩"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 期望 brotli 压缩, 实际: $encoding"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_brotli_preferred() {
|
||
local url="$1"
|
||
local desc="${2:-$url}"
|
||
local encoding
|
||
encoding=$(curl -s -D - -o /dev/null -H "Accept-Encoding: gzip, br" "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||
if echo "$encoding" | grep -qi "br"; then
|
||
echo " ✓ $desc — 优先返回 brotli 压缩"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 期望优先 brotli, 实际: $encoding"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_not_brotli() {
|
||
local url="$1"
|
||
local desc="${2:-$url}"
|
||
local encoding
|
||
encoding=$(curl -s -D - -o /dev/null -H "Accept-Encoding: br" "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||
if [[ -z "$encoding" ]]; then
|
||
echo " ✓ $desc — 无 Content-Encoding(未压缩)"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 期望不压缩, 实际: $encoding"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_gzip() {
|
||
local url="$1"
|
||
local desc="${2:-$url}"
|
||
local encoding
|
||
encoding=$(curl -s -D - -o /dev/null -H "Accept-Encoding: gzip" "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||
if echo "$encoding" | grep -qi "gzip"; then
|
||
echo " ✓ $desc — 返回 gzip 压缩"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 期望 gzip 压缩, 实际: $encoding"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_not_gzip() {
|
||
local url="$1"
|
||
local desc="${2:-$url}"
|
||
local encoding
|
||
encoding=$(curl -s -D - -o /dev/null -H "Accept-Encoding: gzip" "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||
if [[ -z "$encoding" ]]; then
|
||
echo " ✓ $desc — 无 Content-Encoding(未压缩)"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 期望不压缩, 实际: $encoding"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_304() {
|
||
local url="$1"
|
||
local etag="$2"
|
||
local desc="${3:-$url}"
|
||
local status
|
||
status=$(curl -s -o /dev/null -w "%{http_code}" -H "If-None-Match: $etag" "$url")
|
||
if [[ "$status" == "304" ]]; then
|
||
echo " ✓ $desc — 缓存命中 HTTP 304"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 期望 304, 实际 $status"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_304_modified_since() {
|
||
local url="$1"
|
||
local mtime="$2"
|
||
local desc="${3:-$url}"
|
||
local status
|
||
status=$(curl -s -o /dev/null -w "%{http_code}" -H "If-Modified-Since: $mtime" "$url")
|
||
if [[ "$status" == "304" ]]; then
|
||
echo " ✓ $desc — If-Modified-Since 命中 HTTP 304"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 期望 304, 实际 $status"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_post_multipart() {
|
||
local url="$1"
|
||
local desc="${2:-$url}"
|
||
local resp
|
||
local boundary="----CocoonTestBoundary"
|
||
local body
|
||
body=$(printf -- '--%s\r\nContent-Disposition: form-data; name="file"; filename="upload_test.txt"\r\nContent-Type: text/plain\r\n\r\nCocoon multipart upload test content\r\n--%s--\r\n' "$boundary" "$boundary")
|
||
|
||
resp=$(curl -s -X POST -H "Content-Type: multipart/form-data; boundary=${boundary}" --data-binary "$body" "$url")
|
||
if echo "$resp" | grep -q '"uploaded"'; then
|
||
echo " ✓ $desc — 响应包含上传结果"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 响应期望包含上传结果, 实际: $resp"
|
||
fail
|
||
fi
|
||
|
||
# 验证文件是否被保存
|
||
if [ -f "$ROOT/uploads/upload_test.txt" ]; then
|
||
echo " ✓ $desc — 文件已保存到 uploads/"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 文件未保存到 uploads/"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_post_json() {
|
||
local url="$1"
|
||
local body="$2"
|
||
local expect="$3"
|
||
local desc="${4:-$url}"
|
||
local resp
|
||
resp=$(curl -s -X POST -H "Content-Type: application/json" -d "$body" "$url")
|
||
if echo "$resp" | grep -q "$expect"; then
|
||
echo " ✓ $desc — 响应包含 '$expect'"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 响应期望包含 '$expect', 实际: $resp"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_post_form() {
|
||
local url="$1"
|
||
local body="$2"
|
||
local expect="$3"
|
||
local desc="${4:-$url}"
|
||
local resp
|
||
resp=$(curl -s -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "$body" "$url")
|
||
if echo "$resp" | grep -q "$expect"; then
|
||
echo " ✓ $desc — 响应包含 '$expect'"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 响应期望包含 '$expect', 实际: $resp"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_http2_brotli() {
|
||
local url="$1"
|
||
local desc="${2:-$url}"
|
||
local encoding
|
||
encoding=$(curl --http2 -k -s -D - -o /dev/null "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||
if echo "$encoding" | grep -qi "br"; then
|
||
echo " ✓ $desc — HTTP/2 返回 brotli 压缩"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — HTTP/2 期望 brotli 压缩, 实际: $encoding"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_http2_gzip() {
|
||
local url="$1"
|
||
local desc="${2:-$url}"
|
||
local encoding
|
||
encoding=$(curl --http2 -k -s -D - -o /dev/null "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||
if echo "$encoding" | grep -qi "gzip"; then
|
||
echo " ✓ $desc — HTTP/2 返回 gzip 压缩"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — HTTP/2 期望 gzip 压缩, 实际: $encoding"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_http2_not_compressed() {
|
||
local url="$1"
|
||
local desc="${2:-$url}"
|
||
local encoding
|
||
encoding=$(curl --http2 -k -s -D - -o /dev/null "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||
if [[ -z "$encoding" ]]; then
|
||
echo " ✓ $desc — HTTP/2 无压缩"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — HTTP/2 期望无压缩, 实际: $encoding"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_status_405() {
|
||
local url="$1"
|
||
local desc="${2:-$url}"
|
||
local status
|
||
status=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "$url")
|
||
if [[ "$status" == "405" ]]; then
|
||
echo " ✓ $desc — PUT 返回 405"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 期望 405, 实际 $status"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
assert_access_log() {
|
||
local log_file="$1"
|
||
local desc="${2:-访问日志}"
|
||
if [ -f "$log_file" ] && [ -s "$log_file" ]; then
|
||
local count
|
||
count=$(grep -c '"GET /' "$log_file" || true)
|
||
if [ "$count" -ge 1 ]; then
|
||
echo " ✓ $desc — 日志文件包含请求记录"
|
||
pass
|
||
else
|
||
echo " ✗ $desc — 日志文件无请求记录"
|
||
fail
|
||
fi
|
||
else
|
||
echo " ✗ $desc — 日志文件不存在或为空"
|
||
fail
|
||
fi
|
||
}
|
||
|
||
# ===== 测试开始 =====
|
||
start_server
|
||
|
||
echo ""
|
||
echo "=== 基础功能测试 ==="
|
||
assert_status "$BASE/" "200" "首页 GET"
|
||
assert_status "$BASE/index.html" "200" "index.html GET"
|
||
assert_status "$BASE/nonexist.html" "404" "404 页面"
|
||
assert_status "$BASE/../etc/passwd" "404" "路径遍历防护"
|
||
|
||
echo ""
|
||
echo "=== HEAD 请求测试 ==="
|
||
assert_head_status "$BASE/" "200" "首页 HEAD"
|
||
assert_head_no_body "$BASE/index.html" "HEAD 无响应体"
|
||
|
||
echo ""
|
||
echo "=== 响应头测试 ==="
|
||
assert_header_contains "$BASE/index.html" "Server" "Cocoon" "Server 标识"
|
||
assert_header_contains "$BASE/index.html" "Content-Type" "text/html" "Content-Type"
|
||
assert_header_contains "$BASE/" "Content-Type" "text/html" "目录浏览 Content-Type"
|
||
|
||
echo ""
|
||
echo "=== 目录浏览测试 ==="
|
||
assert_status "$BASE/subdir/" "200" "目录浏览"
|
||
assert_body_contains "$BASE/subdir/" "Index of" "目录列表标题"
|
||
|
||
echo ""
|
||
echo "=== Range 请求测试 ==="
|
||
assert_status_with_header "Range: bytes=0-9" "$BASE/index.html" "206" "Range 206"
|
||
assert_header_contains_with_header "Range: bytes=0-9" "$BASE/index.html" "Content-Range" "bytes 0-9" "Content-Range 头"
|
||
assert_status_with_header "Range: bytes=99999-100000" "$BASE/index.html" "416" "越界 Range 416"
|
||
|
||
echo ""
|
||
echo "=== 缓存协商测试 ==="
|
||
# 先获取 ETag
|
||
ETAG=$(curl -sI "$BASE/index.html" | grep -i "ETag:" | awk '{print $2}' | tr -d '\r')
|
||
echo " 首页 ETag: $ETAG"
|
||
assert_header_contains "$BASE/index.html" "ETag" "\"" "ETag 存在"
|
||
assert_header_contains "$BASE/index.html" "Last-Modified" "" "Last-Modified 存在"
|
||
if [[ -n "$ETAG" ]]; then
|
||
assert_304 "$BASE/index.html" "$ETAG" "If-None-Match 304"
|
||
fi
|
||
# If-Modified-Since 测试
|
||
LM=$(curl -sI "$BASE/index.html" | grep -i "Last-Modified:" | sed 's/Last-Modified: //i' | tr -d '\r')
|
||
if [[ -n "$LM" ]]; then
|
||
assert_304_modified_since "$BASE/index.html" "$LM" "If-Modified-Since 304"
|
||
fi
|
||
|
||
echo ""
|
||
echo "=== Gzip 压缩测试 ==="
|
||
assert_gzip "$BASE/index.html" "HTML gzip"
|
||
assert_gzip "$BASE/style.css" "CSS gzip"
|
||
assert_gzip "$BASE/app.js" "JS gzip"
|
||
assert_gzip "$BASE/api.json" "JSON gzip"
|
||
# 图片不应压缩
|
||
assert_not_gzip "$BASE/image.png" "图片不压缩"
|
||
|
||
echo ""
|
||
echo "=== Brotli 压缩测试 ==="
|
||
assert_brotli "$BASE/index.html" "HTML brotli"
|
||
assert_brotli "$BASE/style.css" "CSS brotli"
|
||
assert_brotli "$BASE/app.js" "JS brotli"
|
||
assert_brotli_preferred "$BASE/index.html" "优先 brotli"
|
||
# 图片不应压缩
|
||
assert_not_brotli "$BASE/image.png" "图片不压缩"
|
||
|
||
echo ""
|
||
echo "=== MIME 类型测试 ==="
|
||
assert_header_contains "$BASE/style.css" "Content-Type" "text/css" "CSS MIME"
|
||
assert_header_contains "$BASE/app.js" "Content-Type" "application/javascript" "JS MIME"
|
||
assert_header_contains "$BASE/api.json" "Content-Type" "application/json" "JSON MIME"
|
||
assert_header_contains "$BASE/image.png" "Content-Type" "image/" "PNG MIME"
|
||
|
||
echo ""
|
||
echo "=== POST 请求测试 ==="
|
||
assert_post_json "$BASE/api/echo" '{"test": "hello"}' '"test"' "POST JSON 回显"
|
||
assert_post_form "$BASE/api/echo" 'name=cocoon&version=1.0' 'cocoon' "POST 表单回显"
|
||
assert_status_405 "$BASE/index.html" "PUT 405"
|
||
|
||
echo ""
|
||
echo "=== 文件上传测试 ==="
|
||
assert_post_multipart "$BASE/upload" "multipart 文件上传"
|
||
|
||
echo ""
|
||
echo "=== 访问日志测试 ==="
|
||
# 带访问日志启动服务器
|
||
kill_server
|
||
sleep 1
|
||
LOG_FILE="$TMPDIR/access.log"
|
||
$SERVER -r "$ROOT" -p 9999 --access-log "$LOG_FILE" > "$TMPDIR/server_access.log" 2>&1 &
|
||
pid_access=$!
|
||
for i in {1..30}; do
|
||
if curl -s -o /dev/null "$BASE/" 2>/dev/null; then
|
||
break
|
||
fi
|
||
sleep 0.1
|
||
done
|
||
# 发送请求并验证日志记录
|
||
curl -s -o /dev/null "$BASE/index.html" -H "User-Agent: CocoonTest/1.0"
|
||
curl -s -o /dev/null "$BASE/nonexist.html" -H "Referer: http://example.com"
|
||
sleep 0.5
|
||
assert_access_log "$LOG_FILE" "访问日志文件生成"
|
||
# 检查日志格式是否包含关键字段
|
||
if grep -q '"GET /index.html HTTP/1.1"' "$LOG_FILE"; then
|
||
echo " ✓ 日志包含 GET /index.html"
|
||
pass
|
||
else
|
||
echo " ✗ 日志缺少 GET /index.html"
|
||
fail
|
||
fi
|
||
if grep -q 'CocoonTest/1.0' "$LOG_FILE"; then
|
||
echo " ✓ 日志包含 User-Agent"
|
||
pass
|
||
else
|
||
echo " ✗ 日志缺少 User-Agent"
|
||
fail
|
||
fi
|
||
if grep -q 'example.com' "$LOG_FILE"; then
|
||
echo " ✓ 日志包含 Referer"
|
||
pass
|
||
else
|
||
echo " ✗ 日志缺少 Referer"
|
||
fail
|
||
fi
|
||
if grep -q '404' "$LOG_FILE"; then
|
||
echo " ✓ 日志包含 404 状态码"
|
||
pass
|
||
else
|
||
echo " ✗ 日志缺少 404 状态码"
|
||
fail
|
||
fi
|
||
|
||
# 恢复普通服务器
|
||
kill_server
|
||
sleep 1
|
||
start_server
|
||
|
||
echo ""
|
||
echo "=== TLS/HTTPS 测试 ==="
|
||
# 启动 HTTPS 服务器(使用测试证书)
|
||
kill_server
|
||
sleep 1
|
||
$SERVER -r "$ROOT" -p 9999 --cert tests/server.crt --key tests/server.key > "$TMPDIR/server_tls.log" 2>&1 &
|
||
pid_tls=$!
|
||
|
||
# 等待服务器就绪(curl -k 忽略证书验证)
|
||
for i in {1..50}; do
|
||
if curl -s -o /dev/null -k --max-time 2 "https://$HOST/" 2>/dev/null; then
|
||
break
|
||
fi
|
||
sleep 0.2
|
||
done
|
||
|
||
# 使用 openssl s_client 验证 TLS 握手
|
||
if command -v openssl >/dev/null 2>&1; then
|
||
tls_resp=$(echo -e "GET / HTTP/1.1\r\nHost: $HOST\r\nConnection: close\r\n\r\n" | \
|
||
timeout 5 openssl s_client -connect "$HOST" -quiet 2>/dev/null | head -20)
|
||
if echo "$tls_resp" | grep -q "HTTP/1.1"; then
|
||
echo " ✓ TLS 握手 + HTTP GET — 通过 HTTPS 获取响应"
|
||
pass
|
||
else
|
||
echo " ✗ TLS 握手 + HTTP GET — 未能通过 HTTPS 获取响应"
|
||
fail
|
||
fi
|
||
else
|
||
echo " ⚠ openssl 未安装,跳过 TLS 握手验证"
|
||
fi
|
||
|
||
# 使用 curl -k 验证 HTTPS 响应体
|
||
assert_body_contains "https://$HOST/" "Cocoon" "HTTPS 首页响应"
|
||
|
||
echo ""
|
||
echo "=== HTTP/2 测试 ==="
|
||
# 使用已启动的 TLS 服务器测试 HTTP/2(ALPN 协商)
|
||
kill_server
|
||
sleep 1
|
||
$SERVER -r "$ROOT" -p 9999 --cert tests/server.crt --key tests/server.key > "$TMPDIR/server_h2.log" 2>&1 &
|
||
pid_h2=$!
|
||
|
||
# 等待服务器就绪
|
||
for i in {1..50}; do
|
||
if curl -s -o /dev/null -k --max-time 2 "https://$HOST/" 2>/dev/null; then
|
||
break
|
||
fi
|
||
sleep 0.2
|
||
done
|
||
|
||
# 验证 HTTP/2 协议版本
|
||
h2_version=$(curl --http2 -k -s -o /dev/null -w "%{http_version}" "https://$HOST/")
|
||
if [[ "$h2_version" == "2" ]]; then
|
||
echo " ✓ HTTP/2 ALPN 协商 — 协议版本 HTTP/2"
|
||
pass
|
||
else
|
||
echo " ✗ HTTP/2 ALPN 协商 — 期望 HTTP/2, 实际 HTTP/$h2_version"
|
||
fail
|
||
fi
|
||
|
||
# HTTP/2 GET 首页
|
||
h2_status=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" "https://$HOST/")
|
||
if [[ "$h2_status" == "200" ]]; then
|
||
echo " ✓ HTTP/2 GET 首页 — HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ HTTP/2 GET 首页 — 期望 200, 实际 $h2_status"
|
||
fail
|
||
fi
|
||
|
||
# HTTP/2 响应体
|
||
h2_body=$(curl --http2 -k -s --compressed "https://$HOST/")
|
||
if echo "$h2_body" | grep -q "Cocoon"; then
|
||
echo " ✓ HTTP/2 响应体 — 包含 'Cocoon'"
|
||
pass
|
||
else
|
||
echo " ✗ HTTP/2 响应体 — 未包含 'Cocoon'"
|
||
fail
|
||
fi
|
||
|
||
# HTTP/2 HEAD 请求
|
||
h2_head_status=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" -I "https://$HOST/index.html")
|
||
if [[ "$h2_head_status" == "200" ]]; then
|
||
echo " ✓ HTTP/2 HEAD 请求 — HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ HTTP/2 HEAD 请求 — 期望 200, 实际 $h2_head_status"
|
||
fail
|
||
fi
|
||
|
||
# HTTP/2 404
|
||
h2_404=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" "https://$HOST/nonexist.html")
|
||
if [[ "$h2_404" == "404" ]]; then
|
||
echo " ✓ HTTP/2 404 请求 — HTTP 404"
|
||
pass
|
||
else
|
||
echo " ✗ HTTP/2 404 请求 — 期望 404, 实际 $h2_404"
|
||
fail
|
||
fi
|
||
|
||
# HTTP/2 缓存协商(ETag 304)
|
||
H2_ETAG=$(curl --http2 -k -sI "https://$HOST/index.html" | grep -i "ETag:" | awk '{print $2}' | tr -d '\r')
|
||
if [[ -n "$H2_ETAG" ]]; then
|
||
h2_304=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" -H "If-None-Match: $H2_ETAG" "https://$HOST/index.html")
|
||
if [[ "$h2_304" == "304" ]]; then
|
||
echo " ✓ HTTP/2 If-None-Match 304 — 缓存命中"
|
||
pass
|
||
else
|
||
echo " ✗ HTTP/2 If-None-Match 304 — 期望 304, 实际 $h2_304"
|
||
fail
|
||
fi
|
||
fi
|
||
|
||
# HTTP/2 压缩测试
|
||
assert_http2_brotli "https://$HOST/index.html" "HTTP/2 HTML brotli"
|
||
assert_http2_brotli "https://$HOST/style.css" "HTTP/2 CSS brotli"
|
||
assert_http2_brotli "https://$HOST/app.js" "HTTP/2 JS brotli"
|
||
|
||
# HTTP/2 目录浏览测试
|
||
h2_dir_status=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" "https://$HOST/subdir/")
|
||
h2_dir_body=$(curl --http2 -k -s "https://$HOST/subdir/")
|
||
if [[ "$h2_dir_status" == "200" ]] && echo "$h2_dir_body" | grep -q "page.html"; then
|
||
echo " ✓ HTTP/2 目录浏览 — HTTP 200,包含 page.html"
|
||
pass
|
||
else
|
||
echo " ✗ HTTP/2 目录浏览 — 期望 200+page.html, 实际 $h2_dir_status"
|
||
fail
|
||
fi
|
||
|
||
# 停止 HTTP/2 服务器,恢复 HTTP 服务器
|
||
kill_server
|
||
sleep 1
|
||
start_server
|
||
|
||
# h2c 测试(明文 HTTP/2)
|
||
echo ""
|
||
echo "=== h2c 测试 ==="
|
||
|
||
# h2c prior knowledge 直接连接
|
||
h2c_pk_status=$(curl --http2-prior-knowledge -s -o /dev/null -w "%{http_code}" "http://$HOST/")
|
||
h2c_pk_version=$(curl --http2-prior-knowledge -s -o /dev/null -w "%{http_version}" "http://$HOST/")
|
||
if [[ "$h2c_pk_status" == "200" && "$h2c_pk_version" == "2" ]]; then
|
||
echo " ✓ h2c prior knowledge — HTTP 200, 协议 HTTP/2"
|
||
pass
|
||
else
|
||
echo " ✗ h2c prior knowledge — 期望 200+HTTP/2, 实际 $h2c_pk_status+HTTP/$h2c_pk_version"
|
||
fail
|
||
fi
|
||
|
||
# h2c prior knowledge 404
|
||
h2c_pk_404=$(curl --http2-prior-knowledge -s -o /dev/null -w "%{http_code}" "http://$HOST/nonexist.html")
|
||
if [[ "$h2c_pk_404" == "404" ]]; then
|
||
echo " ✓ h2c prior knowledge 404 — HTTP 404"
|
||
pass
|
||
else
|
||
echo " ✗ h2c prior knowledge 404 — 期望 404, 实际 $h2c_pk_404"
|
||
fail
|
||
fi
|
||
|
||
# h2c Upgrade 协商
|
||
h2c_up_status=$(curl --http2 -s -o /dev/null -w "%{http_code}" "http://$HOST/")
|
||
h2c_up_version=$(curl --http2 -s -o /dev/null -w "%{http_version}" "http://$HOST/")
|
||
if [[ "$h2c_up_status" == "200" && "$h2c_up_version" == "2" ]]; then
|
||
echo " ✓ h2c Upgrade 协商 — HTTP 200, 协议 HTTP/2"
|
||
pass
|
||
else
|
||
echo " ✗ h2c Upgrade 协商 — 期望 200+HTTP/2, 实际 $h2c_up_status+HTTP/$h2c_up_version"
|
||
fail
|
||
fi
|
||
|
||
# h2c Upgrade 404
|
||
h2c_up_404=$(curl --http2 -s -o /dev/null -w "%{http_code}" "http://$HOST/nonexist.html")
|
||
if [[ "$h2c_up_404" == "404" ]]; then
|
||
echo " ✓ h2c Upgrade 404 — HTTP 404"
|
||
pass
|
||
else
|
||
echo " ✗ h2c Upgrade 404 — 期望 404, 实际 $h2c_up_404"
|
||
fail
|
||
fi
|
||
|
||
# HTTP/2 目录浏览测试
|
||
# subdir 目录没有 index.html,应返回 200 目录列表
|
||
h2c_dir_status=$(curl --http2-prior-knowledge -s -o /dev/null -w "%{http_code}" "http://$HOST/subdir/")
|
||
h2c_dir_body=$(curl --http2-prior-knowledge -s "http://$HOST/subdir/")
|
||
if [[ "$h2c_dir_status" == "200" ]] && echo "$h2c_dir_body" | grep -q "page.html"; then
|
||
echo " ✓ h2c 目录浏览 — HTTP 200,包含 page.html"
|
||
pass
|
||
else
|
||
echo " ✗ h2c 目录浏览 — 期望 200+page.html, 实际 $h2c_dir_status"
|
||
fail
|
||
fi
|
||
|
||
# WebSocket 测试
|
||
# 停止 HTTP 服务器,恢复默认服务器
|
||
kill_server
|
||
sleep 1
|
||
start_server
|
||
|
||
# 中间件测试
|
||
echo ""
|
||
echo "=== 中间件测试 ==="
|
||
|
||
# CORS 测试
|
||
kill_server
|
||
sleep 1
|
||
$SERVER -r "$ROOT" -p 9999 --cors > "$TMPDIR/server_cors.log" 2>&1 &
|
||
for i in {1..30}; do
|
||
if nc -z localhost 9999 2>/dev/null; then break; fi
|
||
sleep 0.1
|
||
done
|
||
cors_status=$(curl -s -o /dev/null -w "%{http_code}" -X OPTIONS "$BASE/" -H "Origin: http://example.com" -H "Access-Control-Request-Method: POST")
|
||
if [[ "$cors_status" == "204" ]]; then
|
||
echo " ✓ CORS OPTIONS 预检 — HTTP 204"
|
||
pass
|
||
else
|
||
echo " ✗ CORS OPTIONS 预检 — 期望 204, 实际 $cors_status"
|
||
fail
|
||
fi
|
||
|
||
# Basic Auth 测试
|
||
kill_server
|
||
sleep 1
|
||
$SERVER -r "$ROOT" -p 9999 --auth-user "admin" --auth-pass "secret" > "$TMPDIR/server_auth.log" 2>&1 &
|
||
for i in {1..30}; do
|
||
if nc -z localhost 9999 2>/dev/null; then break; fi
|
||
sleep 0.1
|
||
done
|
||
auth_no_cred=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/")
|
||
if [[ "$auth_no_cred" == "401" ]]; then
|
||
echo " ✓ Basic Auth 无认证 — HTTP 401"
|
||
pass
|
||
else
|
||
echo " ✗ Basic Auth 无认证 — 期望 401, 实际 $auth_no_cred"
|
||
fail
|
||
fi
|
||
auth_with_cred=$(curl -s -o /dev/null -w "%{http_code}" -u "admin:secret" "$BASE/")
|
||
if [[ "$auth_with_cred" == "200" ]]; then
|
||
echo " ✓ Basic Auth 正确认证 — HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ Basic Auth 正确认证 — 期望 200, 实际 $auth_with_cred"
|
||
fail
|
||
fi
|
||
auth_wrong_pass=$(curl -s -o /dev/null -w "%{http_code}" -u "admin:wrong" "$BASE/")
|
||
if [[ "$auth_wrong_pass" == "401" ]]; then
|
||
echo " ✓ Basic Auth 错误密码 — HTTP 401"
|
||
pass
|
||
else
|
||
echo " ✗ Basic Auth 错误密码 — 期望 401, 实际 $auth_wrong_pass"
|
||
fail
|
||
fi
|
||
|
||
# Rate Limit 测试
|
||
kill_server
|
||
sleep 1
|
||
$SERVER -r "$ROOT" -p 9999 --rate-limit 1 > "$TMPDIR/server_ratelimit.log" 2>&1 &
|
||
for i in {1..30}; do
|
||
if nc -z localhost 9999 2>/dev/null; then break; fi
|
||
sleep 0.1
|
||
done
|
||
rl_first=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/")
|
||
rl_second=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/")
|
||
if [[ "$rl_first" == "200" && "$rl_second" == "429" ]]; then
|
||
echo " ✓ Rate Limit 限流 — 第1次 200, 第2次 429"
|
||
pass
|
||
else
|
||
echo " ✗ Rate Limit 限流 — 期望 200+429, 实际 $rl_first+$rl_second"
|
||
fail
|
||
fi
|
||
|
||
# 恢复默认服务器
|
||
kill_server
|
||
sleep 1
|
||
start_server
|
||
|
||
echo ""
|
||
echo "=== WebSocket 测试 ==="
|
||
|
||
if python3 "$ROOT/../websocket_test.py" > "$TMPDIR/ws_test.log" 2>&1; then
|
||
echo " ✓ WebSocket 握手 + echo — 通过"
|
||
pass
|
||
pass
|
||
else
|
||
echo " ✗ WebSocket 测试失败"
|
||
cat "$TMPDIR/ws_test.log"
|
||
fail
|
||
fail
|
||
fi
|
||
|
||
echo ""
|
||
echo "=== 结果汇总 ==="
|
||
echo "通过: $PASS"
|
||
echo "失败: $FAIL"
|
||
echo ""
|
||
|
||
if [[ "$FAIL" -gt 0 ]]; then
|
||
echo "[test] 查看服务器日志:"
|
||
cat "$TMPDIR/server.log" | tail -20
|
||
exit 1
|
||
fi
|
||
|
||
echo "[test] 全部通过 ✓"
|