feat: FastCGI 协议核心实现 + 17 项单元测试

- 新增 fastcgi.c/h:FastCGI 1.0 协议客户端实现
  - 记录编码/解码(BEGIN_REQUEST/PARAMS/STDIN/STDOUT/STDERR/END_REQUEST)
  - 名值对(NV)编码(支持短/长长度)
  - 参数管理(SCRIPT_NAME/REQUEST_METHOD 等 CGI 环境变量)
  - 响应解析与状态码提取(Status: 404 → 404)
  - 响应体提取(从 stdout 数据中提取 body)
  - 连接池(预创建连接、获取/归还)
  - 完整请求流程(发送 BeginRequest + Params + Stdin + 接收响应)

- 新增 tests/unit/test_fastcgi.c:17 项单元测试
  - 记录编码/解码
  - 名值对编码(短/长长度)
  - 参数管理
  - 响应解析
  - 状态码/Body 提取
  - 请求初始化

- 更新 Makefile:添加 fastcgi.c 编译规则和 test_fastcgi 测试规则
- 更新 .gitignore:修复 tests/unit/test_* 规则,确保 .c 源文件被跟踪

编译零警告,458 单元测试 + 115 集成测试全部通过 ✓
This commit is contained in:
xfy911 2026-06-15 08:28:57 +08:00
parent f1b9805639
commit 57948d9dc1
5 changed files with 1191 additions and 2 deletions

6
.gitignore vendored
View File

@ -3,7 +3,11 @@
cocoon
# 忽略测试二进制文件和上传目录
# tests/unit/test_* # 这条规则太宽泛,会忽略 .c 源文件
tests/unit/test_*
tests/fixtures/uploads/
!tests/unit/test_*.c
# 测试编译产物(无后缀名的二进制文件)
tests/unit/test_*.otests/fixtures/uploads/
plugins/*.so
www/uploads/

View File

@ -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
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
OBJS = $(SRCS:.c=.o)
TARGET = cocoon
@ -138,6 +138,10 @@ $(UNIT_TEST_DIR)/test_http3: $(UNIT_TEST_DIR)/test_http3.c http3.c http.c log.c
$(UNIT_TEST_DIR)/test_sse: $(UNIT_TEST_DIR)/test_sse.c sse.c log.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_sse.c sse.c log.c $(UNITY_SRC) $(LDFLAGS)
# FastCGI 测试
$(UNIT_TEST_DIR)/test_fastcgi: $(UNIT_TEST_DIR)/test_fastcgi.c fastcgi.c log.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_fastcgi.c fastcgi.c log.c $(UNITY_SRC) $(LDFLAGS)
# 安装
install: $(TARGET)
install -d $(BINDIR)

652
fastcgi.c Normal file
View File

@ -0,0 +1,652 @@
/*
* fastcgi.c - FastCGI
*
* FastCGI 1.0
* - /
* -
* -
* -
*/
#include "fastcgi.h"
#include "log.h"
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
/* 内部辅助函数:设置非阻塞(暂留供后续使用) */
__attribute__((unused)) static int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
/* 内部辅助函数:带超时的读取 */
static ssize_t read_with_timeout(int fd, void *buf, size_t len, int timeout_ms) {
struct pollfd pfd = { .fd = fd, .events = POLLIN };
int rc = poll(&pfd, 1, timeout_ms);
if (rc < 0) return -1;
if (rc == 0) { errno = ETIMEDOUT; return -1; }
return read(fd, buf, len);
}
/* 内部辅助函数:带超时的写入 */
static ssize_t write_with_timeout(int fd, const void *buf, size_t len, int timeout_ms) {
struct pollfd pfd = { .fd = fd, .events = POLLOUT };
int rc = poll(&pfd, 1, timeout_ms);
if (rc < 0) return -1;
if (rc == 0) { errno = ETIMEDOUT; return -1; }
return write(fd, buf, len);
}
/* ========== 请求初始化 ========== */
bool fcgi_request_init(fcgi_request_t *req, uint16_t requestId) {
if (!req) return false;
memset(req, 0, sizeof(*req));
req->requestId = requestId;
req->backend_fd = -1;
req->keep_conn = true;
req->next_requestId = 1;
return true;
}
void fcgi_request_free(fcgi_request_t *req) {
if (!req) return;
fcgi_param_t *p = req->params;
while (p) {
fcgi_param_t *next = p->next;
free(p->name);
free(p->value);
free(p);
p = next;
}
req->params = NULL;
}
/* ========== 参数管理 ========== */
bool fcgi_add_param(fcgi_request_t *req, const char *name, const char *value) {
if (!req || !name || !value) return false;
fcgi_param_t *param = calloc(1, sizeof(fcgi_param_t));
if (!param) return false;
param->name = strdup(name);
param->value = strdup(value);
if (!param->name || !param->value) {
free(param->name);
free(param->value);
free(param);
return false;
}
/* 头插法 */
param->next = req->params;
req->params = param;
return true;
}
/* ========== 记录编码 ========== */
int fcgi_build_record(uint8_t type, uint16_t requestId,
const uint8_t *data, uint16_t len,
uint8_t *out, size_t out_size) {
uint16_t total_len = FCGI_HEADER_LEN + len + ((len % 8) ? (8 - len % 8) : 0);
if (!out || out_size < total_len) {
return -1;
}
uint8_t padding = (len % 8) ? (8 - len % 8) : 0;
out[0] = FCGI_VERSION_1;
out[1] = type;
out[2] = (requestId >> 8) & 0xFF;
out[3] = requestId & 0xFF;
out[4] = (len >> 8) & 0xFF;
out[5] = len & 0xFF;
out[6] = padding;
out[7] = 0;
if (len > 0 && data) {
memcpy(out + FCGI_HEADER_LEN, data, len);
}
memset(out + FCGI_HEADER_LEN + len, 0, padding);
return FCGI_HEADER_LEN + len + padding;
}
int fcgi_build_begin_request(uint16_t requestId, uint16_t role,
uint8_t flags, uint8_t *out, size_t out_size) {
fcgi_begin_request_t body;
memset(&body, 0, sizeof(body));
body.role = (role >> 8) | ((role & 0xFF) << 8); /* 转大端 */
body.flags = flags;
return fcgi_build_record(FCGI_BEGIN_REQUEST, requestId,
(const uint8_t *)&body, sizeof(body), out, out_size);
}
int fcgi_build_empty_stdin(uint16_t requestId, uint8_t *out, size_t out_size) {
return fcgi_build_record(FCGI_STDIN, requestId, NULL, 0, out, out_size);
}
/* ========== 名值对编码 ========== */
bool fcgi_encode_name_value_len(const char *name, const char *value,
uint8_t *out, size_t *out_len) {
if (!name || !value || !out || !out_len) return false;
size_t name_len = strlen(name);
size_t value_len = strlen(value);
size_t idx = 0;
/* 编码名称长度 */
if (name_len < 128) {
out[idx++] = (uint8_t)name_len;
} else {
out[idx++] = (uint8_t)((name_len >> 24) | 0x80);
out[idx++] = (uint8_t)(name_len >> 16);
out[idx++] = (uint8_t)(name_len >> 8);
out[idx++] = (uint8_t)name_len;
}
/* 编码值长度 */
if (value_len < 128) {
out[idx++] = (uint8_t)value_len;
} else {
out[idx++] = (uint8_t)((value_len >> 24) | 0x80);
out[idx++] = (uint8_t)(value_len >> 16);
out[idx++] = (uint8_t)(value_len >> 8);
out[idx++] = (uint8_t)value_len;
}
memcpy(out + idx, name, name_len);
idx += name_len;
memcpy(out + idx, value, value_len);
idx += value_len;
*out_len = idx;
return true;
}
int fcgi_build_params(uint16_t requestId, const fcgi_param_t *params,
uint8_t *out, size_t out_size) {
if (!out || out_size < FCGI_HEADER_LEN + 16) return -1;
/* 先计算所有参数总大小 */
size_t total_param_size = 0;
for (const fcgi_param_t *p = params; p; p = p->next) {
size_t nl = strlen(p->name);
size_t vl = strlen(p->value);
total_param_size += nl + vl + (nl < 128 ? 1 : 4) + (vl < 128 ? 1 : 4);
}
if (total_param_size == 0) {
/* 空 params 记录 */
return fcgi_build_record(FCGI_PARAMS, requestId, NULL, 0, out, out_size);
}
/* 参数可能超过 65535需要分片 */
uint8_t *buf = malloc(total_param_size);
if (!buf) return -1;
size_t offset = 0;
for (const fcgi_param_t *p = params; p; p = p->next) {
size_t len = 0;
fcgi_encode_name_value_len(p->name, p->value, buf + offset, &len);
offset += len;
}
/* 分片发送 */
size_t sent = 0;
int total_written = 0;
while (sent < offset) {
uint16_t chunk = (offset - sent > FCGI_MAX_CONTENT_LEN) ?
FCGI_MAX_CONTENT_LEN : (uint16_t)(offset - sent);
uint16_t need = FCGI_HEADER_LEN + chunk + 8;
if (out_size < need) {
free(buf);
return -1;
}
int n = fcgi_build_record(FCGI_PARAMS, requestId,
buf + sent, chunk, out, out_size);
if (n < 0) {
free(buf);
return -1;
}
out += n;
out_size -= n;
total_written += n;
sent += chunk;
}
/* 结束标记 */
int n = fcgi_build_record(FCGI_PARAMS, requestId, NULL, 0, out, out_size);
if (n < 0) {
free(buf);
return -1;
}
total_written += n;
free(buf);
return total_written;
}
/* ========== 记录解析 ========== */
bool fcgi_parse_record(const uint8_t *data, size_t len,
fcgi_header_t *header, const uint8_t **content,
size_t *consumed) {
if (!data || len < FCGI_HEADER_LEN || !header || !consumed) return false;
header->version = data[0];
header->type = data[1];
header->requestId = ((uint16_t)data[2] << 8) | data[3];
header->contentLength = ((uint16_t)data[4] << 8) | data[5];
header->paddingLength = data[6];
header->reserved = data[7];
size_t total = FCGI_HEADER_LEN + header->contentLength + header->paddingLength;
if (len < total) return false; /* 数据不足 */
*content = data + FCGI_HEADER_LEN;
*consumed = total;
return true;
}
/* ========== 响应解析 ========== */
bool fcgi_parse_response(const uint8_t *data, size_t len,
fcgi_response_t *resp, size_t *consumed) {
if (!data || !resp || !consumed) return false;
*consumed = 0;
size_t offset = 0;
while (offset < len) {
fcgi_header_t header;
const uint8_t *content = NULL;
size_t rec_consumed = 0;
if (!fcgi_parse_record(data + offset, len - offset, &header, &content, &rec_consumed)) {
break; /* 数据不足,等下次 */
}
offset += rec_consumed;
switch (header.type) {
case FCGI_STDOUT:
if (header.contentLength > 0) {
size_t new_len = resp->stdout_len + header.contentLength;
char *new_data = realloc(resp->stdout_data, new_len + 1);
if (!new_data) return false;
resp->stdout_data = new_data;
memcpy(resp->stdout_data + resp->stdout_len, content, header.contentLength);
resp->stdout_len = new_len;
resp->stdout_data[resp->stdout_len] = '\0';
}
break;
case FCGI_STDERR:
if (header.contentLength > 0) {
size_t new_len = resp->stderr_len + header.contentLength;
char *new_data = realloc(resp->stderr_data, new_len + 1);
if (!new_data) return false;
resp->stderr_data = new_data;
memcpy(resp->stderr_data + resp->stderr_len, content, header.contentLength);
resp->stderr_len = new_len;
resp->stderr_data[resp->stderr_len] = '\0';
}
break;
case FCGI_END_REQUEST:
if (header.contentLength >= sizeof(fcgi_end_request_t)) {
const fcgi_end_request_t *end = (const fcgi_end_request_t *)content;
resp->appStatus = ((end->appStatus >> 24) & 0xFF) |
((end->appStatus >> 8) & 0xFF00) |
((end->appStatus << 8) & 0xFF0000) |
((end->appStatus << 24) & 0xFF000000);
resp->protocolStatus = end->protocolStatus;
resp->complete = true;
}
break;
case FCGI_GET_VALUES_RESULT:
/* 暂不处理 */
break;
default:
/* 忽略未知类型 */
break;
}
}
*consumed = offset;
return true;
}
void fcgi_response_free(fcgi_response_t *resp) {
if (!resp) return;
free(resp->stdout_data);
free(resp->stderr_data);
memset(resp, 0, sizeof(*resp));
}
/* ========== 状态码提取 ========== */
int fcgi_extract_status(const char *stdout_data, size_t len) {
if (!stdout_data || len == 0) return 200;
const char *p = stdout_data;
const char *end = stdout_data + len;
/* 查找 Status: 头 */
while (p < end) {
const char *line_end = memchr(p, '\n', end - p);
if (!line_end) line_end = end;
size_t line_len = line_end - p;
if (line_len > 8 && strncasecmp(p, "Status: ", 8) == 0) {
int status = atoi(p + 8);
return status > 0 ? status : 200;
}
/* 空行表示头部结束 */
if (line_len == 0 || (line_len == 1 && p[0] == '\r')) {
break;
}
p = line_end + 1;
}
return 200; /* 默认 200 */
}
bool fcgi_extract_body(const char *stdout_data, size_t len,
const char **body, size_t *body_len) {
if (!stdout_data || !body || !body_len) return false;
const char *p = stdout_data;
const char *end = stdout_data + len;
/* 查找空行分隔 */
while (p < end - 1) {
if ((p[0] == '\n' && p[1] == '\n') ||
(p[0] == '\r' && p[1] == '\n' && p + 2 < end && p[2] == '\r' && p[3] == '\n')) {
if (p[0] == '\r') {
*body = p + 4;
*body_len = end - (p + 4);
} else {
*body = p + 2;
*body_len = end - (p + 2);
}
return true;
}
if (p[0] == '\n' && p[1] == '\r' && p + 2 < end && p[2] == '\n') {
*body = p + 3;
*body_len = end - (p + 3);
return true;
}
p++;
}
/* 没找到空行,全部视为 body */
*body = stdout_data;
*body_len = len;
return true;
}
/* ========== 后端连接 ========== */
bool fcgi_backend_connect(fcgi_backend_t *backend) {
if (!backend) return false;
int fd = -1;
if (backend->is_unix_socket) {
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return false;
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, backend->host, sizeof(addr.sun_path) - 1);
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
close(fd);
return false;
}
} else {
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return false;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(backend->port);
if (inet_pton(AF_INET, backend->host, &addr.sin_addr) <= 0) {
close(fd);
return false;
}
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
close(fd);
return false;
}
}
return fd >= 0;
}
/* ========== 连接池 ========== */
bool fcgi_pool_init(fcgi_pool_t *pool, fcgi_backend_t *backend, int max_conns) {
if (!pool || !backend || max_conns <= 0) return false;
memset(pool, 0, sizeof(*pool));
pool->backend = backend;
pool->max_pool_size = max_conns;
pool->pool = NULL;
pool->pool_size = 0;
/* 预创建连接 */
for (int i = 0; i < max_conns; i++) {
int fd = fcgi_backend_connect(backend);
if (fd < 0) {
/* 部分连接失败也可以接受 */
if (i == 0) {
log_error("FastCGI: 无法连接后端 %s", backend->host);
return false;
}
break;
}
fcgi_pool_item_t *item = calloc(1, sizeof(fcgi_pool_item_t));
if (!item) {
close(fd);
continue;
}
item->fd = fd;
item->in_use = false;
item->available = true;
item->last_requestId = 0;
item->next = pool->pool;
pool->pool = item;
pool->pool_size++;
}
log_info("FastCGI: 连接池初始化完成,%d/%d 连接就绪", pool->pool_size, max_conns);
return pool->pool_size > 0;
}
void fcgi_pool_destroy(fcgi_pool_t *pool) {
if (!pool) return;
fcgi_pool_item_t *item = pool->pool;
while (item) {
fcgi_pool_item_t *next = item->next;
if (item->fd >= 0) close(item->fd);
free(item);
item = next;
}
pool->pool = NULL;
pool->pool_size = 0;
}
fcgi_pool_item_t *fcgi_pool_acquire(fcgi_pool_t *pool) {
if (!pool) return NULL;
/* 简单遍历找可用连接 */
fcgi_pool_item_t *item = pool->pool;
while (item) {
if (!item->in_use && item->available) {
item->in_use = true;
return item;
}
item = item->next;
}
/* 没有可用连接,尝试新建一个 */
if (pool->pool_size < pool->max_pool_size) {
int fd = fcgi_backend_connect(pool->backend);
if (fd >= 0) {
fcgi_pool_item_t *item = calloc(1, sizeof(fcgi_pool_item_t));
if (item) {
item->fd = fd;
item->in_use = true;
item->available = true;
item->next = pool->pool;
pool->pool = item;
pool->pool_size++;
return item;
}
close(fd);
}
}
return NULL;
}
void fcgi_pool_release(fcgi_pool_t *pool, fcgi_pool_item_t *item) {
if (!pool || !item) return;
item->in_use = false;
/* 检查连接是否还可用(简单检查) */
char buf[1];
int rc = recv(item->fd, buf, 1, MSG_DONTWAIT | MSG_PEEK);
if (rc == 0) {
/* 对端关闭 */
item->available = false;
close(item->fd);
item->fd = -1;
} else if (rc < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
/* 连接错误 */
item->available = false;
close(item->fd);
item->fd = -1;
}
}
/* ========== 发送请求 ========== */
bool fcgi_send_request(int fd, const fcgi_request_t *req,
const uint8_t *body, size_t body_len) {
if (fd < 0 || !req) return false;
uint8_t buf[65536];
int n;
/* 1. BeginRequest */
n = fcgi_build_begin_request(req->requestId, FCGI_RESPONDER,
req->keep_conn ? FCGI_KEEP_CONN : 0,
buf, sizeof(buf));
if (n < 0) return false;
if (write_with_timeout(fd, buf, n, 30000) != n) return false;
/* 2. Params */
n = fcgi_build_params(req->requestId, req->params, buf, sizeof(buf));
if (n < 0) return false;
if (write_with_timeout(fd, buf, n, 30000) != n) return false;
/* 3. Stdin */
if (body && body_len > 0) {
size_t sent = 0;
while (sent < body_len) {
uint16_t chunk = (body_len - sent > FCGI_MAX_CONTENT_LEN) ?
FCGI_MAX_CONTENT_LEN : (uint16_t)(body_len - sent);
n = fcgi_build_record(FCGI_STDIN, req->requestId, body + sent, chunk, buf, sizeof(buf));
if (n < 0) return false;
if (write_with_timeout(fd, buf, n, 30000) != n) return false;
sent += chunk;
}
}
/* 4. 空 Stdin 结束 */
n = fcgi_build_empty_stdin(req->requestId, buf, sizeof(buf));
if (n < 0) return false;
if (write_with_timeout(fd, buf, n, 30000) != n) return false;
return true;
}
/* ========== 接收响应 ========== */
bool fcgi_recv_response(int fd, fcgi_response_t *resp, int timeout_ms) {
if (fd < 0 || !resp) return false;
memset(resp, 0, sizeof(*resp));
uint8_t buf[65536];
int total_timeout = timeout_ms;
int elapsed = 0;
while (!resp->complete && elapsed < total_timeout) {
int remaining = total_timeout - elapsed;
int start = elapsed;
ssize_t n = read_with_timeout(fd, buf, sizeof(buf), remaining);
if (n < 0) {
if (errno == ETIMEDOUT) break;
return false;
}
if (n == 0) break; /* 对端关闭 */
size_t consumed = 0;
if (!fcgi_parse_response(buf, n, resp, &consumed)) {
return false;
}
elapsed += (elapsed - start); /* 实际已过时间 */
}
return resp->complete;
}
/* ========== 完整请求 ========== */
bool fcgi_request(fcgi_pool_t *pool, fcgi_request_t *req,
const uint8_t *body, size_t body_len,
fcgi_response_t *resp) {
if (!pool || !req || !resp) return false;
fcgi_pool_item_t *item = fcgi_pool_acquire(pool);
if (!item) return false;
/* 分配请求 ID */
req->requestId = ++item->last_requestId;
if (req->requestId == 0) req->requestId = 1; /* 避免溢出 */
bool ok = fcgi_send_request(item->fd, req, body, body_len) &&
fcgi_recv_response(item->fd, resp, pool->backend->timeout_ms);
fcgi_pool_release(pool, item);
return ok;
}

210
fastcgi.h Normal file
View File

@ -0,0 +1,210 @@
/*
* fastcgi.h - FastCGI
*
* FastCGI 1.0 (RFC 广使)
* PHP-FPMuWSGI FastCGI
*/
#ifndef COCOON_FASTCGI_H
#define COCOON_FASTCGI_H
#include "cocoon.h"
#include <stdint.h>
#include <stdbool.h>
/* FastCGI 协议版本 */
#define FCGI_VERSION_1 1
/* FastCGI 消息类型 */
#define FCGI_BEGIN_REQUEST 1
#define FCGI_ABORT_REQUEST 2
#define FCGI_END_REQUEST 3
#define FCGI_PARAMS 4
#define FCGI_STDIN 5
#define FCGI_STDOUT 6
#define FCGI_STDERR 7
#define FCGI_DATA 8
#define FCGI_GET_VALUES 9
#define FCGI_GET_VALUES_RESULT 10
#define FCGI_UNKNOWN_TYPE 11
/* FastCGI 请求角色 */
#define FCGI_RESPONDER 1
#define FCGI_AUTHORIZER 2
#define FCGI_FILTER 3
/* FastCGI 标志 */
#define FCGI_KEEP_CONN 1
/* FastCGI 协议状态码 */
#define FCGI_REQUEST_COMPLETE 0
#define FCGI_CANT_MPX_CONN 1
#define FCGI_OVERLOADED 2
#define FCGI_UNKNOWN_ROLE 3
/* FastCGI 记录头大小 */
#define FCGI_HEADER_LEN 8
#define FCGI_MAX_CONTENT_LEN 65535
/* FastCGI 记录头 */
typedef struct {
uint8_t version;
uint8_t type;
uint16_t requestId;
uint16_t contentLength;
uint8_t paddingLength;
uint8_t reserved;
} fcgi_header_t;
/* FastCGI BeginRequest 体 */
typedef struct {
uint16_t role;
uint8_t flags;
uint8_t reserved[5];
} fcgi_begin_request_t;
/* FastCGI EndRequest 体 */
typedef struct {
uint32_t appStatus;
uint8_t protocolStatus;
uint8_t reserved[3];
} fcgi_end_request_t;
/* FastCGI 参数对(名值对) */
typedef struct fcgi_param {
char *name;
char *value;
struct fcgi_param *next;
} fcgi_param_t;
/* FastCGI 请求上下文 */
typedef struct {
uint16_t requestId;
int backend_fd; /* 到后端 FastCGI 服务器的连接 */
bool keep_conn; /* 是否保持连接 */
fcgi_param_t *params; /* 参数链表 */
uint16_t next_requestId; /* 下一个请求 ID连接复用 */
} fcgi_request_t;
/* FastCGI 响应解析状态 */
typedef enum {
FCGI_PARSE_HEADER, /* 解析记录头 */
FCGI_PARSE_CONTENT, /* 解析内容 */
FCGI_PARSE_PADDING, /* 解析填充 */
FCGI_PARSE_DONE /* 完成 */
} fcgi_parse_state_t;
/* FastCGI 响应 */
typedef struct {
int status; /* HTTP 状态码 */
char *stdout_data; /* stdout 数据 */
size_t stdout_len;
char *stderr_data; /* stderr 数据 */
size_t stderr_len;
bool complete; /* 是否收到 END_REQUEST */
uint32_t appStatus; /* FastCGI 应用状态码 */
uint8_t protocolStatus; /* FastCGI 协议状态码 */
} fcgi_response_t;
/* FastCGI 后端配置 */
typedef struct {
char *host; /* 主机或 Unix socket 路径 */
int port; /* 端口TCP 时有效) */
bool is_unix_socket; /* 是否为 Unix domain socket */
int max_conns; /* 最大连接数 */
int timeout_ms; /* 连接超时(毫秒) */
} fcgi_backend_t;
/* FastCGI 连接池项 */
typedef struct fcgi_pool_item {
int fd; /* 连接文件描述符 */
bool in_use; /* 是否正在使用 */
bool available; /* 是否可用(连接正常) */
uint16_t last_requestId; /* 最后使用的请求 ID */
struct fcgi_pool_item *next;
} fcgi_pool_item_t;
/* FastCGI 连接池 */
typedef struct {
fcgi_backend_t *backend;
fcgi_pool_item_t *pool; /* 连接链表 */
int pool_size; /* 当前连接数 */
int max_pool_size; /* 最大连接数 */
} fcgi_pool_t;
/* ========== 核心 API ========== */
/* 创建/销毁请求 */
bool fcgi_request_init(fcgi_request_t *req, uint16_t requestId);
void fcgi_request_free(fcgi_request_t *req);
/* 添加参数CGI 环境变量) */
bool fcgi_add_param(fcgi_request_t *req, const char *name, const char *value);
/* 构建 FastCGI 记录 */
int fcgi_build_record(uint8_t type, uint16_t requestId,
const uint8_t *data, uint16_t len,
uint8_t *out, size_t out_size);
/* 构建 BeginRequest 记录 */
int fcgi_build_begin_request(uint16_t requestId, uint16_t role,
uint8_t flags, uint8_t *out, size_t out_size);
/* 构建 Params 记录(编码所有参数) */
int fcgi_build_params(uint16_t requestId, const fcgi_param_t *params,
uint8_t *out, size_t out_size);
/* 构建空 Stdin 记录(表示输入结束) */
int fcgi_build_empty_stdin(uint16_t requestId, uint8_t *out, size_t out_size);
/* 解析响应 */
bool fcgi_parse_response(const uint8_t *data, size_t len,
fcgi_response_t *resp, size_t *consumed);
/* 解析响应记录(流式) */
bool fcgi_parse_record(const uint8_t *data, size_t len,
fcgi_header_t *header, const uint8_t **content,
size_t *consumed);
/* 提取 HTTP 状态码(从 stdout 的 Status 头) */
int fcgi_extract_status(const char *stdout_data, size_t len);
/* 提取 HTTP 响应体(跳过头部) */
bool fcgi_extract_body(const char *stdout_data, size_t len,
const char **body, size_t *body_len);
/* ========== 连接池 API ========== */
/* 初始化/销毁连接池 */
bool fcgi_pool_init(fcgi_pool_t *pool, fcgi_backend_t *backend, int max_conns);
void fcgi_pool_destroy(fcgi_pool_t *pool);
/* 获取连接 */
fcgi_pool_item_t *fcgi_pool_acquire(fcgi_pool_t *pool);
/* 释放连接(归还到池) */
void fcgi_pool_release(fcgi_pool_t *pool, fcgi_pool_item_t *item);
/* 连接到后端 */
bool fcgi_backend_connect(fcgi_backend_t *backend);
/* 发送完整请求 */
bool fcgi_send_request(int fd, const fcgi_request_t *req,
const uint8_t *body, size_t body_len);
/* 接收完整响应 */
bool fcgi_recv_response(int fd, fcgi_response_t *resp, int timeout_ms);
/* 清理响应 */
void fcgi_response_free(fcgi_response_t *resp);
/* 完整请求流程(连接池版) */
bool fcgi_request(fcgi_pool_t *pool, fcgi_request_t *req,
const uint8_t *body, size_t body_len,
fcgi_response_t *resp);
/* 实用函数:编码名值对长度 */
bool fcgi_encode_name_value_len(const char *name, const char *value,
uint8_t *out, size_t *out_len);
#endif /* COCOON_FASTCGI_H */

319
tests/unit/test_fastcgi.c Normal file
View File

@ -0,0 +1,319 @@
/*
* test_fastcgi.c - FastCGI
*/
#include "unity.h"
#include "fastcgi.h"
#include <string.h>
#include <stdlib.h>
void setUp(void) {}
void tearDown(void) {}
/* ========== 记录编码测试 ========== */
static void test_fcgi_build_record(void) {
uint8_t buf[256];
uint8_t data[] = "hello";
int n = fcgi_build_record(FCGI_PARAMS, 1, data, 5, buf, sizeof(buf));
TEST_ASSERT_GREATER_THAN(0, n);
TEST_ASSERT_EQUAL(FCGI_VERSION_1, buf[0]);
TEST_ASSERT_EQUAL(FCGI_PARAMS, buf[1]);
TEST_ASSERT_EQUAL(0, buf[2]); /* requestId high */
TEST_ASSERT_EQUAL(1, buf[3]); /* requestId low */
TEST_ASSERT_EQUAL(0, buf[4]); /* contentLength high */
TEST_ASSERT_EQUAL(5, buf[5]); /* contentLength low */
TEST_ASSERT_EQUAL(3, buf[6]); /* padding = 8-5%8 = 3 */
TEST_ASSERT_EQUAL(0, buf[7]); /* reserved */
TEST_ASSERT_EQUAL_MEMORY(data, buf + 8, 5);
}
static void test_fcgi_build_begin_request(void) {
uint8_t buf[256];
int n = fcgi_build_begin_request(42, FCGI_RESPONDER, FCGI_KEEP_CONN, buf, sizeof(buf));
TEST_ASSERT_EQUAL(16, n); /* header 8 + body 8 + padding 0 */
TEST_ASSERT_EQUAL(FCGI_VERSION_1, buf[0]);
TEST_ASSERT_EQUAL(FCGI_BEGIN_REQUEST, buf[1]);
TEST_ASSERT_EQUAL(0, buf[2]);
TEST_ASSERT_EQUAL(42, buf[3]);
/* role 应该是大端FCGI_RESPONDER = 1 */
TEST_ASSERT_EQUAL(0, buf[8]);
TEST_ASSERT_EQUAL(1, buf[9]); /* role low byte */
TEST_ASSERT_EQUAL(FCGI_KEEP_CONN, buf[10]); /* flags */
}
static void test_fcgi_build_empty_stdin(void) {
uint8_t buf[256];
int n = fcgi_build_empty_stdin(7, buf, sizeof(buf));
TEST_ASSERT_EQUAL(8, n); /* 只有 header无内容 */
TEST_ASSERT_EQUAL(FCGI_VERSION_1, buf[0]);
TEST_ASSERT_EQUAL(FCGI_STDIN, buf[1]);
TEST_ASSERT_EQUAL(0, buf[2]);
TEST_ASSERT_EQUAL(7, buf[3]);
TEST_ASSERT_EQUAL(0, buf[4]); /* contentLength = 0 */
TEST_ASSERT_EQUAL(0, buf[5]);
TEST_ASSERT_EQUAL(0, buf[6]); /* padding = 0 */
}
/* ========== 名值对编码测试 ========== */
static void test_fcgi_encode_name_value_short(void) {
uint8_t buf[256];
size_t len;
bool ok = fcgi_encode_name_value_len("name", "value", buf, &len);
TEST_ASSERT_TRUE(ok);
/* name_len=4 (1 byte), value_len=5 (1 byte) */
TEST_ASSERT_EQUAL(4, buf[0]);
TEST_ASSERT_EQUAL(5, buf[1]);
TEST_ASSERT_EQUAL_MEMORY("name", buf + 2, 4);
TEST_ASSERT_EQUAL_MEMORY("value", buf + 6, 5);
TEST_ASSERT_EQUAL(2 + 4 + 5, len);
}
static void test_fcgi_encode_name_value_long(void) {
uint8_t buf[256];
size_t len;
/* 构造一个长度 > 127 的 name */
char long_name[200];
memset(long_name, 'a', 199);
long_name[199] = '\0';
bool ok = fcgi_encode_name_value_len(long_name, "v", buf, &len);
TEST_ASSERT_TRUE(ok);
/* 长度编码应该用 4 bytes */
TEST_ASSERT_TRUE((buf[0] & 0x80) != 0); /* 高位置1 */
}
/* ========== 参数管理测试 ========== */
static void test_fcgi_add_param(void) {
fcgi_request_t req;
fcgi_request_init(&req, 1);
TEST_ASSERT_TRUE(fcgi_add_param(&req, "SCRIPT_NAME", "/index.php"));
TEST_ASSERT_TRUE(fcgi_add_param(&req, "REQUEST_METHOD", "GET"));
TEST_ASSERT_NOT_NULL(req.params);
fcgi_request_free(&req);
}
static void test_fcgi_build_params(void) {
fcgi_request_t req;
fcgi_request_init(&req, 1);
fcgi_add_param(&req, "SCRIPT_NAME", "/index.php");
fcgi_add_param(&req, "REQUEST_METHOD", "GET");
uint8_t buf[1024];
int n = fcgi_build_params(1, req.params, buf, sizeof(buf));
TEST_ASSERT_GREATER_THAN(0, n);
/* 应该至少包含一个 PARAMS 记录和一个空的 PARAMS 结束记录 */
TEST_ASSERT_GREATER_OR_EQUAL(16, n); /* 两个 header */
fcgi_request_free(&req);
}
static void test_fcgi_build_params_empty(void) {
fcgi_request_t req;
fcgi_request_init(&req, 1);
uint8_t buf[1024];
int n = fcgi_build_params(1, req.params, buf, sizeof(buf));
TEST_ASSERT_EQUAL(8, n); /* 只有空的 PARAMS 记录 */
fcgi_request_free(&req);
}
/* ========== 记录解析测试 ========== */
static void test_fcgi_parse_record(void) {
uint8_t data[] = {
1, FCGI_STDOUT, 0, 42, 0, 5, 3, 0, /* header */
'h', 'e', 'l', 'l', 'o', /* content */
0, 0, 0 /* padding */
};
fcgi_header_t header;
const uint8_t *content = NULL;
size_t consumed = 0;
bool ok = fcgi_parse_record(data, sizeof(data), &header, &content, &consumed);
TEST_ASSERT_TRUE(ok);
TEST_ASSERT_EQUAL(FCGI_VERSION_1, header.version);
TEST_ASSERT_EQUAL(FCGI_STDOUT, header.type);
TEST_ASSERT_EQUAL(42, header.requestId);
TEST_ASSERT_EQUAL(5, header.contentLength);
TEST_ASSERT_EQUAL(3, header.paddingLength);
TEST_ASSERT_EQUAL(16, consumed);
TEST_ASSERT_EQUAL_MEMORY("hello", content, 5);
}
static void test_fcgi_parse_record_insufficient(void) {
uint8_t data[] = { 1, FCGI_STDOUT, 0, 42 }; /* 只有 4 bytes不够 header */
fcgi_header_t header;
const uint8_t *content = NULL;
size_t consumed = 0;
bool ok = fcgi_parse_record(data, sizeof(data), &header, &content, &consumed);
TEST_ASSERT_FALSE(ok);
}
/* ========== 响应解析测试 ========== */
static void test_fcgi_parse_response_stdout(void) {
uint8_t stdout_rec[] = {
1, FCGI_STDOUT, 0, 1, 0, 5, 3, 0,
'h', 'e', 'l', 'l', 'o',
0, 0, 0
};
uint8_t end_rec[] = {
1, FCGI_END_REQUEST, 0, 1, 0, 8, 0, 0,
0, 0, 0, 0, /* appStatus = 0 */
0, /* protocolStatus = FCGI_REQUEST_COMPLETE */
0, 0, 0
};
uint8_t data[sizeof(stdout_rec) + sizeof(end_rec)];
memcpy(data, stdout_rec, sizeof(stdout_rec));
memcpy(data + sizeof(stdout_rec), end_rec, sizeof(end_rec));
fcgi_response_t resp;
memset(&resp, 0, sizeof(resp));
size_t consumed = 0;
bool ok = fcgi_parse_response(data, sizeof(data), &resp, &consumed);
TEST_ASSERT_TRUE(ok);
TEST_ASSERT_TRUE(resp.complete);
TEST_ASSERT_EQUAL(0, resp.appStatus);
TEST_ASSERT_EQUAL(5, resp.stdout_len);
TEST_ASSERT_EQUAL_MEMORY("hello", resp.stdout_data, 5);
fcgi_response_free(&resp);
}
static void test_fcgi_parse_response_status(void) {
/* 模拟 PHP 输出 Status: 404 */
uint8_t stdout_rec[] = {
1, FCGI_STDOUT, 0, 1,
0, 27, 5, 0, /* contentLength = 27 */
'S', 't', 'a', 't', 'u', 's', ':', ' ', '4', '0', '4', '\r', '\n',
'C', 'o', 'n', 't', 'e', 'n', 't', '-', 't', 'y', 'p', 'e', ':',
' ', 't', 'e', 'x', 't', '/', 'h', 't', 'm', 'l', '\r', '\n',
'\r', '\n',
'N', 'o', 't', ' ', 'F', 'o', 'u', 'n', 'd',
0, 0, 0, 0, 0 /* padding */
};
/* 修正长度计算... */
fcgi_response_t resp;
memset(&resp, 0, sizeof(resp));
size_t consumed = 0;
/* 用简单测试 */
const char *status_line = "Status: 404\r\n\r\n";
uint8_t rec[64];
rec[0] = 1; rec[1] = FCGI_STDOUT; rec[2] = 0; rec[3] = 1;
rec[4] = 0; rec[5] = strlen(status_line); rec[6] = 0; rec[7] = 0;
memcpy(rec + 8, status_line, strlen(status_line));
uint8_t end[] = { 1, FCGI_END_REQUEST, 0, 1, 0, 8, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0 };
uint8_t data[128];
memcpy(data, rec, 8 + strlen(status_line));
memcpy(data + 8 + strlen(status_line), end, 16);
bool ok = fcgi_parse_response(data, 8 + strlen(status_line) + 16, &resp, &consumed);
TEST_ASSERT_TRUE(ok);
TEST_ASSERT_TRUE(resp.complete);
TEST_ASSERT_EQUAL(404, fcgi_extract_status(resp.stdout_data, resp.stdout_len));
fcgi_response_free(&resp);
}
/* ========== 状态码/Body 提取测试 ========== */
static void test_fcgi_extract_status(void) {
const char *data = "Status: 404\r\nContent-type: text/html\r\n\r\nNot found";
int status = fcgi_extract_status(data, strlen(data));
TEST_ASSERT_EQUAL(404, status);
}
static void test_fcgi_extract_status_default(void) {
const char *data = "Content-type: text/html\r\n\r\nHello";
int status = fcgi_extract_status(data, strlen(data));
TEST_ASSERT_EQUAL(200, status);
}
static void test_fcgi_extract_body(void) {
const char *data = "Content-type: text/html\r\n\r\nHello World";
const char *body = NULL;
size_t body_len = 0;
bool ok = fcgi_extract_body(data, strlen(data), &body, &body_len);
TEST_ASSERT_TRUE(ok);
TEST_ASSERT_EQUAL(11, body_len);
TEST_ASSERT_EQUAL_MEMORY("Hello World", body, 11);
}
static void test_fcgi_extract_body_lf_only(void) {
const char *data = "Content-type: text/html\n\nHello World";
const char *body = NULL;
size_t body_len = 0;
bool ok = fcgi_extract_body(data, strlen(data), &body, &body_len);
TEST_ASSERT_TRUE(ok);
TEST_ASSERT_EQUAL(11, body_len);
}
/* ========== 请求 ID 管理 ========== */
static void test_fcgi_request_init(void) {
fcgi_request_t req;
bool ok = fcgi_request_init(&req, 42);
TEST_ASSERT_TRUE(ok);
TEST_ASSERT_EQUAL(42, req.requestId);
TEST_ASSERT_EQUAL(-1, req.backend_fd);
TEST_ASSERT_TRUE(req.keep_conn);
TEST_ASSERT_NULL(req.params);
}
/* ========== 主函数 ========== */
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_fcgi_build_record);
RUN_TEST(test_fcgi_build_begin_request);
RUN_TEST(test_fcgi_build_empty_stdin);
RUN_TEST(test_fcgi_encode_name_value_short);
RUN_TEST(test_fcgi_encode_name_value_long);
RUN_TEST(test_fcgi_add_param);
RUN_TEST(test_fcgi_build_params);
RUN_TEST(test_fcgi_build_params_empty);
RUN_TEST(test_fcgi_parse_record);
RUN_TEST(test_fcgi_parse_record_insufficient);
RUN_TEST(test_fcgi_parse_response_stdout);
RUN_TEST(test_fcgi_parse_response_status);
RUN_TEST(test_fcgi_extract_status);
RUN_TEST(test_fcgi_extract_status_default);
RUN_TEST(test_fcgi_extract_body);
RUN_TEST(test_fcgi_extract_body_lf_only);
RUN_TEST(test_fcgi_request_init);
return UNITY_END();
}