diff --git a/middleware.c b/middleware.c index 952afc9..a876d2b 100644 --- a/middleware.c +++ b/middleware.c @@ -242,6 +242,21 @@ void cocoon_middleware_cleanup(void) { pthread_mutex_unlock(&g_rate_limit_mutex); } +int cocoon_middleware_list(char names[][32], int count) { + if (!names || count <= 0) return 0; + int n = 0; + for (int i = 0; i < g_count && n < count; i++) { + if (g_registry[i].active) { + size_t len = strlen(g_registry[i].name); + if (len > 31) len = 31; + memcpy(names[n], g_registry[i].name, len); + names[n][len] = '\0'; + n++; + } + } + return n; +} + /* === 内置中间件 === */ int cocoon_middleware_cors(http_request_t *req, cocoon_socket_t fd, void *user_data) { diff --git a/middleware.h b/middleware.h index 08f98a3..69e89f7 100644 --- a/middleware.h +++ b/middleware.h @@ -130,6 +130,17 @@ int cocoon_middleware_basic_auth(http_request_t *req, cocoon_socket_t fd, void * */ int cocoon_middleware_rate_limit(http_request_t *req, cocoon_socket_t fd, void *user_data); +/** + * cocoon_middleware_list - 获取已注册中间件名称列表 + * + * 将名称写入提供的缓冲区数组,每个名称最多 32 字节。 + * + * @param names 名称缓冲区数组(二维字符数组) + * @param count 数组容量(最大能存放多少个名称) + * @return 实际写入的中间件数量 + */ +int cocoon_middleware_list(char names[][32], int count); + /** * cocoon_middleware_init_builtin - 根据配置初始化内置中间件 * diff --git a/server.c b/server.c index fc58b30..22bf390 100644 --- a/server.c +++ b/server.c @@ -28,6 +28,7 @@ #include "websocket.h" #include "platform.h" #include "middleware.h" +#include #include #include #include @@ -72,6 +73,10 @@ struct server_context { /* 全局活跃连接计数器(线程安全) */ static atomic_int g_active_connections = 0; +/* 服务器启动时间 */ +static time_t g_server_start_time = 0; +/* 最大连接数(供健康检查端点使用) */ +static uint32_t g_max_connections = 0; /** * set_nonblocking - 设置 socket 为非阻塞模式 @@ -408,6 +413,83 @@ static bool handle_request(connection_t *conn, const char *root_dir) { return req.keep_alive; } + /* 健康检查端点 */ + if (strcmp(req.path, "/_health") == 0) { + time_t now = time(NULL); + time_t uptime = now - g_server_start_time; + int active = atomic_load(&g_active_connections); + + char mw_names[16][32]; + int mw_count = cocoon_middleware_list(mw_names, 16); + + /* 构建 JSON */ + char body[2048]; + int n = snprintf(body, sizeof(body), + "{\n" + " \"status\": \"ok\",\n" + " \"version\": \"Cocoon/1.0\",\n" + " \"uptime_seconds\": %ld,\n" + " \"connections\": {\n" + " \"active\": %d,\n" + " \"max\": %u\n" + " },\n" + " \"plugins\": {\n" + " \"count\": %zu,\n" + " \"list\": [\n", + uptime, active, + g_max_connections, + cocoon_plugin_count()); + + for (size_t i = 0; i < cocoon_plugin_count(); i++) { + const char *path = cocoon_plugin_get_path(i); + const char *ver = cocoon_plugin_get_version(i); + n += snprintf(body + n, sizeof(body) - n, + " {\"path\":\"%s\",\"version\":\"%s\"}%s\n", + path ? path : "", + ver ? ver : "unknown", + (i + 1 < cocoon_plugin_count()) ? "," : ""); + } + + n += snprintf(body + n, sizeof(body) - n, + " ]\n" + " },\n" + " \"middleware\": {\n" + " \"count\": %d,\n" + " \"names\": [\n", + mw_count); + + for (int i = 0; i < mw_count; i++) { + n += snprintf(body + n, sizeof(body) - n, + " \"%s\"%s\n", + mw_names[i], + (i + 1 < mw_count) ? "," : ""); + } + + n += snprintf(body + n, sizeof(body) - n, + " ]\n" + " }\n" + "}"); + + char header[512]; + int header_len = snprintf(header, sizeof(header), + "HTTP/1.1 200 OK\r\n" + "Content-Type: application/json\r\n" + "Content-Length: %d\r\n" + "Connection: %s\r\n" + "Server: Cocoon/1.0\r\n" + "\r\n", + n, req.keep_alive ? "keep-alive" : "close"); + + send_all(conn->fd, header, (size_t)header_len); + send_all(conn->fd, body, (size_t)n); + + conn->response_status = 200; + access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len, + &req, conn->response_status, n); + http_request_free(&req); + return req.keep_alive; + } + /* 安全路径拼接 */ char real_path[4096]; char root_normalized[4096]; @@ -1001,6 +1083,8 @@ server_context_t *server_create(const cocoon_config_t *config) { ctx->config.root_dir = strdup(config->root_dir); ctx->running = 1; + g_max_connections = config->max_connections; + /* 创建监听 socket */ ctx->listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (ctx->listen_fd == COCOON_INVALID_SOCKET) { @@ -1070,6 +1154,8 @@ server_context_t *server_create(const cocoon_config_t *config) { int server_start(server_context_t *ctx) { if (!ctx) return COCOON_ERROR; + g_server_start_time = time(NULL); + if (ctx->config.threaded) { /* 多线程协程模式 */ uint32_t num_workers = ctx->config.num_workers; diff --git a/tests/integration_test.sh b/tests/integration_test.sh index ff5c65b..7a9bafc 100755 --- a/tests/integration_test.sh +++ b/tests/integration_test.sh @@ -409,7 +409,48 @@ assert_access_log() { start_server echo "" -echo "=== 基础功能测试 ===" +echo "=== 健康检查端点测试 ===" +health_body=$(curl -s "$BASE/_health") +health_status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/_health") +if [[ "$health_status" == "200" ]]; then + echo " ✓ /_health 状态码 — HTTP 200" + pass +else + echo " ✗ /_health 状态码 — 期望 200, 实际 $health_status" + fail +fi +if echo "$health_body" | grep -q '"status": "ok"'; then + echo " ✓ /_health 响应体 — 包含 status: ok" + pass +else + echo " ✗ /_health 响应体 — 缺少 status: ok" + fail +fi +if echo "$health_body" | grep -q '"version": "Cocoon/1.0"'; then + echo " ✓ /_health 响应体 — 包含版本信息" + pass +else + echo " ✗ /_health 响应体 — 缺少版本信息" + fail +fi +if echo "$health_body" | grep -q '"connections"'; then + echo " ✓ /_health 响应体 — 包含连接数信息" + pass +else + echo " ✗ /_health 响应体 — 缺少连接数信息" + fail +fi +if echo "$health_body" | grep -q '"middleware"'; then + echo " ✓ /_health 响应体 — 包含中间件信息" + pass +else + echo " ✗ /_health 响应体 — 缺少中间件信息" + fail +fi + +assert_head_status "$BASE/_health" "200" "健康检查 HEAD" + +assert_status "$BASE/_health/nonexistent" "404" "健康检查路径遍历" assert_status "$BASE/" "200" "首页 GET" assert_status "$BASE/index.html" "200" "index.html GET" assert_status "$BASE/nonexist.html" "404" "404 页面"