feat(middleware): 实现中间件框架(CORS / Basic Auth / Rate Limit)
- 新增 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 通过
This commit is contained in:
parent
e542890e26
commit
dd41464b9c
@ -28,6 +28,11 @@ set(COCOON_SOURCES
|
||||
config.c
|
||||
multipart.c
|
||||
platform.c
|
||||
tls.c
|
||||
http2.c
|
||||
access_log.c
|
||||
websocket.c
|
||||
middleware.c
|
||||
)
|
||||
|
||||
# 头文件路径
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
5
cocoon.h
5
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 === */
|
||||
|
||||
32
config.c
32
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;
|
||||
}
|
||||
|
||||
4
config.h
4
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 */
|
||||
|
||||
8
http.c
8
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,
|
||||
|
||||
3
http.h
3
http.h
@ -86,6 +86,9 @@ typedef struct {
|
||||
|
||||
/* 压缩 */
|
||||
const char *content_encoding;
|
||||
|
||||
/* CORS */
|
||||
const char *cors_origin;
|
||||
} http_response_t;
|
||||
|
||||
/* === API === */
|
||||
|
||||
37
main.c
37
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 <path> 访问日志文件路径(- 表示 stdout)\n");
|
||||
printf(" --cors 启用 CORS 支持\n");
|
||||
printf(" --auth-user <user> Basic Auth 用户名\n");
|
||||
printf(" --auth-pass <pass> Basic Auth 密码\n");
|
||||
printf(" --rate-limit <n> 每秒最大请求数(限流)\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;
|
||||
}
|
||||
|
||||
352
middleware.c
Normal file
352
middleware.c
Normal file
@ -0,0 +1,352 @@
|
||||
/**
|
||||
* middleware.c - 中间件框架实现
|
||||
*
|
||||
* 提供轻量级中间件注册表和内置中间件实现。
|
||||
* 保持零依赖,不使用外部库。
|
||||
*
|
||||
* @author xfy
|
||||
*/
|
||||
|
||||
#include "middleware.h"
|
||||
#include "static.h"
|
||||
#include "log.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
#include <pthread.h>
|
||||
|
||||
/* === 注册表 === */
|
||||
#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"
|
||||
"<html><body>401 Unauthorized</body></html>",
|
||||
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"
|
||||
"<html><body>429 Too Many Requests</body></html>\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);
|
||||
}
|
||||
}
|
||||
142
middleware.h
Normal file
142
middleware.h
Normal file
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* === 中间件配置 === */
|
||||
/**
|
||||
* 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 */
|
||||
20
server.c
20
server.c
@ -26,6 +26,7 @@
|
||||
#include "access_log.h"
|
||||
#include "websocket.h"
|
||||
#include "platform.h"
|
||||
#include "middleware.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@ -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) {
|
||||
|
||||
7
static.c
7
static.c
@ -21,6 +21,13 @@
|
||||
#include <zlib.h>
|
||||
#include <brotli/encode.h>
|
||||
|
||||
|
||||
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 绫诲瀷鏄惁閫傚悎鍘嬬缉
|
||||
*
|
||||
|
||||
9
static.h
9
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 */
|
||||
|
||||
@ -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 测试 ==="
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user