Phase 5: 配置热重载校验
新增 config_validate() 函数,在热重载前对配置进行完整校验: - 端口、工作线程、连接数、超时等数值范围检查 - TLS 证书/密钥、Basic Auth 用户名/密码成对校验 - 代理规则、虚拟主机、FastCGI 规则完整性校验 - 插件路径、日志级别、速率限制等边界检查 server_reload_config() 集成校验逻辑: - 新配置加载后先校验,失败则拒绝重载并保留旧配置 - 错误信息通过日志输出,便于运维排查 新增 22 项单元测试覆盖 config_validate: - 空配置、默认值、各类边界值、成对字段校验 - 代理规则、虚拟主机、FastCGI 规则校验 - 无错误缓冲区时的容错处理 编译:零警告 单元测试:465 个全部通过(原有 443 + 22 新增) 集成测试:115 项全部通过
This commit is contained in:
parent
8e7ce502af
commit
f037ddaa99
153
config.c
153
config.c
@ -654,9 +654,158 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) {
|
||||
}
|
||||
|
||||
/**
|
||||
* config_merge - 将命令行参数合并到基础配置
|
||||
* config_validate - 校验配置是否合法
|
||||
*
|
||||
* 命令行显式指定的值覆盖配置文件中的值。
|
||||
* 在热重载前调用,确保新配置不会导致服务异常。
|
||||
* 校验失败时返回 false 并写入错误信息到 err_buf。
|
||||
*
|
||||
* @param config 待校验的配置
|
||||
* @param err_buf 错误信息缓冲区(可为 NULL)
|
||||
* @param err_size 缓冲区大小
|
||||
* @return true 合法,false 不合法
|
||||
*/
|
||||
bool config_validate(const cocoon_config_t *config, char *err_buf, size_t err_size) {
|
||||
if (!config) {
|
||||
if (err_buf && err_size > 0) {
|
||||
snprintf(err_buf, err_size, "配置指针为 NULL");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#define SET_ERR(msg) do { if (err_buf && err_size > 0) snprintf(err_buf, err_size, "%s", msg); } while(0)
|
||||
|
||||
/* 端口校验 */
|
||||
if (config->port == 0) {
|
||||
SET_ERR("port 不能为 0");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 工作线程数 */
|
||||
if (config->num_workers > 1024) {
|
||||
SET_ERR("num_workers 不能超过 1024");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 最大连接数 */
|
||||
if (config->max_connections > 100000) {
|
||||
SET_ERR("max_connections 不能超过 100000");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 超时时间 */
|
||||
if (config->timeout_ms > 0 && config->timeout_ms < 100) {
|
||||
SET_ERR("timeout_ms 如果设置则必须 >= 100ms");
|
||||
return false;
|
||||
}
|
||||
if (config->timeout_ms > 3600000) {
|
||||
SET_ERR("timeout_ms 不能超过 1 小时(3600000ms)");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 日志级别 */
|
||||
if (config->log_level > LOG_LEVEL_DEBUG) {
|
||||
SET_ERR("log_level 无效,必须在 0-3 之间");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 根目录 */
|
||||
if (config->root_dir && config->root_dir[0] == '\0') {
|
||||
SET_ERR("root_dir 不能为空字符串");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* TLS 证书和密钥必须成对出现 */
|
||||
if ((config->tls_cert && !config->tls_key) || (!config->tls_cert && config->tls_key)) {
|
||||
SET_ERR("TLS 证书和私钥必须同时配置或同时留空");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 访问日志路径 */
|
||||
if (config->access_log_path && config->access_log_path[0] == '\0') {
|
||||
SET_ERR("access_log_path 不能为空字符串");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Basic Auth 用户名密码成对 */
|
||||
if ((config->auth_user && !config->auth_pass) || (!config->auth_user && config->auth_pass)) {
|
||||
SET_ERR("Basic Auth 用户名和密码必须同时配置");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 代理规则校验 */
|
||||
for (size_t i = 0; i < config->num_proxies; i++) {
|
||||
if (config->proxies[i].prefix[0] == '\0') {
|
||||
SET_ERR("proxy 前缀不能为空");
|
||||
return false;
|
||||
}
|
||||
if (config->proxies[i].target[0] == '\0') {
|
||||
SET_ERR("proxy 目标不能为空");
|
||||
return false;
|
||||
}
|
||||
if (config->proxies[i].pool_size > 16) {
|
||||
SET_ERR("proxy pool_size 不能超过 16");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* 虚拟主机校验 */
|
||||
for (size_t i = 0; i < config->num_vhosts; i++) {
|
||||
if (config->vhosts[i].server_name[0] == '\0') {
|
||||
SET_ERR("vhost 域名不能为空");
|
||||
return false;
|
||||
}
|
||||
if (config->vhosts[i].root_dir[0] == '\0') {
|
||||
SET_ERR("vhost 根目录不能为空");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* FastCGI 规则校验 */
|
||||
for (size_t i = 0; i < config->num_fastcgi; i++) {
|
||||
if (config->fastcgi[i].prefix[0] == '\0') {
|
||||
SET_ERR("fastcgi 前缀不能为空");
|
||||
return false;
|
||||
}
|
||||
if (config->fastcgi[i].host[0] == '\0') {
|
||||
SET_ERR("fastcgi 主机不能为空");
|
||||
return false;
|
||||
}
|
||||
if (!config->fastcgi[i].is_unix_socket) {
|
||||
if (config->fastcgi[i].port <= 0 || config->fastcgi[i].port > 65535) {
|
||||
SET_ERR("fastcgi TCP 端口必须在 1-65535 之间");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (config->fastcgi[i].pool_size < 1 || config->fastcgi[i].pool_size > 16) {
|
||||
SET_ERR("fastcgi pool_size 必须在 1-16 之间");
|
||||
return false;
|
||||
}
|
||||
if (config->fastcgi[i].timeout_ms < 1000 || config->fastcgi[i].timeout_ms > 3600000) {
|
||||
SET_ERR("fastcgi timeout_ms 必须在 1000-3600000 之间");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* 速率限制 */
|
||||
if (config->rate_limit > 100000) {
|
||||
SET_ERR("rate_limit 不能超过 100000");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 插件路径 */
|
||||
for (size_t i = 0; i < config->num_plugins; i++) {
|
||||
if (config->plugins[i] == NULL || config->plugins[i][0] == '\0') {
|
||||
SET_ERR("plugin 路径不能为空");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#undef SET_ERR
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* config_merge - 将命令行参数合并到基础配置
|
||||
* 对于字符串字段,会释放旧值并复制新值。
|
||||
*
|
||||
* @param base 基础配置(通常来自配置文件)
|
||||
|
||||
13
config.h
13
config.h
@ -25,6 +25,19 @@
|
||||
*/
|
||||
bool config_load_from_file(const char *path, cocoon_config_t *config);
|
||||
|
||||
/**
|
||||
* config_validate - 校验配置是否合法
|
||||
*
|
||||
* 在热重载前调用,确保新配置不会导致服务异常。
|
||||
* 校验失败时返回 false 并写入错误信息到 err_buf。
|
||||
*
|
||||
* @param config 待校验的配置
|
||||
* @param err_buf 错误信息缓冲区(可为 NULL)
|
||||
* @param err_size 缓冲区大小
|
||||
* @return true 合法,false 不合法
|
||||
*/
|
||||
bool config_validate(const cocoon_config_t *config, char *err_buf, size_t err_size);
|
||||
|
||||
/**
|
||||
* config_merge - 用命令行配置覆盖文件配置
|
||||
*
|
||||
|
||||
19
server.c
19
server.c
@ -1494,6 +1494,25 @@ void server_reload_config(server_context_t *ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* 校验新配置 */
|
||||
char err_buf[256] = {0};
|
||||
if (!config_validate(&new_config, err_buf, sizeof(err_buf))) {
|
||||
log_error("配置热重载被拒绝:新配置校验失败 — %s", err_buf);
|
||||
/* 清理 new_config 分配的内存 */
|
||||
if (new_config.root_dir) free((void*)new_config.root_dir);
|
||||
if (new_config.tls_cert) free((void*)new_config.tls_cert);
|
||||
if (new_config.tls_key) free((void*)new_config.tls_key);
|
||||
if (new_config.access_log_path) free((void*)new_config.access_log_path);
|
||||
if (new_config.auth_user) free((void*)new_config.auth_user);
|
||||
if (new_config.auth_pass) free((void*)new_config.auth_pass);
|
||||
for (size_t i = 0; i < new_config.num_plugins; i++) {
|
||||
free((void*)new_config.plugins[i]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
log_info("配置热重载:新配置校验通过");
|
||||
|
||||
/* 检查不可热重载项 */
|
||||
if (new_config.port != ctx->config.port) {
|
||||
log_warn("端口变化(%d -> %d)需要重启才能生效", ctx->config.port, new_config.port);
|
||||
|
||||
@ -100,7 +100,186 @@ void test_load_null_args(void) {
|
||||
TEST_ASSERT_FALSE(config_load_from_file("/tmp/x", NULL));
|
||||
}
|
||||
|
||||
/* ===== config_merge ===== */
|
||||
/* ===== config_validate ===== */
|
||||
|
||||
void test_validate_null_config(void) {
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(NULL, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "NULL"));
|
||||
}
|
||||
|
||||
void test_validate_valid_default(void) {
|
||||
cocoon_config_t cfg = {0};
|
||||
cfg.port = 8080;
|
||||
cfg.log_level = LOG_LEVEL_INFO;
|
||||
TEST_ASSERT_TRUE(config_validate(&cfg, NULL, 0));
|
||||
}
|
||||
|
||||
void test_validate_port_zero(void) {
|
||||
cocoon_config_t cfg = {.port = 0, .log_level = LOG_LEVEL_INFO};
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "port"));
|
||||
}
|
||||
|
||||
void test_validate_port_max(void) {
|
||||
cocoon_config_t cfg = {.port = 65535, .log_level = LOG_LEVEL_INFO};
|
||||
TEST_ASSERT_TRUE(config_validate(&cfg, NULL, 0));
|
||||
}
|
||||
|
||||
void test_validate_workers_too_high(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .num_workers = 1025};
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "workers"));
|
||||
}
|
||||
|
||||
void test_validate_max_conn_too_high(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .max_connections = 100001};
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "connections"));
|
||||
}
|
||||
|
||||
void test_validate_timeout_too_short(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .timeout_ms = 50};
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "timeout"));
|
||||
}
|
||||
|
||||
void test_validate_timeout_too_long(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .timeout_ms = 3600001};
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "timeout"));
|
||||
}
|
||||
|
||||
void test_validate_log_level_invalid(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = 99};
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "log_level"));
|
||||
}
|
||||
|
||||
void test_validate_empty_root_dir(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .root_dir = ""};
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "root_dir"));
|
||||
}
|
||||
|
||||
void test_validate_tls_mismatch_cert_only(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .tls_cert = "/cert.pem"};
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "TLS"));
|
||||
}
|
||||
|
||||
void test_validate_tls_mismatch_key_only(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .tls_key = "/key.pem"};
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "TLS"));
|
||||
}
|
||||
|
||||
void test_validate_tls_both_set(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .tls_cert = "/cert.pem", .tls_key = "/key.pem"};
|
||||
TEST_ASSERT_TRUE(config_validate(&cfg, NULL, 0));
|
||||
}
|
||||
|
||||
void test_validate_auth_mismatch_user_only(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .auth_user = "admin"};
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "Auth"));
|
||||
}
|
||||
|
||||
void test_validate_proxy_empty_prefix(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .num_proxies = 1};
|
||||
strcpy(cfg.proxies[0].prefix, "");
|
||||
strcpy(cfg.proxies[0].target, "http://localhost:3000");
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "proxy"));
|
||||
}
|
||||
|
||||
void test_validate_proxy_empty_target(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .num_proxies = 1};
|
||||
strcpy(cfg.proxies[0].prefix, "/api");
|
||||
strcpy(cfg.proxies[0].target, "");
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "proxy"));
|
||||
}
|
||||
|
||||
void test_validate_proxy_pool_too_large(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .num_proxies = 1};
|
||||
strcpy(cfg.proxies[0].prefix, "/api");
|
||||
strcpy(cfg.proxies[0].target, "http://localhost:3000");
|
||||
cfg.proxies[0].pool_size = 17;
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "pool_size"));
|
||||
}
|
||||
|
||||
void test_validate_vhost_empty_name(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .num_vhosts = 1};
|
||||
strcpy(cfg.vhosts[0].server_name, "");
|
||||
strcpy(cfg.vhosts[0].root_dir, "/var/www");
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "vhost"));
|
||||
}
|
||||
|
||||
void test_validate_fastcgi_empty_prefix(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .num_fastcgi = 1};
|
||||
strcpy(cfg.fastcgi[0].prefix, "");
|
||||
strcpy(cfg.fastcgi[0].host, "127.0.0.1");
|
||||
cfg.fastcgi[0].port = 9000;
|
||||
cfg.fastcgi[0].pool_size = 4;
|
||||
cfg.fastcgi[0].timeout_ms = 30000;
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "fastcgi"));
|
||||
}
|
||||
|
||||
void test_validate_fastcgi_invalid_port(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .num_fastcgi = 1};
|
||||
strcpy(cfg.fastcgi[0].prefix, "/php");
|
||||
strcpy(cfg.fastcgi[0].host, "127.0.0.1");
|
||||
cfg.fastcgi[0].port = 0;
|
||||
cfg.fastcgi[0].pool_size = 4;
|
||||
cfg.fastcgi[0].timeout_ms = 30000;
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "fastcgi"));
|
||||
}
|
||||
|
||||
void test_validate_fastcgi_pool_too_large(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .num_fastcgi = 1};
|
||||
strcpy(cfg.fastcgi[0].prefix, "/php");
|
||||
strcpy(cfg.fastcgi[0].host, "127.0.0.1");
|
||||
cfg.fastcgi[0].port = 9000;
|
||||
cfg.fastcgi[0].pool_size = 17;
|
||||
cfg.fastcgi[0].timeout_ms = 30000;
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "fastcgi"));
|
||||
}
|
||||
|
||||
void test_validate_rate_limit_too_high(void) {
|
||||
cocoon_config_t cfg = {.port = 8080, .log_level = LOG_LEVEL_INFO, .rate_limit = 100001};
|
||||
char err[256];
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, err, sizeof(err)));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err, "rate_limit"));
|
||||
}
|
||||
|
||||
void test_validate_no_err_buf(void) {
|
||||
cocoon_config_t cfg = {.port = 0, .log_level = LOG_LEVEL_INFO};
|
||||
TEST_ASSERT_FALSE(config_validate(&cfg, NULL, 0));
|
||||
}
|
||||
|
||||
|
||||
void test_merge_override_all(void) {
|
||||
cocoon_config_t base = {
|
||||
@ -390,5 +569,30 @@ int main(void) {
|
||||
RUN_TEST(test_load_plugins_string);
|
||||
RUN_TEST(test_load_plugins_array);
|
||||
RUN_TEST(test_load_plugins_array_multiple);
|
||||
|
||||
/* config_validate */
|
||||
RUN_TEST(test_validate_null_config);
|
||||
RUN_TEST(test_validate_valid_default);
|
||||
RUN_TEST(test_validate_port_zero);
|
||||
RUN_TEST(test_validate_port_max);
|
||||
RUN_TEST(test_validate_workers_too_high);
|
||||
RUN_TEST(test_validate_max_conn_too_high);
|
||||
RUN_TEST(test_validate_timeout_too_short);
|
||||
RUN_TEST(test_validate_timeout_too_long);
|
||||
RUN_TEST(test_validate_log_level_invalid);
|
||||
RUN_TEST(test_validate_empty_root_dir);
|
||||
RUN_TEST(test_validate_tls_mismatch_cert_only);
|
||||
RUN_TEST(test_validate_tls_mismatch_key_only);
|
||||
RUN_TEST(test_validate_tls_both_set);
|
||||
RUN_TEST(test_validate_auth_mismatch_user_only);
|
||||
RUN_TEST(test_validate_proxy_empty_prefix);
|
||||
RUN_TEST(test_validate_proxy_empty_target);
|
||||
RUN_TEST(test_validate_proxy_pool_too_large);
|
||||
RUN_TEST(test_validate_vhost_empty_name);
|
||||
RUN_TEST(test_validate_fastcgi_empty_prefix);
|
||||
RUN_TEST(test_validate_fastcgi_invalid_port);
|
||||
RUN_TEST(test_validate_fastcgi_pool_too_large);
|
||||
RUN_TEST(test_validate_rate_limit_too_high);
|
||||
RUN_TEST(test_validate_no_err_buf);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user