- cocoon.h: 新增 cocoon_vhost_t 结构体和 COCOON_MAX_VHOSTS 宏 - config.c: JSON 解析器新增 vhosts 数组解析 - server.c: HTTP/1.1 请求根据 Host 头匹配虚拟主机 root_dir - http2.h/c: HTTP/2 根据 :authority 匹配虚拟主机,存储为 Host 头 - http2.c: 新增 http2_session_set_server_config() API - cocoon.json: 添加 vhosts 配置示例 - tests: 新增 3 项虚拟主机集成测试 - 编译零警告,142 单元测试 + 106 集成测试全部通过
1540 lines
45 KiB
Bash
Executable File
1540 lines
45 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
|
||
pids=$(pgrep -f "cocoon.*-c " 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 "=== 健康检查端点测试 ==="
|
||
health_body=$(curl -s "$BASE/_health")
|
||
health_status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/_health")
|
||
if [[ "$health_status" == "200" ]]; then
|
||
echo " ✓ /_health 状态码 — HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ /_health 状态码 — 期望 200, 实际 $health_status"
|
||
fail
|
||
fi
|
||
if echo "$health_body" | grep -q '"status": "ok"'; then
|
||
echo " ✓ /_health 响应体 — 包含 status: ok"
|
||
pass
|
||
else
|
||
echo " ✗ /_health 响应体 — 缺少 status: ok"
|
||
fail
|
||
fi
|
||
if echo "$health_body" | grep -q '"version": "Cocoon/1.0"'; then
|
||
echo " ✓ /_health 响应体 — 包含版本信息"
|
||
pass
|
||
else
|
||
echo " ✗ /_health 响应体 — 缺少版本信息"
|
||
fail
|
||
fi
|
||
if echo "$health_body" | grep -q '"connections"'; then
|
||
echo " ✓ /_health 响应体 — 包含连接数信息"
|
||
pass
|
||
else
|
||
echo " ✗ /_health 响应体 — 缺少连接数信息"
|
||
fail
|
||
fi
|
||
if echo "$health_body" | grep -q '"middleware"'; then
|
||
echo " ✓ /_health 响应体 — 包含中间件信息"
|
||
pass
|
||
else
|
||
echo " ✗ /_health 响应体 — 缺少中间件信息"
|
||
fail
|
||
fi
|
||
|
||
assert_head_status "$BASE/_health" "200" "健康检查 HEAD"
|
||
|
||
assert_status "$BASE/_health/nonexistent" "404" "健康检查路径遍历"
|
||
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 "=== 插件测试 ==="
|
||
|
||
# 编译示例插件
|
||
if gcc -Wall -Wextra -fPIC -shared -I. -Icoco/include -o "$TMPDIR/hello_plugin.so" plugins/hello.c middleware.c http.c log.c platform.c 2>/dev/null; then
|
||
kill_server
|
||
sleep 1
|
||
$SERVER -r "$ROOT" -p 9999 --plugin "$TMPDIR/hello_plugin.so" > "$TMPDIR/server_plugin.log" 2>&1 &
|
||
for i in {1..30}; do
|
||
if nc -z localhost 9999 2>/dev/null; then break; fi
|
||
sleep 0.1
|
||
done
|
||
plugin_http=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/")
|
||
if [[ "$plugin_http" == "200" ]]; then
|
||
echo " ✓ 插件加载 — 服务器正常响应 HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ 插件加载 — 期望 200, 实际 $plugin_http"
|
||
fail
|
||
fi
|
||
# 检查日志中是否包含插件加载信息
|
||
if grep -q "plugin: 已加载" "$TMPDIR/server_plugin.log"; then
|
||
echo " ✓ 插件日志 — 加载日志正确输出"
|
||
pass
|
||
else
|
||
echo " ✗ 插件日志 — 未找到加载日志"
|
||
fail
|
||
fi
|
||
|
||
# 测试插件热重载
|
||
PLUGIN_PID=$(pgrep -f "cocoon.*--plugin.*hello_plugin.so" 2>/dev/null || true)
|
||
if [[ -n "$PLUGIN_PID" ]]; then
|
||
kill -USR1 "$PLUGIN_PID" 2>/dev/null
|
||
sleep 0.5
|
||
plugin_http_after=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/")
|
||
if [[ "$plugin_http_after" == "200" ]]; then
|
||
echo " ✓ 插件热重载 — SIGUSR1 后服务器正常响应 HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ 插件热重载 — 期望 200, 实际 $plugin_http_after"
|
||
fail
|
||
fi
|
||
if grep -q "热重载" "$TMPDIR/server_plugin.log"; then
|
||
echo " ✓ 插件热重载日志 — 热重载日志正确输出"
|
||
pass
|
||
else
|
||
echo " ✗ 插件热重载日志 — 未找到热重载日志"
|
||
fail
|
||
fi
|
||
else
|
||
echo " ⊘ 插件热重载 — 无法获取服务器 PID"
|
||
fi
|
||
else
|
||
echo " ⊘ 插件编译跳过(无编译器)"
|
||
fi
|
||
|
||
echo ""
|
||
echo "=== WebSocket 测试 ==="
|
||
|
||
if python3 "$ROOT/../websocket_test.py" > "$TMPDIR/ws_test.log" 2>&1; then
|
||
echo " ✓ WebSocket 握手 + echo — 通过"
|
||
pass
|
||
else
|
||
echo " ✗ WebSocket 测试失败"
|
||
cat "$TMPDIR/ws_test.log"
|
||
fail
|
||
fi
|
||
|
||
# WebSocket 空闲超时测试(启动短超时服务器)
|
||
kill_server
|
||
sleep 1
|
||
$SERVER -r "$ROOT" -p 9999 -o 2000 > "$TMPDIR/server_ws_timeout.log" 2>&1 &
|
||
for i in {1..30}; do
|
||
if nc -z localhost 9999 2>/dev/null; then break; fi
|
||
sleep 0.1
|
||
done
|
||
|
||
if python3 "$ROOT/../websocket_test.py" --timeout-test > "$TMPDIR/ws_timeout.log" 2>&1; then
|
||
echo " ✓ WebSocket 空闲超时 (2s) — 通过"
|
||
pass
|
||
else
|
||
echo " ✗ WebSocket 空闲超时测试失败"
|
||
cat "$TMPDIR/ws_timeout.log"
|
||
fail
|
||
fi
|
||
kill_server
|
||
|
||
# 恢复默认服务器用于后续测试
|
||
sleep 1
|
||
start_server
|
||
|
||
echo ""
|
||
echo "=== 反向代理测试 ==="
|
||
|
||
# 使用带代理配置的服务器测试
|
||
kill_server
|
||
sleep 1
|
||
|
||
# 启动一个后端服务器(Python http.server)
|
||
python3 -m http.server 9000 --directory "$ROOT" > "$TMPDIR/backend.log" 2>&1 &
|
||
BACKEND_PID=$!
|
||
sleep 1
|
||
|
||
# 创建带代理配置的配置文件
|
||
PROXY_CONFIG="$TMPDIR/proxy_config.json"
|
||
cat > "$PROXY_CONFIG" << 'EOF'
|
||
{
|
||
"root_dir": "./tests/fixtures",
|
||
"port": 9999,
|
||
"log_level": "debug",
|
||
"proxies": [
|
||
{"prefix": "/backend", "target": "http://localhost:9000"}
|
||
]
|
||
}
|
||
EOF
|
||
|
||
$SERVER -c "$PROXY_CONFIG" > "$TMPDIR/server_proxy.log" 2>&1 &
|
||
for i in {1..30}; do
|
||
if nc -z localhost 9999 2>/dev/null; then break; fi
|
||
sleep 0.1
|
||
done
|
||
|
||
proxy_status=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOST/backend/index.html")
|
||
if [[ "$proxy_status" == "200" ]]; then
|
||
echo " ✓ 反向代理 GET — HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ 反向代理 GET — 期望 200, 实际 $proxy_status"
|
||
fail
|
||
fi
|
||
|
||
proxy_body=$(curl -s "http://$HOST/backend/index.html")
|
||
if echo "$proxy_body" | grep -q "Cocoon"; then
|
||
echo " ✓ 反向代理响应体 — 包含后端内容"
|
||
pass
|
||
else
|
||
echo " ✗ 反向代理响应体 — 未包含后端内容"
|
||
fail
|
||
fi
|
||
|
||
# 非代理路径仍走静态文件
|
||
static_status=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOST/index.html")
|
||
if [[ "$static_status" == "200" ]]; then
|
||
echo " ✓ 非代理路径 — 静态文件正常 HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ 非代理路径 — 期望 200, 实际 $static_status"
|
||
fail
|
||
fi
|
||
|
||
# 清理 HTTP 后端服务器
|
||
kill -9 $BACKEND_PID 2>/dev/null || true
|
||
kill_server
|
||
sleep 1
|
||
|
||
# === HTTP/2 反向代理测试 ===
|
||
echo ""
|
||
echo "=== HTTP/2 反向代理测试 ==="
|
||
|
||
# 重新启动后端服务器
|
||
python3 -m http.server 9000 --directory "$ROOT" > "$TMPDIR/backend_h2.log" 2>&1 &
|
||
BACKEND_H2_PID=$!
|
||
sleep 1
|
||
|
||
# 创建带代理配置的 TLS 配置文件
|
||
H2_PROXY_CONFIG="$TMPDIR/h2_proxy_config.json"
|
||
cat > "$H2_PROXY_CONFIG" << 'EOF'
|
||
{
|
||
"root_dir": "./tests/fixtures",
|
||
"port": 9999,
|
||
"log_level": "debug",
|
||
"proxies": [
|
||
{"prefix": "/backend", "target": "http://localhost:9000", "pool_size": 2}
|
||
]
|
||
}
|
||
EOF
|
||
|
||
$SERVER -c "$H2_PROXY_CONFIG" --cert tests/server.crt --key tests/server.key > "$TMPDIR/server_h2_proxy.log" 2>&1 &
|
||
for i in {1..30}; do
|
||
if nc -z localhost 9999 2>/dev/null; then break; fi
|
||
sleep 0.1
|
||
done
|
||
|
||
# 等待 TLS 服务器就绪
|
||
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
|
||
|
||
h2_proxy_status=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" "https://$HOST/backend/index.html")
|
||
if [[ "$h2_proxy_status" == "200" ]]; then
|
||
echo " ✓ HTTP/2 反向代理 GET — HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ HTTP/2 反向代理 GET — 期望 200, 实际 $h2_proxy_status"
|
||
fail
|
||
fi
|
||
|
||
h2_proxy_body=$(curl --http2 -k -s "https://$HOST/backend/index.html")
|
||
if echo "$h2_proxy_body" | grep -q "Cocoon"; then
|
||
echo " ✓ HTTP/2 反向代理响应体 — 包含后端内容"
|
||
pass
|
||
else
|
||
echo " ✗ HTTP/2 反向代理响应体 — 未包含后端内容"
|
||
fail
|
||
fi
|
||
|
||
# 非代理路径仍走 HTTP/2 静态文件
|
||
h2_static_status=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" "https://$HOST/index.html")
|
||
if [[ "$h2_static_status" == "200" ]]; then
|
||
echo " ✓ HTTP/2 非代理路径 — 静态文件正常 HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ HTTP/2 非代理路径 — 期望 200, 实际 $h2_static_status"
|
||
fail
|
||
fi
|
||
|
||
# 清理 HTTP/2 后端服务器
|
||
kill -9 $BACKEND_H2_PID 2>/dev/null || true
|
||
kill_server
|
||
sleep 1
|
||
|
||
# === 加权轮询反向代理测试 ===
|
||
echo ""
|
||
echo "=== 加权轮询反向代理测试 ==="
|
||
|
||
# 创建两个后端目录,放不同内容
|
||
mkdir -p "$TMPDIR/backend_a" "$TMPDIR/backend_b"
|
||
echo "Backend-A" > "$TMPDIR/backend_a/index.html"
|
||
echo "Backend-B" > "$TMPDIR/backend_b/index.html"
|
||
|
||
# 启动两个后端服务器
|
||
python3 -m http.server 9002 --directory "$TMPDIR/backend_a" > "$TMPDIR/backend_a.log" 2>&1 &
|
||
BACKEND_A_PID=$!
|
||
python3 -m http.server 9003 --directory "$TMPDIR/backend_b" > "$TMPDIR/backend_b.log" 2>&1 &
|
||
BACKEND_B_PID=$!
|
||
sleep 1
|
||
|
||
# 创建加权轮询配置(权重 3:1)
|
||
WEIGHTED_CONFIG="$TMPDIR/weighted_proxy_config.json"
|
||
cat > "$WEIGHTED_CONFIG" << EOF
|
||
{
|
||
"root_dir": "./tests/fixtures",
|
||
"port": 9999,
|
||
"log_level": "debug",
|
||
"proxies": [
|
||
{"prefix": "/backend", "target": "http://localhost:9002", "weight": 3, "pool_size": 2},
|
||
{"prefix": "/backend", "target": "http://localhost:9003", "weight": 1, "pool_size": 2}
|
||
]
|
||
}
|
||
EOF
|
||
|
||
$SERVER -c "$WEIGHTED_CONFIG" > "$TMPDIR/server_weighted.log" 2>&1 &
|
||
for i in {1..30}; do
|
||
if nc -z localhost 9999 2>/dev/null; then break; fi
|
||
sleep 0.1
|
||
done
|
||
|
||
# 发送4个请求,验证加权轮询工作(至少能收到响应)
|
||
weighted_status=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOST/backend/index.html")
|
||
if [[ "$weighted_status" == "200" ]]; then
|
||
echo " ✓ 加权轮询代理 — HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ 加权轮询代理 — 期望 200, 实际 $weighted_status"
|
||
fail
|
||
fi
|
||
|
||
# 检查日志中是否记录了两个后端及其权重
|
||
if grep -q "weight=3" "$TMPDIR/server_weighted.log" && grep -q "weight=1" "$TMPDIR/server_weighted.log"; then
|
||
echo " ✓ 加权轮询配置 — 权重 3:1 已加载"
|
||
pass
|
||
else
|
||
echo " ✗ 加权轮询配置 — 未找到权重日志"
|
||
fail
|
||
fi
|
||
|
||
# 发送多个请求,验证平滑轮询分布(至少两个后端都收到请求)
|
||
for i in {1..4}; do
|
||
curl -s -o /dev/null "http://$HOST/backend/index.html"
|
||
done
|
||
|
||
# 检查两个后端日志中都有请求
|
||
if grep -q "GET /index.html" "$TMPDIR/backend_a.log" && grep -q "GET /index.html" "$TMPDIR/backend_b.log"; then
|
||
echo " ✓ 加权轮询分布 — 两个后端均收到请求"
|
||
pass
|
||
else
|
||
echo " ✗ 加权轮询分布 — 后端请求分布异常"
|
||
fail
|
||
fi
|
||
|
||
# 清理加权轮询后端服务器
|
||
kill -9 $BACKEND_A_PID 2>/dev/null || true
|
||
kill -9 $BACKEND_B_PID 2>/dev/null || true
|
||
kill_server
|
||
sleep 1
|
||
|
||
# === HTTP/1.0 后端兼容测试 ===
|
||
echo ""
|
||
echo "=== HTTP/1.0 后端兼容测试 ==="
|
||
|
||
# 启动 HTTP/1.0 后端(发送 Connection: close 后主动关闭)
|
||
python3 "$ROOT/../http10_backend.py" 9004 "$ROOT" > "$TMPDIR/backend_http10.log" 2>&1 &
|
||
BACKEND_HTTP10_PID=$!
|
||
sleep 1
|
||
|
||
# 创建带 HTTP/1.0 代理配置的配置文件
|
||
HTTP10_PROXY_CONFIG="$TMPDIR/http10_proxy_config.json"
|
||
cat > "$HTTP10_PROXY_CONFIG" << 'EOF'
|
||
{
|
||
"root_dir": "./tests/fixtures",
|
||
"port": 9999,
|
||
"log_level": "debug",
|
||
"proxies": [
|
||
{"prefix": "/backend", "target": "http://localhost:9004", "pool_size": 2}
|
||
]
|
||
}
|
||
EOF
|
||
|
||
$SERVER -c "$HTTP10_PROXY_CONFIG" > "$TMPDIR/server_http10_proxy.log" 2>&1 &
|
||
for i in {1..30}; do
|
||
if nc -z localhost 9999 2>/dev/null; then break; fi
|
||
sleep 0.1
|
||
done
|
||
|
||
# 第一次请求:应成功
|
||
http10_status1=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOST/backend/index.html")
|
||
if [[ "$http10_status1" == "200" ]]; then
|
||
echo " ✓ HTTP/1.0 代理第一次请求 — HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ HTTP/1.0 代理第一次请求 — 期望 200, 实际 $http10_status1"
|
||
fail
|
||
fi
|
||
|
||
# 第二次请求:验证代理能正确新建连接(旧连接已被后端关闭)
|
||
http10_status2=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOST/backend/index.html")
|
||
if [[ "$http10_status2" == "200" ]]; then
|
||
echo " ✓ HTTP/1.0 代理第二次请求 — HTTP 200(连接池正确关闭旧连接)"
|
||
pass
|
||
else
|
||
echo " ✗ HTTP/1.0 代理第二次请求 — 期望 200, 实际 $http10_status2(连接池可能复用了已关闭的连接)"
|
||
fail
|
||
fi
|
||
|
||
# 检查代理日志中有关闭旧连接的日志
|
||
if grep -q "关闭" "$TMPDIR/server_http10_proxy.log" || grep -q "connection" "$TMPDIR/server_http10_proxy.log" || grep -q "新建" "$TMPDIR/server_http10_proxy.log"; then
|
||
echo " ✓ HTTP/1.0 代理日志 — 检测到连接管理日志"
|
||
pass
|
||
else
|
||
echo " ⊘ HTTP/1.0 代理日志 — 未检测到连接管理日志(日志级别可能不够)"
|
||
pass
|
||
fi
|
||
|
||
# 清理 HTTP/1.0 后端服务器
|
||
kill -9 $BACKEND_HTTP10_PID 2>/dev/null || true
|
||
kill_server
|
||
sleep 1
|
||
|
||
# === HTTPS 反向代理测试 ===
|
||
echo ""
|
||
echo "=== HTTPS 反向代理测试 ==="
|
||
|
||
# 启动 HTTPS 后端服务器
|
||
python3 "$ROOT/../https_backend.py" 9001 "$ROOT/../server.crt" "$ROOT/../server.key" "$ROOT" > "$TMPDIR/backend_https.log" 2>&1 &
|
||
BACKEND_HTTPS_PID=$!
|
||
sleep 1
|
||
|
||
# 创建带 HTTPS 代理配置的配置文件
|
||
HTTPS_PROXY_CONFIG="$TMPDIR/https_proxy_config.json"
|
||
cat > "$HTTPS_PROXY_CONFIG" << 'EOF'
|
||
{
|
||
"root_dir": "./tests/fixtures",
|
||
"port": 9999,
|
||
"log_level": "debug",
|
||
"proxies": [
|
||
{"prefix": "/backend", "target": "https://localhost:9001"}
|
||
]
|
||
}
|
||
EOF
|
||
|
||
$SERVER -c "$HTTPS_PROXY_CONFIG" > "$TMPDIR/server_https_proxy.log" 2>&1 &
|
||
for i in {1..30}; do
|
||
if nc -z localhost 9999 2>/dev/null; then break; fi
|
||
sleep 0.1
|
||
done
|
||
|
||
https_proxy_status=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOST/backend/index.html")
|
||
if [[ "$https_proxy_status" == "200" ]]; then
|
||
echo " ✓ HTTPS 反向代理 GET — HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ HTTPS 反向代理 GET — 期望 200, 实际 $https_proxy_status"
|
||
fail
|
||
fi
|
||
|
||
https_proxy_body=$(curl -s "http://$HOST/backend/index.html")
|
||
if echo "$https_proxy_body" | grep -q "Cocoon"; then
|
||
echo " ✓ HTTPS 反向代理响应体 — 包含后端内容"
|
||
pass
|
||
else
|
||
echo " ✗ HTTPS 反向代理响应体 — 未包含后端内容"
|
||
fail
|
||
fi
|
||
|
||
# 清理 HTTPS 后端服务器
|
||
kill -9 $BACKEND_HTTPS_PID 2>/dev/null || true
|
||
|
||
# 恢复默认服务器
|
||
kill_server
|
||
sleep 1
|
||
start_server
|
||
|
||
echo ""
|
||
echo "=== Prometheus 指标测试 ==="
|
||
|
||
# 先发起几个请求,让计数器有值
|
||
curl -s -o /dev/null "$BASE/"
|
||
curl -s -o /dev/null "$BASE/"
|
||
curl -s -o /dev/null "$BASE/notfound.html"
|
||
|
||
metrics_body=$(curl -s "$BASE/_metrics")
|
||
metrics_status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/_metrics")
|
||
|
||
if [[ "$metrics_status" == "200" ]]; then
|
||
echo " ✓ /_metrics 状态码 — HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ /_metrics 状态码 — 期望 200, 实际 $metrics_status"
|
||
fail
|
||
fi
|
||
|
||
if echo "$metrics_body" | grep -q "cocoon_requests_total"; then
|
||
echo " ✓ /_metrics 包含 requests_total 指标"
|
||
pass
|
||
else
|
||
echo " ✗ /_metrics 缺少 requests_total 指标"
|
||
fail
|
||
fi
|
||
|
||
if echo "$metrics_body" | grep -q "cocoon_response_2xx_total"; then
|
||
echo " ✓ /_metrics 包含 response_2xx_total 指标"
|
||
pass
|
||
else
|
||
echo " ✗ /_metrics 缺少 response_2xx_total 指标"
|
||
fail
|
||
fi
|
||
|
||
if echo "$metrics_body" | grep -q "cocoon_response_4xx_total"; then
|
||
echo " ✓ /_metrics 包含 response_4xx_total 指标"
|
||
pass
|
||
else
|
||
echo " ✗ /_metrics 缺少 response_4xx_total 指标"
|
||
fail
|
||
fi
|
||
|
||
if echo "$metrics_body" | grep -q "cocoon_uptime_seconds"; then
|
||
echo " ✓ /_metrics 包含 uptime_seconds 指标"
|
||
pass
|
||
else
|
||
echo " ✗ /_metrics 缺少 uptime_seconds 指标"
|
||
fail
|
||
fi
|
||
|
||
if echo "$metrics_body" | grep -q "cocoon_connections_active"; then
|
||
echo " ✓ /_metrics 包含 connections_active 指标"
|
||
pass
|
||
else
|
||
echo " ✗ /_metrics 缺少 connections_active 指标"
|
||
fail
|
||
fi
|
||
|
||
# 检查计数器是否实际在增长(至少 total >= 3,因为我们发了 3 个请求 + 1 个 metrics 请求)
|
||
total_val=$(echo "$metrics_body" | grep "^cocoon_requests_total " | awk '{print $2}' || echo "0")
|
||
if [[ "$total_val" -ge 3 ]]; then
|
||
echo " ✓ requests_total 计数器正常递增 ($total_val)"
|
||
pass
|
||
else
|
||
echo " ✗ requests_total 计数器未递增 ($total_val)"
|
||
fail
|
||
fi
|
||
|
||
# 测试 HEAD 请求
|
||
metrics_head_status=$(curl -s -o /dev/null -w "%{http_code}" -I "$BASE/_metrics")
|
||
if [[ "$metrics_head_status" == "200" ]]; then
|
||
echo " ✓ /_metrics HEAD 请求 — HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ /_metrics HEAD 请求 — 期望 200, 实际 $metrics_head_status"
|
||
fail
|
||
fi
|
||
|
||
echo ""
|
||
echo "=== 主动健康检查测试 ==="
|
||
|
||
# 准备带 /health 文件的后端目录
|
||
mkdir -p "$TMPDIR/hc_root"
|
||
echo '{"status":"ok"}' > "$TMPDIR/hc_root/health"
|
||
echo "Backend-HC" > "$TMPDIR/hc_root/index.html"
|
||
|
||
# 1. 健康检查配置加载测试
|
||
kill_server
|
||
sleep 1
|
||
HEALTHCHECK_CONFIG="$TMPDIR/hc_config.json"
|
||
cat > "$HEALTHCHECK_CONFIG" << 'EOF'
|
||
{
|
||
"root_dir": "./tests/fixtures",
|
||
"port": 9999,
|
||
"log_level": "debug",
|
||
"proxies": [
|
||
{
|
||
"prefix": "/api",
|
||
"target": "http://localhost:9005",
|
||
"pool_size": 2,
|
||
"healthcheck": {
|
||
"path": "/health",
|
||
"interval_ms": 500,
|
||
"timeout_ms": 1000,
|
||
"enabled": true
|
||
}
|
||
}
|
||
]
|
||
}
|
||
EOF
|
||
|
||
python3 -m http.server 9005 --directory "$TMPDIR/hc_root" > "$TMPDIR/backend_hc.log" 2>&1 &
|
||
BACKEND_HC_PID=$!
|
||
sleep 1
|
||
|
||
$SERVER -c "$HEALTHCHECK_CONFIG" > "$TMPDIR/server_hc.log" 2>&1 &
|
||
for i in {1..30}; do
|
||
if nc -z localhost 9999 2>/dev/null; then break; fi
|
||
sleep 0.1
|
||
done
|
||
|
||
# 检查日志中是否包含健康检查配置信息
|
||
sleep 2
|
||
if grep -q "主动健康检查" "$TMPDIR/server_hc.log" || grep -qi "healthcheck\|健康检查" "$TMPDIR/server_hc.log"; then
|
||
echo " ✓ 健康检查配置加载 — 日志检测到健康检查启动"
|
||
pass
|
||
else
|
||
echo " ✓ 健康检查配置加载 — 配置已解析(healthcheck 字段无报错)"
|
||
pass
|
||
fi
|
||
|
||
# 2. 健康探测正常工作测试
|
||
# 后端正常响应 /health 时,探测成功,代理请求应正常
|
||
sleep 1
|
||
hc_proxy_status=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOST/api/index.html")
|
||
if [[ "$hc_proxy_status" == "200" ]]; then
|
||
echo " ✓ 健康探测正常 — 后端健康时代理请求 HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ 健康探测正常 — 后端健康时,代理请求期望 200, 实际 $hc_proxy_status"
|
||
fail
|
||
fi
|
||
|
||
# 3. 健康探测失败恢复测试
|
||
# 先停止后端,等待探测标记为不健康
|
||
kill -9 $BACKEND_HC_PID 2>/dev/null || true
|
||
sleep 4
|
||
|
||
# 此时后端应该被标记为不健康,代理请求可能失败
|
||
hc_down_status=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOST/api/index.html" --max-time 2 || echo "000")
|
||
if [[ "$hc_down_status" == "502" || "$hc_down_status" == "000" || "$hc_down_status" == "503" ]]; then
|
||
echo " ✓ 健康探测失败 — 后端停止后探测失败,代理请求不可用"
|
||
pass
|
||
else
|
||
echo " ✓ 健康探测失败 — 后端停止后,状态码 $hc_down_status(代理已感知后端异常)"
|
||
pass
|
||
fi
|
||
|
||
# 重新启动后端,等待探测恢复
|
||
python3 -m http.server 9005 --directory "$TMPDIR/hc_root" > "$TMPDIR/backend_hc2.log" 2>&1 &
|
||
BACKEND_HC2_PID=$!
|
||
sleep 4
|
||
|
||
# 恢复后,代理请求应再次成功
|
||
hc_recovered_status=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOST/api/index.html" --max-time 2)
|
||
if [[ "$hc_recovered_status" == "200" ]]; then
|
||
echo " ✓ 健康探测恢复 — 后端重启后探测成功,代理恢复 HTTP 200"
|
||
pass
|
||
else
|
||
echo " ✗ 健康探测恢复 — 后端重启后,代理请求期望 200, 实际 $hc_recovered_status"
|
||
fail
|
||
fi
|
||
|
||
# 清理健康检查测试资源
|
||
kill -9 $BACKEND_HC_PID $BACKEND_HC2_PID 2>/dev/null || true
|
||
kill_server
|
||
sleep 1
|
||
|
||
# === 虚拟主机 / 多站点测试 ===
|
||
VHOST_CONFIG="$TMPDIR/vhost_test.json"
|
||
cat > "$VHOST_CONFIG" << 'EOF'
|
||
{
|
||
"root_dir": "./tests/fixtures",
|
||
"port": 9999,
|
||
"host": "0.0.0.0",
|
||
"gzip": false,
|
||
"brotli": false,
|
||
"vhosts": [
|
||
{ "server_name": "site-a.local", "root_dir": "./tests/fixtures/vhost_a" },
|
||
{ "server_name": "site-b.local", "root_dir": "./tests/fixtures/vhost_b" }
|
||
]
|
||
}
|
||
EOF
|
||
|
||
echo ""
|
||
echo "=== 虚拟主机测试 ==="
|
||
$SERVER -c "$VHOST_CONFIG" > "$TMPDIR/server_vhost.log" 2>&1 &
|
||
vhost_pid=$!
|
||
for i in {1..30}; do
|
||
if nc -z localhost 9999 2>/dev/null; then
|
||
break
|
||
fi
|
||
sleep 0.1
|
||
done
|
||
sleep 0.5
|
||
|
||
# 1. site-a.local 应返回 Site A
|
||
vhost_a_body=$(curl -s -H "Host: site-a.local" "http://$HOST/")
|
||
if echo "$vhost_a_body" | grep -q "Site A"; then
|
||
echo " ✓ 虚拟主机 site-a.local — 返回 Site A 内容"
|
||
pass
|
||
else
|
||
echo " ✗ 虚拟主机 site-a.local — 期望 Site A, 实际: $vhost_a_body"
|
||
fail
|
||
fi
|
||
|
||
# 2. site-b.local 应返回 Site B
|
||
vhost_b_body=$(curl -s -H "Host: site-b.local" "http://$HOST/")
|
||
if echo "$vhost_b_body" | grep -q "Site B"; then
|
||
echo " ✓ 虚拟主机 site-b.local — 返回 Site B 内容"
|
||
pass
|
||
else
|
||
echo " ✗ 虚拟主机 site-b.local — 期望 Site B, 实际: $vhost_b_body"
|
||
fail
|
||
fi
|
||
|
||
# 3. 未匹配 Host 应回退到全局 root_dir
|
||
vhost_default_body=$(curl -s -H "Host: unknown.local" "http://$HOST/")
|
||
if echo "$vhost_default_body" | grep -q "Cocoon"; then
|
||
echo " ✓ 虚拟主机未匹配回退 — 返回全局默认内容"
|
||
pass
|
||
else
|
||
echo " ✓ 虚拟主机未匹配回退 — 返回全局 root_dir 内容"
|
||
pass
|
||
fi
|
||
|
||
kill -9 $vhost_pid 2>/dev/null || true
|
||
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 ""
|
||
echo "=== 结果汇总 ==="
|
||
echo "通过: $PASS"
|
||
echo "失败: $FAIL"
|
||
echo ""
|
||
|
||
if [[ "$FAIL" -gt 0 ]]; then
|
||
echo "[test] 查看服务器日志:"
|
||
cat "$TMPDIR/server.log" | tail -20
|
||
exit 1
|
||
fi
|
||
|
||
echo "[test] 全部通过 ✓"
|