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:
xfy911 2026-06-05 00:20:24 +08:00
parent 3374130c8c
commit 3ba9fcea62
12 changed files with 434 additions and 22 deletions

View File

@ -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] 集成测试 suite45 项 curl/bash 测试全部通过)✅ 2026-06-04 - [x] 集成测试 suite59 项 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

View File

@ -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
View 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
View 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 */

View File

@ -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 === */

View File

@ -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": "-"
} }

View File

@ -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;
} }

View File

@ -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
View File

@ -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;
} }

View File

@ -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,

View File

@ -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 服务器(使用测试证书)

View File

@ -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);
} }