feat(access_log): 实现 Nginx combined 格式访问日志
- 新增 access_log.c / access_log.h:Nginx combined 格式,线程安全 - cocoon.h / config.c / config.h:支持 access_log 配置字段 - main.c:新增 --access-log CLI 选项,'-' 表示 stdout - server.c:connection_t 扩展 client_addr/addr_len/response_status - server.c:handle_request 中记录 response_status 并调用 access_log_write - Makefile:加入 access_log.c,更新单元测试编译规则 - cocoon.json:示例配置添加 access_log - 集成测试:新增 5 项访问日志测试 - 59 项集成测试全部通过,35 个单元测试全部通过
This commit is contained in:
parent
3374130c8c
commit
3ba9fcea62
@ -20,9 +20,10 @@
|
|||||||
- [x] 连接超时管理(空闲连接自动清理)✅ 2026-06-03
|
- [x] 连接超时管理(空闲连接自动清理)✅ 2026-06-03
|
||||||
- [x] 最大并发连接数限制 ✅ 2026-06-03
|
- [x] 最大并发连接数限制 ✅ 2026-06-03
|
||||||
- [x] 分级日志系统(error / warn / info / debug)✅ 2026-06-03
|
- [x] 分级日志系统(error / warn / info / debug)✅ 2026-06-03
|
||||||
|
- [x] 访问日志(Nginx combined 格式,含 User-Agent / Referer)✅ 2026-06-05
|
||||||
- [x] Gzip 压缩 ✅ 2026-06-03(已接入响应流程)
|
- [x] Gzip 压缩 ✅ 2026-06-03(已接入响应流程)
|
||||||
- [x] Brotli 压缩 ✅ 2026-06-04(优先于 Gzip)
|
- [x] Brotli 压缩 ✅ 2026-06-04(优先于 Gzip)
|
||||||
- [x] 集成测试 suite(45 项 curl/bash 测试全部通过)✅ 2026-06-04
|
- [x] 集成测试 suite(59 项 curl/bash 测试全部通过)✅ 2026-06-05
|
||||||
- [x] 性能基准(wrk: ~16.2K RPS / ~60μs 延迟)✅ 2026-06-03
|
- [x] 性能基准(wrk: ~16.2K RPS / ~60μs 延迟)✅ 2026-06-03
|
||||||
- [x] 请求体解析(POST 支持)✅ 2026-06-03
|
- [x] 请求体解析(POST 支持)✅ 2026-06-03
|
||||||
- Content-Length 读取
|
- Content-Length 读取
|
||||||
@ -47,11 +48,12 @@
|
|||||||
## 当前状态
|
## 当前状态
|
||||||
|
|
||||||
- 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示)
|
- 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示)
|
||||||
- **54 项集成测试全部通过**(新增 2 项 HTTP/2 目录浏览测试)
|
- **59 项集成测试全部通过**(新增 5 项访问日志测试)
|
||||||
- **35 个单元测试全部通过**(Unity 框架)
|
- **35 个单元测试全部通过**(Unity 框架)
|
||||||
- 压测数据:wrk -t4 -c100 -d10s → 16,179 RPS,平均延迟 59.86μs
|
- 压测数据:wrk -t4 -c100 -d10s → 16,179 RPS,平均延迟 59.86μs
|
||||||
- POST 支持:JSON 和 form-urlencoded 回显,multipart 文件上传,Content-Length 解析,8MB 上限
|
- POST 支持:JSON 和 form-urlencoded 回显,multipart 文件上传,Content-Length 解析,8MB 上限
|
||||||
- **配置文件支持**:JSON 格式,8 个字段,命令行参数可覆盖
|
- **配置文件支持**:JSON 格式,9 个字段(含 access_log),命令行参数可覆盖
|
||||||
|
- **访问日志**:Nginx combined 格式,支持文件路径或 stdout(`-`),线程安全(pthread_mutex),记录 User-Agent / Referer / 状态码
|
||||||
- **TLS/HTTPS**:OpenSSL 3.0 Memory BIO + coco 协程集成,自签名证书支持,ALPN 协商 h2/http1.1
|
- **TLS/HTTPS**:OpenSSL 3.0 Memory BIO + coco 协程集成,自签名证书支持,ALPN 协商 h2/http1.1
|
||||||
- **HTTP/2**:完整功能(静态文件服务、目录浏览、缓存协商、压缩、HEAD 请求)
|
- **HTTP/2**:完整功能(静态文件服务、目录浏览、缓存协商、压缩、HEAD 请求)
|
||||||
- **h2c**:明文 HTTP/2 支持(prior knowledge + Upgrade 协商)
|
- **h2c**:明文 HTTP/2 支持(prior knowledge + Upgrade 协商)
|
||||||
@ -64,6 +66,20 @@
|
|||||||
|
|
||||||
## 最近行动记录
|
## 最近行动记录
|
||||||
|
|
||||||
|
- **2026-06-05: 本轮行动 — 访问日志(Nginx combined 格式)**
|
||||||
|
- 新增 `access_log.c` / `access_log.h`:Nginx combined 格式访问日志,线程安全(pthread_mutex)
|
||||||
|
- `cocoon.h`:新增 `access_log_path` 配置字段
|
||||||
|
- `config.c` / `config.h`:JSON 配置和命令行参数支持 `access_log`
|
||||||
|
- `main.c`:新增 `--access-log <path>` CLI 选项,`-` 表示输出到 stdout
|
||||||
|
- `server.c`:扩展 `connection_t` 结构体,添加 `client_addr` / `addr_len` / `response_status`
|
||||||
|
- `server.c`:`handle_request()` 中每个响应路径设置 `response_status`,请求结束时调用 `access_log_write()`
|
||||||
|
- `server.c`:`accept_loop()` 将客户端地址复制到连接上下文
|
||||||
|
- `Makefile`:加入 `access_log.c`,更新单元测试编译规则
|
||||||
|
- `cocoon.json`:示例配置添加 `"access_log": "-"`
|
||||||
|
- `tests/integration_test.sh`:新增 5 项访问日志测试(日志生成、GET 记录、User-Agent、Referer、404 状态码)
|
||||||
|
- 编译通过,零警告
|
||||||
|
- **59 项集成测试全部通过,35 个单元测试全部通过**
|
||||||
|
- 推送到 main
|
||||||
- **2026-06-04: 本轮行动 — http2.c 完整中文 Doxygen 注释**
|
- **2026-06-04: 本轮行动 — http2.c 完整中文 Doxygen 注释**
|
||||||
- 为 http2.c 全部 29 个函数添加中文文档注释
|
- 为 http2.c 全部 29 个函数添加中文文档注释
|
||||||
- 会话管理模块:init, cleanup, create, destroy, is_http2, get, set_context, upgrade
|
- 会话管理模块:init, cleanup, create, destroy, is_http2, get, set_context, upgrade
|
||||||
|
|||||||
14
Makefile
14
Makefile
@ -20,7 +20,7 @@ PREFIX ?= /usr/local
|
|||||||
BINDIR = $(PREFIX)/bin
|
BINDIR = $(PREFIX)/bin
|
||||||
|
|
||||||
# 源文件
|
# 源文件
|
||||||
SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c
|
SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c
|
||||||
OBJS = $(SRCS:.c=.o)
|
OBJS = $(SRCS:.c=.o)
|
||||||
TARGET = cocoon
|
TARGET = cocoon
|
||||||
|
|
||||||
@ -78,8 +78,8 @@ unit-test: $(UNIT_TEST_BINS)
|
|||||||
fi
|
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 $(UNITY_SRC)
|
$(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 $(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 $(UNITY_SRC) $(LDFLAGS)
|
$(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 $(UNITY_SRC) $(LDFLAGS)
|
||||||
|
|
||||||
$(UNIT_TEST_DIR)/test_multipart: $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC)
|
$(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
|
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) -lm
|
||||||
@ -87,14 +87,14 @@ $(UNIT_TEST_DIR)/test_multipart: $(UNIT_TEST_DIR)/test_multipart.c multipart.c $
|
|||||||
$(UNIT_TEST_DIR)/test_http: $(UNIT_TEST_DIR)/test_http.c http.c log.c $(UNITY_SRC)
|
$(UNIT_TEST_DIR)/test_http: $(UNIT_TEST_DIR)/test_http.c http.c log.c $(UNITY_SRC)
|
||||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_http.c http.c log.c $(UNITY_SRC) -lm
|
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_http.c http.c log.c $(UNITY_SRC) -lm
|
||||||
|
|
||||||
$(UNIT_TEST_DIR)/test_static: $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c $(UNITY_SRC)
|
$(UNIT_TEST_DIR)/test_static: $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c access_log.c $(UNITY_SRC)
|
||||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c $(UNITY_SRC) $(LDFLAGS)
|
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c access_log.c $(UNITY_SRC) $(LDFLAGS)
|
||||||
|
|
||||||
$(UNIT_TEST_DIR)/test_log: $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC)
|
$(UNIT_TEST_DIR)/test_log: $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC)
|
||||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC) -lm
|
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC) -lm
|
||||||
|
|
||||||
$(UNIT_TEST_DIR)/test_config: $(UNIT_TEST_DIR)/test_config.c config.c log.c $(UNITY_SRC)
|
$(UNIT_TEST_DIR)/test_config: $(UNIT_TEST_DIR)/test_config.c config.c log.c access_log.c http.c $(UNITY_SRC)
|
||||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_config.c config.c log.c $(UNITY_SRC) -lm
|
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_config.c config.c log.c access_log.c http.c $(UNITY_SRC) -lm
|
||||||
|
|
||||||
# 编译规则
|
# 编译规则
|
||||||
%.o: %.c
|
%.o: %.c
|
||||||
|
|||||||
193
access_log.c
Normal file
193
access_log.c
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
/**
|
||||||
|
* access_log.c - 访问日志模块实现
|
||||||
|
*
|
||||||
|
* 使用简化 Nginx combined 格式:
|
||||||
|
* %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"
|
||||||
|
*
|
||||||
|
* 线程安全:使用 pthread_mutex_t 保护文件写入。
|
||||||
|
*
|
||||||
|
* @author xfy
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "access_log.h"
|
||||||
|
#include "http.h"
|
||||||
|
#include "log.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
|
||||||
|
/* === 内部状态 === */
|
||||||
|
static FILE *g_log_file = NULL;
|
||||||
|
static pthread_mutex_t g_log_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
static bool g_enabled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* format_log_time - 格式化当前时间为日志时间格式
|
||||||
|
*
|
||||||
|
* Nginx 格式: [05/Jun/2026:00:00:00 +0800]
|
||||||
|
*
|
||||||
|
* @param buf 输出缓冲区
|
||||||
|
* @param buf_size 缓冲区大小
|
||||||
|
*/
|
||||||
|
static void format_log_time(char *buf, size_t buf_size) {
|
||||||
|
time_t now = time(NULL);
|
||||||
|
struct tm *tm = localtime(&now);
|
||||||
|
if (!tm) {
|
||||||
|
buf[0] = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 计算时区偏移 */
|
||||||
|
long tz_offset = 0;
|
||||||
|
#if defined(__linux__) || defined(__unix__)
|
||||||
|
tz_offset = tm->tm_gmtoff;
|
||||||
|
#else
|
||||||
|
/* 通用回退 */
|
||||||
|
struct tm gmt = {0};
|
||||||
|
gmtime_r(&now, &gmt);
|
||||||
|
time_t local = mktime(tm);
|
||||||
|
time_t gmt_t = mktime(&gmt);
|
||||||
|
tz_offset = (long)difftime(local, gmt_t);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int tz_hours = (int)(tz_offset / 3600);
|
||||||
|
int tz_mins = (int)((tz_offset % 3600) / 60);
|
||||||
|
if (tz_mins < 0) tz_mins = -tz_mins;
|
||||||
|
|
||||||
|
const char *months[] = {
|
||||||
|
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||||
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
|
||||||
|
};
|
||||||
|
|
||||||
|
snprintf(buf, buf_size, "[%02d/%s/%04d:%02d:%02d:%02d %c%02d%02d]",
|
||||||
|
tm->tm_mday, months[tm->tm_mon], tm->tm_year + 1900,
|
||||||
|
tm->tm_hour, tm->tm_min, tm->tm_sec,
|
||||||
|
tz_offset >= 0 ? '+' : '-', tz_hours, tz_mins);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get_client_ip - 从 sockaddr 提取客户端 IP 字符串
|
||||||
|
*
|
||||||
|
* @param addr sockaddr 结构体
|
||||||
|
* @param addr_len 地址长度
|
||||||
|
* @param buf 输出缓冲区
|
||||||
|
* @param buf_size 缓冲区大小
|
||||||
|
*/
|
||||||
|
static void get_client_ip(const struct sockaddr *addr, socklen_t addr_len,
|
||||||
|
char *buf, size_t buf_size) {
|
||||||
|
(void)addr_len;
|
||||||
|
buf[0] = '\0';
|
||||||
|
|
||||||
|
if (addr->sa_family == AF_INET) {
|
||||||
|
const struct sockaddr_in *sin = (const struct sockaddr_in *)addr;
|
||||||
|
inet_ntop(AF_INET, &sin->sin_addr, buf, (socklen_t)buf_size);
|
||||||
|
} else if (addr->sa_family == AF_INET6) {
|
||||||
|
const struct sockaddr_in6 *sin6 = (const struct sockaddr_in6 *)addr;
|
||||||
|
inet_ntop(AF_INET6, &sin6->sin6_addr, buf, (socklen_t)buf_size);
|
||||||
|
} else {
|
||||||
|
snprintf(buf, buf_size, "unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get_header - 从请求头数组中查找指定头
|
||||||
|
*
|
||||||
|
* @param req HTTP 请求
|
||||||
|
* @param name 头名称(小写)
|
||||||
|
* @return 头值,未找到返回 "-"
|
||||||
|
*/
|
||||||
|
static const char *get_header(const http_request_t *req, const char *name) {
|
||||||
|
for (int i = 0; i < req->num_headers; i++) {
|
||||||
|
if (strcmp(req->headers[i].name, name) == 0) {
|
||||||
|
return req->headers[i].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 公共 API === */
|
||||||
|
|
||||||
|
int access_log_init(const char *path) {
|
||||||
|
pthread_mutex_lock(&g_log_mutex);
|
||||||
|
|
||||||
|
if (g_log_file && g_log_file != stdout) {
|
||||||
|
fclose(g_log_file);
|
||||||
|
}
|
||||||
|
g_log_file = NULL;
|
||||||
|
g_enabled = false;
|
||||||
|
|
||||||
|
if (!path || strcmp(path, "-") == 0 || strcmp(path, "stdout") == 0) {
|
||||||
|
g_log_file = stdout;
|
||||||
|
g_enabled = true;
|
||||||
|
log_info("访问日志输出到 stdout");
|
||||||
|
} else {
|
||||||
|
g_log_file = fopen(path, "a");
|
||||||
|
if (!g_log_file) {
|
||||||
|
log_error("无法打开访问日志文件: %s", path);
|
||||||
|
pthread_mutex_unlock(&g_log_mutex);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
g_enabled = true;
|
||||||
|
log_info("访问日志: %s", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&g_log_mutex);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void access_log_close(void) {
|
||||||
|
pthread_mutex_lock(&g_log_mutex);
|
||||||
|
if (g_log_file && g_log_file != stdout) {
|
||||||
|
fclose(g_log_file);
|
||||||
|
}
|
||||||
|
g_log_file = NULL;
|
||||||
|
g_enabled = false;
|
||||||
|
pthread_mutex_unlock(&g_log_mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool access_log_is_enabled(void) {
|
||||||
|
return g_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void access_log_write(const struct sockaddr *client_addr, socklen_t addr_len,
|
||||||
|
const http_request_t *req, int status_code, int64_t response_bytes) {
|
||||||
|
if (!g_enabled || !req) return;
|
||||||
|
|
||||||
|
char ip[64];
|
||||||
|
get_client_ip(client_addr, addr_len, ip, sizeof(ip));
|
||||||
|
|
||||||
|
char time_str[64];
|
||||||
|
format_log_time(time_str, sizeof(time_str));
|
||||||
|
|
||||||
|
const char *method = http_method_str(req->method);
|
||||||
|
const char *path = req->path;
|
||||||
|
const char *version = req->version[0] ? req->version : "HTTP/1.1";
|
||||||
|
|
||||||
|
const char *referer = get_header(req, "referer");
|
||||||
|
const char *user_agent = get_header(req, "user-agent");
|
||||||
|
|
||||||
|
/* 响应字节 */
|
||||||
|
char bytes_str[32];
|
||||||
|
if (response_bytes < 0) {
|
||||||
|
snprintf(bytes_str, sizeof(bytes_str), "-");
|
||||||
|
} else {
|
||||||
|
snprintf(bytes_str, sizeof(bytes_str), "%ld", (long)response_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_log_mutex);
|
||||||
|
if (g_log_file) {
|
||||||
|
fprintf(g_log_file, "%s - - %s \"%s %s %s\" %d %s \"%s\" \"%s\"\n",
|
||||||
|
ip[0] ? ip : "-",
|
||||||
|
time_str,
|
||||||
|
method, path, version,
|
||||||
|
status_code,
|
||||||
|
bytes_str,
|
||||||
|
referer,
|
||||||
|
user_agent);
|
||||||
|
fflush(g_log_file);
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&g_log_mutex);
|
||||||
|
}
|
||||||
58
access_log.h
Normal file
58
access_log.h
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* access_log.h - 访问日志模块接口
|
||||||
|
*
|
||||||
|
* 提供类似 Nginx combined 格式的访问日志记录。
|
||||||
|
* 格式: %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"
|
||||||
|
*
|
||||||
|
* @author xfy
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef COCOON_ACCESS_LOG_H
|
||||||
|
#define COCOON_ACCESS_LOG_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include "http.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* access_log_init - 初始化访问日志
|
||||||
|
*
|
||||||
|
* 打开日志文件(如果 path 为 NULL 或 "-",则输出到 stdout)。
|
||||||
|
* 线程安全:使用内部互斥锁保护。
|
||||||
|
*
|
||||||
|
* @param path 日志文件路径,NULL 或 "-" 表示 stdout
|
||||||
|
* @return 0 成功,-1 失败
|
||||||
|
*/
|
||||||
|
int access_log_init(const char *path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* access_log_close - 关闭访问日志
|
||||||
|
*
|
||||||
|
* 刷新并关闭日志文件。
|
||||||
|
*/
|
||||||
|
void access_log_close(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* access_log_is_enabled - 检查访问日志是否已启用
|
||||||
|
*
|
||||||
|
* @return true 已启用
|
||||||
|
*/
|
||||||
|
bool access_log_is_enabled(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* access_log_write - 写入一条访问日志记录
|
||||||
|
*
|
||||||
|
* 格式: 192.168.1.1 - - [05/Jun/2026:00:00:00 +0800] "GET /index.html HTTP/1.1" 200 1234 "-" "Mozilla/5.0"
|
||||||
|
*
|
||||||
|
* @param client_addr 客户端地址(sockaddr 结构体)
|
||||||
|
* @param addr_len 地址长度
|
||||||
|
* @param req HTTP 请求指针(实际类型为 http_request_t *,在 http.h 定义)
|
||||||
|
* @param status_code HTTP 响应状态码
|
||||||
|
* @param response_bytes 响应体字节数(-1 表示未知)
|
||||||
|
*/
|
||||||
|
void access_log_write(const struct sockaddr *client_addr, socklen_t addr_len,
|
||||||
|
const http_request_t *req, int status_code, int64_t response_bytes);
|
||||||
|
|
||||||
|
#endif /* COCOON_ACCESS_LOG_H */
|
||||||
1
cocoon.h
1
cocoon.h
@ -41,6 +41,7 @@ typedef struct cocoon_config {
|
|||||||
bool tls_enabled; /**< 是否启用 TLS(由 cert/key 自动推断) */
|
bool tls_enabled; /**< 是否启用 TLS(由 cert/key 自动推断) */
|
||||||
const char *tls_cert; /**< TLS 证书路径 */
|
const char *tls_cert; /**< TLS 证书路径 */
|
||||||
const char *tls_key; /**< TLS 私钥路径 */
|
const char *tls_key; /**< TLS 私钥路径 */
|
||||||
|
const char *access_log_path; /**< 访问日志文件路径(NULL 或 "-" 表示 stdout) */
|
||||||
} cocoon_config_t;
|
} cocoon_config_t;
|
||||||
|
|
||||||
/* === 服务器生命周期 API === */
|
/* === 服务器生命周期 API === */
|
||||||
|
|||||||
@ -7,5 +7,6 @@
|
|||||||
"timeout_ms": 30000,
|
"timeout_ms": 30000,
|
||||||
"log_level": "info",
|
"log_level": "info",
|
||||||
"gzip_enabled": true,
|
"gzip_enabled": true,
|
||||||
"brotli_enabled": true
|
"brotli_enabled": true,
|
||||||
|
"access_log": "-"
|
||||||
}
|
}
|
||||||
|
|||||||
13
config.c
13
config.c
@ -289,6 +289,12 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) {
|
|||||||
} else if (strcmp(key_str, "tls_enabled") == 0) {
|
} else if (strcmp(key_str, "tls_enabled") == 0) {
|
||||||
if (val.type == TOKEN_TRUE) config->tls_enabled = true;
|
if (val.type == TOKEN_TRUE) config->tls_enabled = true;
|
||||||
else if (val.type == TOKEN_FALSE) config->tls_enabled = false;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* 其他字段:忽略(未来扩展预留) */
|
/* 其他字段:忽略(未来扩展预留) */
|
||||||
|
|
||||||
@ -312,7 +318,8 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
|
|||||||
bool has_root_dir, bool has_port, bool has_workers,
|
bool has_root_dir, bool has_port, bool has_workers,
|
||||||
bool has_max_conn, bool has_timeout, bool has_log_level,
|
bool has_max_conn, bool has_timeout, bool has_log_level,
|
||||||
bool has_gzip_enabled, bool has_brotli_enabled,
|
bool has_gzip_enabled, bool has_brotli_enabled,
|
||||||
bool has_tls_cert, bool has_tls_key, bool has_tls_enabled) {
|
bool has_tls_cert, bool has_tls_key, bool has_tls_enabled,
|
||||||
|
bool has_access_log) {
|
||||||
if (!base || !cmdline) return;
|
if (!base || !cmdline) return;
|
||||||
|
|
||||||
/* 命令行显式指定的值覆盖配置文件 */
|
/* 命令行显式指定的值覆盖配置文件 */
|
||||||
@ -336,6 +343,10 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
|
|||||||
base->tls_key = strdup(cmdline->tls_key);
|
base->tls_key = strdup(cmdline->tls_key);
|
||||||
}
|
}
|
||||||
if (has_tls_enabled) base->tls_enabled = cmdline->tls_enabled;
|
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);
|
||||||
|
}
|
||||||
/* threaded 是 flag 参数,命令行指定了就用命令行的 */
|
/* threaded 是 flag 参数,命令行指定了就用命令行的 */
|
||||||
if (cmdline->threaded) base->threaded = true;
|
if (cmdline->threaded) base->threaded = true;
|
||||||
}
|
}
|
||||||
|
|||||||
3
config.h
3
config.h
@ -44,6 +44,7 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
|
|||||||
bool has_root_dir, bool has_port, bool has_workers,
|
bool has_root_dir, bool has_port, bool has_workers,
|
||||||
bool has_max_conn, bool has_timeout, bool has_log_level,
|
bool has_max_conn, bool has_timeout, bool has_log_level,
|
||||||
bool has_gzip_enabled, bool has_brotli_enabled,
|
bool has_gzip_enabled, bool has_brotli_enabled,
|
||||||
bool has_tls_cert, bool has_tls_key, bool has_tls_enabled);
|
bool has_tls_cert, bool has_tls_key, bool has_tls_enabled,
|
||||||
|
bool has_access_log);
|
||||||
|
|
||||||
#endif /* COCOON_CONFIG_H */
|
#endif /* COCOON_CONFIG_H */
|
||||||
|
|||||||
20
main.c
20
main.c
@ -9,6 +9,7 @@
|
|||||||
#include "cocoon.h"
|
#include "cocoon.h"
|
||||||
#include "server.h"
|
#include "server.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "access_log.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@ -55,7 +56,7 @@ static void print_usage(const char *prog) {
|
|||||||
printf(" --key <path> TLS 私钥路径\n");
|
printf(" --key <path> TLS 私钥路径\n");
|
||||||
printf(" --tls 显式启用 TLS(需同时指定 --cert 和 --key)\n");
|
printf(" --tls 显式启用 TLS(需同时指定 --cert 和 --key)\n");
|
||||||
printf(" --no-gzip 禁用 gzip 压缩\n");
|
printf(" --no-gzip 禁用 gzip 压缩\n");
|
||||||
printf(" --no-brotli 禁用 brotli 压缩\n");
|
printf(" --access-log <path> 访问日志文件路径(- 表示 stdout)\n");
|
||||||
printf(" -h 显示此帮助\n");
|
printf(" -h 显示此帮助\n");
|
||||||
printf("\nExample:\n");
|
printf("\nExample:\n");
|
||||||
printf(" %s -c cocoon.json\n", prog);
|
printf(" %s -c cocoon.json\n", prog);
|
||||||
@ -100,6 +101,7 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
|
|||||||
bool has_tls_cert = false;
|
bool has_tls_cert = false;
|
||||||
bool has_tls_key = false;
|
bool has_tls_key = false;
|
||||||
bool has_tls_enabled = false;
|
bool has_tls_enabled = false;
|
||||||
|
bool has_access_log = false;
|
||||||
const char *config_file = NULL;
|
const char *config_file = NULL;
|
||||||
|
|
||||||
for (int i = 1; i < argc; i++) {
|
for (int i = 1; i < argc; i++) {
|
||||||
@ -160,6 +162,10 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
|
|||||||
} else if (strcmp(argv[i], "--tls") == 0) {
|
} else if (strcmp(argv[i], "--tls") == 0) {
|
||||||
config->tls_enabled = true;
|
config->tls_enabled = true;
|
||||||
has_tls_enabled = true;
|
has_tls_enabled = true;
|
||||||
|
} else if (strcmp(argv[i], "--access-log") == 0) {
|
||||||
|
if (++i >= argc) return false;
|
||||||
|
config->access_log_path = strdup(argv[i]);
|
||||||
|
has_access_log = 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);
|
||||||
@ -179,7 +185,8 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
|
|||||||
config_merge(config, config, has_root_dir, has_port, has_workers,
|
config_merge(config, config, has_root_dir, has_port, has_workers,
|
||||||
has_max_conn, has_timeout, has_log_level,
|
has_max_conn, has_timeout, has_log_level,
|
||||||
has_gzip_enabled, has_brotli_enabled,
|
has_gzip_enabled, has_brotli_enabled,
|
||||||
has_tls_cert, has_tls_key, has_tls_enabled);
|
has_tls_cert, has_tls_key, has_tls_enabled,
|
||||||
|
has_access_log);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config->root_dir) {
|
if (!config->root_dir) {
|
||||||
@ -214,6 +221,11 @@ int main(int argc, char *argv[]) {
|
|||||||
/* 设置日志级别 */
|
/* 设置日志级别 */
|
||||||
log_set_level(config.log_level);
|
log_set_level(config.log_level);
|
||||||
|
|
||||||
|
/* 初始化访问日志 */
|
||||||
|
if (config.access_log_path) {
|
||||||
|
access_log_init(config.access_log_path);
|
||||||
|
}
|
||||||
|
|
||||||
/* 创建服务器 */
|
/* 创建服务器 */
|
||||||
g_ctx = server_create(&config);
|
g_ctx = server_create(&config);
|
||||||
if (!g_ctx) {
|
if (!g_ctx) {
|
||||||
@ -228,10 +240,14 @@ int main(int argc, char *argv[]) {
|
|||||||
server_destroy(g_ctx);
|
server_destroy(g_ctx);
|
||||||
g_ctx = NULL;
|
g_ctx = NULL;
|
||||||
|
|
||||||
|
/* 关闭访问日志 */
|
||||||
|
access_log_close();
|
||||||
|
|
||||||
/* 释放配置文件分配的内存 */
|
/* 释放配置文件分配的内存 */
|
||||||
if (config.root_dir) free((void *)config.root_dir);
|
if (config.root_dir) free((void *)config.root_dir);
|
||||||
if (config.tls_cert) free((void *)config.tls_cert);
|
if (config.tls_cert) free((void *)config.tls_cert);
|
||||||
if (config.tls_key) free((void *)config.tls_key);
|
if (config.tls_key) free((void *)config.tls_key);
|
||||||
|
if (config.access_log_path) free((void *)config.access_log_path);
|
||||||
|
|
||||||
return ret == COCOON_OK ? 0 : 1;
|
return ret == COCOON_OK ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
46
server.c
46
server.c
@ -23,6 +23,7 @@
|
|||||||
#include "multipart.h"
|
#include "multipart.h"
|
||||||
#include "tls.h"
|
#include "tls.h"
|
||||||
#include "http2.h"
|
#include "http2.h"
|
||||||
|
#include "access_log.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@ -46,7 +47,7 @@
|
|||||||
/**
|
/**
|
||||||
* connection_t - 单个客户端连接上下文
|
* connection_t - 单个客户端连接上下文
|
||||||
*
|
*
|
||||||
* 包含 socket fd、接收缓冲区、解析状态、超时管理。
|
* 包含 socket fd、接收缓冲区、解析状态、超时管理、客户端地址、响应状态。
|
||||||
*/
|
*/
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int fd; /**< 客户端 socket */
|
int fd; /**< 客户端 socket */
|
||||||
@ -60,6 +61,9 @@ typedef struct {
|
|||||||
coco_coro_t *coro; /**< 当前处理协程 */
|
coco_coro_t *coro; /**< 当前处理协程 */
|
||||||
bool gzip_enabled; /**< 是否启用 gzip 压缩 */
|
bool gzip_enabled; /**< 是否启用 gzip 压缩 */
|
||||||
bool brotli_enabled; /**< 是否启用 brotli 压缩 */
|
bool brotli_enabled; /**< 是否启用 brotli 压缩 */
|
||||||
|
struct sockaddr_storage client_addr; /**< 客户端地址 */
|
||||||
|
socklen_t addr_len; /**< 地址长度 */
|
||||||
|
int response_status; /**< 最后响应的 HTTP 状态码 */
|
||||||
} connection_t;
|
} connection_t;
|
||||||
struct server_context {
|
struct server_context {
|
||||||
int listen_fd; /**< 监听 socket */
|
int listen_fd; /**< 监听 socket */
|
||||||
@ -335,6 +339,7 @@ static bool handle_post_request(int fd, const http_request_t *req, const char *r
|
|||||||
*
|
*
|
||||||
* 从缓冲区解析请求,判断是文件还是目录,调用对应的服务函数。
|
* 从缓冲区解析请求,判断是文件还是目录,调用对应的服务函数。
|
||||||
* 新增:支持 POST 请求体读取和简单回显。
|
* 新增:支持 POST 请求体读取和简单回显。
|
||||||
|
* 新增:记录响应状态码到访问日志。
|
||||||
*
|
*
|
||||||
* @param conn 连接上下文
|
* @param conn 连接上下文
|
||||||
* @param root_dir 静态资源根目录
|
* @param root_dir 静态资源根目录
|
||||||
@ -350,7 +355,10 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
/* 格式错误 */
|
/* 格式错误 */
|
||||||
|
conn->response_status = 400;
|
||||||
static_send_error(conn->fd, 400, false);
|
static_send_error(conn->fd, 400, false);
|
||||||
|
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
|
||||||
|
&req, conn->response_status, -1);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,7 +372,11 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
if (req.content_length > 0) {
|
if (req.content_length > 0) {
|
||||||
size_t need = (size_t)req.content_length;
|
size_t need = (size_t)req.content_length;
|
||||||
if (conn_read_body(conn, &req, need) != 0) {
|
if (conn_read_body(conn, &req, need) != 0) {
|
||||||
|
conn->response_status = 413;
|
||||||
static_send_error(conn->fd, 413, req.keep_alive); /* Payload Too Large */
|
static_send_error(conn->fd, 413, req.keep_alive); /* Payload Too Large */
|
||||||
|
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
|
||||||
|
&req, conn->response_status, -1);
|
||||||
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -372,13 +384,19 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
/* 处理 POST */
|
/* 处理 POST */
|
||||||
if (req.method == HTTP_POST) {
|
if (req.method == HTTP_POST) {
|
||||||
bool keep = handle_post_request(conn->fd, &req, conn->root_dir);
|
bool keep = handle_post_request(conn->fd, &req, conn->root_dir);
|
||||||
|
conn->response_status = 200; /* POST 目前总是返回 200 */
|
||||||
|
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
|
||||||
|
&req, conn->response_status, -1);
|
||||||
http_request_free(&req);
|
http_request_free(&req);
|
||||||
return keep;
|
return keep;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 只支持 GET 和 HEAD */
|
/* 只支持 GET 和 HEAD */
|
||||||
if (req.method != HTTP_GET && req.method != HTTP_HEAD) {
|
if (req.method != HTTP_GET && req.method != HTTP_HEAD) {
|
||||||
|
conn->response_status = 405;
|
||||||
static_send_error(conn->fd, 405, req.keep_alive);
|
static_send_error(conn->fd, 405, req.keep_alive);
|
||||||
|
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
|
||||||
|
&req, conn->response_status, -1);
|
||||||
http_request_free(&req);
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
@ -393,7 +411,10 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
|
|
||||||
int n = snprintf(real_path, sizeof(real_path), "%s%s", root_normalized, req.path);
|
int n = snprintf(real_path, sizeof(real_path), "%s%s", root_normalized, req.path);
|
||||||
if (n < 0 || (size_t)n >= sizeof(real_path)) {
|
if (n < 0 || (size_t)n >= sizeof(real_path)) {
|
||||||
|
conn->response_status = 400;
|
||||||
static_send_error(conn->fd, 400, req.keep_alive);
|
static_send_error(conn->fd, 400, req.keep_alive);
|
||||||
|
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
|
||||||
|
&req, conn->response_status, -1);
|
||||||
http_request_free(&req);
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
@ -403,7 +424,10 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
char resolved[4096];
|
char resolved[4096];
|
||||||
if (!realpath(real_path, resolved) ||
|
if (!realpath(real_path, resolved) ||
|
||||||
strncmp(resolved, root_normalized, strlen(root_normalized)) != 0) {
|
strncmp(resolved, root_normalized, strlen(root_normalized)) != 0) {
|
||||||
|
conn->response_status = 403;
|
||||||
static_send_error(conn->fd, 403, req.keep_alive);
|
static_send_error(conn->fd, 403, req.keep_alive);
|
||||||
|
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
|
||||||
|
&req, conn->response_status, -1);
|
||||||
http_request_free(&req);
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
@ -413,7 +437,10 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
/* 判断文件类型 */
|
/* 判断文件类型 */
|
||||||
struct stat st;
|
struct stat st;
|
||||||
if (stat(real_path, &st) != 0) {
|
if (stat(real_path, &st) != 0) {
|
||||||
|
conn->response_status = 404;
|
||||||
static_send_error(conn->fd, 404, req.keep_alive);
|
static_send_error(conn->fd, 404, req.keep_alive);
|
||||||
|
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
|
||||||
|
&req, conn->response_status, -1);
|
||||||
http_request_free(&req);
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
@ -422,7 +449,10 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
/* 目录:尝试 index.html */
|
/* 目录:尝试 index.html */
|
||||||
char index_path[4096];
|
char index_path[4096];
|
||||||
if (snprintf(index_path, sizeof(index_path), "%s/index.html", real_path) >= (int)sizeof(index_path)) {
|
if (snprintf(index_path, sizeof(index_path), "%s/index.html", real_path) >= (int)sizeof(index_path)) {
|
||||||
|
conn->response_status = 400;
|
||||||
static_send_error(conn->fd, 400, req.keep_alive);
|
static_send_error(conn->fd, 400, req.keep_alive);
|
||||||
|
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
|
||||||
|
&req, conn->response_status, -1);
|
||||||
http_request_free(&req);
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
@ -431,22 +461,31 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
/* 有 index.html,作为文件服务 */
|
/* 有 index.html,作为文件服务 */
|
||||||
http_request_t index_req = req;
|
http_request_t index_req = req;
|
||||||
if (snprintf(index_req.path, sizeof(index_req.path), "%s/index.html", req.path) >= (int)sizeof(index_req.path)) {
|
if (snprintf(index_req.path, sizeof(index_req.path), "%s/index.html", req.path) >= (int)sizeof(index_req.path)) {
|
||||||
|
conn->response_status = 400;
|
||||||
static_send_error(conn->fd, 400, req.keep_alive);
|
static_send_error(conn->fd, 400, req.keep_alive);
|
||||||
|
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
|
||||||
|
&req, conn->response_status, -1);
|
||||||
http_request_free(&req);
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
|
conn->response_status = 200;
|
||||||
static_serve_file(conn->fd, &index_req, root_dir, conn->gzip_enabled, conn->brotli_enabled);
|
static_serve_file(conn->fd, &index_req, root_dir, conn->gzip_enabled, conn->brotli_enabled);
|
||||||
} else {
|
} else {
|
||||||
/* 无 index.html,生成目录列表 */
|
/* 无 index.html,生成目录列表 */
|
||||||
|
conn->response_status = 200;
|
||||||
static_serve_directory(conn->fd, &req, root_dir, real_path);
|
static_serve_directory(conn->fd, &req, root_dir, real_path);
|
||||||
}
|
}
|
||||||
} else if (S_ISREG(st.st_mode)) {
|
} else if (S_ISREG(st.st_mode)) {
|
||||||
/* 普通文件 */
|
/* 普通文件 */
|
||||||
|
conn->response_status = 200;
|
||||||
static_serve_file(conn->fd, &req, root_dir, conn->gzip_enabled, conn->brotli_enabled);
|
static_serve_file(conn->fd, &req, root_dir, conn->gzip_enabled, conn->brotli_enabled);
|
||||||
} else {
|
} else {
|
||||||
|
conn->response_status = 403;
|
||||||
static_send_error(conn->fd, 403, req.keep_alive);
|
static_send_error(conn->fd, 403, req.keep_alive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
|
||||||
|
&req, conn->response_status, -1);
|
||||||
http_request_free(&req);
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
@ -747,7 +786,7 @@ static void accept_loop(void *arg) {
|
|||||||
log_info("连接空闲超时: %u ms", ctx->config.timeout_ms > 0 ? ctx->config.timeout_ms : CONN_TIMEOUT_MS);
|
log_info("连接空闲超时: %u ms", ctx->config.timeout_ms > 0 ? ctx->config.timeout_ms : CONN_TIMEOUT_MS);
|
||||||
|
|
||||||
while (ctx->running) {
|
while (ctx->running) {
|
||||||
struct sockaddr_in client_addr;
|
struct sockaddr_storage client_addr;
|
||||||
socklen_t addr_len = sizeof(client_addr);
|
socklen_t addr_len = sizeof(client_addr);
|
||||||
|
|
||||||
int client_fd = accept(ctx->listen_fd,
|
int client_fd = accept(ctx->listen_fd,
|
||||||
@ -828,6 +867,9 @@ static void accept_loop(void *arg) {
|
|||||||
conn->timeout_ms = ctx->config.timeout_ms > 0 ? ctx->config.timeout_ms : CONN_TIMEOUT_MS;
|
conn->timeout_ms = ctx->config.timeout_ms > 0 ? ctx->config.timeout_ms : CONN_TIMEOUT_MS;
|
||||||
conn->gzip_enabled = ctx->config.gzip_enabled;
|
conn->gzip_enabled = ctx->config.gzip_enabled;
|
||||||
conn->brotli_enabled = ctx->config.brotli_enabled;
|
conn->brotli_enabled = ctx->config.brotli_enabled;
|
||||||
|
memcpy(&conn->client_addr, &client_addr, sizeof(client_addr));
|
||||||
|
conn->addr_len = addr_len;
|
||||||
|
conn->response_status = 0;
|
||||||
|
|
||||||
atomic_fetch_add(&g_active_connections, 1);
|
atomic_fetch_add(&g_active_connections, 1);
|
||||||
log_debug("新连接 fd=%d,当前活跃连接: %d", client_fd,
|
log_debug("新连接 fd=%d,当前活跃连接: %d", client_fd,
|
||||||
|
|||||||
@ -372,6 +372,25 @@ assert_status_405() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert_access_log() {
|
||||||
|
local log_file="$1"
|
||||||
|
local desc="${2:-访问日志}"
|
||||||
|
if [ -f "$log_file" ] && [ -s "$log_file" ]; then
|
||||||
|
local count
|
||||||
|
count=$(grep -c '"GET /' "$log_file" || true)
|
||||||
|
if [ "$count" -ge 1 ]; then
|
||||||
|
echo " ✓ $desc — 日志文件包含请求记录"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ $desc — 日志文件无请求记录"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ✗ $desc — 日志文件不存在或为空"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ===== 测试开始 =====
|
# ===== 测试开始 =====
|
||||||
start_server
|
start_server
|
||||||
|
|
||||||
@ -455,6 +474,60 @@ echo ""
|
|||||||
echo "=== 文件上传测试 ==="
|
echo "=== 文件上传测试 ==="
|
||||||
assert_post_multipart "$BASE/upload" "multipart 文件上传"
|
assert_post_multipart "$BASE/upload" "multipart 文件上传"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 访问日志测试 ==="
|
||||||
|
# 带访问日志启动服务器
|
||||||
|
kill_server
|
||||||
|
sleep 1
|
||||||
|
LOG_FILE="$TMPDIR/access.log"
|
||||||
|
$SERVER -r "$ROOT" -p 9999 --access-log "$LOG_FILE" > "$TMPDIR/server_access.log" 2>&1 &
|
||||||
|
pid_access=$!
|
||||||
|
for i in {1..30}; do
|
||||||
|
if curl -s -o /dev/null "$BASE/" 2>/dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
# 发送请求并验证日志记录
|
||||||
|
curl -s -o /dev/null "$BASE/index.html" -H "User-Agent: CocoonTest/1.0"
|
||||||
|
curl -s -o /dev/null "$BASE/nonexist.html" -H "Referer: http://example.com"
|
||||||
|
sleep 0.5
|
||||||
|
assert_access_log "$LOG_FILE" "访问日志文件生成"
|
||||||
|
# 检查日志格式是否包含关键字段
|
||||||
|
if grep -q '"GET /index.html HTTP/1.1"' "$LOG_FILE"; then
|
||||||
|
echo " ✓ 日志包含 GET /index.html"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ 日志缺少 GET /index.html"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
if grep -q 'CocoonTest/1.0' "$LOG_FILE"; then
|
||||||
|
echo " ✓ 日志包含 User-Agent"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ 日志缺少 User-Agent"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
if grep -q 'example.com' "$LOG_FILE"; then
|
||||||
|
echo " ✓ 日志包含 Referer"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ 日志缺少 Referer"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
if grep -q '404' "$LOG_FILE"; then
|
||||||
|
echo " ✓ 日志包含 404 状态码"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ 日志缺少 404 状态码"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 恢复普通服务器
|
||||||
|
kill_server
|
||||||
|
sleep 1
|
||||||
|
start_server
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== TLS/HTTPS 测试 ==="
|
echo "=== TLS/HTTPS 测试 ==="
|
||||||
# 启动 HTTPS 服务器(使用测试证书)
|
# 启动 HTTPS 服务器(使用测试证书)
|
||||||
|
|||||||
@ -126,7 +126,7 @@ void test_merge_override_all(void) {
|
|||||||
.brotli_enabled = false
|
.brotli_enabled = false
|
||||||
};
|
};
|
||||||
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);
|
||||||
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);
|
||||||
@ -155,7 +155,7 @@ void test_merge_no_override(void) {
|
|||||||
.log_level = LOG_LEVEL_DEBUG
|
.log_level = LOG_LEVEL_DEBUG
|
||||||
};
|
};
|
||||||
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);
|
||||||
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);
|
||||||
@ -178,7 +178,7 @@ void test_merge_partial_override(void) {
|
|||||||
.log_level = LOG_LEVEL_DEBUG
|
.log_level = LOG_LEVEL_DEBUG
|
||||||
};
|
};
|
||||||
config_merge(&base, &cmdline,
|
config_merge(&base, &cmdline,
|
||||||
true, false, false, true, false, false, false, false, false, false, false);
|
true, false, false, true, 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 */
|
||||||
@ -243,7 +243,7 @@ void test_merge_cmdline_null_root_dir(void) {
|
|||||||
/* cmdline root_dir 为 NULL,不应覆盖 base */
|
/* cmdline root_dir 为 NULL,不应覆盖 base */
|
||||||
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);
|
config_merge(&base, &cmdline, true, true, 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);
|
||||||
@ -251,7 +251,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);
|
config_merge(NULL, NULL, true, true, true, true, true, true, true, true, true, true, true, true);
|
||||||
TEST_ASSERT_TRUE(1);
|
TEST_ASSERT_TRUE(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user