diff --git a/.gitignore b/.gitignore index 9b85720..7d39d76 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,10 @@ tests/unit/test_* !tests/unit/test_*.c # 测试编译产物(无后缀名的二进制文件) -tests/unit/test_*.otests/fixtures/uploads/ +tests/unit/test_* +!tests/unit/test_*.c + +# 上传目录 +tests/fixtures/uploads/ plugins/*.so www/uploads/ diff --git a/Makefile b/Makefile index a484c91..d315a5b 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ PREFIX ?= /usr/local BINDIR = $(PREFIX)/bin # 源文件 -SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c healthcheck.c middleware_ext.c load_balance.c grpc.c http3.c sse.c fastcgi.c +SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c healthcheck.c middleware_ext.c load_balance.c grpc.c http3.c sse.c fastcgi.c fcgi_handler.c OBJS = $(SRCS:.c=.o) TARGET = cocoon @@ -90,8 +90,8 @@ unit-test: $(UNIT_TEST_BINS) fi # 单元测试编译规则 -$(UNIT_TEST_DIR)/test_server: $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c healthcheck.c sse.c $(UNITY_SRC) - $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c healthcheck.c sse.c $(UNITY_SRC) $(LDFLAGS) +$(UNIT_TEST_DIR)/test_server: $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c healthcheck.c sse.c fastcgi.c fcgi_handler.c $(UNITY_SRC) + $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c healthcheck.c sse.c fastcgi.c fcgi_handler.c $(UNITY_SRC) $(LDFLAGS) $(UNIT_TEST_DIR)/test_multipart: $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) -lm diff --git a/cocoon.h b/cocoon.h index ecc4205..668f42a 100644 --- a/cocoon.h +++ b/cocoon.h @@ -86,6 +86,17 @@ typedef struct cocoon_config { /* 虚拟主机 */ cocoon_vhost_t vhosts[COCOON_MAX_VHOSTS]; size_t num_vhosts; + /* FastCGI 配置 */ +#define COCOON_MAX_FASTCGI_RULES 4 + struct { + char prefix[256]; /**< 路径前缀匹配 */ + char host[256]; /**< 后端主机或 Unix socket 路径 */ + int port; /**< 端口(TCP 时有效) */ + bool is_unix_socket; /**< 是否为 Unix domain socket */ + int pool_size; /**< 连接池大小 */ + int timeout_ms; /**< 请求超时 */ + } fastcgi[COCOON_MAX_FASTCGI_RULES]; + size_t num_fastcgi; } cocoon_config_t; /* === 服务器生命周期 API === */ diff --git a/config.c b/config.c index 7a001e6..c5a82fd 100644 --- a/config.c +++ b/config.c @@ -566,6 +566,75 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) { } 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); + } } /* 其他字段:忽略(未来扩展预留) */ free(key_str); diff --git a/fcgi_handler.c b/fcgi_handler.c new file mode 100644 index 0000000..7cba61b --- /dev/null +++ b/fcgi_handler.c @@ -0,0 +1,280 @@ +/** + * fcgi_handler.c - FastCGI 服务器集成实现 + * + * 将 HTTP 请求转换为 FastCGI 请求,通过连接池转发到后端。 + * + * @author xfy + */ + +#include "fcgi_handler.h" +#include "static.h" +#include "log.h" +#include "platform.h" +#include +#include +#include + +/** + * build_cgi_params - 从 HTTP 请求构建 CGI 环境变量 + * + * 构建标准 CGI 参数(SCRIPT_NAME, PATH_INFO, REQUEST_METHOD 等)。 + */ +static bool build_cgi_params(fcgi_request_t *fcgi_req, const http_request_t *req, + const char *script_name, const char *path_info) { + char buf[256]; + + /* 从 path 中分离 query string */ + const char *query = ""; + char path_copy[HTTP_MAX_PATH]; + strncpy(path_copy, req->path, sizeof(path_copy) - 1); + path_copy[sizeof(path_copy) - 1] = '\0'; + char *q = strchr(path_copy, '?'); + if (q) { + *q = '\0'; + query = q + 1; + } + + /* 基本 CGI 变量 */ + if (!fcgi_add_param(fcgi_req, "REQUEST_METHOD", http_method_str(req->method))) return false; + if (!fcgi_add_param(fcgi_req, "SCRIPT_NAME", script_name)) return false; + if (!fcgi_add_param(fcgi_req, "PATH_INFO", path_info ? path_info : "")) return false; + if (!fcgi_add_param(fcgi_req, "QUERY_STRING", query)) return false; + + /* 协议信息 */ + if (!fcgi_add_param(fcgi_req, "SERVER_PROTOCOL", req->version[0] ? req->version : "HTTP/1.1")) return false; + + /* 内容长度 */ + if (req->content_length > 0) { + snprintf(buf, sizeof(buf), "%ld", (long)req->content_length); + if (!fcgi_add_param(fcgi_req, "CONTENT_LENGTH", buf)) return false; + } + if (req->content_type[0]) { + if (!fcgi_add_param(fcgi_req, "CONTENT_TYPE", req->content_type)) return false; + } + + /* 客户端信息 */ + if (!fcgi_add_param(fcgi_req, "REMOTE_ADDR", "")) return false; + + /* HTTP 头转 CGI 变量 */ + for (int i = 0; i < req->num_headers; i++) { + const char *name = req->headers[i].name; + const char *value = req->headers[i].value; + if (!name || !value) continue; + + /* 跳过 Host 头(已处理) */ + if (strcasecmp(name, "Host") == 0) { + if (!fcgi_add_param(fcgi_req, "HTTP_HOST", value)) return false; + continue; + } + /* 其他头转 HTTP_ 前缀 */ + char cgi_name[256]; + int n = snprintf(cgi_name, sizeof(cgi_name), "HTTP_%s", name); + if (n < 0 || (size_t)n >= sizeof(cgi_name)) continue; + + /* 将 - 替换为 _ */ + for (int j = 0; cgi_name[j]; j++) { + if (cgi_name[j] == '-') cgi_name[j] = '_'; + if (cgi_name[j] >= 'a' && cgi_name[j] <= 'z') cgi_name[j] -= 32; + } + + if (!fcgi_add_param(fcgi_req, cgi_name, value)) return false; + } + + return true; +} + +/** + * send_http_response - 将 FastCGI 响应回写给客户端 + * + * 解析 FastCGI 响应,提取 HTTP 状态码和响应体, + * 构造标准 HTTP 响应发给客户端。 + */ +static bool send_http_response(cocoon_socket_t client_fd, fcgi_response_t *resp, + bool keep_alive) { + if (!resp || !resp->complete || !resp->stdout_data) { + static_send_error(client_fd, 502, keep_alive); + return keep_alive; + } + + /* 提取 HTTP 状态码 */ + int status = fcgi_extract_status(resp->stdout_data, resp->stdout_len); + if (status <= 0) status = 200; + + /* 提取响应体 */ + const char *body = NULL; + size_t body_len = 0; + if (!fcgi_extract_body(resp->stdout_data, resp->stdout_len, &body, &body_len)) { + body = resp->stdout_data; + body_len = resp->stdout_len; + } + + /* 检查 FastCGI 响应是否已经包含完整的 HTTP 头 */ + bool has_http_headers = false; + if (resp->stdout_len > 8) { + /* 检查是否以 HTTP/ 开头 */ + if (strncmp(resp->stdout_data, "HTTP/", 5) == 0) { + has_http_headers = true; + } + } + + if (has_http_headers) { + /* 直接转发完整的 HTTP 响应(包含状态行和头) */ + send_all(client_fd, resp->stdout_data, resp->stdout_len); + return keep_alive; + } + + /* 检查 stdout 是否包含原始头(以 Status: 或 Content-Type: 开头) */ + const char *raw_headers = resp->stdout_data; + size_t headers_len = body - resp->stdout_data; + + if (headers_len > 0 && strncasecmp(raw_headers, "Content-Type:", 13) == 0) { + /* 原始头以 Content-Type 开头,构造状态行 */ + char status_line[256]; + int n = snprintf(status_line, sizeof(status_line), + "HTTP/1.1 %d OK\r\n", status); + send_all(client_fd, status_line, (size_t)n); + send_all(client_fd, raw_headers, headers_len); + send_all(client_fd, "\r\n", 2); + if (body_len > 0) { + send_all(client_fd, body, body_len); + } + } else if (headers_len > 0 && strncasecmp(raw_headers, "Status:", 7) == 0) { + /* 有 Status: 头,跳过它,用提取出的状态码 */ + char status_line[256]; + int n = snprintf(status_line, sizeof(status_line), + "HTTP/1.1 %d OK\r\n", status); + send_all(client_fd, status_line, (size_t)n); + + /* 跳过 Status 行,发送其余头部 */ + const char *p = raw_headers; + const char *end = raw_headers + headers_len; + while (p < end) { + const char *line_end = memchr(p, '\n', end - p); + if (!line_end) line_end = end; + if (strncasecmp(p, "Status:", 7) != 0) { + send_all(client_fd, p, (size_t)(line_end - p + 1)); + } + p = line_end + 1; + } + send_all(client_fd, "\r\n", 2); + if (body_len > 0) { + send_all(client_fd, body, body_len); + } + } else { + /* 无额外头,构造标准响应 */ + char header[512]; + int n = snprintf(header, sizeof(header), + "HTTP/1.1 %d OK\r\n" + "Content-Type: text/html\r\n" + "Content-Length: %zu\r\n" + "Connection: %s\r\n" + "Server: Cocoon/1.0\r\n" + "\r\n", + status, body_len, + keep_alive ? "keep-alive" : "close"); + send_all(client_fd, header, (size_t)n); + if (body_len > 0) { + send_all(client_fd, body, body_len); + } + } + + return keep_alive; +} + +/* ========== 公共 API ========== */ + +bool fcgi_handler_init(cocoon_fcgi_config_t *cfg, const cocoon_config_t *config) { + if (!cfg || !config) return false; + memset(cfg, 0, sizeof(*cfg)); + + for (size_t i = 0; i < config->num_fastcgi; i++) { + if (cfg->count >= COCOON_MAX_FASTCGI_RULES) break; + + cocoon_fcgi_rule_t *rule = &cfg->rules[cfg->count]; + strncpy(rule->prefix, config->fastcgi[i].prefix, sizeof(rule->prefix) - 1); + rule->prefix[sizeof(rule->prefix) - 1] = '\0'; + + /* 初始化后端配置 */ + fcgi_backend_t *be = &rule->backend; + memset(be, 0, sizeof(*be)); + strncpy(be->host, config->fastcgi[i].host, sizeof(be->host) - 1); + be->host[sizeof(be->host) - 1] = '\0'; + be->port = config->fastcgi[i].port; + be->is_unix_socket = config->fastcgi[i].is_unix_socket; + be->max_conns = config->fastcgi[i].pool_size > 0 ? config->fastcgi[i].pool_size : 4; + be->timeout_ms = config->fastcgi[i].timeout_ms > 0 ? config->fastcgi[i].timeout_ms : 30000; + + /* 初始化连接池 */ + if (!fcgi_pool_init(&rule->pool, be, be->max_conns)) { + log_warn("FastCGI: 连接池初始化失败 %s", be->host); + continue; + } + + log_info("FastCGI: 规则 #%zu %s -> %s:%d (pool=%d)", + cfg->count, rule->prefix, be->host, be->port, be->max_conns); + cfg->count++; + } + + return true; +} + +void fcgi_handler_destroy(cocoon_fcgi_config_t *cfg) { + if (!cfg) return; + for (size_t i = 0; i < cfg->count; i++) { + fcgi_pool_destroy(&cfg->rules[i].pool); + } + cfg->count = 0; +} + +cocoon_fcgi_rule_t *fcgi_handler_match(cocoon_fcgi_config_t *cfg, const char *path) { + if (!cfg || !path) return NULL; + for (size_t i = 0; i < cfg->count; i++) { + if (strncmp(path, cfg->rules[i].prefix, strlen(cfg->rules[i].prefix)) == 0) { + return &cfg->rules[i]; + } + } + return NULL; +} + +bool fcgi_handler_forward(cocoon_socket_t client_fd, const http_request_t *req, + cocoon_fcgi_rule_t *rule) { + if (!req || !rule) return false; + + /* 构建 FastCGI 请求 */ + fcgi_request_t fcgi_req; + if (!fcgi_request_init(&fcgi_req, 1)) { + static_send_error(client_fd, 502, req->keep_alive); + return req->keep_alive; + } + + /* 构建 CGI 参数 */ + const char *path_info = req->path + strlen(rule->prefix); + if (!path_info || path_info[0] == '\0') path_info = ""; + if (!build_cgi_params(&fcgi_req, req, rule->prefix, path_info)) { + fcgi_request_free(&fcgi_req); + static_send_error(client_fd, 502, req->keep_alive); + return req->keep_alive; + } + + /* 发送请求并接收响应 */ + fcgi_response_t resp; + memset(&resp, 0, sizeof(resp)); + + bool ok = fcgi_request(&rule->pool, &fcgi_req, + (const uint8_t *)req->body, req->body_len, + &resp); + + fcgi_request_free(&fcgi_req); + + if (!ok || !resp.complete) { + fcgi_response_free(&resp); + static_send_error(client_fd, 502, req->keep_alive); + return req->keep_alive; + } + + /* 发送响应给客户端 */ + bool keep = send_http_response(client_fd, &resp, req->keep_alive); + fcgi_response_free(&resp); + + return keep; +} diff --git a/fcgi_handler.h b/fcgi_handler.h new file mode 100644 index 0000000..c299952 --- /dev/null +++ b/fcgi_handler.h @@ -0,0 +1,81 @@ +/** + * fcgi_handler.h - FastCGI 服务器集成模块 + * + * 将 FastCGI 协议接入 Cocoon 请求处理流程, + * 支持路径前缀匹配到 FastCGI 后端(PHP-FPM 等)。 + * + * @author xfy + */ + +#ifndef COCOON_FCGI_HANDLER_H +#define COCOON_FCGI_HANDLER_H + +#include "cocoon.h" +#include "platform.h" +#include "http.h" +#include "fastcgi.h" +#include + +/** + * cocoon_fcgi_rule_t - FastCGI 路由规则 + * + * 路径前缀匹配 + 后端连接池。 + */ +typedef struct { + char prefix[256]; /**< 路径前缀(如 "/api.php") */ + fcgi_pool_t pool; /**< 后端连接池 */ + fcgi_backend_t backend; /**< 后端配置(内嵌在规则中) */ +} cocoon_fcgi_rule_t; + +/** + * cocoon_fcgi_config_t - FastCGI 配置集合 + */ +typedef struct { + cocoon_fcgi_rule_t rules[COCOON_MAX_FASTCGI_RULES]; + size_t count; +} cocoon_fcgi_config_t; + +/** + * fcgi_handler_init - 初始化 FastCGI 处理器 + * + * 根据服务器配置创建连接池。 + * + * @param cfg 输出配置 + * @param config 服务器配置(含 fastcgi 数组) + * @return true 成功 + */ +bool fcgi_handler_init(cocoon_fcgi_config_t *cfg, const cocoon_config_t *config); + +/** + * fcgi_handler_destroy - 销毁 FastCGI 处理器 + * + * 关闭所有连接池。 + * + * @param cfg FastCGI 配置 + */ +void fcgi_handler_destroy(cocoon_fcgi_config_t *cfg); + +/** + * fcgi_handler_match - 匹配路径到 FastCGI 规则 + * + * @param cfg FastCGI 配置 + * @param path 请求路径 + * @return 匹配的规则,未匹配返回 NULL + */ +cocoon_fcgi_rule_t *fcgi_handler_match(cocoon_fcgi_config_t *cfg, const char *path); + +/** + * fcgi_handler_forward - 将 HTTP 请求转发到 FastCGI 后端 + * + * 构建 CGI 环境变量,通过 FastCGI 连接池发送请求, + * 将后端响应(含 HTTP 头)回写给客户端。 + * + * @param client_fd 客户端 socket + * @param req HTTP 请求 + * @param rule FastCGI 规则 + * @return true 保持连接,false 关闭 + */ +bool fcgi_handler_forward(cocoon_socket_t client_fd, const http_request_t *req, + cocoon_fcgi_rule_t *rule); + +#endif /* COCOON_FCGI_HANDLER_H */ diff --git a/server.c b/server.c index 66ef4e4..d7743db 100644 --- a/server.c +++ b/server.c @@ -32,6 +32,7 @@ #include "healthcheck.h" #include "sse.h" #include "config.h" +#include "fcgi_handler.h" #include #include #include @@ -77,10 +78,10 @@ struct server_context { volatile int running; /**< 运行标志 */ coco_sched_t *sched; /**< 协程调度器 */ const char *config_file_path; /**< 配置文件路径(用于热重载) */ + /* FastCGI 配置 */ + cocoon_fcgi_config_t fcgi_config; volatile int reload_requested; /**< 配置热重载请求标志 */ }; - -/* 全局活跃连接计数器(线程安全) */ static atomic_int g_active_connections = 0; /* 服务器启动时间 */ static time_t g_server_start_time = 0; @@ -514,6 +515,20 @@ static bool handle_request(connection_t *conn, const char *root_dir) { } } + /* FastCGI 检查 */ + if (conn->ctx && conn->ctx->fcgi_config.count > 0) { + cocoon_fcgi_rule_t *fcgi_rule = fcgi_handler_match(&conn->ctx->fcgi_config, req.path); + if (fcgi_rule) { + bool keep = fcgi_handler_forward(conn->fd, &req, fcgi_rule); + conn->response_status = 200; /* FastCGI 响应状态由后端决定 */ + update_metrics(conn->response_status); + access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len, + &req, conn->response_status, -1); + http_request_free(&req); + return keep; + } + } + /* 只支持 GET 和 HEAD */ if (req.method != HTTP_GET && req.method != HTTP_HEAD) { conn->response_status = 405; @@ -1373,6 +1388,11 @@ server_context_t *server_create(const cocoon_config_t *config, const char *confi } } + /* 初始化 FastCGI 处理器 */ + if (!fcgi_handler_init(&ctx->fcgi_config, &ctx->config)) { + log_warn("FastCGI 初始化失败,FastCGI 路由不可用"); + } + /* 启动主动健康检查 */ healthcheck_manager_init(&ctx->hc_manager); healthcheck_start(&ctx->hc_manager, &ctx->proxy_config, ctx->config.timeout_ms); @@ -1589,6 +1609,9 @@ void server_destroy(server_context_t *ctx) { /* 关闭反向代理连接池 */ proxy_config_destroy(&ctx->proxy_config); + /* 关闭 FastCGI 连接池 */ + fcgi_handler_destroy(&ctx->fcgi_config); + /* 卸载插件 */ cocoon_plugin_unload_all();