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:
parent
de88bbc692
commit
a86b8a4856
6
Makefile
6
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
|
||||
|
||||
92
proxy.c
92
proxy.c
@ -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
172
proxy_tls.c
Normal 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
60
proxy_tls.h
Normal 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 握手。
|
||||
* 证书验证默认关闭(适用于内部反向代理场景)。
|
||||
* 自动设置 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 */
|
||||
33
tests/https_backend.py
Normal file
33
tests/https_backend.py
Normal 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()
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user