cocoon/config.c
xfy911 f38effff49
Some checks failed
CI / build (push) Failing after 26s
feat: ACME 配置集成 + 自动续期后台协程
- cocoon_config_t 新增 ACME 字段(enabled, directory_url, email, domains,
  cert_path, key_path, renew_days),支持最多 8 个域名
- config.c: JSON 解析 acme 配置块、校验逻辑、config_merge 支持
- main.c: 新增 --acme 命令行开关
- server.c: server_start 启动 ACME 自动续期后台协程(检查周期 24h,
  提前 renew_days 触发重新签发)
- acme.c: 实现 acme_save_certificate 证书持久化、
  acme_cert_days_until_expiry 过期天数检查
- acme.h: 新增 save_certificate / cert_days_until_expiry 接口声明
- 新增 tests/unit/test_acme_config.c:覆盖配置解析、校验、合并、
  证书过期检查、证书保存共 6 项测试
- 修复 test_config.c 中 config_merge 调用以匹配新签名
2026-06-16 12:45:08 +08:00

1108 lines
47 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* config.c - JSON 配置文件解析实现
*
* 极简 JSON 解析器,只处理 cocoon 配置所需的字段:
* - 字符串root_dir, log_level
* - 整数port, num_workers, max_connections, timeout_ms
* - 布尔值threaded
*
* 保持零依赖,不引入外部 JSON 库。
*
* @author xfy
*/
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
/* === 内部:极简 JSON token 类型 === */
typedef enum {
TOKEN_EOF,
TOKEN_LBRACE, /* { */
TOKEN_RBRACE, /* } */
TOKEN_STRING, /* "..." */
TOKEN_NUMBER, /* 123 */
TOKEN_TRUE, /* true */
TOKEN_FALSE, /* false */
TOKEN_LBRACKET, /* [ */
TOKEN_RBRACKET, /* ] */
TOKEN_COMMA, /* , */
TOKEN_COLON, /* : */
TOKEN_INVALID /* 错误 */
} token_type_t;
typedef struct {
token_type_t type;
const char *start;
size_t len;
int line;
} token_t;
typedef struct {
const char *src;
size_t pos;
size_t len;
int line;
} parser_t;
/* === 内部parser 辅助函数 === */
/**
* parser_init - 初始化 JSON 解析器
*
* @param p 解析器状态结构
* @param src JSON 源字符串
* @param len 源字符串长度
*/
static void parser_init(parser_t *p, const char *src, size_t len) {
p->src = src;
p->pos = 0;
p->len = len;
p->line = 1;
}
/**
* parser_skip_ws - 跳过空白字符和注释
*
* 支持空格、制表符、换行,以及 // 行注释。
*
* @param p 解析器状态结构
*/
static void parser_skip_ws(parser_t *p) {
while (p->pos < p->len) {
char c = p->src[p->pos];
if (c == ' ' || c == '\t' || c == '\r') {
p->pos++;
} else if (c == '\n') {
p->pos++;
p->line++;
} else if (c == '/' && p->pos + 1 < p->len && p->src[p->pos + 1] == '/') {
/* 跳过 // 注释 */
p->pos += 2;
while (p->pos < p->len && p->src[p->pos] != '\n') p->pos++;
} else {
break;
}
}
}
/**
* parser_next_token - 读取下一个 JSON token
*
* 支持的 token 类型字符串、数字、true、false、
* 以及各种分隔符({ } [ ] , :)。
*
* @param p 解析器状态结构
* @return 下一个 token
*/
static token_t parser_next_token(parser_t *p);
// 期望当前 token 为字符串并返回其值(释放时需调用者 free
static char *parser_expect_string(parser_t *p, token_t t) {
if (t.type != TOKEN_STRING) {
return NULL;
}
return strndup(t.start, t.len);
}
// 跳过当前值(对象或数组或基本值)
static void parser_skip_value(parser_t *p) {
token_t t = parser_next_token(p);
if (t.type == TOKEN_LBRACE) {
while (1) {
t = parser_next_token(p);
if (t.type == TOKEN_RBRACE) break;
if (t.type == TOKEN_STRING) {
t = parser_next_token(p); // colon
if (t.type == TOKEN_COLON) {
parser_skip_value(p); // value
}
t = parser_next_token(p); // comma or rbrace
if (t.type == TOKEN_RBRACE) break;
if (t.type != TOKEN_COMMA) break;
} else {
break;
}
}
} else if (t.type == TOKEN_LBRACKET) {
while (1) {
t = parser_next_token(p);
if (t.type == TOKEN_RBRACKET) break;
if (t.type == TOKEN_COMMA) continue;
if (t.type == TOKEN_EOF) break;
parser_skip_value(p);
t = parser_next_token(p);
if (t.type == TOKEN_RBRACKET) break;
if (t.type != TOKEN_COMMA) break;
}
} else if (t.type == TOKEN_STRING) {
// skip already consumed
} else if (t.type == TOKEN_NUMBER) {
// skip already consumed
} else if (t.type == TOKEN_TRUE || t.type == TOKEN_FALSE) {
// skip already consumed
}
}
// 返回当前数字 token 的字符串值
static char *parser_number_str(parser_t *p, token_t t) {
if (t.type != TOKEN_NUMBER) {
return NULL;
}
return strndup(t.start, t.len);
}
static token_t parser_next_token(parser_t *p) {
parser_skip_ws(p);
token_t t = {TOKEN_INVALID, NULL, 0, p->line};
if (p->pos >= p->len) {
t.type = TOKEN_EOF;
return t;
}
const char *start = p->src + p->pos;
char c = start[0];
switch (c) {
case '{': p->pos++; t.type = TOKEN_LBRACE; return t;
case '}': p->pos++; t.type = TOKEN_RBRACE; return t;
case '[': p->pos++; t.type = TOKEN_LBRACKET; return t;
case ']': p->pos++; t.type = TOKEN_RBRACKET; return t;
case ',': p->pos++; t.type = TOKEN_COMMA; return t;
case ':': p->pos++; t.type = TOKEN_COLON; return t;
case '"': {
/* 字符串 */
p->pos++; /* skip " */
t.start = p->src + p->pos;
while (p->pos < p->len && p->src[p->pos] != '"') {
if (p->src[p->pos] == '\\' && p->pos + 1 < p->len) {
p->pos += 2; /* skip escaped */
} else {
p->pos++;
}
}
t.len = (size_t)(p->src + p->pos - t.start);
if (p->pos < p->len) p->pos++; /* skip closing " */
t.type = TOKEN_STRING;
return t;
}
default: {
if (isdigit(c) || c == '-') {
/* 数字 */
t.start = start;
p->pos++;
while (p->pos < p->len && (isdigit(p->src[p->pos]) || p->src[p->pos] == '.')) {
p->pos++;
}
t.len = (size_t)(p->src + p->pos - t.start);
t.type = TOKEN_NUMBER;
return t;
} else if (strncmp(start, "true", 4) == 0) {
p->pos += 4;
t.type = TOKEN_TRUE;
return t;
} else if (strncmp(start, "false", 5) == 0) {
p->pos += 5;
t.type = TOKEN_FALSE;
return t;
}
/* 未知 token */
p->pos++;
t.type = TOKEN_INVALID;
return t;
}
}
}
/**
* token_expect - 期望下一个 token 为指定类型
*
* @param p 解析器状态结构
* @param expected 期望的 token 类型
* @return true 匹配false 不匹配
*/
static bool token_expect(parser_t *p, token_type_t expected) {
token_t t = parser_next_token(p);
return t.type == expected;
}
/**
* token_str_dup - 将字符串 token 复制为 C 字符串
*
* 处理 JSON 字符串中的转义序列(\n \t \r \\ \")。
*
* @param t 字符串 token
* @return 新分配的 C 字符串,调用者负责释放;失败返回 NULL
*/
static char *token_str_dup(const token_t *t) {
char *buf = (char *)malloc(t->len + 1);
if (!buf) return NULL;
size_t j = 0;
for (size_t i = 0; i < t->len; i++) {
if (t->start[i] == '\\' && i + 1 < t->len) {
char next = t->start[i + 1];
switch (next) {
case 'n': buf[j++] = '\n'; i++; break;
case 't': buf[j++] = '\t'; i++; break;
case 'r': buf[j++] = '\r'; i++; break;
case '\\': buf[j++] = '\\'; i++; break;
case '"': buf[j++] = '"'; i++; break;
default: buf[j++] = t->start[i]; break;
}
} else {
buf[j++] = t->start[i];
}
}
buf[j] = '\0';
return buf;
}
/**
* token_to_long - 将数字 token 转换为长整型
*
* @param t 数字 token
* @return 转换后的数值
*/
static long token_to_long(const token_t *t) {
char buf[32] = {0};
size_t n = t->len < 31 ? t->len : 31;
memcpy(buf, t->start, n);
return strtol(buf, NULL, 10);
}
/**
* str_to_log_level - 将字符串转换为日志级别
*
* @param str 日志级别字符串error/warn/info/debug
* @return 对应的日志级别枚举值,无效时返回 LOG_LEVEL_INFO
*/
static log_level_t str_to_log_level(const char *str) {
if (strcmp(str, "error") == 0) return LOG_LEVEL_ERROR;
if (strcmp(str, "warn") == 0) return LOG_LEVEL_WARN;
if (strcmp(str, "info") == 0) return LOG_LEVEL_INFO;
if (strcmp(str, "debug") == 0) return LOG_LEVEL_DEBUG;
return LOG_LEVEL_INFO; /* 默认 */
}
/* === 公共 API === */
/**
* config_load_from_file - 从 JSON 配置文件加载配置
*
* 解析 cocoon.json 格式,支持以下字段:
* root_dir, port, threaded, num_workers, max_connections, timeout_ms,
* log_level, gzip_enabled, brotli_enabled, tls_cert, tls_key, tls_enabled,
* access_log, cors_enabled, auth_user, auth_pass, rate_limit,
* plugins字符串或数组, proxies对象数组
*
* 未知字段将被静默忽略,便于向后兼容。
*
* @param path 配置文件路径
* @param config 输出配置结构体
* @return true 成功false 失败(会输出错误信息到 stderr
*/
bool config_load_from_file(const char *path, cocoon_config_t *config) {
if (!path || !config) return false;
FILE *fp = fopen(path, "r");
if (!fp) {
fprintf(stderr, "[Config] 无法打开配置文件: %s (%s)\n", path, strerror(errno));
return false;
}
/* 读取文件内容 */
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
fseek(fp, 0, SEEK_SET);
if (size <= 0 || size > 65536) { /* 限制 64KB */
fclose(fp);
fprintf(stderr, "[Config] 配置文件过大或为空\n");
return false;
}
char *buf = (char *)malloc((size_t)size + 1);
if (!buf) {
fclose(fp);
return false;
}
size_t read = fread(buf, 1, (size_t)size, fp);
fclose(fp);
buf[read] = '\0';
parser_t p;
parser_init(&p, buf, read);
/* 期望 { */
if (!token_expect(&p, TOKEN_LBRACE)) {
fprintf(stderr, "[Config] 配置文件格式错误: 期望 '{'\n");
free(buf);
return false;
}
/* 解析键值对 */
while (1) {
token_t key = parser_next_token(&p);
if (key.type == TOKEN_RBRACE) break; /* 空对象 {} */
if (key.type != TOKEN_STRING) {
fprintf(stderr, "[Config] 第 %d 行: 期望字符串键\n", key.line);
free(buf);
return false;
}
if (!token_expect(&p, TOKEN_COLON)) {
fprintf(stderr, "[Config] 第 %d 行: 键后缺少 ':'\n", key.line);
free(buf);
return false;
}
token_t val = parser_next_token(&p);
char *key_str = token_str_dup(&key);
if (strcmp(key_str, "root_dir") == 0 && val.type == TOKEN_STRING) {
char *v = token_str_dup(&val);
if (v) {
free((void *)config->root_dir); /* 释放旧的 */
config->root_dir = v;
}
} else if (strcmp(key_str, "port") == 0 && val.type == TOKEN_NUMBER) {
long v = token_to_long(&val);
if (v > 0 && v < 65536) config->port = (uint16_t)v;
} else if (strcmp(key_str, "threaded") == 0) {
if (val.type == TOKEN_TRUE) config->threaded = true;
else if (val.type == TOKEN_FALSE) config->threaded = false;
} else if (strcmp(key_str, "num_workers") == 0 && val.type == TOKEN_NUMBER) {
long v = token_to_long(&val);
if (v > 0 && v < 1024) config->num_workers = (uint32_t)v;
} else if (strcmp(key_str, "max_connections") == 0 && val.type == TOKEN_NUMBER) {
long v = token_to_long(&val);
if (v >= 0 && v < 1000000) config->max_connections = (uint32_t)v;
} else if (strcmp(key_str, "timeout_ms") == 0 && val.type == TOKEN_NUMBER) {
long v = token_to_long(&val);
if (v >= 0 && v < 3600000) config->timeout_ms = (uint32_t)v;
} else if (strcmp(key_str, "log_level") == 0 && val.type == TOKEN_STRING) {
char *v = token_str_dup(&val);
if (v) {
config->log_level = str_to_log_level(v);
free(v);
}
} else if (strcmp(key_str, "gzip_enabled") == 0) {
if (val.type == TOKEN_TRUE) config->gzip_enabled = true;
else if (val.type == TOKEN_FALSE) config->gzip_enabled = false;
} else if (strcmp(key_str, "brotli_enabled") == 0) {
if (val.type == TOKEN_TRUE) config->brotli_enabled = true;
else if (val.type == TOKEN_FALSE) config->brotli_enabled = false;
} else if (strcmp(key_str, "tls_cert") == 0 && val.type == TOKEN_STRING) {
char *v = token_str_dup(&val);
if (v) {
free((void *)config->tls_cert);
config->tls_cert = v;
}
} else if (strcmp(key_str, "tls_key") == 0 && val.type == TOKEN_STRING) {
char *v = token_str_dup(&val);
if (v) {
free((void *)config->tls_key);
config->tls_key = v;
}
} else if (strcmp(key_str, "tls_enabled") == 0) {
if (val.type == TOKEN_TRUE) config->tls_enabled = true;
else if (val.type == TOKEN_FALSE) config->tls_enabled = false;
} else if (strcmp(key_str, "access_log") == 0 && val.type == TOKEN_STRING) {
char *v = token_str_dup(&val);
if (v) {
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;
} else if (strcmp(key_str, "plugins") == 0) {
if (val.type == TOKEN_STRING) {
char *v = token_str_dup(&val);
if (v && config->num_plugins < COCOON_MAX_PLUGINS) {
config->plugins[config->num_plugins++] = v;
}
} else if (val.type == TOKEN_LBRACKET) {
/* 解析字符串数组 */
while (1) {
token_t item = parser_next_token(&p);
if (item.type == TOKEN_RBRACKET) break;
if (item.type == TOKEN_STRING) {
char *v = token_str_dup(&item);
if (v && config->num_plugins < COCOON_MAX_PLUGINS) {
config->plugins[config->num_plugins++] = v;
}
} else {
fprintf(stderr, "[Config] 第 %d 行: plugins 数组期望字符串项\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 行: plugins 数组期望 ',' 或 ']'\n", sep.line);
break;
}
}
} else {
fprintf(stderr, "[Config] 第 %d 行: plugins 期望字符串或数组\n", val.line);
}
} else if (strcmp(key_str, "proxies") == 0) {
if (val.type == TOKEN_LBRACKET) {
/* 解析对象数组 */
while (1) {
token_t item = parser_next_token(&p);
if (item.type == TOKEN_RBRACKET) break;
if (item.type == TOKEN_LBRACE) {
char prefix[256] = {0};
char target[256] = {0};
/* 解析对象内的键值对 */
while (1) {
token_t pkey = parser_next_token(&p);
if (pkey.type == TOKEN_RBRACE) break;
if (pkey.type != TOKEN_STRING) {
fprintf(stderr, "[Config] 第 %d 行: proxy 对象期望字符串键\n", pkey.line);
break;
}
if (!token_expect(&p, TOKEN_COLON)) break;
token_t pval = parser_next_token(&p);
char *pk = token_str_dup(&pkey);
if (strcmp(pk, "prefix") == 0 && pval.type == TOKEN_STRING) {
char *v = token_str_dup(&pval);
if (v) { strncpy(prefix, v, sizeof(prefix)-1); free(v); }
} else if (strcmp(pk, "target") == 0 && pval.type == TOKEN_STRING) {
char *v = token_str_dup(&pval);
if (v) { strncpy(target, v, sizeof(target)-1); free(v); }
} else if (strcmp(pk, "pool_size") == 0 && pval.type == TOKEN_NUMBER) {
char *v = token_str_dup(&pval);
if (v) { config->proxies[config->num_proxies].pool_size = (uint32_t)atoi(v); free(v); }
} else if (strcmp(pk, "weight") == 0 && pval.type == TOKEN_NUMBER) {
char *v = token_str_dup(&pval);
if (v) { config->proxies[config->num_proxies].weight = (uint32_t)atoi(v); free(v); }
} else if (strcmp(pk, "healthcheck") == 0 && pval.type == TOKEN_LBRACE) {
/* 解析 healthcheck 子对象 */
while (1) {
token_t hc_key = parser_next_token(&p);
if (hc_key.type == TOKEN_RBRACE) break;
if (hc_key.type != TOKEN_STRING) {
token_t skip_sep = parser_next_token(&p);
if (skip_sep.type == TOKEN_RBRACE) break;
continue;
}
if (!token_expect(&p, TOKEN_COLON)) break;
token_t hc_val = parser_next_token(&p);
char *hk = token_str_dup(&hc_key);
if (strcmp(hk, "path") == 0 && hc_val.type == TOKEN_STRING) {
char *v = token_str_dup(&hc_val);
if (v) { strncpy(config->proxies[config->num_proxies].healthcheck.path, v, sizeof(config->proxies[0].healthcheck.path)-1); free(v); }
} else if (strcmp(hk, "interval_ms") == 0 && hc_val.type == TOKEN_NUMBER) {
config->proxies[config->num_proxies].healthcheck.interval_ms = (uint32_t)token_to_long(&hc_val);
} else if (strcmp(hk, "timeout_ms") == 0 && hc_val.type == TOKEN_NUMBER) {
config->proxies[config->num_proxies].healthcheck.timeout_ms = (uint32_t)token_to_long(&hc_val);
} else if (strcmp(hk, "enabled") == 0) {
if (hc_val.type == TOKEN_TRUE) config->proxies[config->num_proxies].healthcheck.enabled = true;
else if (hc_val.type == TOKEN_FALSE) config->proxies[config->num_proxies].healthcheck.enabled = false;
}
free(hk);
token_t hc_sep = parser_next_token(&p);
if (hc_sep.type == TOKEN_RBRACE) break;
if (hc_sep.type != TOKEN_COMMA) break;
}
}
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) {
size_t prefix_len = strlen(prefix);
if (prefix_len >= sizeof(config->proxies[0].prefix)) prefix_len = sizeof(config->proxies[0].prefix) - 1;
memcpy(config->proxies[config->num_proxies].prefix, prefix, prefix_len);
config->proxies[config->num_proxies].prefix[prefix_len] = '\0';
size_t target_len = strlen(target);
if (target_len >= sizeof(config->proxies[0].target)) target_len = sizeof(config->proxies[0].target) - 1;
memcpy(config->proxies[config->num_proxies].target, target, target_len);
config->proxies[config->num_proxies].target[target_len] = '\0';
if (config->proxies[config->num_proxies].pool_size == 0) {
config->proxies[config->num_proxies].pool_size = 4; /* 默认连接池大小 */
}
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);
}
} else if (strcmp(key_str, "vhosts") == 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 server_name[256] = {0};
char root_dir[512] = {0};
/* 解析对象内的键值对 */
while (1) {
token_t vkey = parser_next_token(&p);
if (vkey.type == TOKEN_RBRACE) break;
if (vkey.type != TOKEN_STRING) {
fprintf(stderr, "[Config] 第 %d 行: vhost 对象期望字符串键\n", vkey.line);
break;
}
if (!token_expect(&p, TOKEN_COLON)) break;
token_t vval = parser_next_token(&p);
char *vk = token_str_dup(&vkey);
if (strcmp(vk, "server_name") == 0 && vval.type == TOKEN_STRING) {
char *v = token_str_dup(&vval);
if (v) { strncpy(server_name, v, sizeof(server_name)-1); free(v); }
} else if (strcmp(vk, "root_dir") == 0 && vval.type == TOKEN_STRING) {
char *v = token_str_dup(&vval);
if (v) { strncpy(root_dir, v, sizeof(root_dir)-1); free(v); }
}
free(vk);
token_t vsep = parser_next_token(&p);
if (vsep.type == TOKEN_RBRACE) break;
if (vsep.type != TOKEN_COMMA) {
fprintf(stderr, "[Config] 第 %d 行: vhost 对象期望 ',' 或 '}'\n", vsep.line);
break;
}
}
if (server_name[0] && root_dir[0] && config->num_vhosts < COCOON_MAX_VHOSTS) {
size_t sn_len = strlen(server_name);
if (sn_len >= sizeof(config->vhosts[0].server_name)) sn_len = sizeof(config->vhosts[0].server_name) - 1;
memcpy(config->vhosts[config->num_vhosts].server_name, server_name, sn_len);
config->vhosts[config->num_vhosts].server_name[sn_len] = '\0';
size_t rd_len = strlen(root_dir);
if (rd_len >= sizeof(config->vhosts[0].root_dir)) rd_len = sizeof(config->vhosts[0].root_dir) - 1;
memcpy(config->vhosts[config->num_vhosts].root_dir, root_dir, rd_len);
config->vhosts[config->num_vhosts].root_dir[rd_len] = '\0';
config->num_vhosts++;
}
} else {
fprintf(stderr, "[Config] 第 %d 行: vhosts 数组期望对象项\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 行: vhosts 数组期望 ',' 或 ']'\n", sep.line);
break;
}
}
} else {
fprintf(stderr, "[Config] 第 %d 行: vhosts 期望数组\n", val.line);
}
} else if (strcmp(key_str, "fastcgi") == 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 host[256] = {0};
int port = 0;
bool is_unix = false;
int pool_size = 4;
int timeout_ms = 30000;
while (1) {
token_t fkey = parser_next_token(&p);
if (fkey.type == TOKEN_RBRACE) break;
if (fkey.type != TOKEN_STRING) {
token_t skip_sep = parser_next_token(&p);
if (skip_sep.type == TOKEN_RBRACE) break;
continue;
}
if (!token_expect(&p, TOKEN_COLON)) break;
token_t fval = parser_next_token(&p);
char *fk = token_str_dup(&fkey);
if (strcmp(fk, "prefix") == 0 && fval.type == TOKEN_STRING) {
char *v = token_str_dup(&fval);
if (v) { strncpy(prefix, v, sizeof(prefix)-1); free(v); }
} else if (strcmp(fk, "host") == 0 && fval.type == TOKEN_STRING) {
char *v = token_str_dup(&fval);
if (v) { strncpy(host, v, sizeof(host)-1); free(v); }
} else if (strcmp(fk, "port") == 0 && fval.type == TOKEN_NUMBER) {
port = (int)token_to_long(&fval);
} else if (strcmp(fk, "unix_socket") == 0) {
if (fval.type == TOKEN_TRUE) is_unix = true;
else if (fval.type == TOKEN_FALSE) is_unix = false;
} else if (strcmp(fk, "pool_size") == 0 && fval.type == TOKEN_NUMBER) {
pool_size = (int)token_to_long(&fval);
} else if (strcmp(fk, "timeout_ms") == 0 && fval.type == TOKEN_NUMBER) {
timeout_ms = (int)token_to_long(&fval);
}
free(fk);
token_t fsep = parser_next_token(&p);
if (fsep.type == TOKEN_RBRACE) break;
if (fsep.type != TOKEN_COMMA) break;
}
if (prefix[0] && host[0] && config->num_fastcgi < COCOON_MAX_FASTCGI_RULES) {
size_t i = config->num_fastcgi;
snprintf(config->fastcgi[i].prefix, sizeof(config->fastcgi[i].prefix), "%s", prefix);
snprintf(config->fastcgi[i].host, sizeof(config->fastcgi[i].host), "%s", host);
config->fastcgi[i].port = port;
config->fastcgi[i].is_unix_socket = is_unix;
config->fastcgi[i].pool_size = pool_size > 0 ? pool_size : 4;
config->fastcgi[i].timeout_ms = timeout_ms > 0 ? timeout_ms : 30000;
config->num_fastcgi++;
}
} else {
fprintf(stderr, "[Config] 第 %d 行: fastcgi 数组期望对象项\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 行: fastcgi 数组期望 ',' 或 ']'\n", sep.line);
break;
}
}
} else {
fprintf(stderr, "[Config] 第 %d 行: fastcgi 期望数组\n", val.line);
}
} else if (strcmp(key_str, "cache") == 0) {
if (val.type == TOKEN_LBRACE) {
while (1) {
token_t ckey = parser_next_token(&p);
if (ckey.type == TOKEN_RBRACE) break;
if (ckey.type != TOKEN_STRING) {
parser_skip_value(&p);
continue;
}
char *ck_str = parser_expect_string(&p, ckey);
token_t csep = parser_next_token(&p);
if (csep.type != TOKEN_COLON) {
free(ck_str);
parser_skip_value(&p);
continue;
}
token_t cval = parser_next_token(&p);
if (strcmp(ck_str, "enabled") == 0 && cval.type == TOKEN_TRUE) {
config->cache_enabled = true;
} else if (strcmp(ck_str, "enabled") == 0 && cval.type == TOKEN_FALSE) {
config->cache_enabled = false;
} else if (strcmp(ck_str, "max_size") == 0 && cval.type == TOKEN_NUMBER) {
config->cache_max_size = (size_t)strtoul(parser_number_str(&p, cval), NULL, 10);
} else if (strcmp(ck_str, "ttl_seconds") == 0 && cval.type == TOKEN_NUMBER) {
config->cache_ttl_seconds = (uint32_t)strtoul(parser_number_str(&p, cval), NULL, 10);
} else if (strcmp(ck_str, "max_entry_size") == 0 && cval.type == TOKEN_NUMBER) {
config->cache_max_entry_size = (size_t)strtoul(parser_number_str(&p, cval), NULL, 10);
}
free(ck_str);
token_t csep2 = parser_next_token(&p);
if (csep2.type == TOKEN_RBRACE) break;
if (csep2.type != TOKEN_COMMA) break;
}
} else {
fprintf(stderr, "[Config] 第 %d 行: cache 期望对象\n", val.line);
}
} else if (strcmp(key_str, "acme") == 0) {
if (val.type == TOKEN_LBRACE) {
while (1) {
token_t akey = parser_next_token(&p);
if (akey.type == TOKEN_RBRACE) break;
if (akey.type != TOKEN_STRING) {
parser_skip_value(&p);
continue;
}
char *ak_str = parser_expect_string(&p, akey);
token_t asep = parser_next_token(&p);
if (asep.type != TOKEN_COLON) {
free(ak_str);
parser_skip_value(&p);
continue;
}
token_t aval = parser_next_token(&p);
if (strcmp(ak_str, "enabled") == 0 && aval.type == TOKEN_TRUE) {
config->acme_enabled = true;
} else if (strcmp(ak_str, "enabled") == 0 && aval.type == TOKEN_FALSE) {
config->acme_enabled = false;
} else if (strcmp(ak_str, "directory_url") == 0 && aval.type == TOKEN_STRING) {
char *v = token_str_dup(&aval);
if (v) {
snprintf(config->acme_directory_url, sizeof(config->acme_directory_url), "%s", v);
free(v);
}
} else if (strcmp(ak_str, "email") == 0 && aval.type == TOKEN_STRING) {
char *v = token_str_dup(&aval);
if (v) {
snprintf(config->acme_email, sizeof(config->acme_email), "%s", v);
free(v);
}
} else if (strcmp(ak_str, "domains") == 0 && aval.type == TOKEN_LBRACKET) {
while (1) {
token_t ditem = parser_next_token(&p);
if (ditem.type == TOKEN_RBRACKET) break;
if (ditem.type == TOKEN_STRING && config->acme_num_domains < 8) {
char *v = token_str_dup(&ditem);
if (v) {
snprintf(config->acme_domains[config->acme_num_domains], sizeof(config->acme_domains[0]), "%s", v);
config->acme_num_domains++;
free(v);
}
}
token_t dsep = parser_next_token(&p);
if (dsep.type == TOKEN_RBRACKET) break;
if (dsep.type != TOKEN_COMMA) break;
}
} else if (strcmp(ak_str, "cert_path") == 0 && aval.type == TOKEN_STRING) {
char *v = token_str_dup(&aval);
if (v) {
snprintf(config->acme_cert_path, sizeof(config->acme_cert_path), "%s", v);
free(v);
}
} else if (strcmp(ak_str, "key_path") == 0 && aval.type == TOKEN_STRING) {
char *v = token_str_dup(&aval);
if (v) {
snprintf(config->acme_key_path, sizeof(config->acme_key_path), "%s", v);
free(v);
}
} else if (strcmp(ak_str, "renew_days") == 0 && aval.type == TOKEN_NUMBER) {
config->acme_renew_days = (uint32_t)token_to_long(&aval);
}
free(ak_str);
token_t asep2 = parser_next_token(&p);
if (asep2.type == TOKEN_RBRACE) break;
if (asep2.type != TOKEN_COMMA) break;
}
} else {
fprintf(stderr, "[Config] 第 %d 行: acme 期望对象\n", val.line);
}
} /* 其他字段:忽略(未来扩展预留) */
free(key_str);
/* 检查逗号或右括号 */
token_t sep = parser_next_token(&p);
if (sep.type == TOKEN_RBRACE) break;
if (sep.type != TOKEN_COMMA) {
fprintf(stderr, "[Config] 第 %d 行: 期望 ',' 或 '}'\n", sep.line);
free(buf);
return false;
}
}
free(buf);
return true;
}
/**
* 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;
}
}
/* 缓存配置校验 */
if (config->cache_enabled) {
if (config->cache_max_size > 0 && config->cache_max_size < 4096) {
SET_ERR("cache max_size 如果指定,至少 4096 字节");
return false;
}
if (config->cache_ttl_seconds > 0 && config->cache_ttl_seconds < 1) {
SET_ERR("cache ttl_seconds 至少 1 秒");
return false;
}
if (config->cache_max_entry_size > 0 && config->cache_max_entry_size < 256) {
SET_ERR("cache max_entry_size 如果指定,至少 256 字节");
return false;
}
if (config->cache_max_entry_size > config->cache_max_size && config->cache_max_size > 0) {
SET_ERR("cache max_entry_size 不能超过 max_size");
return false;
}
}
/* ACME 配置校验 */
if (config->acme_enabled) {
if (config->acme_email[0] == '\0') {
SET_ERR("ACME 邮箱不能为空");
return false;
}
if (config->acme_num_domains == 0) {
SET_ERR("ACME 域名列表不能为空");
return false;
}
for (size_t i = 0; i < config->acme_num_domains; i++) {
if (config->acme_domains[i][0] == '\0') {
SET_ERR("ACME 域名不能为空");
return false;
}
}
if (config->acme_cert_path[0] == '\0') {
SET_ERR("ACME 证书保存路径不能为空");
return false;
}
if (config->acme_key_path[0] == '\0') {
SET_ERR("ACME 私钥保存路径不能为空");
return false;
}
if (config->acme_renew_days == 0 || config->acme_renew_days > 90) {
SET_ERR("ACME renew_days 必须在 1-90 之间");
return false;
}
}
#undef SET_ERR
return true;
}
/**
* config_merge - 将命令行参数合并到基础配置
* 对于字符串字段,会释放旧值并复制新值。
*
* @param base 基础配置(通常来自配置文件)
* @param cmdline 命令行配置
* @param has_root_dir 是否显式指定 root_dir
* @param has_port 是否显式指定 port
* @param has_workers 是否显式指定 num_workers
* @param has_max_conn 是否显式指定 max_connections
* @param has_timeout 是否显式指定 timeout_ms
* @param has_log_level 是否显式指定 log_level
* @param has_gzip_enabled 是否显式指定 gzip_enabled
* @param has_brotli_enabled 是否显式指定 brotli_enabled
* @param has_tls_cert 是否显式指定 tls_cert
* @param has_tls_key 是否显式指定 tls_key
* @param has_tls_enabled 是否显式指定 tls_enabled
* @param has_access_log 是否显式指定 access_log
* @param has_cors_enabled 是否显式指定 cors_enabled
* @param has_auth_user 是否显式指定 auth_user
* @param has_auth_pass 是否显式指定 auth_pass
* @param has_rate_limit 是否显式指定 rate_limit
* @param has_plugins 是否显式指定 plugins
*/
void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
bool has_root_dir, bool has_port, bool has_workers,
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_cors_enabled, bool has_auth_user, bool has_auth_pass,
bool has_rate_limit,
bool has_plugins,
bool has_cache_enabled, bool has_cache_max_size,
bool has_cache_ttl_seconds, bool has_cache_max_entry_size,
bool has_acme_enabled) {
if (!base || !cmdline) return;
/* 命令行显式指定的值覆盖配置文件 */
if (has_root_dir && cmdline->root_dir) {
free((void *)base->root_dir);
base->root_dir = strdup(cmdline->root_dir);
}
if (has_port) base->port = cmdline->port;
if (has_workers) base->num_workers = cmdline->num_workers;
if (has_max_conn) base->max_connections = cmdline->max_connections;
if (has_timeout) base->timeout_ms = cmdline->timeout_ms;
if (has_log_level) base->log_level = cmdline->log_level;
if (has_gzip_enabled) base->gzip_enabled = cmdline->gzip_enabled;
if (has_brotli_enabled) base->brotli_enabled = cmdline->brotli_enabled;
if (has_tls_cert && cmdline->tls_cert) {
free((void *)base->tls_cert);
base->tls_cert = strdup(cmdline->tls_cert);
}
if (has_tls_key && cmdline->tls_key) {
free((void *)base->tls_key);
base->tls_key = strdup(cmdline->tls_key);
}
if (has_tls_enabled) base->tls_enabled = cmdline->tls_enabled;
if (has_access_log && cmdline->access_log_path) {
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;
if (has_plugins) {
for (size_t i = 0; i < cmdline->num_plugins && i < COCOON_MAX_PLUGINS; i++) {
if (base->num_plugins < COCOON_MAX_PLUGINS) {
base->plugins[base->num_plugins++] = strdup(cmdline->plugins[i]);
}
}
}
if (has_cache_enabled) base->cache_enabled = cmdline->cache_enabled;
if (has_cache_max_size) base->cache_max_size = cmdline->cache_max_size;
if (has_cache_ttl_seconds) base->cache_ttl_seconds = cmdline->cache_ttl_seconds;
if (has_cache_max_entry_size) base->cache_max_entry_size = cmdline->cache_max_entry_size;
if (has_acme_enabled) base->acme_enabled = cmdline->acme_enabled;
/* threaded 是 flag 参数,命令行指定了就用命令行的 */
if (cmdline->threaded) base->threaded = true;
}