merge main into feature/windows-compat-impl: resolve conflicts for TLS, HTTP/2, WebSocket, access log

This commit is contained in:
xfy911 2026-06-05 16:17:55 +08:00
commit 22c933a99d
26 changed files with 4047 additions and 136 deletions

View File

@ -20,48 +20,59 @@
- [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] 集成测试 suite37 项 curl/bash 测试全部通过)✅ 2026-06-03 - [x] Brotli 压缩 ✅ 2026-06-04优先于 Gzip
- [x] 集成测试 suite61 项 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 读取
- JSON / form-urlencoded 回显 - JSON / form-urlencoded 回显
- **multipart/form-data 文件上传**(保存到 root_dir/uploads/)✅ 2026-06-04 - **multipart/form-data 文件上传**(保存到 root_dir/uploads/)✅ 2026-06-04
- [x] C 语言单元测试框架 ✅ 2026-06-03Unity 框架113 个测试全部通过) - [x] C 语言单元测试框架 ✅ 2026-06-03Unity 框架127 个测试全部通过)
### Phase 3 — 扩展 ### Phase 3 — 扩展(已完成)
- [x] **配置文件支持** — JSON 配置替代纯命令行 ✅ 2026-06-04 - [x] **配置文件支持** — JSON 配置替代纯命令行 ✅ 2026-06-04
- `config.c` / `config.h`:极简 JSON 解析器(数字、字符串、布尔、注释) - `config.c` / `config.h`:极简 JSON 解析器(数字、字符串、布尔、注释)
- `cocoon_config_t` 结构体root_dir / port / threaded / num_workers / max_connections / timeout_ms / log_level / gzip_enabled / brotli_enabled - `cocoon_config_t` 结构体root_dir / port / threaded / num_workers / max_connections / timeout_ms / log_level / gzip_enabled / brotli_enabled / access_log
- `config_merge()`:命令行参数覆盖配置文件 - `config_merge()`:命令行参数覆盖配置文件
- `cocoon.json` 示例配置 - `cocoon.json` 示例配置
- `--no-gzip` / `--no-brotli` 命令行选项禁用压缩 - `--no-gzip` / `--no-brotli` 命令行选项禁用压缩
- [x] **Brotli 压缩** — 比 gzip 更高压缩率,现代浏览器均支持 ✅ 2026-06-04 - [x] **Brotli 压缩** — 比 gzip 更高压缩率,现代浏览器均支持 ✅ 2026-06-04
- [x] **HTTPS / TLS** — OpenSSL Memory BIO 集成,支持命令行与配置文件启用 ✅ 2026-06-04
- [x] **HTTP/2** — nghttp2 完整实现TLS ALPN 协商 + 静态文件服务 + 缓存 + 目录浏览)✅ 2026-06-04
- [x] **h2c 升级支持** — 明文 HTTP/2PRI 魔术字直接连接 + Upgrade: h2c 协商)✅ 2026-06-04
- [x] **HTTP/2 目录浏览** — 目录无 index.html 时返回目录列表 ✅ 2026-06-04
- [x] **WebSocket 支持** — RFC 6455 握手 + 帧解析/编码 + echo 服务器 ✅ 2026-06-05
- [x] **Windows 兼容性** — 跨平台抽象层,支持 Linux/macOS/Windows(MinGW/MSVC) ✅ 2026-06-04 - [x] **Windows 兼容性** — 跨平台抽象层,支持 Linux/macOS/Windows(MinGW/MSVC) ✅ 2026-06-04
- [ ] HTTPS / TLS
- [ ] HTTP/2
### Phase 4 — 生态 ### Phase 4 — 生态
- [ ] WebSocket 支持 - [ ] WebSocket 广播/消息路由
- [ ] 中间件机制 - [ ] 中间件机制
- [ ] 插件系统 - [ ] 插件系统
## 当前状态 ## 当前状态
- 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示) - 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示)
- **37 项集成测试全部通过**GET/HEAD/POST/404/Range/304/gzip/brotli/MIME/目录浏览/路径防护/文件上传) - **61 项集成测试全部通过**GET/HEAD/POST/404/Range/304/gzip/brotli/MIME/目录浏览/路径防护/文件上传/TLS/HTTP/2/h2c/WebSocket/访问日志)
- **113 个单元测试全部通过**Unity 框架,覆盖 http.c / static.c / multipart.c / config.c / server.c 核心逻辑) - **127 个单元测试全部通过**Unity 框架)
- 压测数据wrk -t4 -c100 -d10s → 16,179 RPS平均延迟 59.86μs - 压测数据wrk -t4 -c100 -d10s → 16,179 RPS平均延迟 59.86μs单线程
- 多线程模式(-t -w 4已修复主线程 accept + pollclient_handler 1MB 协程栈)
- POST 支持JSON 和 form-urlencoded 回显multipart 文件上传Content-Length 解析8MB 上限 - POST 支持JSON 和 form-urlencoded 回显multipart 文件上传Content-Length 解析8MB 上限
- **配置文件支持**JSON 格式9 个字段,命令行参数可覆盖 - **配置文件支持**JSON 格式9 个字段(含 access_log命令行参数可覆盖
- **访问日志**Nginx combined 格式,支持文件路径或 stdout`-`线程安全pthread_mutex记录 User-Agent / Referer / 状态码
- **TLS/HTTPS**OpenSSL 3.0 Memory BIO + coco 协程集成自签名证书支持ALPN 协商 h2/http1.1
- **HTTP/2**完整功能静态文件服务、目录浏览、缓存协商、压缩、HEAD 请求)
- **h2c**:明文 HTTP/2 支持prior knowledge + Upgrade 协商)
- **WebSocket**RFC 6455 握手、文本/二进制帧 echo、ping/pong/close
- **Windows 兼容性**:跨平台抽象层 platform.h + platform.cCMakeLists.txt 自动检测平台Makefile 在 Windows 下自动链接 ws2_32 - **Windows 兼容性**:跨平台抽象层 platform.h + platform.cCMakeLists.txt 自动检测平台Makefile 在 Windows 下自动链接 ws2_32
- README / Makefile / CMakeLists.txt 已更新
## 待办池 ## 待办池
1. **[高] HTTPS / TLS** — 生产部署必备 1. **[高] Phase 4 生态功能** — WebSocket 广播/消息路由、中间件机制、插件系统
2. **[高] HTTP/2 多路复用** — 长期演进方向 2. **[中] 完善 WebSocket 功能** — 消息路由、广播、连接管理面板
3. **[中] 完善 Doxygen 中文注释** — 所有模块的公共 API 需要完整文档 3. **[低] Doxygen 中文注释** — tls.c, config.c, multipart.c, access_log.c, main.c, websocket.c 等模块待补充
4. **[低] 性能优化** — 连接池复用、零拷贝优化、压缩预缓存
## Windows 兼容性实现详情 ## Windows 兼容性实现详情
@ -92,68 +103,31 @@
- `main.c`:信号处理跨平台化,新增 `cocoon_socket_init()`/`cocoon_socket_cleanup()` - `main.c`:信号处理跨平台化,新增 `cocoon_socket_init()`/`cocoon_socket_cleanup()`
- `CMakeLists.txt`:全新跨平台构建配置,自动检测 Windows/POSIX - `CMakeLists.txt`:全新跨平台构建配置,自动检测 Windows/POSIX
- `Makefile`:添加 `platform.c`Windows 下自动链接 `ws2_32` - `Makefile`:添加 `platform.c`Windows 下自动链接 `ws2_32`
- 单元测试113 个通过Linux 验证集成测试37 项通过 - 单元测试127 个通过Linux 验证集成测试61 项通过
- 推送到 feature/windows-compat-impl - 推送到 feature/windows-compat-impl
- 2026-06-04: **本轮行动 — Brotli 压缩支持** - 2026-06-05: **本轮行动 — 实现 WebSocket 支持RFC 6455**
- `static.c``brotli_compress()` 函数,使用 `libbrotlienc` 高质量压缩(质量 11窗口 22 - 新增 `websocket.h` / `websocket.c`WebSocket 协议完整实现
- `static_serve_file()`:优先 Brotli回退 Gzip不可压缩/二进制文件跳过 - 帧解析:支持 FIN、opcode、payload length7/16/64 位、mask 解掩码
- `http.c`:解析 `Accept-Encoding: br``http_request_t` 新增 `accept_brotli` 字段 - 帧编码:服务器端发送(无掩码),支持文本/二进制/close/ping/pong
- `cocoon.h``cocoon_config_t` 新增 `brotli_enabled` 字段(默认 true - 握手Sec-WebSocket-Key + SHA1 + Base64 计算 Accept
- `config.c` / `config.h`:解析 `brotli_enabled` JSON 字段,`config_merge()` 新增 `has_brotli_enabled` 参数 - `server.c`:添加 `is_websocket_upgrade_request()` 检测,握手后进入 `ws_handle_connection()`
- `main.c``--no-brotli` 命令行选项,默认启用 Brotli - 连接处理:文本/二进制 echo、ping/pong 自动响应、close 帧处理
- `server.c`:连接结构体新增 `brotli_enabled`,传递至 `static_serve_file()` - 集成测试:`tests/websocket_test.py`Python 标准库,零依赖)
- `Makefile`:链接 `-lbrotlienc`,单元测试编译也添加 - 编译通过,零警告
- 单元测试:`test_static.c` 新增 4 个 Brotli 测试(可压缩/不可压缩/小数据/溢出) - **61 项集成测试全部通过127 个单元测试全部通过**
- 单元测试:`test_config.c` 新增 `brotli_enabled` 解析与 merge 测试,修复所有 `config_merge()` 调用签名 - 推送到 main2775033
- 集成测试:`assert_brotli()` / `assert_brotli_preferred()` / `assert_not_brotli()`,新增 5 项 Brotli 测试 - 2026-06-05: **本轮行动 — 访问日志Nginx combined 格式)**
- `cocoon.json` 示例配置添加 `brotli_enabled: true` - 新增 `access_log.c` / `access_log.h`Nginx combined 格式访问日志线程安全pthread_mutex
- README更新特性描述、curl 示例、命令行参数、路线图 - `cocoon.h`:新增 `access_log_path` 配置字段
- 单元测试113 个全部通过集成测试37 项全部通过 - `config.c` / `config.h`JSON 配置和命令行参数支持 `access_log`
- 推送到 main - `main.c`:新增 `--access-log <path>` CLI 选项,`-` 表示输出到 stdout
- 2026-06-04: **本轮行动 — 配置文件支持 + README 修正 + bug 修复** - `server.c`:扩展 `connection_t` 结构体,添加 `client_addr` / `addr_len` / `response_status`
- `config.c` / `config.h`:极简 JSON 配置解析器(支持数字、字符串、布尔 true/false、// 注释) - `server.c``handle_request()` 中每个响应路径设置 `response_status`,请求结束时调用 `access_log_write()`
- `cocoon_config_t` 扩展 `gzip_enabled` 字段(默认 true - `server.c``accept_loop()` 将客户端地址复制到连接上下文
- `main.c``-c <file>` 加载配置文件,`--no-gzip` 禁用压缩,命令行参数覆盖配置文件 - `Makefile`:加入 `access_log.c`,更新单元测试编译规则
- `config_merge()`:命令行显式指定值覆盖配置文件,新增 `has_gzip_enabled` 参数 - `cocoon.json`:示例配置添加 `"access_log": "-"`
- **修复 `-r` 参数 strdup 崩溃**`parse_args``config->root_dir = strdup(argv[i])`,避免 `free()` 野指针 - `tests/integration_test.sh`:新增 5 项访问日志测试
- **修复 signal handler 双重 free**`main()` 中 server 指针仅在 `server != NULL` 时调用 `server_destroy()` - 编译通过,零警告
- 单元测试:`test_config.c` 17 个测试配置加载、merge、边界条件、gzip_enabled全部通过 - **59 项集成测试全部通过127 个单元测试全部通过**
- 集成测试32 项全部通过,配置文件启动验证通过
- `cocoon.json` 示例配置更新,添加 `gzip_enabled` 字段
- README 修正:
- "极简配置" → "配置文件"(已支持 JSON 配置)
- 安全设计:"仅允许 GET/HEAD" → "支持 GET/HEAD/POST"
- `-v` 参数说明:"显示版本号" → "详细日志输出debug 级别)"
- 路线图:配置文件支持打勾,添加 Brotli 压缩待办
- 推送到 main
- 2026-06-04: **本轮行动 — multipart 文件上传**
- 实现 `multipart.c` / `multipart.h`boundary 提取、multipart 解析、part 内存管理
- `server.c` 集成POST 请求检测 multipart保存文件到 `root_dir/uploads/`
- 修复 boundary 解析条件(`>= b_len` 替代 `> b_len`),修复边界扫描越界
- 创建 `tests/unit/test_multipart.c`12 个测试boundary 提取、解析、边界条件
- 修复集成测试 CRLF 问题:使用 `printf` 生成真实 CRLF 字节
- 集成测试 32 项全部通过,单元测试 99 个全部通过
- 推送到 main
- 2026-06-03: 项目初始化,核心模块全部实现
- 2026-06-03: 添加缓存协商ETag + Last-Modified + 304修复编译警告
- 2026-06-03: 添加连接空闲超时管理 + 最大并发限制 + 分级日志系统
- 2026-06-03: 创建集成测试套件 + 性能基准 + 更新文档
- 2026-06-03: 添加 POST 请求体解析支持30 项测试全部通过README 更新
- 2026-06-03: **本轮行动 — 单元测试框架**
- 引入 Unity 测试框架ThrowTheSwitch
- 创建 `tests/unit/test_http.c`35 个测试):
- 请求解析方法、路径、头部、Range、缓存、编码、边界条件
- 响应格式化状态行、缓存头、Range、缓冲区溢出
- MIME 类型:常见类型、大小写、无扩展名、特殊类型
- 内存管理:请求体释放
- 创建 `tests/unit/test_static.c`31 个测试):
- 压缩判断:文本/二进制/空值
- 时间处理:格式化、解析、边界
- ETag生成、匹配精确/弱/通配符/空值)
- 路径安全:正常拼接、路径遍历、空值、边界
- HTML 转义:特殊字符、缓冲区溢出
- gzip 压缩:可压缩/不可压缩/小数据/溢出
- socket 层send_all、错误响应404/500/未知)
- 更新 Makefile`make unit-test` 一键编译运行
- 66 个测试全部通过,集成测试保持 30 项通过
- 推送到 main - 推送到 main
- 2026-06-04: 其他历史记录...(省略)

View File

@ -7,7 +7,7 @@ COCO_LIB ?= $(COCO_DIR)/build
CC = gcc CC = gcc
CFLAGS = -Wall -Wextra -O2 -std=c11 -D_GNU_SOURCE -I$(COCO_INCLUDE) CFLAGS = -Wall -Wextra -O2 -std=c11 -D_GNU_SOURCE -I$(COCO_INCLUDE)
LDFLAGS = -L$(COCO_LIB) -lcoco -lpthread -lm -luring -lz -lbrotlienc LDFLAGS = -L$(COCO_LIB) -lcoco -lpthread -lm -luring -lz -lbrotlienc -lssl -lcrypto -lnghttp2
# 调试模式 # 调试模式
DEBUG ?= 0 DEBUG ?= 0
@ -20,13 +20,10 @@ PREFIX ?= /usr/local
BINDIR = $(PREFIX)/bin BINDIR = $(PREFIX)/bin
# 源文件 # 源文件
SRCS = main.c server.c http.c static.c log.c config.c multipart.c platform.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
OBJS = $(SRCS:.c=.o) OBJS = $(SRCS:.c=.o)
TARGET = cocoon TARGET = cocoon
# 链接参数
LDFLAGS = -L$(COCO_LIB) -lcoco -lpthread -lm -luring -lz -lbrotlienc
# Windows (MinGW) 下链接 Winsock 库 # Windows (MinGW) 下链接 Winsock 库
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
LDFLAGS += -lws2_32 LDFLAGS += -lws2_32
@ -86,8 +83,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 platform.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 websocket.c platform.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 platform.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 websocket.c platform.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
@ -95,14 +92,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 $(UNITY_SRC) $(UNIT_TEST_DIR)/test_static: $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c access_log.c platform.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_static.c http.c log.c $(UNITY_SRC) -lm -lz -lbrotlienc $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c access_log.c platform.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 */

2
coco

@ -1 +1 @@
Subproject commit 62dbb13927d5411cb2d35d095e9f887c93c4a13f Subproject commit b45045ee0671c2a3fabeefbbf43c8b29a4a3ba22

View File

@ -19,7 +19,7 @@
#define COCOON_ERROR -1 /**< 通用错误 */ #define COCOON_ERROR -1 /**< 通用错误 */
#define COCOON_NOMEM -2 /**< 内存不足 */ #define COCOON_NOMEM -2 /**< 内存不足 */
#define COCOON_NOTFOUND -3 /**< 文件未找到 */ #define COCOON_NOTFOUND -3 /**< 文件未找到 */
#define COCOONForbidden -4 /**< 禁止访问 */ #define COCOON_FORBIDDEN -4 /**< 禁止访问 */
#define COCOON_BADREQUEST -5 /**< 请求格式错误 */ #define COCOON_BADREQUEST -5 /**< 请求格式错误 */
/* === 服务器配置 === */ /* === 服务器配置 === */
@ -38,6 +38,10 @@ typedef struct cocoon_config {
log_level_t log_level; /**< 日志级别 */ log_level_t log_level; /**< 日志级别 */
bool gzip_enabled; /**< 是否启用 gzip 压缩(默认 true */ bool gzip_enabled; /**< 是否启用 gzip 压缩(默认 true */
bool brotli_enabled; /**< 是否启用 brotli 压缩(默认 true */ bool brotli_enabled; /**< 是否启用 brotli 压缩(默认 true */
bool tls_enabled; /**< 是否启用 TLS由 cert/key 自动推断) */
const char *tls_cert; /**< 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

@ -274,6 +274,27 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) {
} else if (strcmp(key_str, "brotli_enabled") == 0) { } else if (strcmp(key_str, "brotli_enabled") == 0) {
if (val.type == TOKEN_TRUE) config->brotli_enabled = true; if (val.type == TOKEN_TRUE) config->brotli_enabled = true;
else if (val.type == TOKEN_FALSE) config->brotli_enabled = false; else if (val.type == TOKEN_FALSE) config->brotli_enabled = false;
} else if (strcmp(key_str, "tls_cert") == 0 && val.type == TOKEN_STRING) {
char *v = token_str_dup(&val);
if (v) {
free((void *)config->tls_cert);
config->tls_cert = v;
}
} else if (strcmp(key_str, "tls_key") == 0 && val.type == TOKEN_STRING) {
char *v = token_str_dup(&val);
if (v) {
free((void *)config->tls_key);
config->tls_key = v;
}
} else if (strcmp(key_str, "tls_enabled") == 0) {
if (val.type == TOKEN_TRUE) config->tls_enabled = true;
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;
}
} }
/* 其他字段:忽略(未来扩展预留) */ /* 其他字段:忽略(未来扩展预留) */
@ -296,7 +317,9 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) {
void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline, 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_access_log) {
if (!base || !cmdline) return; if (!base || !cmdline) return;
/* 命令行显式指定的值覆盖配置文件 */ /* 命令行显式指定的值覆盖配置文件 */
@ -311,6 +334,19 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
if (has_log_level) base->log_level = cmdline->log_level; if (has_log_level) base->log_level = cmdline->log_level;
if (has_gzip_enabled) base->gzip_enabled = cmdline->gzip_enabled; if (has_gzip_enabled) base->gzip_enabled = cmdline->gzip_enabled;
if (has_brotli_enabled) base->brotli_enabled = cmdline->brotli_enabled; if (has_brotli_enabled) base->brotli_enabled = cmdline->brotli_enabled;
if (has_tls_cert && cmdline->tls_cert) {
free((void *)base->tls_cert);
base->tls_cert = strdup(cmdline->tls_cert);
}
if (has_tls_key && cmdline->tls_key) {
free((void *)base->tls_key);
base->tls_key = strdup(cmdline->tls_key);
}
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

@ -43,6 +43,8 @@ bool config_load_from_file(const char *path, cocoon_config_t *config);
void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline, 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_access_log);
#endif /* COCOON_CONFIG_H */ #endif /* COCOON_CONFIG_H */

1277
http2.c Normal file

File diff suppressed because it is too large Load Diff

175
http2.h Normal file
View File

@ -0,0 +1,175 @@
/**
* http2.h - HTTP/2
*
* 使 nghttp2 HTTP/2
* TLS ALPN h2c
*
* @author xfy
*/
#ifndef COCOON_HTTP2_H
#define COCOON_HTTP2_H
#include "cocoon.h"
#include "http.h"
#include <nghttp2/nghttp2.h>
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* http2_session_t - HTTP/2
*
* HTTP/2
*/
typedef struct {
nghttp2_session *session; /**< nghttp2 会话对象 */
int fd; /**< 底层 socket fd */
bool tls_mode; /**< 是否通过 TLS ALPN 协商 */
struct http2_stream_data *streams; /**< 活跃的流列表(头节点) */
const char *root_dir; /**< 静态资源根目录 */
bool gzip_enabled; /**< 是否启用 gzip 压缩 */
bool brotli_enabled; /**< 是否启用 brotli 压缩 */
} http2_session_t;
/**
* http2_stream_data - HTTP/2
*
*
*/
typedef struct http2_stream_data {
struct http2_stream_data *next;
int32_t stream_id; /**< HTTP/2 流 ID */
http_request_t request; /**< 解析后的 HTTP 请求 */
bool request_complete; /**< 请求是否接收完整 */
int file_fd; /**< 响应文件 fd-1 表示无) */
char *response_body; /**< 响应体(用于动态内容) */
size_t response_len; /**< 响应体长度 */
size_t response_sent; /**< 已发送的响应体字节数 */
} http2_stream_data_t;
/**
* http2_init - HTTP/2
*
*
*
* @return 0 -1
*/
int http2_init(void);
/**
* http2_cleanup - HTTP/2
*/
void http2_cleanup(void);
/**
* http2_session_create - HTTP/2
*
* @param fd socket
* @param tls_mode TLS ALPN true=TLSfalse=h2c
* @return NULL
*/
http2_session_t *http2_session_create(int fd, bool tls_mode);
/**
* http2_session_destroy - HTTP/2
*
* @param h2
*/
void http2_session_destroy(http2_session_t *h2);
/**
* http2_session_is_http2 - HTTP/2
*
* @param fd socket
* @return true HTTP/2
*/
bool http2_session_is_http2(int fd);
/**
* http2_session_get - fd HTTP/2
*
* @param fd socket
* @return NULL HTTP/2
*/
http2_session_t *http2_session_get(int fd);
/**
* http2_recv -
*
* nghttp2
*
* @param h2
* @param buf
* @param len
* @return 0 -1
*/
int http2_recv(http2_session_t *h2, const uint8_t *buf, size_t len);
/**
* http2_send_pending - HTTP/2
*
* socket
*
* @param h2
* @return 0 -1
*/
int http2_send_pending(http2_session_t *h2);
/**
* http2_want_read - nghttp2
*
* @param h2
* @return true
*/
bool http2_want_read(http2_session_t *h2);
/**
* http2_want_write - nghttp2
*
* @param h2
* @return true
*/
bool http2_want_write(http2_session_t *h2);
/**
* http2_on_connection_accepted -
*
* TLS TLS
*
*
* @param fd socket
* @param tls_mode TLS ALPN
* @return 0 -1
*/
int http2_on_connection_accepted(int fd, bool tls_mode);
/**
* http2_session_upgrade - h2c
*
* stream_id=1
* SETTINGS
*
* @param h2 HTTP/2
* @param req HTTP/1.1
* @return 0 -1
*/
int http2_session_upgrade(http2_session_t *h2, const http_request_t *req);
/**
*
* @param h2
* @param root_dir
* @param gzip_enabled gzip
* @param brotli_enabled brotli
*/
void http2_session_set_context(http2_session_t *h2, const char *root_dir, bool gzip_enabled, bool brotli_enabled);
#ifdef __cplusplus
}
#endif
#endif /* COCOON_HTTP2_H */

52
main.c
View File

@ -10,6 +10,7 @@
#include "server.h" #include "server.h"
#include "config.h" #include "config.h"
#include "platform.h" #include "platform.h"
#include "access_log.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -51,8 +52,11 @@ static void print_usage(const char *prog) {
printf(" -o <ms> 连接空闲超时毫秒(默认 30000\n"); printf(" -o <ms> 连接空闲超时毫秒(默认 30000\n");
printf(" -l <level> 日志级别: error, warn, info, debug默认 info\n"); printf(" -l <level> 日志级别: error, warn, info, debug默认 info\n");
printf(" -v 详细日志输出(等同于 -l debug\n"); printf(" -v 详细日志输出(等同于 -l debug\n");
printf(" --cert <path> TLS 证书路径(启用 HTTPS\n");
printf(" --key <path> TLS 私钥路径\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);
@ -82,6 +86,10 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
config->gzip_enabled = true; config->gzip_enabled = true;
config->brotli_enabled = true; config->brotli_enabled = true;
config->tls_cert = NULL;
config->tls_key = NULL;
config->tls_enabled = false;
bool has_root_dir = false; bool has_root_dir = false;
bool has_port = false; bool has_port = false;
bool has_workers = false; bool has_workers = false;
@ -90,6 +98,10 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
bool has_log_level = false; bool has_log_level = false;
bool has_gzip_enabled = false; bool has_gzip_enabled = false;
bool has_brotli_enabled = false; bool has_brotli_enabled = false;
bool has_tls_cert = false;
bool has_tls_key = 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++) {
@ -139,6 +151,21 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
} else if (strcmp(argv[i], "--no-brotli") == 0) { } else if (strcmp(argv[i], "--no-brotli") == 0) {
config->brotli_enabled = false; config->brotli_enabled = false;
has_brotli_enabled = true; has_brotli_enabled = true;
} else if (strcmp(argv[i], "--cert") == 0) {
if (++i >= argc) return false;
config->tls_cert = strdup(argv[i]);
has_tls_cert = true;
} else if (strcmp(argv[i], "--key") == 0) {
if (++i >= argc) return false;
config->tls_key = strdup(argv[i]);
has_tls_key = true;
} else if (strcmp(argv[i], "--tls") == 0) {
config->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);
@ -156,7 +183,10 @@ 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_gzip_enabled, has_brotli_enabled); has_max_conn, has_timeout, has_log_level,
has_gzip_enabled, has_brotli_enabled,
has_tls_cert, has_tls_key, has_tls_enabled,
has_access_log);
} }
if (!config->root_dir) { if (!config->root_dir) {
@ -196,6 +226,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) {
@ -213,11 +248,14 @@ int main(int argc, char *argv[]) {
/* 释放 socket 子系统Windows 下 WSACleanup */ /* 释放 socket 子系统Windows 下 WSACleanup */
cocoon_socket_cleanup(); cocoon_socket_cleanup();
/* 释放配置文件分配的 root_dir */ /* 关闭访问日志 */
if (config.root_dir) { access_log_close();
free((void *)config.root_dir);
config.root_dir = NULL; /* 释放配置文件分配的内存 */
} if (config.root_dir) free((void *)config.root_dir);
if (config.tls_cert) free((void *)config.tls_cert);
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

@ -14,10 +14,20 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
/* ========== POSIX 实现 ========== */ /* ========== POSIX 实现 ========== */
#ifdef COCOON_PLATFORM_POSIX #ifdef COCOON_PLATFORM_POSIX
#include <fcntl.h> /* O_NONBLOCK */
#include <unistd.h> /* close, read, write, lseek, dup2 */
#include <dirent.h> /* opendir, readdir, closedir */
#include <errno.h> /* errno, EAGAIN, EINTR */
#include <sys/sendfile.h> /* sendfile */
#include <signal.h> /* sigaction */
#include <sys/socket.h> /* send */
/** /**
* POSIX socket * POSIX socket
*/ */

389
server.c
View File

@ -21,11 +21,16 @@
#include "cocoon.h" #include "cocoon.h"
#include "log.h" #include "log.h"
#include "multipart.h" #include "multipart.h"
#include "tls.h"
#include "http2.h"
#include "access_log.h"
#include "websocket.h"
#include "platform.h" #include "platform.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdatomic.h> #include <stdatomic.h>
#include <poll.h>
#include "coco.h" #include "coco.h"
@ -37,7 +42,7 @@
/** /**
* connection_t - * connection_t -
* *
* socket fd * socket fd
*/ */
typedef struct { typedef struct {
cocoon_socket_t fd; /**< 客户端 socket */ cocoon_socket_t fd; /**< 客户端 socket */
@ -51,6 +56,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 {
cocoon_socket_t listen_fd; /**< 监听 socket */ cocoon_socket_t listen_fd; /**< 监听 socket */
@ -80,6 +88,7 @@ static void set_nonblocking(cocoon_socket_t fd) {
*/ */
static void close_connection(connection_t *conn) { static void close_connection(connection_t *conn) {
if (conn && conn->fd != COCOON_INVALID_SOCKET) { if (conn && conn->fd != COCOON_INVALID_SOCKET) {
tls_close(conn->fd);
cocoon_socket_close(conn->fd); cocoon_socket_close(conn->fd);
conn->fd = COCOON_INVALID_SOCKET; conn->fd = COCOON_INVALID_SOCKET;
conn->closed = true; conn->closed = true;
@ -104,8 +113,9 @@ static ssize_t conn_read(connection_t *conn) {
ssize_t n; ssize_t n;
/* 尝试使用 coco 的异步 I/O */ if (tls_has_connection(conn->fd)) {
if (coco_sched_get_current() != NULL) { n = tls_read(conn->fd, conn->buf + conn->buf_len, space);
} else if (coco_sched_get_current() != NULL) {
n = coco_read(conn->fd, conn->buf + conn->buf_len, space); n = coco_read(conn->fd, conn->buf + conn->buf_len, space);
} else { } else {
/* 无调度器时直接 read */ /* 无调度器时直接 read */
@ -156,7 +166,9 @@ static int conn_read_body(connection_t *conn, http_request_t *req, size_t need)
/* 从 socket 读取剩余数据 */ /* 从 socket 读取剩余数据 */
while (got < need) { while (got < need) {
ssize_t n; ssize_t n;
if (coco_sched_get_current() != NULL) { if (tls_has_connection(conn->fd)) {
n = tls_read(conn->fd, req->body + got, need - got);
} else if (coco_sched_get_current() != NULL) {
n = coco_read(conn->fd, req->body + got, need - got); n = coco_read(conn->fd, req->body + got, need - got);
} else { } else {
n = cocoon_socket_recv(conn->fd, req->body + got, need - got); n = cocoon_socket_recv(conn->fd, req->body + got, need - got);
@ -322,6 +334,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
@ -337,7 +350,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;
} }
@ -351,7 +367,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;
} }
} }
@ -359,13 +379,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;
} }
@ -380,7 +406,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;
} }
@ -390,7 +419,10 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
char resolved[4096]; char resolved[4096];
if (!cocoon_realpath(real_path, resolved, sizeof(resolved)) || if (!cocoon_realpath(real_path, resolved, sizeof(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;
} }
@ -400,7 +432,10 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
/* 判断文件类型 */ /* 判断文件类型 */
cocoon_stat_t st; cocoon_stat_t st;
if (cocoon_file_stat(real_path, &st) != 0) { if (cocoon_file_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;
} }
@ -409,7 +444,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;
} }
@ -418,22 +456,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 (cocoon_stat_isreg(&st)) { } else if (cocoon_stat_isreg(&st)) {
/* 普通文件 */ /* 普通文件 */
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;
} }
@ -498,36 +545,267 @@ static void conn_cancel_timer(connection_t *conn) {
* *
* @param arg connection_t* * @param arg connection_t*
*/ */
/**
* handle_http2 - HTTP/2
*
* 使 nghttp2 HTTP/2
*
* @param conn
*/
static void handle_http2(connection_t *conn) {
http2_session_t *h2 = http2_session_get(conn->fd);
if (!h2) return;
http2_session_set_context(h2, conn->root_dir, conn->gzip_enabled, conn->brotli_enabled);
uint32_t timeout_ms = conn->timeout_ms > 0 ? conn->timeout_ms : CONN_TIMEOUT_MS;
while (!conn->closed && http2_want_read(h2)) {
/* 读取数据 */
char buf[8192];
ssize_t n;
if (tls_has_connection(conn->fd)) {
n = tls_read(conn->fd, buf, sizeof(buf));
} else if (coco_sched_get_current() != NULL) {
n = coco_read(conn->fd, buf, sizeof(buf));
} else {
n = read(conn->fd, buf, sizeof(buf));
}
if (n > 0) {
conn_reset_timer(conn, timeout_ms);
if (http2_recv(h2, (const uint8_t *)buf, (size_t)n) != 0) {
log_debug("HTTP/2 接收错误 fd=%d", conn->fd);
break;
}
} else if (n == 0) {
break; /* 对端关闭 */
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
continue;
}
break;
}
/* 发送挂起的帧 */
if (http2_want_write(h2)) {
if (http2_send_pending(h2) != 0) {
break;
}
}
}
http2_session_destroy(h2);
}
/* HTTP/2 h2c 连接前言24字节 */
static const char H2C_PREFACE[] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
#define H2C_PREFACE_LEN 24
/**
* is_h2c_upgrade_request - HTTP/1.1 Upgrade: h2c
*
* @param req HTTP
* @return true h2c
*/
static bool is_h2c_upgrade_request(const http_request_t *req) {
if (strcmp(req->version, "HTTP/1.1") != 0) return false;
bool has_upgrade = false;
bool has_connection_upgrade = false;
for (int i = 0; i < req->num_headers; i++) {
if (strcasecmp(req->headers[i].name, "upgrade") == 0) {
if (strcasestr(req->headers[i].value, "h2c") != NULL) {
has_upgrade = true;
}
} else if (strcasecmp(req->headers[i].name, "connection") == 0) {
if (strcasestr(req->headers[i].value, "upgrade") != NULL) {
has_connection_upgrade = true;
}
}
}
return has_upgrade && has_connection_upgrade;
}
/**
* is_websocket_upgrade_request - WebSocket Upgrade
*
* Upgrade: websocketConnection: Upgrade Sec-WebSocket-Key
*
* @param req HTTP
* @return true WebSocket
*/
static bool is_websocket_upgrade_request(const http_request_t *req) {
if (strcmp(req->version, "HTTP/1.1") != 0) return false;
if (req->method != HTTP_GET) return false;
bool has_upgrade = false;
bool has_connection_upgrade = false;
const char *key = NULL;
for (int i = 0; i < req->num_headers; i++) {
if (strcasecmp(req->headers[i].name, "upgrade") == 0) {
if (strcasestr(req->headers[i].value, "websocket") != NULL) {
has_upgrade = true;
}
} else if (strcasecmp(req->headers[i].name, "connection") == 0) {
if (strcasestr(req->headers[i].value, "upgrade") != NULL) {
has_connection_upgrade = true;
}
} else if (strcasecmp(req->headers[i].name, "sec-websocket-key") == 0) {
key = req->headers[i].value;
}
}
return has_upgrade && has_connection_upgrade && key != NULL;
}
/**
* send_h2c_upgrade_response - 101 Switching Protocols
*
* @param conn
* @return true
*/
static bool send_h2c_upgrade_response(connection_t *conn) {
const char *resp =
"HTTP/1.1 101 Switching Protocols\r\n"
"Connection: Upgrade\r\n"
"Upgrade: h2c\r\n"
"\r\n";
return send_all(conn->fd, resp, strlen(resp)) == 0;
}
static void client_handler(void *arg) { static void client_handler(void *arg) {
connection_t *conn = (connection_t *)arg; connection_t *conn = (connection_t *)arg;
if (!conn) return; if (!conn) return;
conn->coro = coco_self(); conn->coro = coco_self();
log_debug("client_handler 启动 fd=%d", conn->fd);
log_debug("fd=%d 检查 HTTP/2...", conn->fd);
/* 检查是否是 HTTP/2 连接TLS ALPN 协商) */
if (http2_session_is_http2(conn->fd)) {
log_debug("fd=%d 是 HTTP/2 连接", conn->fd);
handle_http2(conn);
conn_cancel_timer(conn);
close_connection(conn);
free(conn);
return;
}
log_debug("fd=%d 不是 HTTP/2", conn->fd);
/* 启动空闲定时器 */ /* 启动空闲定时器 */
log_debug("fd=%d 创建定时器...", conn->fd);
uint32_t timeout_ms = conn->timeout_ms > 0 ? conn->timeout_ms : CONN_TIMEOUT_MS; uint32_t timeout_ms = conn->timeout_ms > 0 ? conn->timeout_ms : CONN_TIMEOUT_MS;
conn->timer = coco_timer(timeout_ms, conn_timeout_handler, conn); conn->timer = coco_timer(timeout_ms, conn_timeout_handler, conn);
log_debug("fd=%d 定时器已创建 timer=%p", conn->fd, (void*)conn->timer);
while (!conn->closed) { while (!conn->closed) {
/* 读取数据 */ /* 读取数据 */
ssize_t n = conn_read(conn); ssize_t n = conn_read(conn);
log_debug("fd=%d conn_read 返回 %zd", conn->fd, n);
if (n < 0) { if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) { if (errno == EAGAIN || errno == EWOULDBLOCK) {
/* 数据不足,继续等待 */ log_debug("fd=%d EAGAIN继续等待", conn->fd);
continue; continue;
} }
/* coco_read 返回负值错误码(如 COCO_ERROR_CANCELLED或系统错误 */ /* coco_read 返回负值错误码(如 COCO_ERROR_CANCELLED或系统错误 */
log_debug("fd=%d conn_read 错误: %d %s", conn->fd, (int)n, strerror(errno));
break; break;
} }
if (n == 0) { if (n == 0) {
log_debug("fd=%d 对端关闭", conn->fd);
break; /* 对端关闭 */ break; /* 对端关闭 */
} }
/* 有数据读取,重置定时器 */ /* 有数据读取,重置定时器 */
conn_reset_timer(conn, timeout_ms); conn_reset_timer(conn, timeout_ms);
/* 检查 h2c 直接连接前言 */
if (conn->buf_len >= H2C_PREFACE_LEN &&
memcmp(conn->buf, H2C_PREFACE, H2C_PREFACE_LEN) == 0) {
log_debug("fd=%d 检测到 h2c 直接连接前言", conn->fd);
http2_session_t *h2 = http2_session_create(conn->fd, false);
if (h2) {
http2_session_set_context(h2, conn->root_dir,
conn->gzip_enabled,
conn->brotli_enabled);
if (http2_send_pending(h2) == 0) {
if (http2_recv(h2, (const uint8_t *)conn->buf, conn->buf_len) == 0) {
conn->buf_len = 0;
handle_http2(conn);
break; /* handle_http2 内部已销毁会话 */
}
}
http2_session_destroy(h2);
}
break;
}
/* 检查 HTTP/1.1 Upgrade: h2c 请求 */
http_request_t req;
int parsed = http_parse_request(conn->buf, conn->buf_len, &req);
if (parsed > 0 && is_h2c_upgrade_request(&req)) {
/* 消费已解析的 HTTP/1.1 请求数据 */
if ((size_t)parsed < conn->buf_len) {
memmove(conn->buf, conn->buf + parsed, conn->buf_len - (size_t)parsed);
}
conn->buf_len -= (size_t)parsed;
if (send_h2c_upgrade_response(conn)) {
http2_session_t *h2 = http2_session_create(conn->fd, false);
if (h2) {
http2_session_set_context(h2, conn->root_dir,
conn->gzip_enabled,
conn->brotli_enabled);
if (http2_send_pending(h2) == 0) {
if (http2_session_upgrade(h2, &req) == 0) {
if (http2_send_pending(h2) == 0) {
/* 消费缓冲区中的剩余数据 */
if (conn->buf_len > 0) {
http2_recv(h2, (const uint8_t *)conn->buf, conn->buf_len);
conn->buf_len = 0;
}
handle_http2(conn);
break; /* handle_http2 内部已销毁会话 */
}
}
http2_session_destroy(h2);
}
}
}
http_request_free(&req);
break;
}
/* 检查 WebSocket Upgrade 请求 */
if (parsed > 0 && is_websocket_upgrade_request(&req)) {
const char *key = NULL;
for (int i = 0; i < req.num_headers; i++) {
if (strcasecmp(req.headers[i].name, "sec-websocket-key") == 0) {
key = req.headers[i].value;
break;
}
}
if (key) {
conn_cancel_timer(conn);
if (ws_handshake(conn->fd, key) == 0) {
http_request_free(&req);
/* 消费已解析的请求数据 */
if ((size_t)parsed < conn->buf_len) {
memmove(conn->buf, conn->buf + parsed, conn->buf_len - (size_t)parsed);
}
conn->buf_len -= (size_t)parsed;
ws_handle_connection(conn->fd, conn->timeout_ms);
break;
}
}
}
http_request_free(&req);
/* 尝试处理请求 */ /* 尝试处理请求 */
bool keep = handle_request(conn, conn->root_dir); bool keep = handle_request(conn, conn->root_dir);
log_debug("fd=%d handle_request 返回 %d", conn->fd, keep);
if (!keep) { if (!keep) {
break; break;
} }
@ -535,6 +813,7 @@ static void client_handler(void *arg) {
conn_cancel_timer(conn); conn_cancel_timer(conn);
close_connection(conn); close_connection(conn);
log_debug("client_handler 结束 fd=%d", conn->fd);
free(conn); free(conn);
} }
@ -547,6 +826,7 @@ static void client_handler(void *arg) {
* @param arg * @param arg
*/ */
static void accept_loop(void *arg) { static void accept_loop(void *arg) {
log_debug("=== accept_loop 启动 ===");
server_context_t *ctx = (server_context_t *)arg; server_context_t *ctx = (server_context_t *)arg;
if (!ctx) return; if (!ctx) return;
@ -570,14 +850,14 @@ 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; if (ctx->listen_fd < 0) break;
struct sockaddr_storage client_addr;
socklen_t addr_len = sizeof(client_addr); socklen_t addr_len = sizeof(client_addr);
cocoon_socket_t client_fd = accept(ctx->listen_fd, cocoon_socket_t client_fd = accept(ctx->listen_fd,
(struct sockaddr *)&client_addr, &addr_len); (struct sockaddr *)&client_addr, &addr_len);
if (client_fd == COCOON_INVALID_SOCKET) { if (client_fd == COCOON_INVALID_SOCKET) {
int err = cocoon_get_last_error(); int err = cocoon_get_last_error();
if (err == EAGAIN || err == EWOULDBLOCK || err == EINTR) { if (err == EAGAIN || err == EINTR) {
continue; continue;
} }
log_error("accept 失败: %s", cocoon_strerror(err)); log_error("accept 失败: %s", cocoon_strerror(err));
@ -594,8 +874,16 @@ static void accept_loop(void *arg) {
const char *resp = "HTTP/1.1 503 Service Unavailable\r\n" const char *resp = "HTTP/1.1 503 Service Unavailable\r\n"
"Content-Length: 0\r\n" "Content-Length: 0\r\n"
"Connection: close\r\n\r\n"; "Connection: close\r\n\r\n";
if (tls_has_context()) {
if (tls_accept(client_fd) == 0) {
tls_write(client_fd, resp, strlen(resp));
tls_close(client_fd);
}
cocoon_socket_close(client_fd);
} else {
cocoon_socket_send(client_fd, resp, strlen(resp)); cocoon_socket_send(client_fd, resp, strlen(resp));
cocoon_socket_close(client_fd); cocoon_socket_close(client_fd);
}
continue; continue;
} }
} }
@ -604,10 +892,41 @@ static void accept_loop(void *arg) {
int opt = 1; int opt = 1;
setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, (const char *)&opt, sizeof(opt)); setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, (const char *)&opt, sizeof(opt));
/* 多线程模式下设置 client_fd 非阻塞(用于 coco_read/coco_write */
if (ctx->config.threaded) {
set_nonblocking(client_fd);
}
/* TLS 握手(如果启用) */
if (tls_has_context()) {
if (tls_accept(client_fd) != 0) {
log_warn("TLS 握手失败 fd=%d关闭连接", client_fd);
cocoon_socket_close(client_fd);
continue;
}
/* 检查 ALPN 协商结果,创建 HTTP/2 会话 */
if (tls_negotiated_http2(client_fd)) {
log_debug("fd=%d ALPN 协商为 HTTP/2", client_fd);
if (http2_on_connection_accepted(client_fd, true) != 0) {
log_warn("HTTP/2 会话创建失败 fd=%d", client_fd);
cocoon_socket_close(client_fd);
continue;
}
http2_session_t *h2 = http2_session_get(client_fd);
if (h2) {
http2_session_set_context(h2, ctx->config.root_dir,
ctx->config.gzip_enabled,
ctx->config.brotli_enabled);
}
}
}
/* 创建连接上下文 */ /* 创建连接上下文 */
connection_t *conn = (connection_t *)calloc(1, sizeof(connection_t)); connection_t *conn = (connection_t *)calloc(1, sizeof(connection_t));
if (!conn) { if (!conn) {
close(client_fd); http2_session_t *h2 = http2_session_get(client_fd);
if (h2) http2_session_destroy(h2);
cocoon_socket_close(client_fd);
continue; continue;
} }
conn->fd = client_fd; conn->fd = client_fd;
@ -617,21 +936,33 @@ 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,
atomic_load(&g_active_connections)); atomic_load(&g_active_connections));
if (ctx->config.threaded && coco_sched_get_current()) { if (ctx->config.threaded) {
/* 多线程协程模式:创建协程处理连接 */ /* 多线程模式:使用 coco_go_with_opts 创建工作协程128KB 栈 */
coco_coro_t *coro = coco_create(coco_sched_get_current(), coco_go_opts_t opts = {
client_handler, conn, 0); .stack_size = 1024 * 1024,
.context = NULL,
.priority = -1,
.p_id = -1
};
coco_coro_t *coro = coco_go_with_opts(client_handler, conn, &opts);
if (!coro) { if (!coro) {
log_error("创建协程失败,关闭连接 fd=%d", client_fd); log_error("coco_go_with_opts(client_handler) 返回 NULL无法创建协程 fd=%d", client_fd);
atomic_fetch_sub(&g_active_connections, 1); http2_session_t *h2 = http2_session_get(client_fd);
close_connection(conn); if (h2) http2_session_destroy(h2);
cocoon_socket_close(client_fd);
free(conn); free(conn);
atomic_fetch_sub(&g_active_connections, 1);
continue;
} }
log_debug("client_handler 协程已创建 coro=%p fd=%d", (void*)coro, client_fd);
} else { } else {
/* 单线程模式:直接调用处理函数(阻塞) */ /* 单线程模式:直接调用处理函数(阻塞) */
client_handler(conn); client_handler(conn);
@ -689,8 +1020,15 @@ server_context_t *server_create(const cocoon_config_t *config) {
return NULL; return NULL;
} }
/* 非阻塞模式 */ /* 初始化 TLS 上下文(如果配置了证书) */
set_nonblocking(ctx->listen_fd); if (ctx->config.tls_cert && ctx->config.tls_key) {
if (tls_create_context(ctx->config.tls_cert, ctx->config.tls_key) != 0) {
log_error("TLS 上下文初始化失败");
close(ctx->listen_fd);
free(ctx);
return NULL;
}
}
return ctx; return ctx;
} }
@ -721,14 +1059,17 @@ int server_start(server_context_t *ctx) {
return COCOON_ERROR; return COCOON_ERROR;
} }
/* 在调度器上运行 accept 循环 */ /* 多线程模式下 listen_fd 需要非阻塞(用于 accept + poll 等待) */
/* 注意:当前实现使用主线程直接 accept多线程仅用于工作协程 */ set_nonblocking(ctx->listen_fd);
/* 在主线程中运行 accept_loop */
accept_loop(ctx); accept_loop(ctx);
/* 等待所有协程完成 */
coco_global_sched_wait(); coco_global_sched_wait();
coco_global_sched_stop(); coco_global_sched_stop();
} else { } else {
/* 单线程模式 */ /* 单线程模式listen_fd 保持阻塞 */
accept_loop(ctx); accept_loop(ctx);
} }
@ -745,6 +1086,10 @@ int server_start(server_context_t *ctx) {
void server_stop(server_context_t *ctx) { void server_stop(server_context_t *ctx) {
if (ctx) { if (ctx) {
ctx->running = 0; ctx->running = 0;
if (ctx->listen_fd >= 0) {
close(ctx->listen_fd);
ctx->listen_fd = -1;
}
} }
} }
@ -758,6 +1103,8 @@ void server_stop(server_context_t *ctx) {
void server_destroy(server_context_t *ctx) { void server_destroy(server_context_t *ctx) {
if (!ctx) return; if (!ctx) return;
tls_destroy_context();
if (ctx->listen_fd != COCOON_INVALID_SOCKET) { if (ctx->listen_fd != COCOON_INVALID_SOCKET) {
cocoon_socket_close(ctx->listen_fd); cocoon_socket_close(ctx->listen_fd);
ctx->listen_fd = COCOON_INVALID_SOCKET; ctx->listen_fd = COCOON_INVALID_SOCKET;

View File

@ -11,6 +11,8 @@
#include "static.h" #include "static.h"
#include "cocoon.h" #include "cocoon.h"
#include "platform.h" #include "platform.h"
#include "../coco/include/coco.h"
#include "tls.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -235,9 +237,27 @@ static bool safe_path_join(char *dst, size_t dst_size,
* @return 0 ?1 * @return 0 ?1
*/ */
int send_all(int fd, const char *buf, size_t len) { int send_all(int fd, const char *buf, size_t len) {
if (tls_has_connection(fd)) {
return tls_write(fd, buf, len) == (ssize_t)len ? 0 : -1;
}
size_t sent = 0; size_t sent = 0;
while (sent < len) { while (sent < len) {
ssize_t n = cocoon_socket_send(fd, buf + sent, len - sent); ssize_t n;
if (tls_has_connection(fd)) {
n = tls_write(fd, buf + sent, len - sent);
} else if (coco_sched_get_current() != NULL) {
int ret = coco_write(fd, buf + sent, len - sent);
if (ret < 0) {
if (ret == COCO_ERROR_WOULD_BLOCK) {
continue;
}
return -1;
}
n = ret;
} else {
n = cocoon_socket_send(fd, buf + sent, len - sent);
}
if (n < 0) { if (n < 0) {
int err = cocoon_get_last_error(); int err = cocoon_get_last_error();
if (err == EAGAIN || err == EINTR) continue; if (err == EAGAIN || err == EINTR) continue;
@ -467,10 +487,25 @@ int static_serve_file(int fd, const http_request_t *req, const char *root_dir, b
return COCOON_OK; return COCOON_OK;
} }
if (use_gzip || use_brotli || tls_has_connection(fd)) {
/* 发送压缩后的数据,或 TLS 模式下的文件内容 */
if (use_gzip || use_brotli) { if (use_gzip || use_brotli) {
/* 鍙戦€佸帇缂╁悗鐨勬暟鎹?*/
send_all(fd, compress_buf, (size_t)compress_len); send_all(fd, compress_buf, (size_t)compress_len);
free(compress_buf); free(compress_buf);
} else {
/* 定位到起始位置(文件可能已被压缩读取提前读至末尾) */
cocoon_file_seek(file_fd, send_start, SEEK_SET);
/* TLS 模式:不能使用 sendfile需读取文件后发送 */
char file_buf[65536];
ssize_t remaining = send_length;
while (remaining > 0) {
size_t to_read = (size_t)remaining < sizeof(file_buf) ? (size_t)remaining : sizeof(file_buf);
ssize_t n = cocoon_file_read(file_fd, file_buf, to_read);
if (n <= 0) break;
if (send_all(fd, file_buf, (size_t)n) != 0) break;
remaining -= n;
}
}
cocoon_file_close(file_fd); cocoon_file_close(file_fd);
} else { } else {
/* 浣跨敤璺ㄥ钩鍙版枃浠跺彂閫侊紙Linux sendfile / Windows read+send锛?*/ /* 浣跨敤璺ㄥ钩鍙版枃浠跺彂閫侊紙Linux sendfile / Windows read+send锛?*/

View File

@ -18,7 +18,9 @@ TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"; kill_server' EXIT trap 'rm -rf "$TMPDIR"; kill_server' EXIT
kill_server() { kill_server() {
pkill -f "$SERVER.*$HOST" 2>/dev/null || true pkill -f "cocoon.*-p 9999" 2>/dev/null || true
pkill -f "cocoon.*--cert" 2>/dev/null || true
sleep 0.5
} }
# 启动服务器 # 启动服务器
@ -145,7 +147,7 @@ assert_body_contains() {
local expect="$2" local expect="$2"
local desc="${3:-$url}" local desc="${3:-$url}"
local body local body
body=$(curl -s "$url") body=$(curl -s -k --compressed "$url")
if echo "$body" | grep -q "$expect"; then if echo "$body" | grep -q "$expect"; then
echo "$desc — 响应体包含 '$expect'" echo "$desc — 响应体包含 '$expect'"
pass pass
@ -314,6 +316,48 @@ assert_post_form() {
fi fi
} }
assert_http2_brotli() {
local url="$1"
local desc="${2:-$url}"
local encoding
encoding=$(curl --http2 -k -s -D - -o /dev/null "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
if echo "$encoding" | grep -qi "br"; then
echo "$desc — HTTP/2 返回 brotli 压缩"
pass
else
echo "$desc — HTTP/2 期望 brotli 压缩, 实际: $encoding"
fail
fi
}
assert_http2_gzip() {
local url="$1"
local desc="${2:-$url}"
local encoding
encoding=$(curl --http2 -k -s -D - -o /dev/null "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
if echo "$encoding" | grep -qi "gzip"; then
echo "$desc — HTTP/2 返回 gzip 压缩"
pass
else
echo "$desc — HTTP/2 期望 gzip 压缩, 实际: $encoding"
fail
fi
}
assert_http2_not_compressed() {
local url="$1"
local desc="${2:-$url}"
local encoding
encoding=$(curl --http2 -k -s -D - -o /dev/null "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
if [[ -z "$encoding" ]]; then
echo "$desc — HTTP/2 无压缩"
pass
else
echo "$desc — HTTP/2 期望无压缩, 实际: $encoding"
fail
fi
}
assert_status_405() { assert_status_405() {
local url="$1" local url="$1"
local desc="${2:-$url}" local desc="${2:-$url}"
@ -328,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
@ -411,6 +474,267 @@ 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 "=== TLS/HTTPS 测试 ==="
# 启动 HTTPS 服务器(使用测试证书)
kill_server
sleep 1
$SERVER -r "$ROOT" -p 9999 --cert tests/server.crt --key tests/server.key > "$TMPDIR/server_tls.log" 2>&1 &
pid_tls=$!
# 等待服务器就绪curl -k 忽略证书验证)
for i in {1..50}; do
if curl -s -o /dev/null -k --max-time 2 "https://$HOST/" 2>/dev/null; then
break
fi
sleep 0.2
done
# 使用 openssl s_client 验证 TLS 握手
if command -v openssl >/dev/null 2>&1; then
tls_resp=$(echo -e "GET / HTTP/1.1\r\nHost: $HOST\r\nConnection: close\r\n\r\n" | \
timeout 5 openssl s_client -connect "$HOST" -quiet 2>/dev/null | head -20)
if echo "$tls_resp" | grep -q "HTTP/1.1"; then
echo " ✓ TLS 握手 + HTTP GET — 通过 HTTPS 获取响应"
pass
else
echo " ✗ TLS 握手 + HTTP GET — 未能通过 HTTPS 获取响应"
fail
fi
else
echo " ⚠ openssl 未安装,跳过 TLS 握手验证"
fi
# 使用 curl -k 验证 HTTPS 响应体
assert_body_contains "https://$HOST/" "Cocoon" "HTTPS 首页响应"
echo ""
echo "=== HTTP/2 测试 ==="
# 使用已启动的 TLS 服务器测试 HTTP/2ALPN 协商)
kill_server
sleep 1
$SERVER -r "$ROOT" -p 9999 --cert tests/server.crt --key tests/server.key > "$TMPDIR/server_h2.log" 2>&1 &
pid_h2=$!
# 等待服务器就绪
for i in {1..50}; do
if curl -s -o /dev/null -k --max-time 2 "https://$HOST/" 2>/dev/null; then
break
fi
sleep 0.2
done
# 验证 HTTP/2 协议版本
h2_version=$(curl --http2 -k -s -o /dev/null -w "%{http_version}" "https://$HOST/")
if [[ "$h2_version" == "2" ]]; then
echo " ✓ HTTP/2 ALPN 协商 — 协议版本 HTTP/2"
pass
else
echo " ✗ HTTP/2 ALPN 协商 — 期望 HTTP/2, 实际 HTTP/$h2_version"
fail
fi
# HTTP/2 GET 首页
h2_status=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" "https://$HOST/")
if [[ "$h2_status" == "200" ]]; then
echo " ✓ HTTP/2 GET 首页 — HTTP 200"
pass
else
echo " ✗ HTTP/2 GET 首页 — 期望 200, 实际 $h2_status"
fail
fi
# HTTP/2 响应体
h2_body=$(curl --http2 -k -s --compressed "https://$HOST/")
if echo "$h2_body" | grep -q "Cocoon"; then
echo " ✓ HTTP/2 响应体 — 包含 'Cocoon'"
pass
else
echo " ✗ HTTP/2 响应体 — 未包含 'Cocoon'"
fail
fi
# HTTP/2 HEAD 请求
h2_head_status=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" -I "https://$HOST/index.html")
if [[ "$h2_head_status" == "200" ]]; then
echo " ✓ HTTP/2 HEAD 请求 — HTTP 200"
pass
else
echo " ✗ HTTP/2 HEAD 请求 — 期望 200, 实际 $h2_head_status"
fail
fi
# HTTP/2 404
h2_404=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" "https://$HOST/nonexist.html")
if [[ "$h2_404" == "404" ]]; then
echo " ✓ HTTP/2 404 请求 — HTTP 404"
pass
else
echo " ✗ HTTP/2 404 请求 — 期望 404, 实际 $h2_404"
fail
fi
# HTTP/2 缓存协商ETag 304
H2_ETAG=$(curl --http2 -k -sI "https://$HOST/index.html" | grep -i "ETag:" | awk '{print $2}' | tr -d '\r')
if [[ -n "$H2_ETAG" ]]; then
h2_304=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" -H "If-None-Match: $H2_ETAG" "https://$HOST/index.html")
if [[ "$h2_304" == "304" ]]; then
echo " ✓ HTTP/2 If-None-Match 304 — 缓存命中"
pass
else
echo " ✗ HTTP/2 If-None-Match 304 — 期望 304, 实际 $h2_304"
fail
fi
fi
# HTTP/2 压缩测试
assert_http2_brotli "https://$HOST/index.html" "HTTP/2 HTML brotli"
assert_http2_brotli "https://$HOST/style.css" "HTTP/2 CSS brotli"
assert_http2_brotli "https://$HOST/app.js" "HTTP/2 JS brotli"
# HTTP/2 目录浏览测试
h2_dir_status=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" "https://$HOST/subdir/")
h2_dir_body=$(curl --http2 -k -s "https://$HOST/subdir/")
if [[ "$h2_dir_status" == "200" ]] && echo "$h2_dir_body" | grep -q "page.html"; then
echo " ✓ HTTP/2 目录浏览 — HTTP 200包含 page.html"
pass
else
echo " ✗ HTTP/2 目录浏览 — 期望 200+page.html, 实际 $h2_dir_status"
fail
fi
# 停止 HTTP/2 服务器,恢复 HTTP 服务器
kill_server
sleep 1
start_server
# h2c 测试(明文 HTTP/2
echo ""
echo "=== h2c 测试 ==="
# h2c prior knowledge 直接连接
h2c_pk_status=$(curl --http2-prior-knowledge -s -o /dev/null -w "%{http_code}" "http://$HOST/")
h2c_pk_version=$(curl --http2-prior-knowledge -s -o /dev/null -w "%{http_version}" "http://$HOST/")
if [[ "$h2c_pk_status" == "200" && "$h2c_pk_version" == "2" ]]; then
echo " ✓ h2c prior knowledge — HTTP 200, 协议 HTTP/2"
pass
else
echo " ✗ h2c prior knowledge — 期望 200+HTTP/2, 实际 $h2c_pk_status+HTTP/$h2c_pk_version"
fail
fi
# h2c prior knowledge 404
h2c_pk_404=$(curl --http2-prior-knowledge -s -o /dev/null -w "%{http_code}" "http://$HOST/nonexist.html")
if [[ "$h2c_pk_404" == "404" ]]; then
echo " ✓ h2c prior knowledge 404 — HTTP 404"
pass
else
echo " ✗ h2c prior knowledge 404 — 期望 404, 实际 $h2c_pk_404"
fail
fi
# h2c Upgrade 协商
h2c_up_status=$(curl --http2 -s -o /dev/null -w "%{http_code}" "http://$HOST/")
h2c_up_version=$(curl --http2 -s -o /dev/null -w "%{http_version}" "http://$HOST/")
if [[ "$h2c_up_status" == "200" && "$h2c_up_version" == "2" ]]; then
echo " ✓ h2c Upgrade 协商 — HTTP 200, 协议 HTTP/2"
pass
else
echo " ✗ h2c Upgrade 协商 — 期望 200+HTTP/2, 实际 $h2c_up_status+HTTP/$h2c_up_version"
fail
fi
# h2c Upgrade 404
h2c_up_404=$(curl --http2 -s -o /dev/null -w "%{http_code}" "http://$HOST/nonexist.html")
if [[ "$h2c_up_404" == "404" ]]; then
echo " ✓ h2c Upgrade 404 — HTTP 404"
pass
else
echo " ✗ h2c Upgrade 404 — 期望 404, 实际 $h2c_up_404"
fail
fi
# HTTP/2 目录浏览测试
# subdir 目录没有 index.html应返回 200 目录列表
h2c_dir_status=$(curl --http2-prior-knowledge -s -o /dev/null -w "%{http_code}" "http://$HOST/subdir/")
h2c_dir_body=$(curl --http2-prior-knowledge -s "http://$HOST/subdir/")
if [[ "$h2c_dir_status" == "200" ]] && echo "$h2c_dir_body" | grep -q "page.html"; then
echo " ✓ h2c 目录浏览 — HTTP 200包含 page.html"
pass
else
echo " ✗ h2c 目录浏览 — 期望 200+page.html, 实际 $h2c_dir_status"
fail
fi
# WebSocket 测试
echo ""
echo "=== WebSocket 测试 ==="
if python3 "$ROOT/../websocket_test.py" > "$TMPDIR/ws_test.log" 2>&1; then
echo " ✓ WebSocket 握手 + echo — 通过"
pass
pass
else
echo " ✗ WebSocket 测试失败"
cat "$TMPDIR/ws_test.log"
fail
fail
fi
echo "" echo ""
echo "=== 结果汇总 ===" echo "=== 结果汇总 ==="
echo "通过: $PASS" echo "通过: $PASS"

19
tests/server.crt Normal file
View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUPVEDY05Y47B5Xs305idz++X3wrgwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDYwNDA0MTkzM1oXDTI2MDYw
NTA0MTkzM1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEArRpnw9qH9mw3Px8FvRhgyVqMA1B0sJTlareiHNnIh9kP
W2q2M0alzPiOllysEcEQn//FPPh/cH6gXhsQziNTRK7VPrkCV8+4jV/FbdG74WJx
GMliemPwUvOKf+oLU/OFuyLvbze3VG+Wcwg/tdUxRUwHwu9nDj1F0nCbtGQBV+m3
GS0diluKyYWE2rn54H6F1tnJcNNnfPGXptvFGKgI/6HVret28VYX9iMILy4UnA4H
j7p8+MMyhDXqOi9y+nSPTXM2HaRUSgA0tWxlHsR9gE75+3bNRVU+3GzJaM6lDxzf
jAHQgGWHwibCPJFX4eAgJseromlQKKAtLdToa4ngjwIDAQABo1MwUTAdBgNVHQ4E
FgQUTPTOzkZPBbV1xHfZmPCxv4CQ7LUwHwYDVR0jBBgwFoAUTPTOzkZPBbV1xHfZ
mPCxv4CQ7LUwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEADBVC
KECbcbb8rBPmZkh2sJDtuXnW403+1Tg/YsUoSpLxa8Jq35JWfMR5fnmCAsaOxuNQ
rFXNotVxLhaMK7IwIsyazdRkRAT4SLLLx5bt3p6BY4L9C/SUVlSK0SC1E39AGPB/
vwce9xB4wmUj3o+zcn09r9ARhnr4ccMaOaHiX/opd6T+62XKoer/tSTvx31t44gW
o5IPz7dk20g379jb1PTnq5lJWD1E1ttlRbETlivuMOwg3GjVWRc7smk7AlBnsems
LzyrpySsvAsY/mLbwSJfNlSd00T/VV8WFUCkr/pm0l7WWPx2nHrUu9t4YjJ0UHY/
UQNisxe3vGPxoTpD+A==
-----END CERTIFICATE-----

28
tests/server.key Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCtGmfD2of2bDc/
HwW9GGDJWowDUHSwlOVqt6Ic2ciH2Q9barYzRqXM+I6WXKwRwRCf/8U8+H9wfqBe
GxDOI1NErtU+uQJXz7iNX8Vt0bvhYnEYyWJ6Y/BS84p/6gtT84W7Iu9vN7dUb5Zz
CD+11TFFTAfC72cOPUXScJu0ZAFX6bcZLR2KW4rJhYTaufngfoXW2clw02d88Zem
28UYqAj/odWt63bxVhf2IwgvLhScDgePunz4wzKENeo6L3L6dI9NczYdpFRKADS1
bGUexH2ATvn7ds1FVT7cbMlozqUPHN+MAdCAZYfCJsI8kVfh4CAmx6uiaVAooC0t
1OhrieCPAgMBAAECggEAAOeW7DybokuNfFx2pLbYZqT8/1Vvzq5whn7AL6Npu2AE
hwdHPXxciHYyFJIWah9WrWdiSf/IdreKsgM1MsaXfE/nmGTSINBamA6MkxbaAwMH
MUh4JciY7G4OZr274mvu4nv7wVqKsDvHDwHXh2U+VSzhC4FByh7C2ycwgSBWq3Vw
hm6D/8QOU7ZkIkaMWGc82KgjAXm8jrG2K5k0WWw5IDItQmX7M/ZYN5FS7ArXtbqw
prlr1QdIWQvi/UtG0MQ2ys5ZRqbDTTfwAw06oIhq7e6zqMEjCD9G/WLGwqkRhe86
hVcZ2DM01ge7Pva9XKNxIuh7DMrFnacAE3nnb1w0iQKBgQDZrfpllwKDIGNSauVD
rejbstJDocMlWhuQsj2r+29nnFCqS8OEmG8E9r0uDZpCTUWzY2EgYb8cvTaFI7vs
3owH1pF9nZRr1NZFeli4EuNEJFF10Qf8wp3mwTXweBckDW6ZwWfkgOKWJhAB9Eg7
7GFlpAzgiJmeTa5ztCQpLqwcBwKBgQDLk4ea9quNoF/54U50dWs/Vb9F0X+aYupP
OdmPHYpwfaAO7KCVVSE9qufqTRrrqssIvstODWZ3clxszQC1iiJpjTMhhCTfIOW+
p6ZqU/mLUQ3kPsQuxoCagcn8HhCvuD4wcBG/rIHLr9iBuYjFuFnl+XxpuqUowXG9
mpfGJBiFOQKBgCzFKpXQXDTp6WOfFq43y4e8HKDKWV+KS9cTwbloij4uGS0dTgYs
b9D+imQ5afuu9uHxU52cXPklNVxmwBT0pKpMCDpkuY0ABl3dzvd08wclzRdFObZT
tLQsEtL1cBT7Kj83Vw3ZMEEfbPVp751bfaCw//ECR04WvgWQDEvoDH/rAoGBAJEe
g+ZlvPliv8gjYogGRAOMHqOoqivmI12CwaIRzuRNyVGaQQby/pU1RclyWaRxxfZf
/BGR3996OISexsiixdE0hKR5zMb6bowweqMFZbh4eUyhqbwmaPq7Vr9Qt1hsIHjr
Ctv2HziSC+a92dAIkJ0t8hB0qtOYwnoEv/jRNmGRAoGBAMUo7zXuvPi11es6L/1S
kzh00RV7JdSOZ7cyEdzaGBMeMRpAFrjoy5kiXUj2mPuESqd3FiTR9dgYHrZBirfI
mmCQxp8PZ5f+G2M5JbRslZfinPL7ePX/fijJ1kWMUGVuk3502tf5HjgE9k1/i0He
TIi3EoBZvP9v9Y1ZUom5IFct
-----END PRIVATE KEY-----

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);
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);
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); 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); 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); config_merge(NULL, NULL, true, true, true, true, true, true, true, true, true, true, true, true);
TEST_ASSERT_TRUE(1); TEST_ASSERT_TRUE(1);
} }

View File

@ -2,6 +2,7 @@
#include "server.h" #include "server.h"
#include "config.h" #include "config.h"
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
/* ===== server_create 参数校验 ===== */ /* ===== server_create 参数校验 ===== */
@ -15,6 +16,132 @@ void test_create_null_root_dir(void) {
TEST_ASSERT_NULL(server_create(&cfg)); TEST_ASSERT_NULL(server_create(&cfg));
} }
void test_create_empty_root_dir(void) {
cocoon_config_t cfg = {.root_dir = "", .port = 30001};
/* 空字符串 root_dir 应该也失败(或成功取决于实现) */
server_context_t *ctx = server_create(&cfg);
if (ctx) {
server_destroy(ctx);
}
/* 空字符串可能成功也可能失败,不强制断言 */
TEST_ASSERT_TRUE(1);
}
/* ===== server_create 配置边界测试 ===== */
void test_create_port_zero(void) {
/* port=0 让系统分配随机端口,应该能成功 */
cocoon_config_t cfg = {
.root_dir = "/tmp",
.port = 0,
.threaded = false
};
server_context_t *ctx = server_create(&cfg);
if (ctx) {
server_destroy(ctx);
TEST_ASSERT_TRUE(1);
} else {
TEST_IGNORE_MESSAGE("port 0 create failed, system may restrict");
}
}
void test_create_high_port(void) {
cocoon_config_t cfg = {
.root_dir = "/tmp",
.port = 65535,
.threaded = false
};
server_context_t *ctx = server_create(&cfg);
if (ctx) {
server_destroy(ctx);
TEST_ASSERT_TRUE(1);
} else {
TEST_IGNORE_MESSAGE("port 65535 create failed, possibly in use");
}
}
void test_create_multithread_config(void) {
cocoon_config_t cfg = {
.root_dir = "/tmp",
.port = 30002,
.threaded = true,
.num_workers = 2
};
server_context_t *ctx = server_create(&cfg);
if (ctx) {
server_destroy(ctx);
TEST_ASSERT_TRUE(1);
} else {
TEST_IGNORE_MESSAGE("multithread create failed, possibly port in use");
}
}
void test_create_zero_workers(void) {
/* workers=0 应该自动检测 CPU 核心数 */
cocoon_config_t cfg = {
.root_dir = "/tmp",
.port = 30003,
.threaded = true,
.num_workers = 0
};
server_context_t *ctx = server_create(&cfg);
if (ctx) {
server_destroy(ctx);
TEST_ASSERT_TRUE(1);
} else {
TEST_IGNORE_MESSAGE("zero workers create failed, possibly port in use");
}
}
void test_create_with_timeout(void) {
cocoon_config_t cfg = {
.root_dir = "/tmp",
.port = 30004,
.threaded = false,
.timeout_ms = 5000
};
server_context_t *ctx = server_create(&cfg);
if (ctx) {
server_destroy(ctx);
TEST_ASSERT_TRUE(1);
} else {
TEST_IGNORE_MESSAGE("create with timeout failed, possibly port in use");
}
}
void test_create_with_max_connections(void) {
cocoon_config_t cfg = {
.root_dir = "/tmp",
.port = 30005,
.threaded = false,
.max_connections = 100
};
server_context_t *ctx = server_create(&cfg);
if (ctx) {
server_destroy(ctx);
TEST_ASSERT_TRUE(1);
} else {
TEST_IGNORE_MESSAGE("create with max_connections failed, possibly port in use");
}
}
void test_create_with_compression_flags(void) {
cocoon_config_t cfg = {
.root_dir = "/tmp",
.port = 30006,
.threaded = false,
.gzip_enabled = false,
.brotli_enabled = false
};
server_context_t *ctx = server_create(&cfg);
if (ctx) {
server_destroy(ctx);
TEST_ASSERT_TRUE(1);
} else {
TEST_IGNORE_MESSAGE("create with compression flags failed, possibly port in use");
}
}
/* ===== server_stop / server_destroy 安全路径 ===== */ /* ===== server_stop / server_destroy 安全路径 ===== */
void test_stop_null(void) { void test_stop_null(void) {
@ -29,6 +156,29 @@ void test_destroy_null(void) {
TEST_ASSERT_TRUE(1); TEST_ASSERT_TRUE(1);
} }
void test_stop_then_destroy(void) {
cocoon_config_t cfg = {
.root_dir = "/tmp",
.port = 30007,
.threaded = false
};
server_context_t *ctx = server_create(&cfg);
if (!ctx) {
TEST_IGNORE_MESSAGE("server_create failed, possibly port in use");
return;
}
server_stop(ctx);
server_destroy(ctx);
TEST_ASSERT_TRUE(1);
}
void test_double_destroy(void) {
/* 测试二次 destroy 的安全性(虽然用户不应这样做,但不应 crash */
/* 注意:第一次 destroy 后指针已释放,第二次是悬空指针,这个测试有风险 */
/* 暂时跳过,因为 C 中无法安全地 double-free */
TEST_IGNORE_MESSAGE("double destroy test skipped (unsafe in C)");
}
/* ===== server_create 成功后销毁 ===== */ /* ===== server_create 成功后销毁 ===== */
void test_create_and_destroy(void) { void test_create_and_destroy(void) {
@ -49,6 +199,40 @@ void test_create_and_destroy(void) {
} }
} }
void test_create_multiple_instances(void) {
/* 创建两个不同端口的服务器 */
cocoon_config_t cfg1 = {
.root_dir = "/tmp",
.port = 30008,
.threaded = false
};
cocoon_config_t cfg2 = {
.root_dir = "/tmp",
.port = 30009,
.threaded = false
};
server_context_t *ctx1 = server_create(&cfg1);
server_context_t *ctx2 = server_create(&cfg2);
if (ctx1 && ctx2) {
server_destroy(ctx2);
server_destroy(ctx1);
TEST_ASSERT_TRUE(1);
} else {
if (ctx1) server_destroy(ctx1);
if (ctx2) server_destroy(ctx2);
TEST_IGNORE_MESSAGE("multiple instances create failed, possibly port in use");
}
}
/* ===== server_start 参数校验 ===== */
void test_start_null(void) {
/* server_start 传入 NULL 应该返回错误 */
int ret = server_start(NULL);
TEST_ASSERT_EQUAL_INT(COCOON_ERROR, ret);
}
void setUp(void) {} void setUp(void) {}
void tearDown(void) {} void tearDown(void) {}
@ -57,9 +241,21 @@ int main(void) {
RUN_TEST(test_create_null_config); RUN_TEST(test_create_null_config);
RUN_TEST(test_create_null_root_dir); RUN_TEST(test_create_null_root_dir);
RUN_TEST(test_create_empty_root_dir);
RUN_TEST(test_create_port_zero);
RUN_TEST(test_create_high_port);
RUN_TEST(test_create_multithread_config);
RUN_TEST(test_create_zero_workers);
RUN_TEST(test_create_with_timeout);
RUN_TEST(test_create_with_max_connections);
RUN_TEST(test_create_with_compression_flags);
RUN_TEST(test_stop_null); RUN_TEST(test_stop_null);
RUN_TEST(test_destroy_null); RUN_TEST(test_destroy_null);
RUN_TEST(test_stop_then_destroy);
RUN_TEST(test_double_destroy);
RUN_TEST(test_create_and_destroy); RUN_TEST(test_create_and_destroy);
RUN_TEST(test_create_multiple_instances);
RUN_TEST(test_start_null);
return UNITY_END(); return UNITY_END();
} }

245
tests/websocket_test.py Normal file
View File

@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""
WebSocket 集成测试 使用 Python 标准库实现
测试内容
1. HTTP 升级握手101 Switching Protocols
2. 发送文本帧接收 echo 回显
3. 发送关闭帧接收关闭响应
"""
import socket
import base64
import hashlib
import struct
import sys
HOST = "localhost"
PORT = 9999
GUID = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
def build_handshake():
"""构建 WebSocket 握手请求"""
key = base64.b64encode(b"\x00" * 16).decode()
req = (
f"GET /ws HTTP/1.1\r\n"
f"Host: {HOST}:{PORT}\r\n"
f"Upgrade: websocket\r\n"
f"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\n"
f"Sec-WebSocket-Version: 13\r\n"
f"\r\n"
)
return req, key
def parse_response(data):
"""解析 HTTP 101 响应"""
lines = data.split(b"\r\n")
status = lines[0].decode()
headers = {}
for line in lines[1:]:
if line == b"":
break
if b":" in line:
k, v = line.split(b":", 1)
headers[k.decode().strip().lower()] = v.decode().strip()
return status, headers
def compute_accept(key):
"""计算 Sec-WebSocket-Accept"""
concat = key.encode() + GUID
digest = hashlib.sha1(concat).digest()
return base64.b64encode(digest).decode()
def build_frame(opcode, payload, masked=True):
"""构建 WebSocket 帧(客户端发送,带掩码)"""
length = len(payload)
header = bytearray()
header.append(0x80 | opcode) # FIN=1, opcode
if length <= 125:
header.append((0x80 if masked else 0x00) | length)
elif length <= 65535:
header.append((0x80 if masked else 0x00) | 126)
header.extend(struct.pack(">H", length))
else:
header.append((0x80 if masked else 0x00) | 127)
header.extend(struct.pack(">Q", length))
if masked:
mask = b"\x12\x34\x56\x78"
masked_payload = bytearray()
for i, b in enumerate(payload):
masked_payload.append(b ^ mask[i % 4])
return bytes(header) + mask + bytes(masked_payload)
return bytes(header) + payload
def parse_frame(data):
"""解析服务器发来的 WebSocket 帧(无掩码)"""
if len(data) < 2:
return None, 0
b0 = data[0]
b1 = data[1]
fin = (b0 >> 7) & 1
opcode = b0 & 0x0F
payload_len = b1 & 0x7F
offset = 2
if payload_len == 126:
if len(data) < 4:
return None, 0
payload_len = struct.unpack(">H", data[2:4])[0]
offset = 4
elif payload_len == 127:
if len(data) < 10:
return None, 0
payload_len = struct.unpack(">Q", data[2:10])[0]
offset = 10
# 服务器不掩码
if len(data) < offset + payload_len:
return None, 0
payload = data[offset:offset + payload_len]
return {"opcode": opcode, "fin": fin, "payload": payload}, offset + payload_len
def recv_all(sock, n):
"""接收至少 n 字节数据"""
data = b""
while len(data) < n:
chunk = sock.recv(n - len(data))
if not chunk:
break
data += chunk
return data
def recv_frame(sock):
"""接收一个完整帧(自动读取足够的数据)"""
data = b""
while True:
frame, consumed = parse_frame(data)
if frame is not None:
return frame, consumed
chunk = sock.recv(1024)
if not chunk:
return None, 0
data += chunk
def test_handshake():
"""测试 WebSocket 握手"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))
req, key = build_handshake()
sock.send(req.encode())
data = b""
while b"\r\n\r\n" not in data:
chunk = sock.recv(1024)
if not chunk:
break
data += chunk
status, headers = parse_response(data)
if "101" not in status:
print(f"FAIL: 期望 101实际: {status}")
sock.close()
return False
accept = compute_accept(key)
if headers.get("sec-websocket-accept") != accept:
print(f"FAIL: Sec-WebSocket-Accept 不匹配")
print(f" 期望: {accept}")
print(f" 实际: {headers.get('sec-websocket-accept')}")
sock.close()
return False
print("PASS: WebSocket 握手 101 + Sec-WebSocket-Accept 正确")
sock.close()
return True
def test_echo():
"""测试文本帧 echo"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5.0)
sock.connect((HOST, PORT))
req, key = build_handshake()
sock.send(req.encode())
data = b""
while b"\r\n\r\n" not in data:
chunk = sock.recv(1024)
if not chunk:
break
data += chunk
# 发送文本帧
msg = b"Hello, Cocoon!"
frame = build_frame(0x01, msg)
sock.send(frame)
# 接收 echo 回显
frame, _ = recv_frame(sock)
if frame is None:
print("FAIL: 未收到响应帧")
sock.close()
return False
if frame["opcode"] != 0x01:
print(f"FAIL: 期望文本帧(1),实际操作码: {frame['opcode']}")
sock.close()
return False
if frame["payload"] != msg:
print(f"FAIL: 回显内容不匹配")
print(f" 期望: {msg}")
print(f" 实际: {frame['payload']}")
sock.close()
return False
print("PASS: 文本帧 echo 正确")
# 发送关闭帧
close_frame = build_frame(0x08, b"\x03\xe8") # 1000
sock.send(close_frame)
# 接收关闭响应
frame, _ = recv_frame(sock)
if frame is None or frame["opcode"] != 0x08:
print("FAIL: 未收到关闭帧响应")
sock.close()
return False
print("PASS: 关闭帧响应正确")
sock.close()
return True
if __name__ == "__main__":
passed = 0
failed = 0
if test_handshake():
passed += 1
else:
failed += 1
if test_echo():
passed += 1
else:
failed += 1
print(f"\n通过: {passed}, 失败: {failed}")
sys.exit(0 if failed == 0 else 1)

381
tls.c Normal file
View File

@ -0,0 +1,381 @@
/**
* tls.c - TLS/SSL
*
* 使 OpenSSL Memory BIO coco I/O
* - SSL_read read BIO
* - SSL_write write BIO
* - I/O coco_read/coco_write read/write
*
* @author xfy
*/
#include "tls.h"
#include "log.h"
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/bio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include "../coco/include/coco.h"
/* 内部TLS 连接条目 */
typedef struct {
SSL *ssl;
BIO *rbio; /* 从 socket 读取的加密数据写入此处 */
BIO *wbio; /* SSL_write 产出的加密数据从此读取 */
} tls_conn_t;
/* fd → TLS 连接映射动态数组O(1) 索引) */
static tls_conn_t **g_map = NULL;
static int g_map_cap = 0;
static SSL_CTX *g_ctx = NULL;
/* 内部O(1) fd 查表 */
static tls_conn_t* tls_lookup(int fd) {
if (fd >= 0 && fd < g_map_cap) return g_map[fd];
return NULL;
}
static void tls_map_set(int fd, tls_conn_t *t) {
if (fd >= g_map_cap) {
int old_cap = g_map_cap;
g_map_cap = fd + 1;
g_map = (tls_conn_t **)realloc(g_map, g_map_cap * sizeof(tls_conn_t *));
for (int i = old_cap; i < g_map_cap; i++) g_map[i] = NULL;
}
g_map[fd] = t;
}
static void tls_map_clear(int fd) {
if (fd >= 0 && fd < g_map_cap) g_map[fd] = NULL;
}
/* 内部:从 socket 读取原始数据(协程感知) */
static ssize_t socket_read(int fd, void *buf, size_t len) {
if (coco_sched_get_current() != NULL) {
return coco_read(fd, buf, len);
}
return read(fd, buf, len);
}
/* 内部:向 socket 写入原始数据 */
static ssize_t socket_write_all(int fd, const void *buf, size_t len) {
if (coco_sched_get_current() != NULL) {
size_t sent = 0;
while (sent < len) {
ssize_t n = coco_write(fd, (const char *)buf + sent, len - sent);
if (n > 0) {
sent += (size_t)n;
} else if (n < 0) {
if (n == -EAGAIN || n == -EINTR || n == COCO_ERROR_WOULD_BLOCK) continue;
return -1;
} else {
return -1;
}
}
return (ssize_t)sent;
} else {
size_t sent = 0;
while (sent < len) {
ssize_t n = write(fd, (const char *)buf + sent, len - sent);
if (n > 0) {
sent += (size_t)n;
} else if (n < 0) {
if (errno == EAGAIN || errno == EINTR) continue;
return -1;
} else {
return -1;
}
}
return (ssize_t)sent;
}
}
/* 内部:将 write BIO 中的加密数据刷到 socket */
static int flush_wbio(int fd, BIO *wbio) {
char buf[16384];
int pending = BIO_read(wbio, buf, sizeof(buf));
while (pending > 0) {
if (socket_write_all(fd, buf, (size_t)pending) < 0) return -1;
pending = BIO_read(wbio, buf, sizeof(buf));
}
return 0;
}
/* 内部ALPN 选择回调,优先选择 h2否则 http/1.1 */
static int tls_alpn_select_cb(SSL *ssl __attribute__((unused)), const unsigned char **out,
unsigned char *outlen, const unsigned char *in,
unsigned int inlen, void *arg __attribute__((unused))) {
const unsigned char *h2 = (const unsigned char *)"h2";
const unsigned char *http11 = (const unsigned char *)"http/1.1";
unsigned int h2_len = 2;
unsigned int http11_len = 8;
/* 客户端发送的 ALPN 列表格式:长度前缀字符串序列 */
const unsigned char *p = in;
unsigned int remaining = inlen;
while (remaining > 0) {
unsigned char len = *p;
p++;
remaining--;
if (len > remaining) break;
/* 优先匹配 h2 */
if (len == h2_len && memcmp(p, h2, h2_len) == 0) {
*out = p;
*outlen = h2_len;
return SSL_TLSEXT_ERR_OK;
}
/* 次选 http/1.1 */
if (len == http11_len && memcmp(p, http11, http11_len) == 0) {
*out = p;
*outlen = http11_len;
return SSL_TLSEXT_ERR_OK;
}
p += len;
remaining -= len;
}
return SSL_TLSEXT_ERR_NOACK;
}
/* ===== 公共 API ===== */
int tls_create_context(const char *cert_path, const char *key_path) {
if (!cert_path || !key_path) return -1;
SSL_library_init();
OpenSSL_add_all_algorithms();
SSL_load_error_strings();
const SSL_METHOD *method = TLS_server_method();
if (!method) return -1;
g_ctx = SSL_CTX_new(method);
if (!g_ctx) return -1;
/* 最低 TLS 1.2 */
SSL_CTX_set_min_proto_version(g_ctx, TLS1_2_VERSION);
/* 加载证书 */
if (SSL_CTX_use_certificate_file(g_ctx, cert_path, SSL_FILETYPE_PEM) <= 0) {
log_error("加载 TLS 证书失败: %s", cert_path);
SSL_CTX_free(g_ctx);
g_ctx = NULL;
return -1;
}
/* 加载私钥 */
if (SSL_CTX_use_PrivateKey_file(g_ctx, key_path, SSL_FILETYPE_PEM) <= 0) {
log_error("加载 TLS 私钥失败: %s", key_path);
SSL_CTX_free(g_ctx);
g_ctx = NULL;
return -1;
}
/* 验证私钥与证书匹配 */
if (!SSL_CTX_check_private_key(g_ctx)) {
log_error("TLS 私钥与证书不匹配");
SSL_CTX_free(g_ctx);
g_ctx = NULL;
return -1;
}
/* 注册 ALPN 回调,支持 HTTP/2 和 HTTP/1.1 协商 */
SSL_CTX_set_alpn_select_cb(g_ctx, tls_alpn_select_cb, NULL);
log_info("TLS 上下文已初始化: cert=%s", cert_path);
return 0;
}
void tls_destroy_context(void) {
if (g_ctx) {
SSL_CTX_free(g_ctx);
g_ctx = NULL;
}
if (g_map) {
for (int i = 0; i < g_map_cap; i++) {
if (g_map[i]) {
if (g_map[i]->ssl) SSL_free(g_map[i]->ssl);
if (g_map[i]->rbio) BIO_free(g_map[i]->rbio);
if (g_map[i]->wbio) BIO_free(g_map[i]->wbio);
free(g_map[i]);
}
}
free(g_map);
g_map = NULL;
g_map_cap = 0;
}
}
bool tls_has_context(void) {
return g_ctx != NULL;
}
int tls_accept(int fd) {
if (!g_ctx) return -1;
tls_conn_t *t = (tls_conn_t *)calloc(1, sizeof(tls_conn_t));
if (!t) return -1;
t->ssl = SSL_new(g_ctx);
if (!t->ssl) {
free(t);
return -1;
}
/* 创建 Memory BIO 对 */
t->rbio = BIO_new(BIO_s_mem());
t->wbio = BIO_new(BIO_s_mem());
if (!t->rbio || !t->wbio) {
SSL_free(t->ssl);
free(t);
return -1;
}
BIO_set_mem_eof_return(t->rbio, -1);
BIO_set_mem_eof_return(t->wbio, -1);
SSL_set_bio(t->ssl, t->rbio, t->wbio);
SSL_set_accept_state(t->ssl);
tls_map_set(fd, t);
/* 执行握手循环 */
while (1) {
int ret = SSL_do_handshake(t->ssl);
if (ret == 1) {
log_debug("TLS 握手完成 fd=%d", fd);
return 0; /* 成功 */
}
int err = SSL_get_error(t->ssl, ret);
if (err == SSL_ERROR_WANT_READ) {
/* 尝试先刷出 write BIO 中的数据 */
if (flush_wbio(fd, t->wbio) < 0) goto fail;
/* 从 socket 读取加密数据 */
char buf[8192];
ssize_t n = socket_read(fd, buf, sizeof(buf));
if (n > 0) {
BIO_write(t->rbio, buf, (int)n);
} else if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
/* 非阻塞模式下暂时无数据,继续尝试 */
continue;
}
goto fail;
} else {
goto fail;
}
} else if (err == SSL_ERROR_WANT_WRITE) {
if (flush_wbio(fd, t->wbio) < 0) goto fail;
} else {
log_error("TLS 握手失败 fd=%d, err=%d", fd, err);
goto fail;
}
}
fail:
tls_map_clear(fd);
SSL_free(t->ssl);
free(t);
return -1;
}
ssize_t tls_read(int fd, void *buf, size_t len) {
tls_conn_t *t = tls_lookup(fd);
if (!t || !t->ssl) return -1;
while (1) {
int ret = SSL_read(t->ssl, buf, (int)len);
if (ret > 0) return (ssize_t)ret;
int err = SSL_get_error(t->ssl, ret);
if (err == SSL_ERROR_WANT_READ) {
/* 从 socket 读取加密数据 */
char tmp[8192];
ssize_t n = socket_read(fd, tmp, sizeof(tmp));
if (n > 0) {
BIO_write(t->rbio, tmp, (int)n);
} else {
return n; /* 0 或 -1 */
}
} else if (err == SSL_ERROR_WANT_WRITE) {
if (flush_wbio(fd, t->wbio) < 0) return -1;
} else if (err == SSL_ERROR_ZERO_RETURN) {
return 0; /* 对端关闭 */
} else {
return -1;
}
}
}
ssize_t tls_write(int fd, const void *buf, size_t len) {
tls_conn_t *t = tls_lookup(fd);
if (!t || !t->ssl) return -1;
size_t total = 0;
while (total < len) {
int ret = SSL_write(t->ssl, (const char *)buf + total, (int)(len - total));
if (ret > 0) {
total += (size_t)ret;
/* 刷出 write BIO */
if (flush_wbio(fd, t->wbio) < 0) return -1;
} else {
int err = SSL_get_error(t->ssl, ret);
if (err == SSL_ERROR_WANT_READ) {
char tmp[8192];
ssize_t n = socket_read(fd, tmp, sizeof(tmp));
if (n > 0) {
BIO_write(t->rbio, tmp, (int)n);
} else {
return -1;
}
} else if (err == SSL_ERROR_WANT_WRITE) {
if (flush_wbio(fd, t->wbio) < 0) return -1;
} else {
return -1;
}
}
}
return (ssize_t)total;
}
void tls_close(int fd) {
tls_conn_t *t = tls_lookup(fd);
if (!t) return;
if (t->ssl) {
SSL_shutdown(t->ssl);
/* 刷出最后的 close_notify */
flush_wbio(fd, t->wbio);
SSL_free(t->ssl);
}
/* rbio/wbio 已被 SSL_free 释放SSL_set_bio 转移所有权) */
tls_map_clear(fd);
free(t);
}
bool tls_has_connection(int fd) {
return tls_lookup(fd) != NULL;
}
bool tls_negotiated_http2(int fd) {
tls_conn_t *t = tls_lookup(fd);
if (!t || !t->ssl) return false;
const unsigned char *alpn = NULL;
unsigned int alpn_len = 0;
SSL_get0_alpn_selected(t->ssl, &alpn, &alpn_len);
if (alpn_len == 2 && memcmp(alpn, "h2", 2) == 0) {
return true;
}
return false;
}

102
tls.h Normal file
View File

@ -0,0 +1,102 @@
/**
* tls.h - TLS/SSL
*
* OpenSSL HTTPS 使 Memory BIO coco I/O
*
* :
* - SSL_CTX
* - SSL* + Memory BIO
* - fdSSL 使 send_all TLS
* - SSL_read/write WANT_READ/WANT_WRITE coco_read/write
*
* @author xfy
*/
#ifndef COCOON_TLS_H
#define COCOON_TLS_H
#include <stdbool.h>
#include <stddef.h>
#include <sys/types.h>
/**
* tls_create_context - TLS
*
* OpenSSL
*
* @param cert_path PEM
* @param key_path PEM
* @return 0 -1
*/
int tls_create_context(const char *cert_path, const char *key_path);
/**
* tls_destroy_context - TLS
*/
void tls_destroy_context(void);
/**
* tls_has_context - TLS
*/
bool tls_has_context(void);
/**
* tls_accept - fd SSL
*
* WANT_READ/WANT_WRITE yield
*
* @param fd socket
* @return 0 -1
*/
int tls_accept(int fd);
/**
* tls_read - TLS
*
* yield
*
* @param fd socket
* @param buf
* @param len
* @return 0 -1
*/
ssize_t tls_read(int fd, void *buf, size_t len);
/**
* tls_write - TLS
*
* @param fd socket
* @param buf
* @param len
* @return -1
*/
ssize_t tls_write(int fd, const void *buf, size_t len);
/**
* tls_close - TLS
*
* SSL_shutdown fd
*
* @param fd socket
*/
void tls_close(int fd);
/**
* tls_has_connection - fd TLS
*
* @param fd socket
* @return true TLS
*/
bool tls_has_connection(int fd);
/**
* tls_negotiated_http2 - ALPN HTTP/2
*
* TLS
*
* @param fd socket
* @return true h2
*/
bool tls_negotiated_http2(int fd);
#endif /* COCOON_TLS_H */

338
websocket.c Normal file
View File

@ -0,0 +1,338 @@
/**
* @file websocket.c
* @brief WebSocket RFC 6455
*
*
* echo 广
*/
#include "websocket.h"
#include "log.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <openssl/sha.h>
#include <sys/socket.h>
/* WebSocket 魔数字符串GUID */
static const char WS_GUID[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
/* 基础 64 编码表 */
static const char BASE64_TABLE[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/**
* @brief Base64 使
*
* @param in
* @param in_len
* @param out 4/3 * in_len + 4
* @return
*/
static size_t base64_encode(const uint8_t *in, size_t in_len, char *out) {
size_t i, j;
for (i = 0, j = 0; i + 2 < in_len; i += 3, j += 4) {
uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8) | in[i + 2];
out[j] = BASE64_TABLE[(v >> 18) & 0x3F];
out[j + 1] = BASE64_TABLE[(v >> 12) & 0x3F];
out[j + 2] = BASE64_TABLE[(v >> 6) & 0x3F];
out[j + 3] = BASE64_TABLE[v & 0x3F];
}
if (i < in_len) {
uint32_t v = (uint32_t)in[i] << 16;
if (i + 1 < in_len) v |= (uint32_t)in[i + 1] << 8;
out[j] = BASE64_TABLE[(v >> 18) & 0x3F];
out[j + 1] = BASE64_TABLE[(v >> 12) & 0x3F];
out[j + 2] = (i + 1 < in_len) ? BASE64_TABLE[(v >> 6) & 0x3F] : '=';
out[j + 3] = '=';
j += 4;
}
out[j] = '\0';
return j;
}
/**
* @brief WebSocket Sec-WebSocket-Accept
*
* key + GUID SHA1 Base64
*
* @param key Sec-WebSocket-Key
* @param accept 32
* @return 0
*/
static int ws_compute_accept(const char *key, char *accept) {
char concat[128];
size_t key_len = strlen(key);
if (key_len + sizeof(WS_GUID) >= sizeof(concat)) return -1;
memcpy(concat, key, key_len);
memcpy(concat + key_len, WS_GUID, sizeof(WS_GUID) - 1);
concat[key_len + sizeof(WS_GUID) - 1] = '\0';
unsigned char digest[SHA_DIGEST_LENGTH];
SHA1((unsigned char *)concat, key_len + sizeof(WS_GUID) - 1, digest);
base64_encode(digest, SHA_DIGEST_LENGTH, accept);
return 0;
}
/**
* @brief HTTP
*
* @param fd socket
* @param buf
* @param len
* @return 0 -1
*/
static int ws_send_all(int fd, const char *buf, size_t len) {
size_t sent = 0;
while (sent < len) {
ssize_t n = send(fd, buf + sent, len - sent, MSG_NOSIGNAL);
if (n > 0) {
sent += (size_t)n;
} else if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) {
continue;
} else {
return -1;
}
}
return 0;
}
int ws_handshake(int fd, const char *key) {
char accept[32];
if (ws_compute_accept(key, accept) != 0) return -1;
char response[512];
int n = snprintf(response, sizeof(response),
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: %s\r\n"
"Server: Cocoon/1.0\r\n"
"\r\n",
accept);
if (n < 0 || (size_t)n >= sizeof(response)) return -1;
return ws_send_all(fd, response, (size_t)n);
}
int ws_parse_frame(const uint8_t *data, size_t len, ws_frame_t *frame, size_t *consumed) {
if (len < 2) return -1; /* 至少需要 2 字节 */
uint8_t b0 = data[0];
uint8_t b1 = data[1];
frame->fin = (b0 >> 7) & 1;
frame->opcode = b0 & 0x0F;
frame->masked = (b1 >> 7) & 1;
uint64_t payload_len = b1 & 0x7F;
size_t header_len = 2;
if (payload_len == 126) {
if (len < 4) return -1;
payload_len = ((uint64_t)data[2] << 8) | data[3];
header_len = 4;
} else if (payload_len == 127) {
if (len < 10) return -1;
payload_len = 0;
for (int i = 0; i < 8; i++) {
payload_len = (payload_len << 8) | data[2 + i];
}
header_len = 10;
}
if (frame->masked) {
if (len < header_len + 4) return -1;
memcpy(frame->mask_key, data + header_len, 4);
header_len += 4;
}
if (len < header_len + payload_len) return -1;
frame->payload_len = payload_len;
if (payload_len > 0) {
frame->payload = (uint8_t *)malloc(payload_len + 1);
if (!frame->payload) return -2;
memcpy(frame->payload, data + header_len, payload_len);
if (frame->masked) {
for (uint64_t i = 0; i < payload_len; i++) {
frame->payload[i] ^= frame->mask_key[i % 4];
}
}
frame->payload[payload_len] = '\0';
} else {
frame->payload = NULL;
}
*consumed = header_len + payload_len;
return 0;
}
void ws_frame_free(ws_frame_t *frame) {
if (frame && frame->payload) {
free(frame->payload);
frame->payload = NULL;
}
}
int ws_send_frame(int fd, uint8_t opcode, const uint8_t *payload, size_t len) {
uint8_t header[14];
size_t header_len = 0;
header[0] = 0x80 | (opcode & 0x0F); /* FIN=1, opcode */
if (len <= 125) {
header[1] = (uint8_t)len;
header_len = 2;
} else if (len <= 65535) {
header[1] = 126;
header[2] = (uint8_t)(len >> 8);
header[3] = (uint8_t)(len & 0xFF);
header_len = 4;
} else {
header[1] = 127;
for (int i = 0; i < 8; i++) {
header[2 + i] = (uint8_t)(len >> (56 - i * 8));
}
header_len = 10;
}
/* 服务器发送不掩码 */
if (ws_send_all(fd, (char *)header, header_len) != 0) return -1;
if (len > 0 && ws_send_all(fd, (char *)payload, len) != 0) return -1;
return 0;
}
int ws_send_text(int fd, const char *text) {
return ws_send_frame(fd, WS_OP_TEXT, (const uint8_t *)text, strlen(text));
}
int ws_send_close(int fd, uint16_t code, const char *reason) {
uint8_t payload[128];
size_t len = 0;
if (code != 0) {
payload[0] = (uint8_t)(code >> 8);
payload[1] = (uint8_t)(code & 0xFF);
len = 2;
if (reason) {
size_t rlen = strlen(reason);
if (rlen > sizeof(payload) - 3) rlen = sizeof(payload) - 3;
memcpy(payload + 2, reason, rlen);
len += rlen;
}
}
return ws_send_frame(fd, WS_OP_CLOSE, payload, len);
}
int ws_send_ping(int fd) {
return ws_send_frame(fd, WS_OP_PING, NULL, 0);
}
int ws_send_pong(int fd, const uint8_t *payload, size_t len) {
return ws_send_frame(fd, WS_OP_PONG, payload, len);
}
/**
* @brief
*/
static ssize_t ws_read_data(int fd, uint8_t *buf, size_t max_len) {
ssize_t n = recv(fd, buf, max_len, 0);
return n;
}
void ws_handle_connection(int fd, uint32_t timeout_ms) {
(void)timeout_ms; /* TODO: 超时处理 */
uint8_t buf[8192];
size_t buf_len = 0;
bool closed = false;
log_info("WebSocket 连接建立 fd=%d", fd);
while (!closed) {
ssize_t n = ws_read_data(fd, buf + buf_len, sizeof(buf) - buf_len);
if (n <= 0) {
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) {
continue;
}
log_debug("WebSocket fd=%d 读取结束或错误", fd);
break;
}
buf_len += (size_t)n;
/* 解析帧 */
size_t parsed = 0;
while (parsed < buf_len) {
ws_frame_t frame = {0};
size_t consumed = 0;
int ret = ws_parse_frame(buf + parsed, buf_len - parsed, &frame, &consumed);
if (ret == -1) {
break; /* 数据不完整,等待更多 */
}
if (ret == -2) {
log_warn("WebSocket fd=%d 收到畸形帧", fd);
ws_send_close(fd, 1002, "Protocol error");
closed = true;
break;
}
parsed += consumed;
switch (frame.opcode) {
case WS_OP_TEXT:
log_debug("WebSocket fd=%d 收到文本: %s", fd,
frame.payload ? (char *)frame.payload : "(empty)");
if (frame.fin) {
ws_send_text(fd, frame.payload ? (char *)frame.payload : "");
}
break;
case WS_OP_BINARY:
log_debug("WebSocket fd=%d 收到二进制 %llu 字节", fd,
(unsigned long long)frame.payload_len);
/* 简单 echo 回二进制 */
ws_send_frame(fd, WS_OP_BINARY, frame.payload, frame.payload_len);
break;
case WS_OP_CLOSE:
log_info("WebSocket fd=%d 收到关闭帧", fd);
ws_send_close(fd, 1000, NULL);
closed = true;
break;
case WS_OP_PING:
log_debug("WebSocket fd=%d 收到 Ping", fd);
ws_send_pong(fd, frame.payload, frame.payload_len);
break;
case WS_OP_PONG:
log_debug("WebSocket fd=%d 收到 Pong", fd);
break;
case WS_OP_CONT:
log_debug("WebSocket fd=%d 收到继续帧", fd);
break;
default:
log_warn("WebSocket fd=%d 未知操作码 %d", fd, frame.opcode);
break;
}
ws_frame_free(&frame);
}
/* 移动剩余数据到缓冲区开头 */
if (parsed > 0 && parsed < buf_len) {
memmove(buf, buf + parsed, buf_len - parsed);
buf_len -= parsed;
} else if (parsed == buf_len) {
buf_len = 0;
}
}
log_info("WebSocket 连接关闭 fd=%d", fd);
}

130
websocket.h Normal file
View File

@ -0,0 +1,130 @@
#ifndef WEBSOCKET_H
#define WEBSOCKET_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
/**
* @file websocket.h
* @brief WebSocket
*
* RFC 6455 WebSocket
* mask=0
*/
/**
* @brief WebSocket
*/
typedef enum {
WS_OP_CONT = 0x0, /**< 继续帧 */
WS_OP_TEXT = 0x1, /**< 文本帧 */
WS_OP_BINARY = 0x2, /**< 二进制帧 */
WS_OP_CLOSE = 0x8, /**< 关闭帧 */
WS_OP_PING = 0x9, /**< Ping 帧 */
WS_OP_PONG = 0xA /**< Pong 帧 */
} ws_opcode_t;
/**
* @brief WebSocket
*/
typedef struct {
uint8_t opcode; /**< 操作码 */
bool fin; /**< 是否为最后一帧 */
bool masked; /**< 是否掩码 */
uint64_t payload_len;/**< 负载长度 */
uint8_t mask_key[4]; /**< 掩码密钥(仅客户端发送时有效) */
uint8_t *payload; /**< 负载数据(已解掩码) */
} ws_frame_t;
/**
* @brief WebSocket
*
* RFC 6455 Sec-WebSocket-Key SHA1 + Base64
*
* @param fd socket
* @param key Sec-WebSocket-Key
* @return 0 -1
*/
int ws_handshake(int fd, const char *key);
/**
* @brief WebSocket
*
* -1 frame
*
* @param data
* @param len
* @param frame
* @param consumed
* @return 0 -1 -2
*/
int ws_parse_frame(const uint8_t *data, size_t len, ws_frame_t *frame, size_t *consumed);
/**
* @brief
*
* @param frame
*/
void ws_frame_free(ws_frame_t *frame);
/**
* @brief WebSocket
*
* @param fd socket
* @param opcode
* @param payload
* @param len
* @return 0 -1
*/
int ws_send_frame(int fd, uint8_t opcode, const uint8_t *payload, size_t len);
/**
* @brief
*
* @param fd socket
* @param text UTF-8
* @return 0 -1
*/
int ws_send_text(int fd, const char *text);
/**
* @brief
*
* @param fd socket
* @param code 1000
* @param reason NULL
* @return 0 -1
*/
int ws_send_close(int fd, uint16_t code, const char *reason);
/**
* @brief Ping
*
* @param fd socket
* @return 0 -1
*/
int ws_send_ping(int fd);
/**
* @brief Pong
*
* @param fd socket
* @param payload Ping NULL
* @param len
* @return 0 -1
*/
int ws_send_pong(int fd, const uint8_t *payload, size_t len);
/**
* @brief WebSocket
*
* WebSocket //ping/pong/close
* echo
*
* @param fd socket
* @param timeout_ms 0
*/
void ws_handle_connection(int fd, uint32_t timeout_ms);
#endif /* WEBSOCKET_H */

1
www/index.html Normal file
View File

@ -0,0 +1 @@
<html><body>Hello Cocoon</body></html>