feat(proxy): 实现反向代理支持
- 新增 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 项集成测试全部通过。
This commit is contained in:
parent
b04904cc78
commit
cc06fc073f
@ -35,18 +35,16 @@
|
|||||||
|
|
||||||
### Phase 3 — 扩展(已完成)
|
### Phase 3 — 扩展(已完成)
|
||||||
- [x] **配置文件支持** — JSON 配置替代纯命令行 ✅ 2026-06-04
|
- [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] **Brotli 压缩** — 比 gzip 更高压缩率,现代浏览器均支持 ✅ 2026-06-04
|
||||||
- [x] **HTTPS / TLS** — OpenSSL Memory BIO 集成,支持命令行与配置文件启用 ✅ 2026-06-04
|
- [x] **HTTPS / TLS** — OpenSSL Memory BIO 集成 ✅ 2026-06-04
|
||||||
- [x] **HTTP/2** — nghttp2 完整实现(TLS ALPN 协商 + 静态文件服务 + 缓存 + 目录浏览)✅ 2026-06-04
|
- [x] **HTTP/2** — nghttp2 完整实现(TLS ALPN + h2c + 静态文件 + 缓存)✅ 2026-06-04
|
||||||
- [x] **h2c 升级支持** — 明文 HTTP/2(PRI 魔术字直接连接 + Upgrade: h2c 协商)✅ 2026-06-04
|
- [x] **HTTP/2 目录浏览** — 无 index.html 时返回目录列表 ✅ 2026-06-04
|
||||||
- [x] **HTTP/2 目录浏览** — 目录无 index.html 时返回目录列表 ✅ 2026-06-04
|
- [x] **WebSocket 支持** — RFC 6455 握手 + 帧解析/编码 + echo + 广播/频道 ✅ 2026-06-05
|
||||||
- [x] **WebSocket 支持** — RFC 6455 握手 + 帧解析/编码 + echo 服务器 ✅ 2026-06-05
|
- [x] **Windows 兼容性** — 跨平台抽象层 ✅ 2026-06-04
|
||||||
- [x] **Windows 兼容性** — 跨平台抽象层,支持 Linux/macOS/Windows(MinGW/MSVC) ✅ 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 — 生态
|
### Phase 4 — 生态
|
||||||
- [x] **WebSocket 广播/频道路由** — 全局连接注册表 + 广播/定向发送 API ✅ 2026-06-05
|
- [x] **WebSocket 广播/频道路由** — 全局连接注册表 + 广播/定向发送 API ✅ 2026-06-05
|
||||||
@ -55,19 +53,20 @@
|
|||||||
|
|
||||||
## 当前状态
|
## 当前状态
|
||||||
|
|
||||||
- 所有 Phase 1-4 核心功能已完成
|
- 所有 Phase 1-4 核心功能已完成,反向代理 MVP 已加入
|
||||||
- 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示)
|
- 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示)
|
||||||
- **142 个单元测试全部通过**(Unity 框架,新增 12 项 WebSocket 测试)
|
- **142 个单元测试全部通过**(Unity 框架)
|
||||||
- **75 项集成测试全部通过**(新增 7 项健康检查端点测试)
|
- **80 项集成测试全部通过**(新增 3 项反向代理测试)
|
||||||
- 压测数据:wrk -t4 -c100 -d10s → 16,179 RPS,平均延迟 59.86μs(单线程)
|
- 压测数据:wrk -t4 -c100 -d10s → 16,179 RPS,平均延迟 59.86μs(单线程)
|
||||||
- 多线程模式(-t -w 4)已修复(主线程 accept + poll,client_handler 1MB 协程栈)
|
- 多线程模式(-t -w 4)已修复(主线程 accept + poll,client_handler 1MB 协程栈)
|
||||||
- POST 支持:JSON 和 form-urlencoded 回显,multipart 文件上传,Content-Length 解析,8MB 上限
|
- POST 支持:JSON 和 form-urlencoded 回显,multipart 文件上传,Content-Length 解析,8MB 上限
|
||||||
- 配置文件支持:JSON 格式,13 个字段,命令行参数可覆盖
|
- 配置文件支持:JSON 格式,14 个字段(含 `proxies` 数组),命令行参数可覆盖
|
||||||
- 访问日志:Nginx combined 格式,支持文件路径或 stdout(`-`),线程安全(pthread_mutex),记录 User-Agent / Referer / 状态码
|
- 访问日志:Nginx combined 格式,支持文件路径或 stdout(`-`),线程安全(pthread_mutex),记录 User-Agent / Referer / 状态码
|
||||||
- TLS/HTTPS:OpenSSL 3.0 Memory BIO + coco 协程集成,自签名证书支持,ALPN 协商 h2/http1.1
|
- TLS/HTTPS:OpenSSL 3.0 Memory BIO + coco 协程集成,自签名证书支持,ALPN 协商 h2/http1.1
|
||||||
- HTTP/2:完整功能(静态文件服务、目录浏览、缓存协商、压缩、HEAD 请求)
|
- HTTP/2:完整功能(静态文件服务、目录浏览、缓存协商、压缩、HEAD 请求)
|
||||||
- h2c:明文 HTTP/2 支持(prior knowledge + Upgrade 协商)
|
- 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
|
- 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 请求体支持**
|
- 2026-06-07: **本轮行动 — HTTP/2 POST 请求体支持**
|
||||||
- `on_data_chunk_recv_callback`: 实现请求体收集,将 DATA 帧数据追加到 `stream->request.body`
|
- `on_data_chunk_recv_callback`: 实现请求体收集,将 DATA 帧数据追加到 `stream->request.body`
|
||||||
- `on_frame_recv_callback`: 根据请求方法选择处理方式(GET/HEAD → `http2_serve_static`,POST → `http2_serve_post`)
|
- `on_frame_recv_callback`: 根据请求方法选择处理方式(GET/HEAD → `http2_serve_static`,POST → `http2_serve_post`)
|
||||||
|
|||||||
6
Makefile
6
Makefile
@ -20,7 +20,7 @@ PREFIX ?= /usr/local
|
|||||||
BINDIR = $(PREFIX)/bin
|
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)
|
OBJS = $(SRCS:.c=.o)
|
||||||
TARGET = cocoon
|
TARGET = cocoon
|
||||||
|
|
||||||
@ -90,8 +90,8 @@ unit-test: $(UNIT_TEST_BINS)
|
|||||||
fi
|
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)
|
$(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 $(UNITY_SRC) $(LDFLAGS)
|
$(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)
|
$(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
|
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) -lm
|
||||||
|
|||||||
7
cocoon.h
7
cocoon.h
@ -28,6 +28,7 @@
|
|||||||
*
|
*
|
||||||
* 命令行参数解析后的配置。
|
* 命令行参数解析后的配置。
|
||||||
*/#define COCOON_MAX_PLUGINS 8 /**< 最大插件数量 */
|
*/#define COCOON_MAX_PLUGINS 8 /**< 最大插件数量 */
|
||||||
|
#define COCOON_MAX_PROXY_RULES 8 /**< 最大代理规则数量 */
|
||||||
|
|
||||||
typedef struct cocoon_config {
|
typedef struct cocoon_config {
|
||||||
const char *root_dir; /**< 静态资源根目录 */
|
const char *root_dir; /**< 静态资源根目录 */
|
||||||
@ -51,6 +52,12 @@ typedef struct cocoon_config {
|
|||||||
/* 插件配置 */
|
/* 插件配置 */
|
||||||
const char *plugins[COCOON_MAX_PLUGINS]; /**< 插件路径列表 */
|
const char *plugins[COCOON_MAX_PLUGINS]; /**< 插件路径列表 */
|
||||||
size_t num_plugins; /**< 插件数量 */
|
size_t num_plugins; /**< 插件数量 */
|
||||||
|
/* 代理配置 */
|
||||||
|
struct {
|
||||||
|
char prefix[256];
|
||||||
|
char target[256];
|
||||||
|
} proxies[COCOON_MAX_PROXY_RULES];
|
||||||
|
size_t num_proxies;
|
||||||
} cocoon_config_t;
|
} cocoon_config_t;
|
||||||
|
|
||||||
/* === 服务器生命周期 API === */
|
/* === 服务器生命周期 API === */
|
||||||
|
|||||||
@ -9,5 +9,8 @@
|
|||||||
"gzip_enabled": true,
|
"gzip_enabled": true,
|
||||||
"brotli_enabled": true,
|
"brotli_enabled": true,
|
||||||
"plugins": ["plugins/hello.so"],
|
"plugins": ["plugins/hello.so"],
|
||||||
"access_log": "-"
|
"access_log": "-",
|
||||||
|
"proxies": [
|
||||||
|
{"prefix": "/api", "target": "http://localhost:3000"}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
53
config.c
53
config.c
@ -346,6 +346,59 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) {
|
|||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "[Config] 第 %d 行: plugins 期望字符串或数组\n", val.line);
|
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);
|
free(key_str);
|
||||||
|
|||||||
293
proxy.c
Normal file
293
proxy.c
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
/**
|
||||||
|
* proxy.c - 反向代理实现
|
||||||
|
*
|
||||||
|
* 轻量级 HTTP/1.1 反向代理,支持流式转发。
|
||||||
|
*
|
||||||
|
* @author xfy
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "proxy.h"
|
||||||
|
#include "log.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
71
proxy.h
Normal file
71
proxy.h
Normal file
@ -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 <stdbool.h>
|
||||||
|
|
||||||
|
#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 */
|
||||||
23
server.c
23
server.c
@ -28,6 +28,7 @@
|
|||||||
#include "websocket.h"
|
#include "websocket.h"
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
#include "middleware.h"
|
#include "middleware.h"
|
||||||
|
#include "proxy.h"
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@ -62,11 +63,13 @@ typedef struct {
|
|||||||
struct sockaddr_storage client_addr; /**< 客户端地址 */
|
struct sockaddr_storage client_addr; /**< 客户端地址 */
|
||||||
socklen_t addr_len; /**< 地址长度 */
|
socklen_t addr_len; /**< 地址长度 */
|
||||||
int response_status; /**< 最后响应的 HTTP 状态码 */
|
int response_status; /**< 最后响应的 HTTP 状态码 */
|
||||||
|
server_context_t *ctx; /**< 服务器上下文(用于访问代理配置) */
|
||||||
} connection_t;
|
} connection_t;
|
||||||
struct server_context {
|
struct server_context {
|
||||||
cocoon_socket_t listen_fd; /**< 监听 socket */
|
cocoon_socket_t listen_fd; /**< 监听 socket */
|
||||||
cocoon_config_t config; /**< 配置副本 */
|
cocoon_config_t config; /**< 配置副本 */
|
||||||
cocoon_middleware_config_t mw_config; /**< 中间件配置(持久化,避免栈变量悬空) */
|
cocoon_middleware_config_t mw_config; /**< 中间件配置(持久化,避免栈变量悬空) */
|
||||||
|
cocoon_proxy_config_t proxy_config; /**< 反向代理配置 */
|
||||||
volatile int running; /**< 运行标志 */
|
volatile int running; /**< 运行标志 */
|
||||||
coco_sched_t *sched; /**< 协程调度器 */
|
coco_sched_t *sched; /**< 协程调度器 */
|
||||||
};
|
};
|
||||||
@ -403,6 +406,19 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
return keep;
|
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 */
|
/* 只支持 GET 和 HEAD */
|
||||||
if (req.method != HTTP_GET && req.method != HTTP_HEAD) {
|
if (req.method != HTTP_GET && req.method != HTTP_HEAD) {
|
||||||
conn->response_status = 405;
|
conn->response_status = 405;
|
||||||
@ -1033,6 +1049,7 @@ static void accept_loop(void *arg) {
|
|||||||
memcpy(&conn->client_addr, &client_addr, sizeof(client_addr));
|
memcpy(&conn->client_addr, &client_addr, sizeof(client_addr));
|
||||||
conn->addr_len = addr_len;
|
conn->addr_len = addr_len;
|
||||||
conn->response_status = 0;
|
conn->response_status = 0;
|
||||||
|
conn->ctx = ctx;
|
||||||
|
|
||||||
atomic_fetch_add(&g_active_connections, 1);
|
atomic_fetch_add(&g_active_connections, 1);
|
||||||
log_debug("新连接 fd=%d,当前活跃连接: %d", client_fd,
|
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;
|
ctx->mw_config.rate_limit = ctx->config.rate_limit;
|
||||||
cocoon_middleware_init_builtin(&ctx->mw_config);
|
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++) {
|
for (size_t i = 0; i < ctx->config.num_plugins; i++) {
|
||||||
if (cocoon_plugin_load(ctx->config.plugins[i]) != 0) {
|
if (cocoon_plugin_load(ctx->config.plugins[i]) != 0) {
|
||||||
|
|||||||
@ -949,6 +949,73 @@ kill_server
|
|||||||
sleep 1
|
sleep 1
|
||||||
start_server
|
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 "=== 结果汇总 ==="
|
echo "=== 结果汇总 ==="
|
||||||
echo "通过: $PASS"
|
echo "通过: $PASS"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user