fix(proxy): 修复连接池 HTTP/1.0 兼容与配置合并 bug
- proxy.c: 当后端发送 Connection: close 或关闭连接时,不再归还已关闭连接 到池中。避免复用已关闭连接导致请求失败。 - http2.c: HTTP/2 代理检测到 HTTP/1.0 响应时默认关闭连接,不再错误归还。 - main.c: 修复 -c 配置文件与 --cert/--key 同时使用时 config_merge 的 use-after-free 问题。先加载配置文件,再用命令行参数覆盖。 - tests/http10_backend.py: 新增 HTTP/1.0 后端模拟器,用于测试连接池兼容。 - tests/integration_test.sh: 添加 HTTP/1.0 后端兼容测试,验证两次连续请求 均成功(第二次必须新建连接)。
This commit is contained in:
parent
25a0b100c1
commit
90601e0f67
7
http2.c
7
http2.c
@ -1584,6 +1584,7 @@ static void http2_serve_proxy(http2_session_t *h2, http2_stream_data_t *stream)
|
||||
|
||||
/* 判断后端是否发送 Connection: close */
|
||||
bool backend_wants_close = false;
|
||||
bool is_http10 = strncmp(response_buf, "HTTP/1.0", 8) == 0;
|
||||
{
|
||||
const char *p = headers;
|
||||
while (*p) {
|
||||
@ -1592,6 +1593,8 @@ static void http2_serve_proxy(http2_session_t *h2, http2_stream_data_t *stream)
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (strncasecmp(p, "close", 5) == 0) {
|
||||
backend_wants_close = true;
|
||||
} else if (strncasecmp(p, "keep-alive", 10) == 0) {
|
||||
backend_wants_close = false; /* 显式 keep-alive */
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -1600,6 +1603,10 @@ static void http2_serve_proxy(http2_session_t *h2, http2_stream_data_t *stream)
|
||||
p = nl + 2;
|
||||
}
|
||||
}
|
||||
/* HTTP/1.0 默认关闭,除非显式 keep-alive */
|
||||
if (is_http10 && !backend_wants_close) {
|
||||
backend_wants_close = true;
|
||||
}
|
||||
|
||||
/* 归还或关闭连接 */
|
||||
if (backend_wants_close) {
|
||||
|
||||
93
main.c
93
main.c
@ -103,74 +103,52 @@ static void print_usage(const char *prog) {
|
||||
*/
|
||||
static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
|
||||
/* 默认值 */
|
||||
config->root_dir = NULL;
|
||||
memset(config, 0, sizeof(*config));
|
||||
config->port = 8080;
|
||||
config->threaded = false;
|
||||
config->num_workers = 0;
|
||||
config->max_connections = 0;
|
||||
config->timeout_ms = 0;
|
||||
config->log_level = LOG_LEVEL_INFO;
|
||||
config->gzip_enabled = true;
|
||||
config->brotli_enabled = true;
|
||||
|
||||
config->tls_cert = NULL;
|
||||
config->tls_key = NULL;
|
||||
config->tls_enabled = false;
|
||||
config->access_log_path = NULL;
|
||||
|
||||
config->cors_enabled = false;
|
||||
config->auth_user = NULL;
|
||||
config->auth_pass = NULL;
|
||||
config->rate_limit = 0;
|
||||
|
||||
config->num_plugins = 0;
|
||||
|
||||
bool has_root_dir = false;
|
||||
bool has_port = false;
|
||||
bool has_workers = false;
|
||||
bool has_max_conn = false;
|
||||
bool has_timeout = false;
|
||||
bool has_log_level = false;
|
||||
bool has_gzip_enabled = false;
|
||||
bool has_brotli_enabled = false;
|
||||
bool has_tls_cert = false;
|
||||
bool has_tls_key = false;
|
||||
bool has_tls_enabled = false;
|
||||
bool has_access_log = false;
|
||||
bool has_cors_enabled = false;
|
||||
bool has_auth_user = false;
|
||||
bool has_auth_pass = false;
|
||||
bool has_rate_limit = false;
|
||||
bool has_plugins = false;
|
||||
/* 第一阶段:查找配置文件路径 */
|
||||
const char *config_file = NULL;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "-c") == 0) {
|
||||
if (++i >= argc) return false;
|
||||
config_file = argv[i];
|
||||
}
|
||||
}
|
||||
|
||||
/* 第二阶段:加载配置文件(如果有) */
|
||||
if (config_file) {
|
||||
if (!config_load_from_file(config_file, config)) {
|
||||
fprintf(stderr, "Error: 无法加载配置文件: %s\n", config_file);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* 第三阶段:解析命令行参数,覆盖配置文件 */
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "-c") == 0) {
|
||||
i++; /* 已处理,跳过 */
|
||||
} else if (strcmp(argv[i], "-r") == 0) {
|
||||
if (++i >= argc) return false;
|
||||
free((void *)config->root_dir);
|
||||
config->root_dir = strdup(argv[i]);
|
||||
has_root_dir = true;
|
||||
} else if (strcmp(argv[i], "-p") == 0) {
|
||||
if (++i >= argc) return false;
|
||||
config->port = (uint16_t)atoi(argv[i]);
|
||||
if (config->port == 0) config->port = 8080;
|
||||
has_port = true;
|
||||
} else if (strcmp(argv[i], "-t") == 0) {
|
||||
config->threaded = true;
|
||||
} else if (strcmp(argv[i], "-w") == 0) {
|
||||
if (++i >= argc) return false;
|
||||
config->num_workers = (uint32_t)atoi(argv[i]);
|
||||
has_workers = true;
|
||||
} else if (strcmp(argv[i], "-m") == 0) {
|
||||
if (++i >= argc) return false;
|
||||
config->max_connections = (uint32_t)atoi(argv[i]);
|
||||
has_max_conn = true;
|
||||
} else if (strcmp(argv[i], "-o") == 0) {
|
||||
if (++i >= argc) return false;
|
||||
config->timeout_ms = (uint32_t)atoi(argv[i]);
|
||||
has_timeout = true;
|
||||
} else if (strcmp(argv[i], "-l") == 0) {
|
||||
if (++i >= argc) return false;
|
||||
if (strcmp(argv[i], "error") == 0) config->log_level = LOG_LEVEL_ERROR;
|
||||
@ -181,51 +159,43 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
|
||||
fprintf(stderr, "Unknown log level: %s\n", argv[i]);
|
||||
return false;
|
||||
}
|
||||
has_log_level = true;
|
||||
} else if (strcmp(argv[i], "-v") == 0) {
|
||||
config->log_level = LOG_LEVEL_DEBUG;
|
||||
has_log_level = true;
|
||||
} else if (strcmp(argv[i], "--no-gzip") == 0) {
|
||||
config->gzip_enabled = false;
|
||||
has_gzip_enabled = true;
|
||||
} else if (strcmp(argv[i], "--no-brotli") == 0) {
|
||||
config->brotli_enabled = false;
|
||||
has_brotli_enabled = true;
|
||||
} else if (strcmp(argv[i], "--cert") == 0) {
|
||||
if (++i >= argc) return false;
|
||||
free((void *)config->tls_cert);
|
||||
config->tls_cert = strdup(argv[i]);
|
||||
has_tls_cert = true;
|
||||
} else if (strcmp(argv[i], "--key") == 0) {
|
||||
if (++i >= argc) return false;
|
||||
free((void *)config->tls_key);
|
||||
config->tls_key = strdup(argv[i]);
|
||||
has_tls_key = true;
|
||||
} else if (strcmp(argv[i], "--tls") == 0) {
|
||||
config->tls_enabled = true;
|
||||
has_tls_enabled = true;
|
||||
} else if (strcmp(argv[i], "--access-log") == 0) {
|
||||
if (++i >= argc) return false;
|
||||
free((void *)config->access_log_path);
|
||||
config->access_log_path = strdup(argv[i]);
|
||||
has_access_log = true;
|
||||
} else if (strcmp(argv[i], "--cors") == 0) {
|
||||
config->cors_enabled = true;
|
||||
has_cors_enabled = true;
|
||||
} else if (strcmp(argv[i], "--auth-user") == 0) {
|
||||
if (++i >= argc) return false;
|
||||
free((void *)config->auth_user);
|
||||
config->auth_user = strdup(argv[i]);
|
||||
has_auth_user = true;
|
||||
} else if (strcmp(argv[i], "--auth-pass") == 0) {
|
||||
if (++i >= argc) return false;
|
||||
free((void *)config->auth_pass);
|
||||
config->auth_pass = strdup(argv[i]);
|
||||
has_auth_pass = true;
|
||||
} else if (strcmp(argv[i], "--rate-limit") == 0) {
|
||||
if (++i >= argc) return false;
|
||||
config->rate_limit = (uint32_t)atoi(argv[i]);
|
||||
has_rate_limit = true;
|
||||
} else if (strcmp(argv[i], "--plugin") == 0) {
|
||||
if (++i >= argc) return false;
|
||||
if (config->num_plugins < COCOON_MAX_PLUGINS) {
|
||||
config->plugins[config->num_plugins++] = strdup(argv[i]);
|
||||
has_plugins = true;
|
||||
} else {
|
||||
fprintf(stderr, "Error: 最多支持 %d 个插件\n", COCOON_MAX_PLUGINS);
|
||||
return false;
|
||||
@ -239,23 +209,6 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
|
||||
}
|
||||
}
|
||||
|
||||
/* 如果有配置文件,先加载 */
|
||||
if (config_file) {
|
||||
if (!config_load_from_file(config_file, config)) {
|
||||
fprintf(stderr, "Error: 无法加载配置文件: %s\n", config_file);
|
||||
return false;
|
||||
}
|
||||
/* 用命令行参数覆盖配置文件 */
|
||||
config_merge(config, config, has_root_dir, has_port, has_workers,
|
||||
has_max_conn, has_timeout, has_log_level,
|
||||
has_gzip_enabled, has_brotli_enabled,
|
||||
has_tls_cert, has_tls_key, has_tls_enabled,
|
||||
has_access_log,
|
||||
has_cors_enabled, has_auth_user, has_auth_pass,
|
||||
has_rate_limit,
|
||||
has_plugins);
|
||||
}
|
||||
|
||||
if (!config->root_dir) {
|
||||
fprintf(stderr, "Error: 必须指定静态资源根目录(-r <dir> 或配置文件)\n");
|
||||
return false;
|
||||
|
||||
11
proxy.c
11
proxy.c
@ -546,6 +546,8 @@ static bool proxy_relay_backend(cocoon_socket_t client_fd, const http_request_t
|
||||
/* 流式转发响应回客户端 */
|
||||
bool success = false;
|
||||
ssize_t total = 0;
|
||||
bool connection_closed = false; /* 后端是否主动关闭连接 */
|
||||
|
||||
if (send_ok) {
|
||||
char relay_buf[8192];
|
||||
bool recv_ok = true;
|
||||
@ -562,8 +564,11 @@ static bool proxy_relay_backend(cocoon_socket_t client_fd, const http_request_t
|
||||
break;
|
||||
}
|
||||
if (r == 0) {
|
||||
/* 后端关闭连接,如果尚未转发任何数据则视为失败 */
|
||||
/* 后端关闭连接,如果尚未转发任何数据则视为失败
|
||||
* 否则标记 connection_closed,请求成功但连接不可复用
|
||||
*/
|
||||
if (total == 0) recv_ok = false;
|
||||
connection_closed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -585,8 +590,8 @@ static bool proxy_relay_backend(cocoon_socket_t client_fd, const http_request_t
|
||||
|
||||
if (total_forwarded) *total_forwarded = total;
|
||||
|
||||
/* 归还或关闭连接:只有后端未关闭时才归还 */
|
||||
if (success && total > 0) {
|
||||
/* 归还或关闭连接:只有后端未主动关闭时才归还 */
|
||||
if (success && total > 0 && !connection_closed) {
|
||||
proxy_pool_release(backend, backend_fd, tls_conn);
|
||||
} else {
|
||||
if (use_https && tls_conn) {
|
||||
|
||||
101
tests/http10_backend.py
Normal file
101
tests/http10_backend.py
Normal file
@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
http10_backend.py — HTTP/1.0 后端模拟器
|
||||
|
||||
模拟一个 HTTP/1.0 后端,响应后发送 Connection: close 并立即关闭连接。
|
||||
用于验证反向代理连接池的 HTTP/1.0 兼容性。
|
||||
|
||||
用法: python3 http10_backend.py <port> <directory>
|
||||
"""
|
||||
|
||||
import sys
|
||||
import socket
|
||||
import os
|
||||
import mimetypes
|
||||
|
||||
HTTP10_RESPONSE = """HTTP/1.0 200 OK
|
||||
Connection: close
|
||||
Content-Type: {ctype}
|
||||
Content-Length: {length}
|
||||
|
||||
""".replace("\n", "\r\n")
|
||||
|
||||
HTTP10_NOT_FOUND = """HTTP/1.0 404 Not Found
|
||||
Connection: close
|
||||
Content-Type: text/plain
|
||||
Content-Length: 13
|
||||
|
||||
404 Not Found
|
||||
""".replace("\n", "\r\n")
|
||||
|
||||
|
||||
def handle_client(conn, addr, doc_root):
|
||||
try:
|
||||
data = conn.recv(4096)
|
||||
if not data:
|
||||
return
|
||||
|
||||
# 解析请求行
|
||||
lines = data.decode("utf-8", errors="replace").split("\r\n")
|
||||
if not lines:
|
||||
return
|
||||
parts = lines[0].split()
|
||||
if len(parts) < 2:
|
||||
return
|
||||
|
||||
path = parts[1]
|
||||
if path == "/":
|
||||
path = "/index.html"
|
||||
|
||||
# 去掉路径中的前缀(如 /backend)
|
||||
if path.startswith("/backend"):
|
||||
path = path[len("/backend"):]
|
||||
if not path.startswith("/"):
|
||||
path = "/" + path
|
||||
|
||||
# 安全路径
|
||||
safe_path = os.path.normpath(os.path.join(doc_root, path.lstrip("/")))
|
||||
print(f"[http10] doc_root={doc_root}, path={path}, safe_path={safe_path}", flush=True)
|
||||
print(f"[http10] abspath={os.path.abspath(doc_root)}", flush=True)
|
||||
print(f"[http10] exists={os.path.exists(safe_path)}, isfile={os.path.isfile(safe_path) if os.path.exists(safe_path) else False}", flush=True)
|
||||
if not os.path.abspath(safe_path).startswith(os.path.abspath(doc_root)):
|
||||
conn.sendall(HTTP10_NOT_FOUND.encode())
|
||||
return
|
||||
|
||||
if os.path.exists(safe_path) and os.path.isfile(safe_path):
|
||||
with open(safe_path, "rb") as f:
|
||||
body = f.read()
|
||||
ctype, _ = mimetypes.guess_type(safe_path)
|
||||
if not ctype:
|
||||
ctype = "application/octet-stream"
|
||||
header = HTTP10_RESPONSE.format(ctype=ctype, length=len(body))
|
||||
conn.sendall(header.encode() + body)
|
||||
else:
|
||||
conn.sendall(HTTP10_NOT_FOUND.encode())
|
||||
except Exception as e:
|
||||
print(f"[http10] 错误: {e}", file=sys.stderr)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print("用法: python3 http10_backend.py <port> <directory>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
port = int(sys.argv[1])
|
||||
doc_root = sys.argv[2]
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s.bind(("", port))
|
||||
s.listen(5)
|
||||
print(f"[http10] 启动于端口 {port}, 根目录: {doc_root}")
|
||||
|
||||
while True:
|
||||
conn, addr = s.accept()
|
||||
handle_client(conn, addr, doc_root)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1159,6 +1159,68 @@ 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 反向代理测试 ==="
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user