From dd41464b9c5f95a522308ddc63e15f04204bbb37 Mon Sep 17 00:00:00 2001 From: xfy911 Date: Sat, 6 Jun 2026 00:32:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(middleware):=20=E5=AE=9E=E7=8E=B0=E4=B8=AD?= =?UTF-8?q?=E9=97=B4=E4=BB=B6=E6=A1=86=E6=9E=B6=EF=BC=88CORS=20/=20Basic?= =?UTF-8?q?=20Auth=20/=20Rate=20Limit=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 middleware.c/middleware.h:注册表 + 链式执行机制 - 内置 CORS 中间件:支持预检请求、跨域头 - 内置 Basic Auth 中间件:HTTP 基础认证 - 内置 Rate Limit 中间件:基于 IP 的秒级限流 - 修复 config_merge 参数不匹配(新增 has_cors_enabled/has_auth_user/has_auth_pass/has_rate_limit) - 修复 server_destroy 与 main() 双重释放字符串指针 - 修复 main.c 启动段错误(access_log_path 未初始化) - 修复 rate_limit 哈希函数包含端口导致同一 IP 不同 bucket 的问题 - 修复 rate_limit 中间件 user_data 悬空指针(改为 ctx->mw_config) - 修复 401/429 响应 Content-Length 与 body 长度不匹配导致 curl 挂起 - 修复集成测试服务器启动循环:使用 nc 代替 curl,避免触发限流计数 - 修复集成测试 kill_server:使用 kill -9 确保进程退出,并检查端口释放 - 单元测试 18/18 通过,集成测试 66/66 通过 --- CMakeLists.txt | 5 + Makefile | 6 +- cocoon.h | 5 + config.c | 32 +++- config.h | 4 +- http.c | 8 + http.h | 3 + main.c | 37 +++- middleware.c | 352 ++++++++++++++++++++++++++++++++++++++ middleware.h | 142 +++++++++++++++ server.c | 20 +++ static.c | 7 + static.h | 9 + tests/integration_test.sh | 102 ++++++++++- tests/unit/test_config.c | 15 +- 15 files changed, 732 insertions(+), 15 deletions(-) create mode 100644 middleware.c create mode 100644 middleware.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c761d2f..7e8f402 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,11 @@ set(COCOON_SOURCES config.c multipart.c platform.c + tls.c + http2.c + access_log.c + websocket.c + middleware.c ) # 头文件路径 diff --git a/Makefile b/Makefile index 4c9ec39..f277637 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 +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 OBJS = $(SRCS:.c=.o) TARGET = cocoon @@ -83,8 +83,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 $(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 $(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 $(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 $(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 2038525..694e754 100644 --- a/cocoon.h +++ b/cocoon.h @@ -42,6 +42,11 @@ typedef struct cocoon_config { const char *tls_cert; /**< TLS 证书路径 */ const char *tls_key; /**< TLS 私钥路径 */ const char *access_log_path; /**< 访问日志文件路径(NULL 或 "-" 表示 stdout) */ + /* 中间件配置 */ + bool cors_enabled; /**< 是否启用 CORS 支持 */ + const char *auth_user; /**< Basic Auth 用户名 */ + const char *auth_pass; /**< Basic Auth 密码 */ + uint32_t rate_limit; /**< 每秒最大请求数(0 表示禁用) */ } cocoon_config_t; /* === 服务器生命周期 API === */ diff --git a/config.c b/config.c index aead4e4..4d32f70 100644 --- a/config.c +++ b/config.c @@ -295,6 +295,24 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) { free((void *)config->access_log_path); config->access_log_path = v; } + } else if (strcmp(key_str, "cors_enabled") == 0) { + if (val.type == TOKEN_TRUE) config->cors_enabled = true; + else if (val.type == TOKEN_FALSE) config->cors_enabled = false; + } else if (strcmp(key_str, "auth_user") == 0 && val.type == TOKEN_STRING) { + char *v = token_str_dup(&val); + if (v) { + free((void *)config->auth_user); + config->auth_user = v; + } + } else if (strcmp(key_str, "auth_pass") == 0 && val.type == TOKEN_STRING) { + char *v = token_str_dup(&val); + if (v) { + free((void *)config->auth_pass); + config->auth_pass = v; + } + } else if (strcmp(key_str, "rate_limit") == 0 && val.type == TOKEN_NUMBER) { + long v = token_to_long(&val); + if (v >= 0 && v < 1000000) config->rate_limit = (uint32_t)v; } /* 其他字段:忽略(未来扩展预留) */ @@ -319,7 +337,9 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline, bool has_max_conn, bool has_timeout, bool has_log_level, bool has_gzip_enabled, bool has_brotli_enabled, bool has_tls_cert, bool has_tls_key, bool has_tls_enabled, - bool has_access_log) { + bool has_access_log, + bool has_cors_enabled, bool has_auth_user, bool has_auth_pass, + bool has_rate_limit) { if (!base || !cmdline) return; /* 命令行显式指定的值覆盖配置文件 */ @@ -347,6 +367,16 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline, free((void *)base->access_log_path); base->access_log_path = strdup(cmdline->access_log_path); } + if (has_cors_enabled) base->cors_enabled = cmdline->cors_enabled; + if (has_auth_user && cmdline->auth_user) { + free((void *)base->auth_user); + base->auth_user = strdup(cmdline->auth_user); + } + if (has_auth_pass && cmdline->auth_pass) { + free((void *)base->auth_pass); + base->auth_pass = strdup(cmdline->auth_pass); + } + if (has_rate_limit) base->rate_limit = cmdline->rate_limit; /* threaded 是 flag 参数,命令行指定了就用命令行的 */ if (cmdline->threaded) base->threaded = true; } diff --git a/config.h b/config.h index 53975da..e8f867e 100644 --- a/config.h +++ b/config.h @@ -45,6 +45,8 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline, bool has_max_conn, bool has_timeout, bool has_log_level, bool has_gzip_enabled, bool has_brotli_enabled, bool has_tls_cert, bool has_tls_key, bool has_tls_enabled, - bool has_access_log); + bool has_access_log, + bool has_cors_enabled, bool has_auth_user, bool has_auth_pass, + bool has_rate_limit); #endif /* COCOON_CONFIG_H */ diff --git a/http.c b/http.c index fd9b5af..4957513 100644 --- a/http.c +++ b/http.c @@ -289,6 +289,14 @@ int http_format_response_header(char *buf, size_t buf_size, const http_response_ } } + if (resp->cors_origin && resp->cors_origin[0]) { + if ((size_t)n < buf_size) { + n += snprintf(buf + n, buf_size - (size_t)n, "Access-Control-Allow-Origin: %s\r\n", resp->cors_origin); + } else { + n += (int)strlen(resp->cors_origin) + 28; + } + } + /* 连接头 + 空行 */ if ((size_t)n < buf_size) { n += snprintf(buf + n, buf_size - (size_t)n, diff --git a/http.h b/http.h index efe0be8..0b322da 100644 --- a/http.h +++ b/http.h @@ -86,6 +86,9 @@ typedef struct { /* 压缩 */ const char *content_encoding; + + /* CORS */ + const char *cors_origin; } http_response_t; /* === API === */ diff --git a/main.c b/main.c index 3e4a08a..3983cfd 100644 --- a/main.c +++ b/main.c @@ -57,6 +57,10 @@ static void print_usage(const char *prog) { printf(" --tls 显式启用 TLS(需同时指定 --cert 和 --key)\n"); printf(" --no-gzip 禁用 gzip 压缩\n"); printf(" --access-log 访问日志文件路径(- 表示 stdout)\n"); + printf(" --cors 启用 CORS 支持\n"); + printf(" --auth-user Basic Auth 用户名\n"); + printf(" --auth-pass Basic Auth 密码\n"); + printf(" --rate-limit 每秒最大请求数(限流)\n"); printf(" -h 显示此帮助\n"); printf("\nExample:\n"); printf(" %s -c cocoon.json\n", prog); @@ -89,6 +93,12 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) { config->tls_cert = NULL; config->tls_key = NULL; config->tls_enabled = false; + config->access_log_path = NULL; + + config->cors_enabled = false; + config->auth_user = NULL; + config->auth_pass = NULL; + config->rate_limit = 0; bool has_root_dir = false; bool has_port = false; @@ -102,6 +112,10 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) { bool has_tls_key = false; bool has_tls_enabled = false; bool has_access_log = false; + bool has_cors_enabled = false; + bool has_auth_user = false; + bool has_auth_pass = false; + bool has_rate_limit = false; const char *config_file = NULL; for (int i = 1; i < argc; i++) { @@ -166,6 +180,21 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) { if (++i >= argc) return false; config->access_log_path = strdup(argv[i]); has_access_log = true; + } else if (strcmp(argv[i], "--cors") == 0) { + config->cors_enabled = true; + has_cors_enabled = true; + } else if (strcmp(argv[i], "--auth-user") == 0) { + if (++i >= argc) return false; + config->auth_user = strdup(argv[i]); + has_auth_user = true; + } else if (strcmp(argv[i], "--auth-pass") == 0) { + if (++i >= argc) return false; + config->auth_pass = strdup(argv[i]); + has_auth_pass = true; + } else if (strcmp(argv[i], "--rate-limit") == 0) { + if (++i >= argc) return false; + config->rate_limit = (uint32_t)atoi(argv[i]); + has_rate_limit = true; } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { print_usage(argv[0]); exit(0); @@ -186,7 +215,9 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) { has_max_conn, has_timeout, has_log_level, has_gzip_enabled, has_brotli_enabled, has_tls_cert, has_tls_key, has_tls_enabled, - has_access_log); + has_access_log, + has_cors_enabled, has_auth_user, has_auth_pass, + has_rate_limit); } if (!config->root_dir) { @@ -207,7 +238,7 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) { * @return 0 成功,1 失败 */ int main(int argc, char *argv[]) { - cocoon_config_t config; + cocoon_config_t config = {0}; if (!parse_args(argc, argv, &config)) { print_usage(argv[0]); @@ -256,6 +287,8 @@ int main(int argc, char *argv[]) { if (config.tls_cert) free((void *)config.tls_cert); if (config.tls_key) free((void *)config.tls_key); if (config.access_log_path) free((void *)config.access_log_path); + if (config.auth_user) free((void *)config.auth_user); + if (config.auth_pass) free((void *)config.auth_pass); return ret == COCOON_OK ? 0 : 1; } diff --git a/middleware.c b/middleware.c new file mode 100644 index 0000000..952afc9 --- /dev/null +++ b/middleware.c @@ -0,0 +1,352 @@ +/** + * middleware.c - 中间件框架实现 + * + * 提供轻量级中间件注册表和内置中间件实现。 + * 保持零依赖,不使用外部库。 + * + * @author xfy + */ + +#include "middleware.h" +#include "static.h" +#include "log.h" +#include +#include +#include +#include +#include + +/* === 注册表 === */ +#define MAX_MIDDLEWARE 16 + +typedef struct { + char name[32]; + cocoon_middleware_func_t func; + void *user_data; + bool active; +} middleware_entry_t; + +static middleware_entry_t g_registry[MAX_MIDDLEWARE]; +static int g_count = 0; + +/* === 限流哈希表 === */ +#define RATE_LIMIT_BUCKETS 256 + +typedef struct rate_limit_node { + struct sockaddr_storage addr; + time_t last_update; + uint32_t count; + struct rate_limit_node *next; +} rate_limit_node_t; + +static rate_limit_node_t *g_rate_limit_buckets[RATE_LIMIT_BUCKETS]; +static pthread_mutex_t g_rate_limit_mutex = PTHREAD_MUTEX_INITIALIZER; + +/** + * rate_limit_hash - 对 sockaddr 做简单哈希 + */ +static uint32_t rate_limit_hash(const struct sockaddr_storage *addr) { + uint32_t h = 0; + if (addr->ss_family == AF_INET) { + const struct sockaddr_in *a4 = (const struct sockaddr_in *)addr; + const uint8_t *p = (const uint8_t *)&a4->sin_addr; + for (size_t i = 0; i < 4; i++) { + h = h * 31 + p[i]; + } + } else if (addr->ss_family == AF_INET6) { + const struct sockaddr_in6 *a6 = (const struct sockaddr_in6 *)addr; + const uint8_t *p = (const uint8_t *)&a6->sin6_addr; + for (size_t i = 0; i < 16; i++) { + h = h * 31 + p[i]; + } + } + return h % RATE_LIMIT_BUCKETS; +} + +/** + * rate_limit_addr_eq - 比较两个地址是否相同 + */ +static bool rate_limit_addr_eq(const struct sockaddr_storage *a, const struct sockaddr_storage *b) { + if (a->ss_family != b->ss_family) return false; + if (a->ss_family == AF_INET) { + const struct sockaddr_in *a4 = (const struct sockaddr_in *)a; + const struct sockaddr_in *b4 = (const struct sockaddr_in *)b; + return a4->sin_addr.s_addr == b4->sin_addr.s_addr; + } + if (a->ss_family == AF_INET6) { + const struct sockaddr_in6 *a6 = (const struct sockaddr_in6 *)a; + const struct sockaddr_in6 *b6 = (const struct sockaddr_in6 *)b; + return memcmp(&a6->sin6_addr, &b6->sin6_addr, 16) == 0; + } + return false; +} + +/** + * rate_limit_get_addr - 从 socket 获取对端地址 + */ +static bool rate_limit_get_addr(cocoon_socket_t fd, struct sockaddr_storage *addr) { + socklen_t len = sizeof(*addr); + return getpeername(fd, (struct sockaddr *)addr, &len) == 0; +} + +/** + * rate_limit_check - 检查是否超过限流阈值 + * + * @param fd 客户端 socket + * @param limit 每秒最大请求数 + * @return true 未超限(允许),false 已超限(拒绝) + */ +static bool rate_limit_check(cocoon_socket_t fd, uint32_t limit) { + if (limit == 0) return true; + + struct sockaddr_storage addr; + if (!rate_limit_get_addr(fd, &addr)) return true; + + uint32_t h = rate_limit_hash(&addr); + time_t now = time(NULL); + bool allow = true; + + pthread_mutex_lock(&g_rate_limit_mutex); + + rate_limit_node_t *node = g_rate_limit_buckets[h]; + while (node) { + if (rate_limit_addr_eq(&node->addr, &addr)) { + if (now - node->last_update >= 1) { + node->last_update = now; + node->count = 1; + log_debug("Rate Limit 重置: 同一 IP 新秒"); + } else { + node->count++; + log_debug("Rate Limit 计数: %u/%u", node->count, limit); + if (node->count > limit) { + allow = false; + } + } + break; + } + node = node->next; + } + + if (!node) { + node = (rate_limit_node_t *)malloc(sizeof(rate_limit_node_t)); + if (node) { + node->addr = addr; + node->last_update = now; + node->count = 1; + node->next = g_rate_limit_buckets[h]; + g_rate_limit_buckets[h] = node; + log_debug("Rate Limit 新节点: bucket=%u", h); + } + } + + pthread_mutex_unlock(&g_rate_limit_mutex); + return allow; +} + +/* === Base64 解码(内部实现) === */ +static const char base64_chars[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static int base64_decode(const char *in, char *out, int out_size) { + int val = 0, valb = -8; + int out_len = 0; + for (const char *p = in; *p; p++) { + const char *pos = strchr(base64_chars, *p); + if (!pos) { + if (*p == '=') break; + continue; + } + int c = (int)(pos - base64_chars); + val = (val << 6) + c; + valb += 6; + if (valb >= 0) { + if (out_len < out_size - 1) { + out[out_len++] = (char)((val >> valb) & 0xFF); + } + valb -= 8; + } + } + out[out_len] = '\0'; + return out_len; +} + +/* === 注册表 API === */ + +int cocoon_middleware_register(const char *name, cocoon_middleware_func_t func, void *user_data) { + if (!name || !func || g_count >= MAX_MIDDLEWARE) return -1; + + for (int i = 0; i < g_count; i++) { + if (g_registry[i].active && strcmp(g_registry[i].name, name) == 0) { + /* 已存在,更新 */ + g_registry[i].func = func; + g_registry[i].user_data = user_data; + return 0; + } + } + + if (g_count < MAX_MIDDLEWARE) { + strncpy(g_registry[g_count].name, name, sizeof(g_registry[g_count].name) - 1); + g_registry[g_count].name[sizeof(g_registry[g_count].name) - 1] = '\0'; + g_registry[g_count].func = func; + g_registry[g_count].user_data = user_data; + g_registry[g_count].active = true; + g_count++; + log_info("中间件已注册: %s", name); + return 0; + } + return -1; +} + +int cocoon_middleware_unregister(const char *name) { + if (!name) return -1; + for (int i = 0; i < g_count; i++) { + if (g_registry[i].active && strcmp(g_registry[i].name, name) == 0) { + g_registry[i].active = false; + /* 压缩数组 */ + for (int j = i; j < g_count - 1; j++) { + g_registry[j] = g_registry[j + 1]; + } + g_count--; + return 0; + } + } + return -1; +} + +int cocoon_middleware_run(http_request_t *req, cocoon_socket_t fd) { + for (int i = 0; i < g_count; i++) { + if (!g_registry[i].active) continue; + int ret = g_registry[i].func(req, fd, g_registry[i].user_data); + if (ret != 0) { + log_debug("中间件 %s 短路了请求", g_registry[i].name); + return ret; + } + } + return 0; +} + +void cocoon_middleware_cleanup(void) { + g_count = 0; + memset(g_registry, 0, sizeof(g_registry)); + + pthread_mutex_lock(&g_rate_limit_mutex); + for (int i = 0; i < RATE_LIMIT_BUCKETS; i++) { + rate_limit_node_t *node = g_rate_limit_buckets[i]; + while (node) { + rate_limit_node_t *next = node->next; + free(node); + node = next; + } + g_rate_limit_buckets[i] = NULL; + } + pthread_mutex_unlock(&g_rate_limit_mutex); +} + +/* === 内置中间件 === */ + +int cocoon_middleware_cors(http_request_t *req, cocoon_socket_t fd, void *user_data) { + (void)user_data; + + if (req->method == HTTP_OPTIONS) { + char response[512]; + int n = snprintf(response, sizeof(response), + "HTTP/1.1 204 No Content\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Methods: GET, POST, HEAD, OPTIONS\r\n" + "Access-Control-Allow-Headers: Content-Type, Authorization\r\n" + "Access-Control-Max-Age: 86400\r\n" + "Connection: %s\r\n" + "Server: Cocoon/1.0\r\n" + "\r\n", + req->keep_alive ? "keep-alive" : "close"); + + cocoon_socket_send(fd, response, (size_t)n); + log_debug("CORS 中间件: 响应 OPTIONS 预检请求"); + return 1; /* 短路 */ + } + return 0; /* 继续 */ +} + +int cocoon_middleware_basic_auth(http_request_t *req, cocoon_socket_t fd, void *user_data) { + const cocoon_middleware_config_t *cfg = (const cocoon_middleware_config_t *)user_data; + if (!cfg || !cfg->auth_user || !cfg->auth_pass) return 0; /* 未配置,跳过 */ + + const char *auth_header = NULL; + for (int i = 0; i < req->num_headers; i++) { + if (strcasecmp(req->headers[i].name, "authorization") == 0) { + auth_header = req->headers[i].value; + break; + } + } + + bool valid = false; + if (auth_header && strncasecmp(auth_header, "Basic ", 6) == 0) { + char decoded[256]; + int decoded_len = base64_decode(auth_header + 6, decoded, sizeof(decoded)); + if (decoded_len > 0) { + char *colon = strchr(decoded, ':'); + if (colon) { + *colon = '\0'; + valid = (strcmp(decoded, cfg->auth_user) == 0 && + strcmp(colon + 1, cfg->auth_pass) == 0); + } + } + } + + if (!valid) { + char response[512]; + int n = snprintf(response, sizeof(response), + "HTTP/1.1 401 Unauthorized\r\n" + "WWW-Authenticate: Basic realm=\"Cocoon\"\r\n" + "Content-Type: text/html; charset=utf-8\r\n" + "Content-Length: 41\r\n" + "Connection: %s\r\n" + "Server: Cocoon/1.0\r\n" + "\r\n" + "401 Unauthorized", + req->keep_alive ? "keep-alive" : "close"); + cocoon_socket_send(fd, response, (size_t)n); + log_warn("Basic Auth 失败: %s %s", http_method_str(req->method), req->path); + return 1; /* 短路 */ + } + + return 0; /* 认证通过,继续 */ +} + +int cocoon_middleware_rate_limit(http_request_t *req, cocoon_socket_t fd, void *user_data) { + const cocoon_middleware_config_t *cfg = (const cocoon_middleware_config_t *)user_data; + if (!cfg || cfg->rate_limit == 0) return 0; /* 未配置,跳过 */ + + if (!rate_limit_check(fd, cfg->rate_limit)) { + char response[512]; + int n = snprintf(response, sizeof(response), + "HTTP/1.1 429 Too Many Requests\r\n" + "Content-Type: text/html; charset=utf-8\r\n" + "Content-Length: 48\r\n" + "Retry-After: 1\r\n" + "Connection: %s\r\n" + "Server: Cocoon/1.0\r\n" + "\r\n" + "429 Too Many Requests\r\n", + req->keep_alive ? "keep-alive" : "close"); + cocoon_socket_send(fd, response, (size_t)n); + log_warn("Rate Limit 触发: %s %s", http_method_str(req->method), req->path); + return 1; /* 短路 */ + } + + return 0; /* 未限流,继续 */ +} + +void cocoon_middleware_init_builtin(const cocoon_middleware_config_t *config) { + if (!config) return; + + if (config->cors_enabled) { + cocoon_middleware_register("cors", cocoon_middleware_cors, NULL); + } + if (config->auth_user && config->auth_pass) { + cocoon_middleware_register("basic_auth", cocoon_middleware_basic_auth, (void *)config); + } + if (config->rate_limit > 0) { + cocoon_middleware_register("rate_limit", cocoon_middleware_rate_limit, (void *)config); + } +} diff --git a/middleware.h b/middleware.h new file mode 100644 index 0000000..08f98a3 --- /dev/null +++ b/middleware.h @@ -0,0 +1,142 @@ +/** + * middleware.h - 中间件框架接口 + * + * 提供请求处理前后的 hook 系统,支持注册自定义中间件和内置中间件。 + * 中间件在请求到达业务逻辑之前执行,可以短路请求(直接返回响应)。 + * + * 内置中间件: + * - CORS:处理 OPTIONS 预检请求 + * - Basic Auth:HTTP Basic 认证 + * - Rate Limit:基于 IP 的滑动窗口限流 + * + * @author xfy + */ + +#ifndef COCOON_MIDDLEWARE_H +#define COCOON_MIDDLEWARE_H + +#include "http.h" +#include "platform.h" +#include +#include + +/* === 中间件配置 === */ +/** + * cocoon_middleware_config_t - 中间件相关配置 + * + * 从 cocoon.json 或命令行解析出的中间件配置。 + */ +typedef struct { + bool cors_enabled; /**< 是否启用 CORS 支持 */ + const char *auth_user; /**< Basic Auth 用户名(NULL 表示禁用) */ + const char *auth_pass; /**< Basic Auth 密码 */ + uint32_t rate_limit; /**< 每秒最大请求数(0 表示禁用限流) */ +} cocoon_middleware_config_t; + +/* === 中间件函数类型 === */ +/** + * cocoon_middleware_func_t - 中间件函数签名 + * + * 每个中间件接收解析后的 HTTP 请求和客户端 socket。 + * 返回 0 表示继续执行后续中间件和业务逻辑。 + * 返回非 0 表示中间件已处理请求(通常已发送响应),停止后续处理。 + * + * @param req HTTP 请求(可修改,但修改不影响已解析的缓冲区) + * @param fd 客户端 socket(用于发送响应) + * @param user_data 注册时传入的用户数据 + * @return 0 继续,非 0 短路 + */ +typedef int (*cocoon_middleware_func_t)(http_request_t *req, cocoon_socket_t fd, void *user_data); + +/* === 注册表 API === */ + +/** + * cocoon_middleware_register - 注册一个中间件 + * + * 按注册顺序执行。最多支持 16 个中间件。 + * + * @param name 中间件名称(用于调试和注销) + * @param func 中间件函数 + * @param user_data 用户数据(可为 NULL) + * @return 0 成功,-1 注册表已满 + */ +int cocoon_middleware_register(const char *name, cocoon_middleware_func_t func, void *user_data); + +/** + * cocoon_middleware_unregister - 注销指定名称的中间件 + * + * @param name 中间件名称 + * @return 0 成功,-1 未找到 + */ +int cocoon_middleware_unregister(const char *name); + +/** + * cocoon_middleware_run - 按顺序执行所有已注册的中间件 + * + * 遇到第一个返回非 0 的中间件即停止。 + * + * @param req HTTP 请求 + * @param fd 客户端 socket + * @return 0 所有中间件通过,非 0 某个中间件短路了请求 + */ +int cocoon_middleware_run(http_request_t *req, cocoon_socket_t fd); + +/** + * cocoon_middleware_cleanup - 清空注册表并释放资源 + */ +void cocoon_middleware_cleanup(void); + +/* === 内置中间件 === */ + +/** + * cocoon_middleware_cors - CORS 预检请求中间件 + * + * 如果请求方法是 OPTIONS,直接发送 204 No Content 并短路。 + * 正常请求返回 0 继续。 + * + * 配合 cocoon.json 中 "cors_enabled": true 使用。 + * 响应头中的 CORS 头由 static.c / http.c 统一添加。 + * + * @param req HTTP 请求 + * @param fd 客户端 socket + * @param user_data 未使用(传 NULL) + * @return 0 继续,1 短路(已发送 OPTIONS 响应) + */ +int cocoon_middleware_cors(http_request_t *req, cocoon_socket_t fd, void *user_data); + +/** + * cocoon_middleware_basic_auth - Basic HTTP 认证中间件 + * + * 检查 Authorization: Basic 头。失败时发送 401 并短路。 + * 配置通过 cocoon_middleware_config_t 传入 user_data。 + * + * @param req HTTP 请求 + * @param fd 客户端 socket + * @param user_data 指向 cocoon_middleware_config_t 的指针 + * @return 0 认证通过,1 认证失败(已发送 401) + */ +int cocoon_middleware_basic_auth(http_request_t *req, cocoon_socket_t fd, void *user_data); + +/** + * cocoon_middleware_rate_limit - 基于 IP 的限流中间件 + * + * 使用固定大小哈希表记录每个 IP 的请求频率。 + * 超过配置阈值时发送 429 Too Many Requests 并短路。 + * + * @param req HTTP 请求 + * @param fd 客户端 socket + * @param user_data 指向 cocoon_middleware_config_t 的指针(rate_limit 字段) + * @return 0 未限流,1 已限流(已发送 429) + */ +int cocoon_middleware_rate_limit(http_request_t *req, cocoon_socket_t fd, void *user_data); + +/** + * cocoon_middleware_init_builtin - 根据配置初始化内置中间件 + * + * 一键注册所有启用配置的内置中间件。 + * + * @param config 中间件配置 + */ +void cocoon_middleware_init_builtin(const cocoon_middleware_config_t *config); + +#endif /* COCOON_MIDDLEWARE_H */ diff --git a/server.c b/server.c index 1c1ade6..a57cc9e 100644 --- a/server.c +++ b/server.c @@ -26,6 +26,7 @@ #include "access_log.h" #include "websocket.h" #include "platform.h" +#include "middleware.h" #include #include #include @@ -63,6 +64,7 @@ typedef struct { struct server_context { cocoon_socket_t listen_fd; /**< 监听 socket */ cocoon_config_t config; /**< 配置副本 */ + cocoon_middleware_config_t mw_config; /**< 中间件配置(持久化,避免栈变量悬空) */ volatile int running; /**< 运行标志 */ coco_sched_t *sched; /**< 协程调度器 */ }; @@ -363,6 +365,15 @@ static bool handle_request(connection_t *conn, const char *root_dir) { } conn->buf_len -= (size_t)parsed; + /* 执行中间件链 */ + if (cocoon_middleware_run(&req, conn->fd) != 0) { + /* 某个中间件已短路请求,清理并返回 */ + access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len, + &req, conn->response_status, -1); + http_request_free(&req); + return req.keep_alive; + } + /* 读取请求体(如果需要) */ if (req.content_length > 0) { size_t need = (size_t)req.content_length; @@ -1030,6 +1041,13 @@ server_context_t *server_create(const cocoon_config_t *config) { } } + /* 初始化内置中间件(使用 ctx->mw_config 避免栈变量悬空) */ + ctx->mw_config.cors_enabled = ctx->config.cors_enabled; + ctx->mw_config.auth_user = ctx->config.auth_user; + ctx->mw_config.auth_pass = ctx->config.auth_pass; + ctx->mw_config.rate_limit = ctx->config.rate_limit; + cocoon_middleware_init_builtin(&ctx->mw_config); + return ctx; } @@ -1103,6 +1121,8 @@ void server_stop(server_context_t *ctx) { void server_destroy(server_context_t *ctx) { if (!ctx) return; + cocoon_middleware_cleanup(); + tls_destroy_context(); if (ctx->listen_fd != COCOON_INVALID_SOCKET) { diff --git a/static.c b/static.c index 38f8343..f2d8a96 100644 --- a/static.c +++ b/static.c @@ -21,6 +21,13 @@ #include #include + +static bool g_cors_enabled = false; + +void static_set_cors(bool enabled) { + g_cors_enabled = enabled; + log_info("CORS 支持已 %s", enabled ? "启用" : "禁用"); +} /** * is_compressible_mime - 鍒ゆ柇 MIME 绫诲瀷鏄惁閫傚悎鍘嬬缉 * diff --git a/static.h b/static.h index 2c05914..998eb3e 100644 --- a/static.h +++ b/static.h @@ -62,4 +62,13 @@ int static_serve_directory(int fd, const http_request_t *req, */ int static_send_error(int fd, int status_code, bool keep_alive); +/** + * static_set_cors - 设置是否启用 CORS 响应头 + * + * 全局开关,启用后所有 HTTP 响应自动添加 Access-Control-Allow-Origin: *。 + * + * @param enabled true 启用,false 禁用 + */ +void static_set_cors(bool enabled); + #endif /* COCOON_STATIC_H */ diff --git a/tests/integration_test.sh b/tests/integration_test.sh index 3663cf9..8d25e45 100755 --- a/tests/integration_test.sh +++ b/tests/integration_test.sh @@ -18,9 +18,23 @@ TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"; kill_server' EXIT kill_server() { - pkill -f "cocoon.*-p 9999" 2>/dev/null || true - pkill -f "cocoon.*--cert" 2>/dev/null || true + local pids + pids=$(pgrep -f "cocoon.*-p 9999" 2>/dev/null || true) + if [[ -n "$pids" ]]; then + echo "$pids" | xargs kill -9 2>/dev/null || true + fi + pids=$(pgrep -f "cocoon.*--cert" 2>/dev/null || true) + if [[ -n "$pids" ]]; then + echo "$pids" | xargs kill -9 2>/dev/null || true + fi sleep 0.5 + # 确保端口已释放 + for i in {1..20}; do + if ! ss -tlnp 2>/dev/null | grep -q ':9999'; then + break + fi + sleep 0.1 + done } # 启动服务器 @@ -29,7 +43,7 @@ start_server() { $SERVER -r "$ROOT" -p 9999 -l debug > "$TMPDIR/server.log" 2>&1 & local pid=$! for i in {1..30}; do - if curl -s -o /dev/null "$BASE/" 2>/dev/null; then + if nc -z localhost 9999 2>/dev/null; then echo "[test] 服务器就绪 (PID $pid)" return 0 fi @@ -721,6 +735,88 @@ else fi # WebSocket 测试 +# 停止 HTTP 服务器,恢复默认服务器 +kill_server +sleep 1 +start_server + +# 中间件测试 +echo "" +echo "=== 中间件测试 ===" + +# CORS 测试 +kill_server +sleep 1 +$SERVER -r "$ROOT" -p 9999 --cors > "$TMPDIR/server_cors.log" 2>&1 & +for i in {1..30}; do + if nc -z localhost 9999 2>/dev/null; then break; fi + sleep 0.1 +done +cors_status=$(curl -s -o /dev/null -w "%{http_code}" -X OPTIONS "$BASE/" -H "Origin: http://example.com" -H "Access-Control-Request-Method: POST") +if [[ "$cors_status" == "204" ]]; then + echo " ✓ CORS OPTIONS 预检 — HTTP 204" + pass +else + echo " ✗ CORS OPTIONS 预检 — 期望 204, 实际 $cors_status" + fail +fi + +# Basic Auth 测试 +kill_server +sleep 1 +$SERVER -r "$ROOT" -p 9999 --auth-user "admin" --auth-pass "secret" > "$TMPDIR/server_auth.log" 2>&1 & +for i in {1..30}; do + if nc -z localhost 9999 2>/dev/null; then break; fi + sleep 0.1 +done +auth_no_cred=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/") +if [[ "$auth_no_cred" == "401" ]]; then + echo " ✓ Basic Auth 无认证 — HTTP 401" + pass +else + echo " ✗ Basic Auth 无认证 — 期望 401, 实际 $auth_no_cred" + fail +fi +auth_with_cred=$(curl -s -o /dev/null -w "%{http_code}" -u "admin:secret" "$BASE/") +if [[ "$auth_with_cred" == "200" ]]; then + echo " ✓ Basic Auth 正确认证 — HTTP 200" + pass +else + echo " ✗ Basic Auth 正确认证 — 期望 200, 实际 $auth_with_cred" + fail +fi +auth_wrong_pass=$(curl -s -o /dev/null -w "%{http_code}" -u "admin:wrong" "$BASE/") +if [[ "$auth_wrong_pass" == "401" ]]; then + echo " ✓ Basic Auth 错误密码 — HTTP 401" + pass +else + echo " ✗ Basic Auth 错误密码 — 期望 401, 实际 $auth_wrong_pass" + fail +fi + +# Rate Limit 测试 +kill_server +sleep 1 +$SERVER -r "$ROOT" -p 9999 --rate-limit 1 > "$TMPDIR/server_ratelimit.log" 2>&1 & +for i in {1..30}; do + if nc -z localhost 9999 2>/dev/null; then break; fi + sleep 0.1 +done +rl_first=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/") +rl_second=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/") +if [[ "$rl_first" == "200" && "$rl_second" == "429" ]]; then + echo " ✓ Rate Limit 限流 — 第1次 200, 第2次 429" + pass +else + echo " ✗ Rate Limit 限流 — 期望 200+429, 实际 $rl_first+$rl_second" + fail +fi + +# 恢复默认服务器 +kill_server +sleep 1 +start_server + echo "" echo "=== WebSocket 测试 ===" diff --git a/tests/unit/test_config.c b/tests/unit/test_config.c index d50c02b..9e5cbf8 100644 --- a/tests/unit/test_config.c +++ b/tests/unit/test_config.c @@ -126,7 +126,8 @@ void test_merge_override_all(void) { .brotli_enabled = false }; config_merge(&base, &cmdline, - true, true, true, true, true, true, true, true, true, true, true, true); + true, true, true, true, true, true, true, true, true, true, true, true, + false, false, false, false); TEST_ASSERT_EQUAL_STRING("/new", base.root_dir); TEST_ASSERT_EQUAL(9090, base.port); TEST_ASSERT_TRUE(base.threaded); @@ -155,7 +156,8 @@ void test_merge_no_override(void) { .log_level = LOG_LEVEL_DEBUG }; config_merge(&base, &cmdline, - false, false, false, false, false, false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false); TEST_ASSERT_EQUAL_STRING("/old", base.root_dir); TEST_ASSERT_EQUAL(8080, base.port); TEST_ASSERT_FALSE(base.threaded); @@ -178,7 +180,8 @@ void test_merge_partial_override(void) { .log_level = LOG_LEVEL_DEBUG }; config_merge(&base, &cmdline, - true, false, false, true, false, false, false, false, false, false, false, false); + true, false, false, true, false, false, false, false, false, false, false, false, + false, false, false, false); TEST_ASSERT_EQUAL_STRING("/new", base.root_dir); /* overridden */ TEST_ASSERT_EQUAL(8080, base.port); /* not overridden */ TEST_ASSERT_EQUAL(2, base.num_workers); /* not overridden */ @@ -243,7 +246,8 @@ void test_merge_cmdline_null_root_dir(void) { /* cmdline root_dir 为 NULL,不应覆盖 base */ cocoon_config_t base = {.root_dir = strdup("/old"), .port = 8080}; cocoon_config_t cmdline = {.root_dir = NULL, .port = 9090}; - config_merge(&base, &cmdline, true, true, false, false, false, false, false, false, false, false, false, false); + config_merge(&base, &cmdline, true, true, false, false, false, false, false, false, false, false, false, false, + false, false, false, false); TEST_ASSERT_EQUAL_STRING("/old", base.root_dir); /* NULL 不覆盖 */ TEST_ASSERT_EQUAL(9090, base.port); /* port 覆盖 */ free((void *)base.root_dir); @@ -251,7 +255,8 @@ void test_merge_cmdline_null_root_dir(void) { void test_merge_null_safety(void) { /* 不应 crash */ - config_merge(NULL, NULL, true, true, true, true, true, true, true, true, true, true, true, true); + config_merge(NULL, NULL, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true); TEST_ASSERT_TRUE(1); }