1006 lines
42 KiB
C
1006 lines
42 KiB
C
|
||
/**
|
||
* 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;
|
||
strncpy(config->fastcgi[i].prefix, prefix, sizeof(config->fastcgi[0].prefix)-1);
|
||
config->fastcgi[i].prefix[sizeof(config->fastcgi[0].prefix)-1] = '\0';
|
||
strncpy(config->fastcgi[i].host, host, sizeof(config->fastcgi[0].host)-1);
|
||
config->fastcgi[i].host[sizeof(config->fastcgi[0].host)-1] = '\0';
|
||
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);
|
||
}
|
||
} /* 其他字段:忽略(未来扩展预留) */
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
#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) {
|
||
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;
|
||
/* threaded 是 flag 参数,命令行指定了就用命令行的 */
|
||
if (cmdline->threaded) base->threaded = true;
|
||
}
|