From a86b8a4856fb5809a6f5f98220af243d0bb98161 Mon Sep 17 00:00:00 2001 From: xfy911 Date: Mon, 8 Jun 2026 11:37:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(proxy):=20HTTPS=20=E5=8F=8D=E5=90=91?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E5=90=8E=E7=AB=AF=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 proxy_tls.c/h:轻量级 OpenSSL 客户端封装 - proxy.c:支持 HTTPS 后端连接(TLS 握手 + SNI + 透传 X-Forwarded-Proto: https) - 集成测试:新增 HTTPS 后端 Python 脚本 + 2 项代理测试 - 编译零警告,142 单元测试 + 82 集成测试全部通过 --- Makefile | 6 +- proxy.c | 92 +++++++++++++++----- proxy_tls.c | 172 ++++++++++++++++++++++++++++++++++++++ proxy_tls.h | 60 +++++++++++++ tests/https_backend.py | 33 ++++++++ tests/integration_test.sh | 53 +++++++++++- 6 files changed, 392 insertions(+), 24 deletions(-) create mode 100644 proxy_tls.c create mode 100644 proxy_tls.h create mode 100644 tests/https_backend.py diff --git a/Makefile b/Makefile index bf1527b..496d9a3 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ PREFIX ?= /usr/local BINDIR = $(PREFIX)/bin # 源文件 -SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c +SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c OBJS = $(SRCS:.c=.o) TARGET = cocoon @@ -90,8 +90,8 @@ unit-test: $(UNIT_TEST_BINS) fi # 单元测试编译规则 -$(UNIT_TEST_DIR)/test_server: $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c $(UNITY_SRC) - $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c $(UNITY_SRC) $(LDFLAGS) +$(UNIT_TEST_DIR)/test_server: $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c $(UNITY_SRC) + $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c $(UNITY_SRC) $(LDFLAGS) $(UNIT_TEST_DIR)/test_multipart: $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) -lm diff --git a/proxy.c b/proxy.c index 45a1f57..5708d40 100644 --- a/proxy.c +++ b/proxy.c @@ -7,6 +7,7 @@ */ #include "proxy.h" +#include "proxy_tls.h" #include "log.h" #include #include @@ -204,18 +205,43 @@ static int send_all_fd(cocoon_socket_t fd, const char *data, size_t len) { return 0; } +/** + * proxy_send_all_tls - 通过 TLS 连接发送全部数据 + */ +static int proxy_send_all_tls(proxy_tls_conn_t *conn, const char *data, size_t len) { + size_t sent = 0; + while (sent < len) { + ssize_t n = proxy_tls_write(conn, data + sent, len - sent); + if (n > 0) { + sent += (size_t)n; + } else if (n < 0) { + if (errno == EAGAIN || errno == EINTR) continue; + return -1; + } else { + return -1; + } + } + return 0; +} + bool proxy_forward(cocoon_socket_t client_fd, const http_request_t *req, const cocoon_proxy_rule_t *rule, const struct sockaddr_storage *client_addr) { - /* 目前不支持 HTTPS 后端 */ - if (rule->target_https) { - log_warn("HTTPS 后端暂未支持,拒绝代理: %s", rule->target_host); - return false; - } + bool use_https = rule->target_https; + cocoon_socket_t backend_fd = COCOON_INVALID_SOCKET; + proxy_tls_conn_t *tls_conn = NULL; - cocoon_socket_t backend_fd = proxy_connect_backend(rule); - if (backend_fd == COCOON_INVALID_SOCKET) { - return false; + if (use_https) { + tls_conn = proxy_tls_connect(rule->target_host, rule->target_port); + if (!tls_conn) { + log_error("连接 HTTPS 后端失败: %s:%d", rule->target_host, rule->target_port); + return false; + } + } else { + backend_fd = proxy_connect_backend(rule); + if (backend_fd == COCOON_INVALID_SOCKET) { + return false; + } } /* 构建转发路径 */ @@ -232,13 +258,14 @@ bool proxy_forward(cocoon_socket_t client_fd, const http_request_t *req, "%s %s HTTP/1.1\r\n" "Host: %s:%d\r\n" "X-Forwarded-For: %s\r\n" - "X-Forwarded-Proto: http\r\n" + "X-Forwarded-Proto: %s\r\n" "Connection: close\r\n", http_method_str(req->method), forwarded_path, rule->target_host, rule->target_port, - xff); + xff, + use_https ? "https" : "http"); /* 透传常见请求头 */ if (req->content_type[0] != '\0') { @@ -253,18 +280,34 @@ bool proxy_forward(cocoon_socket_t client_fd, const http_request_t *req, n += snprintf(request_buf + n, sizeof(request_buf) - n, "\r\n"); /* 发送请求头 */ - if (send_all_fd(backend_fd, request_buf, (size_t)n) != 0) { - log_error("转发请求头到后端失败"); - cocoon_socket_close(backend_fd); - return false; + if (use_https) { + if (proxy_send_all_tls(tls_conn, request_buf, (size_t)n) != 0) { + log_error("转发请求头到 HTTPS 后端失败"); + proxy_tls_close(tls_conn); + return false; + } + } else { + if (send_all_fd(backend_fd, request_buf, (size_t)n) != 0) { + log_error("转发请求头到后端失败"); + cocoon_socket_close(backend_fd); + return false; + } } /* 转发请求体 */ if (req->body && req->body_len > 0) { - if (send_all_fd(backend_fd, req->body, req->body_len) != 0) { - log_error("转发请求体到后端失败"); - cocoon_socket_close(backend_fd); - return false; + if (use_https) { + if (proxy_send_all_tls(tls_conn, req->body, req->body_len) != 0) { + log_error("转发请求体到 HTTPS 后端失败"); + proxy_tls_close(tls_conn); + return false; + } + } else { + if (send_all_fd(backend_fd, req->body, req->body_len) != 0) { + log_error("转发请求体到后端失败"); + cocoon_socket_close(backend_fd); + return false; + } } } @@ -272,7 +315,12 @@ bool proxy_forward(cocoon_socket_t client_fd, const http_request_t *req, char relay_buf[8192]; ssize_t total_forwarded = 0; while (1) { - ssize_t r = recv(backend_fd, relay_buf, sizeof(relay_buf), 0); + ssize_t r; + if (use_https) { + r = proxy_tls_read(tls_conn, relay_buf, sizeof(relay_buf)); + } else { + r = recv(backend_fd, relay_buf, sizeof(relay_buf), 0); + } if (r < 0) { if (errno == EAGAIN || errno == EINTR) continue; break; @@ -290,6 +338,10 @@ bool proxy_forward(cocoon_socket_t client_fd, const http_request_t *req, req->path, rule->target_host, rule->target_port, forwarded_path, total_forwarded); - cocoon_socket_close(backend_fd); + if (use_https) { + proxy_tls_close(tls_conn); + } else { + cocoon_socket_close(backend_fd); + } return req->keep_alive; } diff --git a/proxy_tls.c b/proxy_tls.c new file mode 100644 index 0000000..8d84178 --- /dev/null +++ b/proxy_tls.c @@ -0,0 +1,172 @@ +/** + * proxy_tls.c - 反向代理客户端 TLS 实现 + * + * 使用 OpenSSL 作为 TLS 客户端连接到 HTTPS 后端。 + * 独立模块,不共享服务器端 TLS 上下文。 + * + * @author xfy + */ + +#include "proxy_tls.h" +#include "log.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "platform.h" + +struct proxy_tls_conn { + cocoon_socket_t fd; + SSL *ssl; + SSL_CTX *ctx; +}; + +/** + * proxy_tls_connect_tcp - 建立到后端的 TCP 连接 + */ +static cocoon_socket_t proxy_tls_connect_tcp(const char *host, uint16_t port) { + struct hostent *h = gethostbyname(host); + if (!h) { + log_error("代理 TLS: 无法解析主机 %s", host); + return COCOON_INVALID_SOCKET; + } + + cocoon_socket_t fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd == COCOON_INVALID_SOCKET) { + log_error("代理 TLS: 创建 socket 失败"); + return COCOON_INVALID_SOCKET; + } + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + memcpy(&addr.sin_addr, h->h_addr_list[0], (size_t)h->h_length); + + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + log_error("代理 TLS: 连接后端失败 %s:%d", host, port); + cocoon_socket_close(fd); + return COCOON_INVALID_SOCKET; + } + + return fd; +} + +proxy_tls_conn_t *proxy_tls_connect(const char *host, uint16_t port) { + if (!host || host[0] == '\0') return NULL; + + /* 建立 TCP 连接 */ + cocoon_socket_t fd = proxy_tls_connect_tcp(host, port); + if (fd == COCOON_INVALID_SOCKET) return NULL; + + /* 创建客户端 SSL 上下文 */ + const SSL_METHOD *method = TLS_client_method(); + if (!method) { + cocoon_socket_close(fd); + return NULL; + } + + SSL_CTX *ctx = SSL_CTX_new(method); + if (!ctx) { + cocoon_socket_close(fd); + return NULL; + } + + /* 最低 TLS 1.2 */ + SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); + + /* 跳过证书验证(内部反向代理场景) */ + SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL); + + /* 创建 SSL 连接 */ + SSL *ssl = SSL_new(ctx); + if (!ssl) { + SSL_CTX_free(ctx); + cocoon_socket_close(fd); + return NULL; + } + + /* 设置 SNI */ + SSL_set_tlsext_host_name(ssl, host); + + /* 绑定 socket 到 SSL */ + SSL_set_fd(ssl, fd); + + /* 执行握手 */ + int ret = SSL_connect(ssl); + if (ret != 1) { + int err = SSL_get_error(ssl, ret); + log_error("代理 TLS: 握手失败 %s:%d, err=%d", host, port, err); + SSL_free(ssl); + SSL_CTX_free(ctx); + cocoon_socket_close(fd); + return NULL; + } + + log_debug("代理 TLS: 握手成功 %s:%d", host, port); + + proxy_tls_conn_t *conn = (proxy_tls_conn_t *)calloc(1, sizeof(proxy_tls_conn_t)); + if (!conn) { + SSL_free(ssl); + SSL_CTX_free(ctx); + cocoon_socket_close(fd); + return NULL; + } + + conn->fd = fd; + conn->ssl = ssl; + conn->ctx = ctx; + return conn; +} + +ssize_t proxy_tls_read(proxy_tls_conn_t *conn, void *buf, size_t len) { + if (!conn || !conn->ssl) return -1; + + int ret = SSL_read(conn->ssl, buf, (int)len); + if (ret > 0) return (ssize_t)ret; + + int err = SSL_get_error(conn->ssl, ret); + if (err == SSL_ERROR_ZERO_RETURN) return 0; /* 对端关闭 */ + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + errno = EAGAIN; + return -1; + } + return -1; +} + +ssize_t proxy_tls_write(proxy_tls_conn_t *conn, const void *buf, size_t len) { + if (!conn || !conn->ssl) return -1; + + int ret = SSL_write(conn->ssl, buf, (int)len); + if (ret > 0) return (ssize_t)ret; + + int err = SSL_get_error(conn->ssl, ret); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + errno = EAGAIN; + return -1; + } + return -1; +} + +void proxy_tls_close(proxy_tls_conn_t *conn) { + if (!conn) return; + + if (conn->ssl) { + SSL_shutdown(conn->ssl); + SSL_free(conn->ssl); + } + if (conn->ctx) { + SSL_CTX_free(conn->ctx); + } + if (conn->fd != COCOON_INVALID_SOCKET) { + cocoon_socket_close(conn->fd); + } + free(conn); +} diff --git a/proxy_tls.h b/proxy_tls.h new file mode 100644 index 0000000..11254e0 --- /dev/null +++ b/proxy_tls.h @@ -0,0 +1,60 @@ +/** + * proxy_tls.h - 反向代理客户端 TLS 模块 + * + * 轻量级 OpenSSL 客户端封装,用于连接 HTTPS 后端。 + * 不依赖服务器端 TLS 模块(tls.c),保持职责分离。 + * + * @author xfy + */ + +#ifndef COCOON_PROXY_TLS_H +#define COCOON_PROXY_TLS_H + +#include +#include +#include + +/* 不透明连接句柄 */ +typedef struct proxy_tls_conn proxy_tls_conn_t; + +/** + * proxy_tls_connect - 连接到 HTTPS 后端并执行 TLS 握手 + * + * 创建客户端 SSL 上下文,建立 TCP 连接,完成 TLS 握手。 + * 证书验证默认关闭(适用于内部反向代理场景)。 + * 自动设置 SNI(Server Name Indication)。 + * + * @param host 目标主机名 + * @param port 目标端口 + * @return TLS 连接句柄,失败返回 NULL + */ +proxy_tls_conn_t *proxy_tls_connect(const char *host, uint16_t port); + +/** + * proxy_tls_read - 从 TLS 连接读取解密数据 + * + * @param conn TLS 连接句柄 + * @param buf 读取缓冲区 + * @param len 最大读取长度 + * @return 实际读取字节数,0 对端关闭,-1 错误 + */ +ssize_t proxy_tls_read(proxy_tls_conn_t *conn, void *buf, size_t len); + +/** + * proxy_tls_write - 向 TLS 连接写入明文数据 + * + * @param conn TLS 连接句柄 + * @param buf 数据缓冲区 + * @param len 数据长度 + * @return 实际写入字节数,-1 错误 + */ +ssize_t proxy_tls_write(proxy_tls_conn_t *conn, const void *buf, size_t len); + +/** + * proxy_tls_close - 关闭 TLS 连接并释放资源 + * + * @param conn TLS 连接句柄 + */ +void proxy_tls_close(proxy_tls_conn_t *conn); + +#endif /* COCOON_PROXY_TLS_H */ diff --git a/tests/https_backend.py b/tests/https_backend.py new file mode 100644 index 0000000..0a53595 --- /dev/null +++ b/tests/https_backend.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +""" +https_backend.py — 简单的 HTTPS 后端服务器,用于反向代理 HTTPS 测试。 + +用法: python3 https_backend.py +""" +import sys +import ssl +from http.server import HTTPServer, SimpleHTTPRequestHandler + +if len(sys.argv) < 5: + print("用法: python3 https_backend.py ") + sys.exit(1) + +port = int(sys.argv[1]) +cert_file = sys.argv[2] +key_file = sys.argv[3] +directory = sys.argv[4] + +class Handler(SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=directory, **kwargs) + + def log_message(self, format, *args): + # 抑制日志输出 + pass + +context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) +context.load_cert_chain(cert_file, key_file) + +server = HTTPServer(("localhost", port), Handler) +server.socket = context.wrap_socket(server.socket, server_side=True) +server.serve_forever() diff --git a/tests/integration_test.sh b/tests/integration_test.sh index 0a07cb0..e44132b 100755 --- a/tests/integration_test.sh +++ b/tests/integration_test.sh @@ -1012,8 +1012,59 @@ else fail fi -# 清理后端服务器 +# 清理 HTTP 后端服务器 kill -9 $BACKEND_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