From 90601e0f67f7c3b79bba8e8d0c9e74ea6aa40ee5 Mon Sep 17 00:00:00 2001 From: xfy911 Date: Tue, 9 Jun 2026 12:35:26 +0800 Subject: [PATCH] =?UTF-8?q?fix(proxy):=20=E4=BF=AE=E5=A4=8D=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E6=B1=A0=20HTTP/1.0=20=E5=85=BC=E5=AE=B9=E4=B8=8E?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=90=88=E5=B9=B6=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 后端兼容测试,验证两次连续请求 均成功(第二次必须新建连接)。 --- http2.c | 7 +++ main.c | 93 +++++++++-------------------------- proxy.c | 11 +++-- tests/http10_backend.py | 101 ++++++++++++++++++++++++++++++++++++++ tests/integration_test.sh | 62 +++++++++++++++++++++++ 5 files changed, 201 insertions(+), 73 deletions(-) create mode 100644 tests/http10_backend.py diff --git a/http2.c b/http2.c index 7d6b73a..13a769a 100644 --- a/http2.c +++ b/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) { diff --git a/main.c b/main.c index 7fe43ac..f6a577a 100644 --- a/main.c +++ b/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 或配置文件)\n"); return false; diff --git a/proxy.c b/proxy.c index 126e6bf..4fcfc65 100644 --- a/proxy.c +++ b/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) { diff --git a/tests/http10_backend.py b/tests/http10_backend.py new file mode 100644 index 0000000..89703d1 --- /dev/null +++ b/tests/http10_backend.py @@ -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 +""" + +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 ", 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() diff --git a/tests/integration_test.sh b/tests/integration_test.sh index da555d7..7152995 100755 --- a/tests/integration_test.sh +++ b/tests/integration_test.sh @@ -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 反向代理测试 ==="