feat: ACME 配置集成 + 自动续期后台协程
Some checks failed
CI / build (push) Failing after 26s

- 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 调用以匹配新签名
This commit is contained in:
xfy911 2026-06-16 12:45:08 +08:00
parent 276fd1ca45
commit f38effff49
10 changed files with 640 additions and 10 deletions

View File

@ -151,8 +151,12 @@ $(UNIT_TEST_DIR)/test_dashboard: $(UNIT_TEST_DIR)/test_dashboard.c dashboard.c s
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_dashboard.c dashboard.c sse.c log.c $(UNITY_SRC) $(LDFLAGS) $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_dashboard.c dashboard.c sse.c log.c $(UNITY_SRC) $(LDFLAGS)
# ACME 测试 # ACME 测试
$(UNIT_TEST_DIR)/test_acme: $(UNIT_TEST_DIR)/test_acme.c acme.c log.c $(UNITY_SRC) $(UNIT_TEST_DIR)/test_acme: $(UNIT_TEST_DIR)/test_acme.c acme.c log.c platform.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_acme.c acme.c log.c $(UNITY_SRC) $(LDFLAGS) $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_acme.c acme.c log.c platform.c $(UNITY_SRC) $(LDFLAGS)
# ACME 配置集成测试
$(UNIT_TEST_DIR)/test_acme_config: $(UNIT_TEST_DIR)/test_acme_config.c config.c acme.c log.c access_log.c http.c platform.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_acme_config.c config.c acme.c log.c access_log.c http.c platform.c $(UNITY_SRC) $(LDFLAGS)
# 安装 # 安装
install: $(TARGET) install: $(TARGET)

99
acme.c
View File

@ -22,7 +22,9 @@
#include <openssl/bio.h> #include <openssl/bio.h>
#include <openssl/buffer.h> #include <openssl/buffer.h>
#include <openssl/x509v3.h> #include <openssl/x509v3.h>
#include <openssl/x509.h>
#include <curl/curl.h> #include <curl/curl.h>
#include "platform.h"
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
@ -946,7 +948,102 @@ int acme_get_keyauth(acme_ctx_t *ctx, const char *token, char **out) {
return 0; return 0;
} }
/* ===== 一键签发 ===== */ /* ===== 证书保存与过期检查 ===== */
int acme_save_certificate(const char *cert_pem, const char *key_pem,
const char *cert_path, const char *key_path) {
if (!cert_pem || !key_pem || !cert_path || !key_path) return -1;
/* 确保证书目录存在 */
char cert_dir[512];
snprintf(cert_dir, sizeof(cert_dir), "%s", cert_path);
char *last_slash = strrchr(cert_dir, '/');
if (last_slash) {
*last_slash = '\0';
cocoon_mkdir(cert_dir);
}
/* 确保私钥目录存在 */
char key_dir[512];
snprintf(key_dir, sizeof(key_dir), "%s", key_path);
last_slash = strrchr(key_dir, '/');
if (last_slash) {
*last_slash = '\0';
cocoon_mkdir(key_dir);
}
FILE *fp = fopen(cert_path, "w");
if (!fp) {
log_error("无法打开证书文件写入: %s", cert_path);
return -1;
}
fwrite(cert_pem, 1, strlen(cert_pem), fp);
fclose(fp);
fp = fopen(key_path, "w");
if (!fp) {
log_error("无法打开私钥文件写入: %s", key_path);
return -1;
}
fwrite(key_pem, 1, strlen(key_pem), fp);
fclose(fp);
log_info("证书已保存: %s", cert_path);
log_info("私钥已保存: %s", key_path);
return 0;
}
int acme_cert_days_until_expiry(const char *cert_path) {
if (!cert_path || access(cert_path, R_OK) != 0) {
return -1;
}
FILE *fp = fopen(cert_path, "r");
if (!fp) return -1;
X509 *cert = PEM_read_X509(fp, NULL, NULL, NULL);
fclose(fp);
if (!cert) return -1;
ASN1_TIME *not_after = X509_get_notAfter(cert);
if (!not_after) {
X509_free(cert);
return -1;
}
/* 解析 ASN1_TIME */
struct tm tm = {0};
const char *str = (const char *)not_after->data;
int year = 0, mon = 0, day = 0, hour = 0, min = 0, sec = 0;
if (not_after->length >= 13) {
/* UTCTime: YYMMDDHHMMSSZ (13 chars) or GeneralizedTime: YYYYMMDDHHMMSSZ (15 chars) */
if (not_after->length >= 15 && str[0] == '2' && str[1] == '0') {
/* GeneralizedTime */
sscanf(str, "%4d%2d%2d%2d%2d%2dZ", &year, &mon, &day, &hour, &min, &sec);
} else {
/* UTCTime */
sscanf(str, "%2d%2d%2d%2d%2d%2dZ", &year, &mon, &day, &hour, &min, &sec);
year += (year >= 50) ? 1900 : 2000;
}
tm.tm_year = year - 1900;
tm.tm_mon = mon - 1;
tm.tm_mday = day;
tm.tm_hour = hour;
tm.tm_min = min;
tm.tm_sec = sec;
}
time_t expiry = timegm(&tm);
time_t now = time(NULL);
X509_free(cert);
if (expiry == (time_t)-1) return -1;
double diff = difftime(expiry, now);
return (int)(diff / 86400.0);
}
/* ===== 一键签发(完整流程) ===== */
int acme_issue_certificate(acme_ctx_t *ctx, const char **domains, size_t num_domains, int acme_issue_certificate(acme_ctx_t *ctx, const char **domains, size_t num_domains,
const char *email, char **cert_pem, char **key_pem) { const char *email, char **cert_pem, char **key_pem) {

20
acme.h
View File

@ -230,6 +230,26 @@ int acme_get_keyauth(acme_ctx_t *ctx, const char *token, char **out);
int acme_issue_certificate(acme_ctx_t *ctx, const char **domains, size_t num_domains, int acme_issue_certificate(acme_ctx_t *ctx, const char **domains, size_t num_domains,
const char *email, char **cert_pem, char **key_pem); const char *email, char **cert_pem, char **key_pem);
/**
* acme_save_certificate -
*
* @param cert_pem PEM
* @param key_pem PEM
* @param cert_path
* @param key_path
* @return 0 -1
*/
int acme_save_certificate(const char *cert_pem, const char *key_pem,
const char *cert_path, const char *key_path);
/**
* acme_cert_days_until_expiry -
*
* @param cert_path
* @return <0 0
*/
int acme_cert_days_until_expiry(const char *cert_path);
/* HTTP 请求辅助(内部使用,暴露给单元测试) */ /* HTTP 请求辅助(内部使用,暴露给单元测试) */
typedef struct { typedef struct {
int status; int status;

View File

@ -102,6 +102,15 @@ typedef struct cocoon_config {
size_t cache_max_size; /**< 最大缓存总容量(字节,默认 64MB */ size_t cache_max_size; /**< 最大缓存总容量(字节,默认 64MB */
uint32_t cache_ttl_seconds; /**< 缓存 TTL 秒数(默认 60 */ uint32_t cache_ttl_seconds; /**< 缓存 TTL 秒数(默认 60 */
size_t cache_max_entry_size; /**< 单条缓存最大大小(字节,默认 1MB */ size_t cache_max_entry_size; /**< 单条缓存最大大小(字节,默认 1MB */
/* ACME 自动证书配置 */
bool acme_enabled; /**< 是否启用 ACME 自动证书(默认 false */
char acme_directory_url[512]; /**< ACME 目录 URL默认 Let's Encrypt 生产环境) */
char acme_email[256]; /**< ACME 账户邮箱 */
char acme_domains[8][256]; /**< 需要签发证书的域名列表 */
size_t acme_num_domains; /**< 域名数量 */
char acme_cert_path[512]; /**< 证书保存路径 */
char acme_key_path[512]; /**< 私钥保存路径 */
uint32_t acme_renew_days; /**< 到期前 N 天自动续期(默认 30 */
} cocoon_config_t; } cocoon_config_t;
/* === 服务器生命周期 API === */ /* === 服务器生命周期 API === */

106
config.c
View File

@ -726,6 +726,78 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) {
} else { } else {
fprintf(stderr, "[Config] 第 %d 行: cache 期望对象\n", val.line); 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); free(key_str);
@ -911,6 +983,36 @@ bool config_validate(const cocoon_config_t *config, char *err_buf, size_t err_si
} }
} }
/* 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 #undef SET_ERR
return true; return true;
} }
@ -949,7 +1051,8 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
bool has_rate_limit, bool has_rate_limit,
bool has_plugins, bool has_plugins,
bool has_cache_enabled, bool has_cache_max_size, bool has_cache_enabled, bool has_cache_max_size,
bool has_cache_ttl_seconds, bool has_cache_max_entry_size) { bool has_cache_ttl_seconds, bool has_cache_max_entry_size,
bool has_acme_enabled) {
if (!base || !cmdline) return; if (!base || !cmdline) return;
/* 命令行显式指定的值覆盖配置文件 */ /* 命令行显式指定的值覆盖配置文件 */
@ -998,6 +1101,7 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
if (has_cache_max_size) base->cache_max_size = cmdline->cache_max_size; 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_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_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 参数,命令行指定了就用命令行的 */ /* threaded 是 flag 参数,命令行指定了就用命令行的 */
if (cmdline->threaded) base->threaded = true; if (cmdline->threaded) base->threaded = true;
} }

View File

@ -63,6 +63,7 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
bool has_rate_limit, bool has_rate_limit,
bool has_plugins, bool has_plugins,
bool has_cache_enabled, bool has_cache_max_size, bool has_cache_enabled, bool has_cache_max_size,
bool has_cache_ttl_seconds, bool has_cache_max_entry_size); bool has_cache_ttl_seconds, bool has_cache_max_entry_size,
bool has_acme_enabled);
#endif /* COCOON_CONFIG_H */ #endif /* COCOON_CONFIG_H */

3
main.c
View File

@ -89,6 +89,7 @@ static void print_usage(const char *prog) {
printf(" --auth-pass <pass> Basic Auth 密码\n"); printf(" --auth-pass <pass> Basic Auth 密码\n");
printf(" --rate-limit <n> 每秒最大请求数(限流)\n"); printf(" --rate-limit <n> 每秒最大请求数(限流)\n");
printf(" --plugin <path> 加载插件(可多次指定)\n"); printf(" --plugin <path> 加载插件(可多次指定)\n");
printf(" --acme 启用 ACME 自动证书\n");
printf("\nSignals:\n"); printf("\nSignals:\n");
printf(" SIGHUP 热重载配置文件(无需重启服务器)\n"); printf(" SIGHUP 热重载配置文件(无需重启服务器)\n");
printf(" SIGUSR1 热重载所有插件(无需重启服务器)\n"); printf(" SIGUSR1 热重载所有插件(无需重启服务器)\n");
@ -207,6 +208,8 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
fprintf(stderr, "Error: 最多支持 %d 个插件\n", COCOON_MAX_PLUGINS); fprintf(stderr, "Error: 最多支持 %d 个插件\n", COCOON_MAX_PLUGINS);
return false; return false;
} }
} else if (strcmp(argv[i], "--acme") == 0) {
config->acme_enabled = true;
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
print_usage(argv[0]); print_usage(argv[0]);
exit(0); exit(0);

121
server.c
View File

@ -1468,6 +1468,54 @@ server_context_t *server_create(const cocoon_config_t *config, const char *confi
} }
} }
/* 初始化 ACME 自动证书 */
if (ctx->config.acme_enabled) {
const char *directory_url = ctx->config.acme_directory_url[0] ?
ctx->config.acme_directory_url :
"https://acme-v02.api.letsencrypt.org/directory";
/* 检查现有证书是否有效 */
int days_left = acme_cert_days_until_expiry(ctx->config.acme_cert_path);
if (days_left >= (int)ctx->config.acme_renew_days) {
log_info("ACME: 现有证书仍有效(剩余 %d 天),跳过签发", days_left);
} else {
if (days_left < 0) {
log_info("ACME: 证书不存在或已过期,开始签发新证书");
} else {
log_info("ACME: 证书即将过期(剩余 %d 天,阈值 %u 天),开始续期", days_left, ctx->config.acme_renew_days);
}
ctx->acme = acme_create(directory_url, NULL);
if (ctx->acme) {
char *cert_pem = NULL;
char *key_pem = NULL;
const char *domains[8];
for (size_t i = 0; i < ctx->config.acme_num_domains; i++) {
domains[i] = ctx->config.acme_domains[i];
}
if (acme_issue_certificate(ctx->acme, domains, ctx->config.acme_num_domains,
ctx->config.acme_email, &cert_pem, &key_pem) == 0) {
if (acme_save_certificate(cert_pem, key_pem,
ctx->config.acme_cert_path,
ctx->config.acme_key_path) == 0) {
log_info("ACME: 证书签发并保存成功");
} else {
log_error("ACME: 证书保存失败");
}
free(cert_pem);
free(key_pem);
} else {
log_error("ACME: 证书签发失败");
}
/* ACME 上下文继续保留,用于挑战响应和后续续期 */
} else {
log_error("ACME: 客户端初始化失败");
}
}
}
/* 启动主动健康检查 */ /* 启动主动健康检查 */
healthcheck_manager_init(&ctx->hc_manager); healthcheck_manager_init(&ctx->hc_manager);
healthcheck_start(&ctx->hc_manager, &ctx->proxy_config, ctx->config.timeout_ms); healthcheck_start(&ctx->hc_manager, &ctx->proxy_config, ctx->config.timeout_ms);
@ -1475,6 +1523,70 @@ server_context_t *server_create(const cocoon_config_t *config, const char *confi
return ctx; return ctx;
} }
/**
* acme_renewal_coroutine - ACME
*
* 24
*
* @param arg
*/
static void acme_renewal_coroutine(void *arg) {
server_context_t *ctx = (server_context_t *)arg;
if (!ctx || !ctx->config.acme_enabled) return;
/* 如果 ACME 上下文未创建(证书已存在且有效),创建一个用于续期 */
if (!ctx->acme) {
const char *directory_url = ctx->config.acme_directory_url[0] ?
ctx->config.acme_directory_url :
"https://acme-v02.api.letsencrypt.org/directory";
ctx->acme = acme_create(directory_url, NULL);
if (!ctx->acme) {
log_error("ACME 续期: 客户端初始化失败");
return;
}
}
const uint32_t check_interval_ms = 24 * 60 * 60 * 1000; /* 24 小时 */
while (ctx->running) {
coco_sleep(check_interval_ms);
if (!ctx->running) break;
int days_left = acme_cert_days_until_expiry(ctx->config.acme_cert_path);
if (days_left < 0 || days_left < (int)ctx->config.acme_renew_days) {
if (days_left < 0) {
log_info("ACME 续期: 证书已过期或不存在,开始重新签发");
} else {
log_info("ACME 续期: 证书即将过期(剩余 %d 天),开始续期", days_left);
}
char *cert_pem = NULL;
char *key_pem = NULL;
const char *domains[8];
for (size_t i = 0; i < ctx->config.acme_num_domains; i++) {
domains[i] = ctx->config.acme_domains[i];
}
if (acme_issue_certificate(ctx->acme, domains, ctx->config.acme_num_domains,
ctx->config.acme_email, &cert_pem, &key_pem) == 0) {
if (acme_save_certificate(cert_pem, key_pem,
ctx->config.acme_cert_path,
ctx->config.acme_key_path) == 0) {
log_info("ACME 续期: 证书续期并保存成功");
} else {
log_error("ACME 续期: 证书保存失败");
}
free(cert_pem);
free(key_pem);
} else {
log_error("ACME 续期: 证书签发失败");
}
} else {
log_debug("ACME 续期: 证书仍有效(剩余 %d 天),跳过", days_left);
}
}
}
/** /**
* server_start - * server_start -
* *
@ -1506,6 +1618,11 @@ int server_start(server_context_t *ctx) {
/* 多线程模式下 listen_fd 需要非阻塞(用于 accept + poll 等待) */ /* 多线程模式下 listen_fd 需要非阻塞(用于 accept + poll 等待) */
set_nonblocking(ctx->listen_fd); set_nonblocking(ctx->listen_fd);
/* 启动 ACME 自动续期后台协程 */
if (ctx->config.acme_enabled) {
coco_go(acme_renewal_coroutine, ctx);
}
/* 在主线程中运行 accept_loop */ /* 在主线程中运行 accept_loop */
accept_loop(ctx); accept_loop(ctx);
@ -1514,6 +1631,10 @@ int server_start(server_context_t *ctx) {
coco_global_sched_stop(); coco_global_sched_stop();
} else { } else {
/* 单线程模式listen_fd 保持阻塞 */ /* 单线程模式listen_fd 保持阻塞 */
/* 启动 ACME 自动续期后台协程 */
if (ctx->config.acme_enabled) {
coco_go(acme_renewal_coroutine, ctx);
}
accept_loop(ctx); accept_loop(ctx);
} }

View File

@ -0,0 +1,271 @@
/**
* test_acme_config.c - ACME
*
* ACME
*
* @author xfy
*/
#include "unity.h"
#include "acme.h"
#include "config.h"
#include "cocoon.h"
#include "log.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void setUp(void) {
log_set_level(LOG_LEVEL_ERROR);
}
void tearDown(void) {
}
/**
* test_acme_config_parse - ACME JSON
*/
static void test_acme_config_parse(void) {
cocoon_config_t config = {0};
config.port = 8080;
config.log_level = LOG_LEVEL_INFO;
config.gzip_enabled = true;
config.brotli_enabled = true;
const char *json = "{"
"\"root_dir\":\"/var/www\","
"\"acme\":{"
" \"enabled\":true,"
" \"directory_url\":\"https://acme-staging-v02.api.letsencrypt.org/directory\","
" \"email\":\"admin@example.com\","
" \"domains\":[\"example.com\",\"www.example.com\"],"
" \"cert_path\":\"/etc/cocoon/cert.pem\","
" \"key_path\":\"/etc/cocoon/key.pem\","
" \"renew_days\":14"
"}"
"}";
FILE *fp = fopen("/tmp/test_acme_config.json", "w");
TEST_ASSERT_NOT_NULL(fp);
fwrite(json, 1, strlen(json), fp);
fclose(fp);
bool ok = config_load_from_file("/tmp/test_acme_config.json", &config);
TEST_ASSERT_TRUE(ok);
TEST_ASSERT_TRUE(config.acme_enabled == true);
TEST_ASSERT_EQUAL_STRING("https://acme-staging-v02.api.letsencrypt.org/directory", config.acme_directory_url);
TEST_ASSERT_EQUAL_STRING("admin@example.com", config.acme_email);
TEST_ASSERT_EQUAL_size_t(2, config.acme_num_domains);
TEST_ASSERT_EQUAL_STRING("example.com", config.acme_domains[0]);
TEST_ASSERT_EQUAL_STRING("www.example.com", config.acme_domains[1]);
TEST_ASSERT_EQUAL_STRING("/etc/cocoon/cert.pem", config.acme_cert_path);
TEST_ASSERT_EQUAL_STRING("/etc/cocoon/key.pem", config.acme_key_path);
TEST_ASSERT_EQUAL_UINT(14, config.acme_renew_days);
/* 校验 */
char err_buf[256] = {0};
ok = config_validate(&config, err_buf, sizeof(err_buf));
TEST_ASSERT_TRUE(ok);
free((void*)config.root_dir);
}
/**
* test_acme_config_validation - ACME
*/
static void test_acme_config_validation(void) {
cocoon_config_t config = {0};
config.port = 8080;
config.log_level = LOG_LEVEL_INFO;
config.gzip_enabled = true;
config.brotli_enabled = true;
config.root_dir = strdup("/var/www");
config.acme_enabled = true;
char err_buf[256] = {0};
bool ok;
/* 缺少邮箱 */
ok = config_validate(&config, err_buf, sizeof(err_buf));
TEST_ASSERT_FALSE(ok);
TEST_ASSERT_NOT_NULL(strstr(err_buf, "邮箱"));
/* 缺少域名 */
strcpy(config.acme_email, "admin@example.com");
ok = config_validate(&config, err_buf, sizeof(err_buf));
TEST_ASSERT_FALSE(ok);
TEST_ASSERT_NOT_NULL(strstr(err_buf, "域名"));
/* 缺少证书路径 */
strcpy(config.acme_domains[0], "example.com");
config.acme_num_domains = 1;
ok = config_validate(&config, err_buf, sizeof(err_buf));
TEST_ASSERT_FALSE(ok);
TEST_ASSERT_NOT_NULL(strstr(err_buf, "证书"));
/* 缺少私钥路径 */
strcpy(config.acme_cert_path, "/etc/cocoon/cert.pem");
ok = config_validate(&config, err_buf, sizeof(err_buf));
TEST_ASSERT_FALSE(ok);
TEST_ASSERT_NOT_NULL(strstr(err_buf, "私钥"));
/* renew_days 越界 */
strcpy(config.acme_key_path, "/etc/cocoon/key.pem");
config.acme_renew_days = 0;
ok = config_validate(&config, err_buf, sizeof(err_buf));
TEST_ASSERT_FALSE(ok);
TEST_ASSERT_NOT_NULL(strstr(err_buf, "renew_days"));
config.acme_renew_days = 91;
ok = config_validate(&config, err_buf, sizeof(err_buf));
TEST_ASSERT_FALSE(ok);
/* 完整配置通过校验 */
config.acme_renew_days = 30;
ok = config_validate(&config, err_buf, sizeof(err_buf));
TEST_ASSERT_TRUE(ok);
free((void*)config.root_dir);
}
/**
* test_acme_config_defaults - ACME
*/
static void test_acme_config_defaults(void) {
cocoon_config_t config = {0};
config.port = 8080;
config.log_level = LOG_LEVEL_INFO;
config.gzip_enabled = true;
config.brotli_enabled = true;
config.root_dir = strdup("/var/www");
char err_buf[256] = {0};
bool ok = config_validate(&config, err_buf, sizeof(err_buf));
/* 未启用 ACME不应校验 ACME 字段 */
TEST_ASSERT_TRUE(ok);
free((void*)config.root_dir);
}
/**
* test_acme_cert_expiry -
*/
static void test_acme_cert_expiry(void) {
/* 不存在的证书 */
int days = acme_cert_days_until_expiry("/nonexistent/cert.pem");
TEST_ASSERT_TRUE(days < 0);
/* 生成一个自签名证书用于测试 */
const char *cert_path = "/tmp/test_acme_cert.pem";
const char *key_path = "/tmp/test_acme_key.pem";
FILE *fp = fopen(cert_path, "w");
if (!fp) {
/* 无法创建测试证书,跳过 */
return;
}
fclose(fp);
/* 空文件应该返回 -1 */
days = acme_cert_days_until_expiry(cert_path);
TEST_ASSERT_TRUE(days < 0);
/* 使用 OpenSSL 生成一个有效期 90 天的自签名证书 */
char cmd[512];
snprintf(cmd, sizeof(cmd),
"openssl req -x509 -newkey rsa:2048 -keyout %s -out %s "
"-days 90 -nodes -subj '/CN=test.example.com' 2>/dev/null",
key_path, cert_path);
int ret = system(cmd);
if (ret != 0) {
/* openssl 不可用,跳过 */
remove(cert_path);
return;
}
days = acme_cert_days_until_expiry(cert_path);
/* 应该接近 90 天 */
TEST_ASSERT_TRUE(days >= 85 && days <= 90);
remove(cert_path);
remove(key_path);
}
/**
* test_acme_save_certificate -
*/
static void test_acme_save_certificate(void) {
const char *cert_pem = "-----BEGIN CERTIFICATE-----\n"
"MIIBkTCB+wIJAKHBfpE";
const char *key_pem = "-----BEGIN PRIVATE KEY-----\n"
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg";
const char *cert_path = "/tmp/test_save_cert.pem";
const char *key_path = "/tmp/test_save_key.pem";
int ret = acme_save_certificate(cert_pem, key_pem, cert_path, key_path);
TEST_ASSERT_EQUAL_INT(0, ret);
/* 验证文件存在且内容正确 */
FILE *fp = fopen(cert_path, "r");
TEST_ASSERT_NOT_NULL(fp);
char buf[256];
size_t n = fread(buf, 1, sizeof(buf), fp);
fclose(fp);
TEST_ASSERT_TRUE(n > 0);
TEST_ASSERT_EQUAL_INT(0, strncmp(buf, cert_pem, strlen(cert_pem)));
fp = fopen(key_path, "r");
TEST_ASSERT_NOT_NULL(fp);
n = fread(buf, 1, sizeof(buf), fp);
fclose(fp);
TEST_ASSERT_TRUE(n > 0);
TEST_ASSERT_EQUAL_INT(0, strncmp(buf, key_pem, strlen(key_pem)));
remove(cert_path);
remove(key_path);
}
/**
* test_acme_config_merge - ACME
*/
static void test_acme_config_merge(void) {
cocoon_config_t base = {0};
base.port = 8080;
base.log_level = LOG_LEVEL_INFO;
base.gzip_enabled = true;
base.brotli_enabled = true;
base.root_dir = strdup("/var/www");
cocoon_config_t cmdline = {0};
cmdline.acme_enabled = true;
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,
true); /* has_acme_enabled */
TEST_ASSERT_TRUE(base.acme_enabled == true);
free((void*)base.root_dir);
}
/**
* main -
*/
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_acme_config_parse);
RUN_TEST(test_acme_config_validation);
RUN_TEST(test_acme_config_defaults);
RUN_TEST(test_acme_cert_expiry);
RUN_TEST(test_acme_save_certificate);
RUN_TEST(test_acme_config_merge);
return UNITY_END();
}

View File

@ -306,7 +306,7 @@ void test_merge_override_all(void) {
}; };
config_merge(&base, &cmdline, 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, false, false, false, false, false); false, false, false, false, false, false, false, false, false, false);
TEST_ASSERT_EQUAL_STRING("/new", base.root_dir); TEST_ASSERT_EQUAL_STRING("/new", base.root_dir);
TEST_ASSERT_EQUAL(9090, base.port); TEST_ASSERT_EQUAL(9090, base.port);
TEST_ASSERT_TRUE(base.threaded); TEST_ASSERT_TRUE(base.threaded);
@ -336,7 +336,7 @@ void test_merge_no_override(void) {
}; };
config_merge(&base, &cmdline, 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, 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_STRING("/old", base.root_dir);
TEST_ASSERT_EQUAL(8080, base.port); TEST_ASSERT_EQUAL(8080, base.port);
TEST_ASSERT_FALSE(base.threaded); TEST_ASSERT_FALSE(base.threaded);
@ -360,7 +360,7 @@ void test_merge_partial_override(void) {
}; };
config_merge(&base, &cmdline, 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, false, false, false, false, false); false, false, false, false, false, false, false, false, false, false);
TEST_ASSERT_EQUAL_STRING("/new", base.root_dir); /* overridden */ TEST_ASSERT_EQUAL_STRING("/new", base.root_dir); /* overridden */
TEST_ASSERT_EQUAL(8080, base.port); /* not overridden */ TEST_ASSERT_EQUAL(8080, base.port); /* not overridden */
TEST_ASSERT_EQUAL(2, base.num_workers); /* not overridden */ TEST_ASSERT_EQUAL(2, base.num_workers); /* not overridden */
@ -426,7 +426,7 @@ void test_merge_cmdline_null_root_dir(void) {
cocoon_config_t base = {.root_dir = strdup("/old"), .port = 8080}; cocoon_config_t base = {.root_dir = strdup("/old"), .port = 8080};
cocoon_config_t cmdline = {.root_dir = NULL, .port = 9090}; 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, false, 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_STRING("/old", base.root_dir); /* NULL 不覆盖 */
TEST_ASSERT_EQUAL(9090, base.port); /* port 覆盖 */ TEST_ASSERT_EQUAL(9090, base.port); /* port 覆盖 */
free((void *)base.root_dir); free((void *)base.root_dir);
@ -435,7 +435,7 @@ void test_merge_cmdline_null_root_dir(void) {
void test_merge_null_safety(void) { void test_merge_null_safety(void) {
/* 不应 crash */ /* 不应 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, true, true, true, true, false); true, true, true, true, true, true, true, true, false, false);
TEST_ASSERT_TRUE(1); TEST_ASSERT_TRUE(1);
} }