feat(proxy): HTTPS 反向代理后端支持

- 新增 proxy_tls.c/h:轻量级 OpenSSL 客户端封装
- proxy.c:支持 HTTPS 后端连接(TLS 握手 + SNI + 透传 X-Forwarded-Proto: https)
- 集成测试:新增 HTTPS 后端 Python 脚本 + 2 项代理测试
- 编译零警告,142 单元测试 + 82 集成测试全部通过
This commit is contained in:
xfy911 2026-06-08 11:37:30 +08:00
parent de88bbc692
commit a86b8a4856
6 changed files with 392 additions and 24 deletions

View File

@ -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

92
proxy.c
View File

@ -7,6 +7,7 @@
*/
#include "proxy.h"
#include "proxy_tls.h"
#include "log.h"
#include <stdio.h>
#include <stdlib.h>
@ -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;
}

172
proxy_tls.c Normal file
View File

@ -0,0 +1,172 @@
/**
* proxy_tls.c - TLS
*
* 使 OpenSSL TLS HTTPS
* TLS
*
* @author xfy
*/
#include "proxy_tls.h"
#include "log.h"
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#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);
}

60
proxy_tls.h Normal file
View File

@ -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 <stddef.h>
#include <stdint.h>
#include <sys/types.h>
/* 不透明连接句柄 */
typedef struct proxy_tls_conn proxy_tls_conn_t;
/**
* proxy_tls_connect - HTTPS TLS
*
* SSL TCP TLS
*
* SNIServer 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 */

33
tests/https_backend.py Normal file
View File

@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""
https_backend.py 简单的 HTTPS 后端服务器用于反向代理 HTTPS 测试
用法: python3 https_backend.py <port> <cert_file> <key_file> <directory>
"""
import sys
import ssl
from http.server import HTTPServer, SimpleHTTPRequestHandler
if len(sys.argv) < 5:
print("用法: python3 https_backend.py <port> <cert_file> <key_file> <directory>")
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()

View File

@ -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