feat(tls): 添加 TLS/HTTPS 支持

- 新增 tls.c/tls.h,使用 OpenSSL Memory BIO 与 coco 协程 I/O 集成
- 支持 TLS 1.2+,通过命令行 --cert/--key 或配置文件启用
- 所有静态文件、目录浏览、POST/文件上传功能均已支持 HTTPS
- 集成测试新增 TLS 握手 + HTTPS 响应验证
- 单元测试 127 项通过,集成测试 39 项通过
This commit is contained in:
xfy911 2026-06-04 12:50:28 +08:00
parent 8f9d420447
commit 7747ec921f
13 changed files with 641 additions and 31 deletions

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
# 调试模式 # 调试模式
DEBUG ?= 0 DEBUG ?= 0
@ -20,7 +20,7 @@ PREFIX ?= /usr/local
BINDIR = $(PREFIX)/bin BINDIR = $(PREFIX)/bin
# 源文件 # 源文件
SRCS = main.c server.c http.c static.c log.c config.c multipart.c SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c
OBJS = $(SRCS:.c=.o) OBJS = $(SRCS:.c=.o)
TARGET = cocoon TARGET = cocoon
@ -78,8 +78,8 @@ unit-test: $(UNIT_TEST_BINS)
fi fi
# 单元测试编译规则 # 单元测试编译规则
$(UNIT_TEST_DIR)/test_server: $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c $(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 $(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 $(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 $(UNITY_SRC) $(LDFLAGS)
$(UNIT_TEST_DIR)/test_multipart: $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) $(UNIT_TEST_DIR)/test_multipart: $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) -lm $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) -lm
@ -87,8 +87,8 @@ $(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 $(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 $(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

View File

@ -38,6 +38,9 @@ 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 私钥路径 */
} cocoon_config_t; } cocoon_config_t;
/* === 服务器生命周期 API === */ /* === 服务器生命周期 API === */

View File

@ -274,6 +274,21 @@ 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;
} }
/* 其他字段:忽略(未来扩展预留) */ /* 其他字段:忽略(未来扩展预留) */
@ -296,7 +311,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) {
if (!base || !cmdline) return; if (!base || !cmdline) return;
/* 命令行显式指定的值覆盖配置文件 */ /* 命令行显式指定的值覆盖配置文件 */
@ -311,6 +327,15 @@ 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;
/* threaded 是 flag 参数,命令行指定了就用命令行的 */ /* threaded 是 flag 参数,命令行指定了就用命令行的 */
if (cmdline->threaded) base->threaded = true; if (cmdline->threaded) base->threaded = true;
} }

View File

@ -43,6 +43,7 @@ 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);
#endif /* COCOON_CONFIG_H */ #endif /* COCOON_CONFIG_H */

34
main.c
View File

@ -51,6 +51,9 @@ 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(" --no-brotli 禁用 brotli 压缩\n");
printf(" -h 显示此帮助\n"); printf(" -h 显示此帮助\n");
@ -82,6 +85,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 +97,9 @@ 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;
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 +149,17 @@ 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], "-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 +177,9 @@ 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);
} }
if (!config->root_dir) { if (!config->root_dir) {
@ -205,11 +228,10 @@ int main(int argc, char *argv[]) {
server_destroy(g_ctx); server_destroy(g_ctx);
g_ctx = NULL; g_ctx = NULL;
/* 释放配置文件分配的 root_dir */ /* 释放配置文件分配的内存 */
if (config.root_dir) { if (config.root_dir) free((void *)config.root_dir);
free((void *)config.root_dir); if (config.tls_cert) free((void *)config.tls_cert);
config.root_dir = NULL; if (config.tls_key) free((void *)config.tls_key);
}
return ret == COCOON_OK ? 0 : 1; return ret == COCOON_OK ? 0 : 1;
} }

View File

@ -21,6 +21,7 @@
#include "cocoon.h" #include "cocoon.h"
#include "log.h" #include "log.h"
#include "multipart.h" #include "multipart.h"
#include "tls.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -88,6 +89,7 @@ static void set_nonblocking(int fd) {
*/ */
static void close_connection(connection_t *conn) { static void close_connection(connection_t *conn) {
if (conn && conn->fd >= 0) { if (conn && conn->fd >= 0) {
tls_close(conn->fd);
close(conn->fd); close(conn->fd);
conn->fd = -1; conn->fd = -1;
conn->closed = true; conn->closed = true;
@ -112,8 +114,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 */
@ -164,7 +167,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 = read(conn->fd, req->body + got, need - got); n = read(conn->fd, req->body + got, need - got);
@ -600,9 +605,17 @@ 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";
ssize_t wr = write(client_fd, resp, strlen(resp)); if (tls_has_context()) {
(void)wr; if (tls_accept(client_fd) == 0) {
close(client_fd); tls_write(client_fd, resp, strlen(resp));
tls_close(client_fd);
}
close(client_fd);
} else {
ssize_t wr = write(client_fd, resp, strlen(resp));
(void)wr;
close(client_fd);
}
continue; continue;
} }
} }
@ -611,6 +624,15 @@ static void accept_loop(void *arg) {
int opt = 1; int opt = 1;
setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
/* TLS 握手(如果启用) */
if (tls_has_context()) {
if (tls_accept(client_fd) != 0) {
log_warn("TLS 握手失败 fd=%d关闭连接", client_fd);
close(client_fd);
continue;
}
}
/* 创建连接上下文 */ /* 创建连接上下文 */
connection_t *conn = (connection_t *)calloc(1, sizeof(connection_t)); connection_t *conn = (connection_t *)calloc(1, sizeof(connection_t));
if (!conn) { if (!conn) {
@ -696,6 +718,16 @@ server_context_t *server_create(const cocoon_config_t *config) {
return NULL; return NULL;
} }
/* 初始化 TLS 上下文(如果配置了证书) */
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;
}
}
/* 非阻塞模式 */ /* 非阻塞模式 */
set_nonblocking(ctx->listen_fd); set_nonblocking(ctx->listen_fd);
@ -765,6 +797,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 >= 0) { if (ctx->listen_fd >= 0) {
close(ctx->listen_fd); close(ctx->listen_fd);
ctx->listen_fd = -1; ctx->listen_fd = -1;

View File

@ -10,6 +10,7 @@
#include "static.h" #include "static.h"
#include "cocoon.h" #include "cocoon.h"
#include "tls.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -239,6 +240,10 @@ 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 = write(fd, buf + sent, len - sent); ssize_t n = write(fd, buf + sent, len - sent);
@ -470,10 +475,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) { if (use_gzip || use_brotli || tls_has_connection(fd)) {
/* 发送压缩后的数据 */ /* 发送压缩后的数据,或 TLS 模式下的文件内容 */
send_all(fd, compress_buf, (size_t)compress_len); if (use_gzip || use_brotli) {
free(compress_buf); send_all(fd, compress_buf, (size_t)compress_len);
free(compress_buf);
} else {
/* 定位到起始位置(文件可能已被压缩读取提前读至末尾) */
lseek(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 = read(file_fd, file_buf, to_read);
if (n <= 0) break;
if (send_all(fd, file_buf, (size_t)n) != 0) break;
remaining -= n;
}
}
close(file_fd); close(file_fd);
} else { } else {
/* 定位到起始位置 */ /* 定位到起始位置 */

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 "$url")
if echo "$body" | grep -q "$expect"; then if echo "$body" | grep -q "$expect"; then
echo "$desc — 响应体包含 '$expect'" echo "$desc — 响应体包含 '$expect'"
pass pass
@ -411,6 +413,45 @@ echo ""
echo "=== 文件上传测试 ===" echo "=== 文件上传测试 ==="
assert_post_multipart "$BASE/upload" "multipart 文件上传" assert_post_multipart "$BASE/upload" "multipart 文件上传"
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 首页响应"
# 停止 HTTPS 服务器,恢复 HTTP 服务器
kill_server
sleep 1
start_server
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);
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);
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);
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);
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);
TEST_ASSERT_TRUE(1); TEST_ASSERT_TRUE(1);
} }

325
tls.c Normal file
View File

@ -0,0 +1,325 @@
/**
* 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;
}
/* ===== 公共 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;
}
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;
}

92
tls.h Normal file
View File

@ -0,0 +1,92 @@
/**
* 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);
#endif /* COCOON_TLS_H */