From cc06fc073f5686efa0bd57a79226fa7d42deb7b3 Mon Sep 17 00:00:00 2001 From: xfy911 Date: Sun, 7 Jun 2026 03:09:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(proxy):=20=E5=AE=9E=E7=8E=B0=E5=8F=8D?= =?UTF-8?q?=E5=90=91=E4=BB=A3=E7=90=86=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 proxy.h / proxy.c:路径前缀匹配 → HTTP/1.1 后端转发 - proxy_init/add_rule/match/forward API - X-Forwarded-For / X-Forwarded-Proto 透传 - 目前仅支持 HTTP 后端,HTTPS 后端拒绝并警告 - cocoon.h:新增 COCOON_MAX_PROXY_RULES=8,cocoon_config_t 添加 proxies 数组 - config.c:JSON 解析器新增 proxies 数组解析(prefix + target) - server.c: - connection_t 新增 ctx 指针,用于 handle_request 访问代理配置 - handle_request 中在静态文件服务前检查代理规则 - Makefile:proxy.c 加入 SRCS 和 test_server 编译规则 - cocoon.json:示例配置添加 proxies 数组 - tests/integration_test.sh:新增 3 项反向代理集成测试 - .cocoon-plan.md:标记反向代理为已完成 编译零警告,142 个单元测试 + 80 项集成测试全部通过。 --- .cocoon-plan.md | 51 ++++--- Makefile | 6 +- cocoon.h | 7 + cocoon.json | 5 +- config.c | 53 +++++++ proxy.c | 293 ++++++++++++++++++++++++++++++++++++++ proxy.h | 71 +++++++++ server.c | 23 +++ tests/integration_test.sh | 67 +++++++++ 9 files changed, 556 insertions(+), 20 deletions(-) create mode 100644 proxy.c create mode 100644 proxy.h diff --git a/.cocoon-plan.md b/.cocoon-plan.md index 8e17b1f..e4bdbbc 100644 --- a/.cocoon-plan.md +++ b/.cocoon-plan.md @@ -35,18 +35,16 @@ ### Phase 3 — 扩展(已完成) - [x] **配置文件支持** — JSON 配置替代纯命令行 ✅ 2026-06-04 - - `config.c` / `config.h`:极简 JSON 解析器(数字、字符串、布尔、注释) - - `cocoon_config_t` 结构体:root_dir / port / threaded / num_workers / max_connections / timeout_ms / log_level / gzip_enabled / brotli_enabled / access_log / cors_enabled / auth_user / auth_pass / rate_limit - - `config_merge()`:命令行参数覆盖配置文件 - - `cocoon.json` 示例配置 - - `--no-gzip` / `--no-brotli` 命令行选项禁用压缩 - [x] **Brotli 压缩** — 比 gzip 更高压缩率,现代浏览器均支持 ✅ 2026-06-04 -- [x] **HTTPS / TLS** — OpenSSL Memory BIO 集成,支持命令行与配置文件启用 ✅ 2026-06-04 -- [x] **HTTP/2** — nghttp2 完整实现(TLS ALPN 协商 + 静态文件服务 + 缓存 + 目录浏览)✅ 2026-06-04 -- [x] **h2c 升级支持** — 明文 HTTP/2(PRI 魔术字直接连接 + Upgrade: h2c 协商)✅ 2026-06-04 -- [x] **HTTP/2 目录浏览** — 目录无 index.html 时返回目录列表 ✅ 2026-06-04 -- [x] **WebSocket 支持** — RFC 6455 握手 + 帧解析/编码 + echo 服务器 ✅ 2026-06-05 -- [x] **Windows 兼容性** — 跨平台抽象层,支持 Linux/macOS/Windows(MinGW/MSVC) ✅ 2026-06-04 +- [x] **HTTPS / TLS** — OpenSSL Memory BIO 集成 ✅ 2026-06-04 +- [x] **HTTP/2** — nghttp2 完整实现(TLS ALPN + h2c + 静态文件 + 缓存)✅ 2026-06-04 +- [x] **HTTP/2 目录浏览** — 无 index.html 时返回目录列表 ✅ 2026-06-04 +- [x] **WebSocket 支持** — RFC 6455 握手 + 帧解析/编码 + echo + 广播/频道 ✅ 2026-06-05 +- [x] **Windows 兼容性** — 跨平台抽象层 ✅ 2026-06-04 +- [x] **反向代理** — 路径前缀匹配 → HTTP/1.1 后端,JSON 配置支持 ✅ 2026-06-07 + - `proxy.h` / `proxy.c`:路径前缀匹配、HTTP/1.1 后端转发、X-Forwarded-* 头透传 + - `cocoon.json`:支持 `proxies` 数组配置 + - 80 项集成测试全部通过(含 3 项代理测试) ### Phase 4 — 生态 - [x] **WebSocket 广播/频道路由** — 全局连接注册表 + 广播/定向发送 API ✅ 2026-06-05 @@ -55,19 +53,20 @@ ## 当前状态 -- 所有 Phase 1-4 核心功能已完成 +- 所有 Phase 1-4 核心功能已完成,反向代理 MVP 已加入 - 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示) -- **142 个单元测试全部通过**(Unity 框架,新增 12 项 WebSocket 测试) -- **75 项集成测试全部通过**(新增 7 项健康检查端点测试) +- **142 个单元测试全部通过**(Unity 框架) +- **80 项集成测试全部通过**(新增 3 项反向代理测试) - 压测数据:wrk -t4 -c100 -d10s → 16,179 RPS,平均延迟 59.86μs(单线程) - 多线程模式(-t -w 4)已修复(主线程 accept + poll,client_handler 1MB 协程栈) - POST 支持:JSON 和 form-urlencoded 回显,multipart 文件上传,Content-Length 解析,8MB 上限 -- 配置文件支持:JSON 格式,13 个字段,命令行参数可覆盖 +- 配置文件支持:JSON 格式,14 个字段(含 `proxies` 数组),命令行参数可覆盖 - 访问日志:Nginx combined 格式,支持文件路径或 stdout(`-`),线程安全(pthread_mutex),记录 User-Agent / Referer / 状态码 - TLS/HTTPS:OpenSSL 3.0 Memory BIO + coco 协程集成,自签名证书支持,ALPN 协商 h2/http1.1 - HTTP/2:完整功能(静态文件服务、目录浏览、缓存协商、压缩、HEAD 请求) - h2c:明文 HTTP/2 支持(prior knowledge + Upgrade 协商) -- WebSocket:RFC 6455 握手、文本/二进制帧 echo、ping/pong/close、**广播/频道** +- WebSocket:RFC 6455 握手、文本/二进制帧 echo、ping/pong/close、广播/频道 +- 反向代理:路径前缀匹配 → HTTP/1.1 后端,X-Forwarded-For / X-Forwarded-Proto 透传,JSON 配置支持 - Windows 兼容性:跨平台抽象层 platform.h + platform.c,CMakeLists.txt 自动检测平台,Makefile 在 Windows 下自动链接 ws2_32 ## 待办池 @@ -82,6 +81,26 @@ ## 最近行动记录 +- 2026-06-07: **本轮行动 — 反向代理支持(MVP)** + - `proxy.h` / `proxy.c`:新增反向代理模块 + - `cocoon_proxy_rule_t` / `cocoon_proxy_config_t`:路径前缀匹配规则(最多 8 条) + - `proxy_init()` / `proxy_add_rule()`:解析 URL(`http://host:port/path`),提取 host / port / path / https 标志 + - `proxy_match()`:根据路径前缀查找匹配规则 + - `proxy_forward()`:连接后端(`gethostbyname` + `socket` + `connect`),流式转发 HTTP/1.1 请求 + 响应 + - 透传头:Host、X-Forwarded-For、X-Forwarded-Proto、Content-Type、Content-Length + - 目前不支持 HTTPS 后端(运行时拒绝并警告) + - `cocoon.h`:新增 `COCOON_MAX_PROXY_RULES = 8`,`cocoon_config_t` 添加 `proxies[8]` 和 `num_proxies` + - `config.c`:JSON 解析器新增 `proxies` 数组解析(对象项含 `prefix` 和 `target` 字符串) + - `server.c`: + - `connection_t` 新增 `server_context_t *ctx` 指针,用于 `handle_request()` 访问代理配置 + - `accept_loop()` 中设置 `conn->ctx = ctx` + - `server_create()` 中初始化代理规则(从配置加载) + - `handle_request()` 中在静态文件服务之前检查代理规则,匹配时调用 `proxy_forward()` + - `Makefile`:`proxy.c` 加入 `SRCS` 和 `test_server` 编译规则 + - `cocoon.json`:示例配置添加 `"proxies": [{"prefix": "/api", "target": "http://localhost:3000"}]` + - `tests/integration_test.sh`:新增 3 项反向代理集成测试(代理 GET 200、响应体包含后端内容、非代理路径静态文件正常) + - 编译通过(零警告),142 个单元测试 + 80 项集成测试全部通过 + - 推送到 main - 2026-06-07: **本轮行动 — HTTP/2 POST 请求体支持** - `on_data_chunk_recv_callback`: 实现请求体收集,将 DATA 帧数据追加到 `stream->request.body` - `on_frame_recv_callback`: 根据请求方法选择处理方式(GET/HEAD → `http2_serve_static`,POST → `http2_serve_post`) diff --git a/Makefile b/Makefile index b144242..bf1527b 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 +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 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 $(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 $(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 $(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_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/cocoon.h b/cocoon.h index a33ddac..a9f6572 100644 --- a/cocoon.h +++ b/cocoon.h @@ -28,6 +28,7 @@ * * 命令行参数解析后的配置。 */#define COCOON_MAX_PLUGINS 8 /**< 最大插件数量 */ +#define COCOON_MAX_PROXY_RULES 8 /**< 最大代理规则数量 */ typedef struct cocoon_config { const char *root_dir; /**< 静态资源根目录 */ @@ -51,6 +52,12 @@ typedef struct cocoon_config { /* 插件配置 */ const char *plugins[COCOON_MAX_PLUGINS]; /**< 插件路径列表 */ size_t num_plugins; /**< 插件数量 */ + /* 代理配置 */ + struct { + char prefix[256]; + char target[256]; + } proxies[COCOON_MAX_PROXY_RULES]; + size_t num_proxies; } cocoon_config_t; /* === 服务器生命周期 API === */ diff --git a/cocoon.json b/cocoon.json index 25dfec7..ca341ba 100644 --- a/cocoon.json +++ b/cocoon.json @@ -9,5 +9,8 @@ "gzip_enabled": true, "brotli_enabled": true, "plugins": ["plugins/hello.so"], - "access_log": "-" + "access_log": "-", + "proxies": [ + {"prefix": "/api", "target": "http://localhost:3000"} + ] } diff --git a/config.c b/config.c index d6002ec..e83f57f 100644 --- a/config.c +++ b/config.c @@ -346,6 +346,59 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) { } else { fprintf(stderr, "[Config] 第 %d 行: plugins 期望字符串或数组\n", val.line); } + } else if (strcmp(key_str, "proxies") == 0) { + if (val.type == TOKEN_LBRACKET) { + /* 解析对象数组 */ + while (1) { + token_t item = parser_next_token(&p); + if (item.type == TOKEN_RBRACKET) break; + if (item.type == TOKEN_LBRACE) { + char prefix[256] = {0}; + char target[256] = {0}; + /* 解析对象内的键值对 */ + while (1) { + token_t pkey = parser_next_token(&p); + if (pkey.type == TOKEN_RBRACE) break; + if (pkey.type != TOKEN_STRING) { + fprintf(stderr, "[Config] 第 %d 行: proxy 对象期望字符串键\n", pkey.line); + break; + } + if (!token_expect(&p, TOKEN_COLON)) break; + token_t pval = parser_next_token(&p); + char *pk = token_str_dup(&pkey); + if (strcmp(pk, "prefix") == 0 && pval.type == TOKEN_STRING) { + char *v = token_str_dup(&pval); + if (v) { strncpy(prefix, v, sizeof(prefix)-1); free(v); } + } else if (strcmp(pk, "target") == 0 && pval.type == TOKEN_STRING) { + char *v = token_str_dup(&pval); + if (v) { strncpy(target, v, sizeof(target)-1); free(v); } + } + free(pk); + token_t psep = parser_next_token(&p); + if (psep.type == TOKEN_RBRACE) break; + if (psep.type != TOKEN_COMMA) { + fprintf(stderr, "[Config] 第 %d 行: proxy 对象期望 ',' 或 '}'\n", psep.line); + break; + } + } + if (prefix[0] && target[0] && config->num_proxies < COCOON_MAX_PROXY_RULES) { + strncpy(config->proxies[config->num_proxies].prefix, prefix, sizeof(config->proxies[0].prefix)-1); + strncpy(config->proxies[config->num_proxies].target, target, sizeof(config->proxies[0].target)-1); + config->num_proxies++; + } + } else { + fprintf(stderr, "[Config] 第 %d 行: proxies 数组期望对象项\n", item.line); + } + token_t sep = parser_next_token(&p); + if (sep.type == TOKEN_RBRACKET) break; + if (sep.type != TOKEN_COMMA) { + fprintf(stderr, "[Config] 第 %d 行: proxies 数组期望 ',' 或 ']'\n", sep.line); + break; + } + } + } else { + fprintf(stderr, "[Config] 第 %d 行: proxies 期望数组\n", val.line); + } } /* 其他字段:忽略(未来扩展预留) */ free(key_str); diff --git a/proxy.c b/proxy.c new file mode 100644 index 0000000..9a0f70a --- /dev/null +++ b/proxy.c @@ -0,0 +1,293 @@ +/** + * proxy.c - 反向代理实现 + * + * 轻量级 HTTP/1.1 反向代理,支持流式转发。 + * + * @author xfy + */ + +#include "proxy.h" +#include "log.h" +#include +#include +#include +#include +#include +#include +#include +#include + +void proxy_init(cocoon_proxy_config_t *cfg) { + memset(cfg, 0, sizeof(*cfg)); + cfg->count = 0; +} + +bool proxy_add_rule(cocoon_proxy_config_t *cfg, const char *prefix, const char *target_url) { + if (cfg->count >= COCOON_MAX_PROXY_RULES) { + log_error("代理规则数量超过上限 %d", COCOON_MAX_PROXY_RULES); + return false; + } + + if (!prefix || !target_url || prefix[0] == '\0' || target_url[0] == '\0') { + return false; + } + + cocoon_proxy_rule_t *rule = &cfg->rules[cfg->count]; + + /* 复制路径前缀 */ + strncpy(rule->path_prefix, prefix, sizeof(rule->path_prefix) - 1); + rule->path_prefix[sizeof(rule->path_prefix) - 1] = '\0'; + + /* 解析目标URL */ + bool https = false; + const char *url = target_url; + + if (strncmp(url, "http://", 7) == 0) { + url += 7; + rule->target_https = false; + } else if (strncmp(url, "https://", 8) == 0) { + url += 8; + rule->target_https = true; + https = true; + } else { + /* 默认 http */ + rule->target_https = false; + } + + /* 提取 host:port 和剩余路径 */ + const char *slash = strchr(url, '/'); + char host_port[256]; + if (slash) { + size_t host_len = (size_t)(slash - url); + if (host_len >= sizeof(host_port)) host_len = sizeof(host_port) - 1; + memcpy(host_port, url, host_len); + host_port[host_len] = '\0'; + strncpy(rule->target_path, slash, sizeof(rule->target_path) - 1); + } else { + strncpy(host_port, url, sizeof(host_port) - 1); + host_port[sizeof(host_port) - 1] = '\0'; + rule->target_path[0] = '\0'; + } + + /* 解析 host 和 port */ + const char *colon = strrchr(host_port, ':'); + if (colon) { + size_t host_len = (size_t)(colon - host_port); + if (host_len >= sizeof(rule->target_host)) host_len = sizeof(rule->target_host) - 1; + memcpy(rule->target_host, host_port, host_len); + rule->target_host[host_len] = '\0'; + rule->target_port = (uint16_t)atoi(colon + 1); + } else { + strncpy(rule->target_host, host_port, sizeof(rule->target_host) - 1); + rule->target_host[sizeof(rule->target_host) - 1] = '\0'; + rule->target_port = https ? 443 : 80; + } + + if (rule->target_port == 0) { + rule->target_port = https ? 443 : 80; + } + + log_info("添加代理规则: %s -> %s://%s:%d%s", + rule->path_prefix, + https ? "https" : "http", + rule->target_host, + rule->target_port, + rule->target_path); + + cfg->count++; + return true; +} + +const cocoon_proxy_rule_t *proxy_match(const cocoon_proxy_config_t *cfg, const char *path) { + if (!cfg || !path) return NULL; + + for (size_t i = 0; i < cfg->count; i++) { + const cocoon_proxy_rule_t *rule = &cfg->rules[i]; + size_t prefix_len = strlen(rule->path_prefix); + if (strncmp(path, rule->path_prefix, prefix_len) == 0) { + return rule; + } + } + return NULL; +} + +/** + * proxy_connect_backend - 连接到目标后端 + */ +static cocoon_socket_t proxy_connect_backend(const cocoon_proxy_rule_t *rule) { + struct hostent *host = gethostbyname(rule->target_host); + if (!host) { + log_error("无法解析主机: %s", rule->target_host); + return COCOON_INVALID_SOCKET; + } + + cocoon_socket_t fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd == COCOON_INVALID_SOCKET) { + log_error("创建 socket 失败"); + return COCOON_INVALID_SOCKET; + } + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(rule->target_port); + memcpy(&addr.sin_addr, host->h_addr_list[0], (size_t)host->h_length); + + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + log_error("连接后端失败: %s:%d", rule->target_host, rule->target_port); + cocoon_socket_close(fd); + return COCOON_INVALID_SOCKET; + } + + return fd; +} + +/** + * proxy_build_forwarded_path - 构建转发路径 + */ +static void proxy_build_forwarded_path(const cocoon_proxy_rule_t *rule, + const char *original_path, + char *out, size_t out_len) { + size_t prefix_len = strlen(rule->path_prefix); + const char *remaining = original_path + prefix_len; + + if (rule->target_path[0] == '\0') { + /* 无目标路径前缀,直接转发剩余部分 */ + if (remaining[0] == '\0') { + strncpy(out, "/", out_len - 1); + } else { + strncpy(out, remaining, out_len - 1); + } + } else { + /* 将目标路径前缀 + 剩余路径拼接 */ + int n = snprintf(out, out_len, "%s%s", rule->target_path, remaining); + if (n < 0 || (size_t)n >= out_len) { + out[out_len - 1] = '\0'; + } + } + out[out_len - 1] = '\0'; +} + +/** + * proxy_build_xff - 构建 X-Forwarded-For 值 + */ +static void proxy_build_xff(const struct sockaddr_storage *client_addr, + char *out, size_t out_len) { + if (client_addr->ss_family == AF_INET) { + struct sockaddr_in *sin = (struct sockaddr_in *)client_addr; + inet_ntop(AF_INET, &sin->sin_addr, out, (socklen_t)out_len); + } else if (client_addr->ss_family == AF_INET6) { + struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)client_addr; + inet_ntop(AF_INET6, &sin6->sin6_addr, out, (socklen_t)out_len); + } else { + strncpy(out, "unknown", out_len - 1); + out[out_len - 1] = '\0'; + } +} + +/** + * send_all_fd - 确保数据全部发送 + */ +static int send_all_fd(cocoon_socket_t fd, const char *data, size_t len) { + size_t sent = 0; + while (sent < len) { + ssize_t n = send(fd, data + sent, len - sent, 0); + if (n < 0) { + if (errno == EAGAIN || errno == EINTR) continue; + return -1; + } + if (n == 0) return -1; + sent += (size_t)n; + } + 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; + } + + cocoon_socket_t backend_fd = proxy_connect_backend(rule); + if (backend_fd == COCOON_INVALID_SOCKET) { + return false; + } + + /* 构建转发路径 */ + char forwarded_path[512]; + proxy_build_forwarded_path(rule, req->path, forwarded_path, sizeof(forwarded_path)); + + /* 构建 X-Forwarded-For */ + char xff[64]; + proxy_build_xff(client_addr, xff, sizeof(xff)); + + /* 构建转发请求 */ + char request_buf[4096]; + int n = snprintf(request_buf, sizeof(request_buf), + "%s %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "X-Forwarded-For: %s\r\n" + "X-Forwarded-Proto: http\r\n" + "Connection: close\r\n", + http_method_str(req->method), + forwarded_path, + rule->target_host, + rule->target_port, + xff); + + /* 透传常见请求头 */ + if (req->content_type[0] != '\0') { + n += snprintf(request_buf + n, sizeof(request_buf) - n, + "Content-Type: %s\r\n", req->content_type); + } + if (req->content_length > 0) { + n += snprintf(request_buf + n, sizeof(request_buf) - n, + "Content-Length: %ld\r\n", (long)req->content_length); + } + + 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 (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; + } + } + + /* 流式转发响应回客户端 */ + char relay_buf[8192]; + ssize_t total_forwarded = 0; + while (1) { + ssize_t r = recv(backend_fd, relay_buf, sizeof(relay_buf), 0); + if (r < 0) { + if (errno == EAGAIN || errno == EINTR) continue; + break; + } + if (r == 0) break; + + if (send_all_fd(client_fd, relay_buf, (size_t)r) != 0) { + log_error("转发响应到客户端失败"); + break; + } + total_forwarded += r; + } + + log_debug("代理完成: %s -> %s:%d%s, 转发 %zd bytes", + req->path, rule->target_host, rule->target_port, + forwarded_path, total_forwarded); + + cocoon_socket_close(backend_fd); + return req->keep_alive; +} diff --git a/proxy.h b/proxy.h new file mode 100644 index 0000000..32150da --- /dev/null +++ b/proxy.h @@ -0,0 +1,71 @@ +/** + * proxy.h - 反向代理模块 + * + * 轻量级反向代理,支持路径前缀匹配到目标后端。 + * + * @author xfy + */ + +#ifndef COCOON_PROXY_H +#define COCOON_PROXY_H + +#include "cocoon.h" +#include "http.h" +#include "platform.h" +#include + +#define COCOON_MAX_PROXY_RULES 8 + +typedef struct { + char path_prefix[256]; + char target_host[256]; + char target_path[256]; + uint16_t target_port; + bool target_https; +} cocoon_proxy_rule_t; + +typedef struct { + cocoon_proxy_rule_t rules[COCOON_MAX_PROXY_RULES]; + size_t count; +} cocoon_proxy_config_t; + +/** + * proxy_init - 初始化代理配置 + */ +void proxy_init(cocoon_proxy_config_t *cfg); + +/** + * proxy_add_rule - 添加代理规则 + * + * @param cfg 代理配置 + * @param prefix 路径前缀(如 "/api/") + * @param target_url 目标URL(如 "http://localhost:3000") + * @return true 成功 + */ +bool proxy_add_rule(cocoon_proxy_config_t *cfg, const char *prefix, const char *target_url); + +/** + * proxy_match - 查找匹配路径的代理规则 + * + * @param cfg 代理配置 + * @param path 请求路径 + * @return 匹配的规则,未匹配返回 NULL + */ +const cocoon_proxy_rule_t *proxy_match(const cocoon_proxy_config_t *cfg, const char *path); + +/** + * proxy_forward - 将请求转发到目标后端 + * + * 建立到后端的连接,转发请求,将响应回写给客户端。 + * + * @param client_fd 客户端 socket + * @param req HTTP 请求 + * @param rule 代理规则 + * @param client_addr 客户端地址(用于 X-Forwarded-For) + * @return true 保持连接,false 关闭 + */ +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); + +#endif /* COCOON_PROXY_H */ diff --git a/server.c b/server.c index 22bf390..67c16be 100644 --- a/server.c +++ b/server.c @@ -28,6 +28,7 @@ #include "websocket.h" #include "platform.h" #include "middleware.h" +#include "proxy.h" #include #include #include @@ -62,11 +63,13 @@ typedef struct { struct sockaddr_storage client_addr; /**< 客户端地址 */ socklen_t addr_len; /**< 地址长度 */ int response_status; /**< 最后响应的 HTTP 状态码 */ + server_context_t *ctx; /**< 服务器上下文(用于访问代理配置) */ } connection_t; struct server_context { cocoon_socket_t listen_fd; /**< 监听 socket */ cocoon_config_t config; /**< 配置副本 */ cocoon_middleware_config_t mw_config; /**< 中间件配置(持久化,避免栈变量悬空) */ + cocoon_proxy_config_t proxy_config; /**< 反向代理配置 */ volatile int running; /**< 运行标志 */ coco_sched_t *sched; /**< 协程调度器 */ }; @@ -403,6 +406,19 @@ static bool handle_request(connection_t *conn, const char *root_dir) { return keep; } + /* 反向代理检查 */ + if (conn->ctx && conn->ctx->proxy_config.count > 0) { + const cocoon_proxy_rule_t *rule = proxy_match(&conn->ctx->proxy_config, req.path); + if (rule) { + bool keep = proxy_forward(conn->fd, &req, rule, &conn->client_addr); + conn->response_status = 200; /* 代理响应状态由后端决定,这里记录一个通用值 */ + access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len, + &req, conn->response_status, -1); + http_request_free(&req); + return keep; + } + } + /* 只支持 GET 和 HEAD */ if (req.method != HTTP_GET && req.method != HTTP_HEAD) { conn->response_status = 405; @@ -1033,6 +1049,7 @@ static void accept_loop(void *arg) { memcpy(&conn->client_addr, &client_addr, sizeof(client_addr)); conn->addr_len = addr_len; conn->response_status = 0; + conn->ctx = ctx; atomic_fetch_add(&g_active_connections, 1); log_debug("新连接 fd=%d,当前活跃连接: %d", client_fd, @@ -1133,6 +1150,12 @@ server_context_t *server_create(const cocoon_config_t *config) { ctx->mw_config.rate_limit = ctx->config.rate_limit; cocoon_middleware_init_builtin(&ctx->mw_config); + /* 初始化反向代理配置 */ + proxy_init(&ctx->proxy_config); + for (size_t i = 0; i < ctx->config.num_proxies; i++) { + proxy_add_rule(&ctx->proxy_config, ctx->config.proxies[i].prefix, ctx->config.proxies[i].target); + } + /* 加载插件 */ for (size_t i = 0; i < ctx->config.num_plugins; i++) { if (cocoon_plugin_load(ctx->config.plugins[i]) != 0) { diff --git a/tests/integration_test.sh b/tests/integration_test.sh index 26e2d90..979ec70 100755 --- a/tests/integration_test.sh +++ b/tests/integration_test.sh @@ -949,6 +949,73 @@ kill_server sleep 1 start_server +echo "" +echo "=== 反向代理测试 ===" + +# 使用带代理配置的服务器测试 +kill_server +sleep 1 + +# 启动一个后端服务器(Python http.server) +python3 -m http.server 9000 --directory "$ROOT" > "$TMPDIR/backend.log" 2>&1 & +BACKEND_PID=$! +sleep 1 + +# 创建带代理配置的配置文件 +PROXY_CONFIG="$TMPDIR/proxy_config.json" +cat > "$PROXY_CONFIG" << 'EOF' +{ + "root_dir": "./tests/fixtures", + "port": 9999, + "log_level": "debug", + "proxies": [ + {"prefix": "/backend", "target": "http://localhost:9000"} + ] +} +EOF + +$SERVER -c "$PROXY_CONFIG" > "$TMPDIR/server_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 + +proxy_status=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOST/backend/index.html") +if [[ "$proxy_status" == "200" ]]; then + echo " ✓ 反向代理 GET — HTTP 200" + pass +else + echo " ✗ 反向代理 GET — 期望 200, 实际 $proxy_status" + fail +fi + +proxy_body=$(curl -s "http://$HOST/backend/index.html") +if echo "$proxy_body" | grep -q "Cocoon"; then + echo " ✓ 反向代理响应体 — 包含后端内容" + pass +else + echo " ✗ 反向代理响应体 — 未包含后端内容" + fail +fi + +# 非代理路径仍走静态文件 +static_status=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOST/index.html") +if [[ "$static_status" == "200" ]]; then + echo " ✓ 非代理路径 — 静态文件正常 HTTP 200" + pass +else + echo " ✗ 非代理路径 — 期望 200, 实际 $static_status" + fail +fi + +# 清理后端服务器 +kill -9 $BACKEND_PID 2>/dev/null || true + +# 恢复默认服务器 +kill_server +sleep 1 +start_server + echo "" echo "=== 结果汇总 ===" echo "通过: $PASS"