From 7747ec921f9249aaa5e035655e6bd3c3c4e50c31 Mon Sep 17 00:00:00 2001 From: xfy911 Date: Thu, 4 Jun 2026 12:50:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(tls):=20=E6=B7=BB=E5=8A=A0=20TLS/HTTPS=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 tls.c/tls.h,使用 OpenSSL Memory BIO 与 coco 协程 I/O 集成 - 支持 TLS 1.2+,通过命令行 --cert/--key 或配置文件启用 - 所有静态文件、目录浏览、POST/文件上传功能均已支持 HTTPS - 集成测试新增 TLS 握手 + HTTPS 响应验证 - 单元测试 127 项通过,集成测试 39 项通过 --- Makefile | 12 +- cocoon.h | 3 + config.c | 27 +++- config.h | 3 +- main.c | 34 +++- server.c | 46 +++++- static.c | 28 +++- tests/integration_test.sh | 45 +++++- tests/server.crt | 19 +++ tests/server.key | 28 ++++ tests/unit/test_config.c | 10 +- tls.c | 325 ++++++++++++++++++++++++++++++++++++++ tls.h | 92 +++++++++++ 13 files changed, 641 insertions(+), 31 deletions(-) create mode 100644 tests/server.crt create mode 100644 tests/server.key create mode 100644 tls.c create mode 100644 tls.h diff --git a/Makefile b/Makefile index 2ec7b55..aa6184e 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ COCO_LIB ?= $(COCO_DIR)/build CC = gcc 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 @@ -20,7 +20,7 @@ PREFIX ?= /usr/local BINDIR = $(PREFIX)/bin # 源文件 -SRCS = main.c server.c http.c static.c log.c config.c multipart.c +SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c OBJS = $(SRCS:.c=.o) TARGET = cocoon @@ -78,8 +78,8 @@ unit-test: $(UNIT_TEST_BINS) 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) - $(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) +$(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 tls.c $(UNITY_SRC) $(LDFLAGS) $(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 @@ -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) $(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) - $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_static.c http.c log.c $(UNITY_SRC) -lm -lz -lbrotlienc +$(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 tls.c $(UNITY_SRC) $(LDFLAGS) $(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 diff --git a/cocoon.h b/cocoon.h index 5079686..7eb82d6 100644 --- a/cocoon.h +++ b/cocoon.h @@ -38,6 +38,9 @@ typedef struct cocoon_config { log_level_t log_level; /**< 日志级别 */ bool gzip_enabled; /**< 是否启用 gzip 压缩(默认 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; /* === 服务器生命周期 API === */ diff --git a/config.c b/config.c index 238257f..d42df08 100644 --- a/config.c +++ b/config.c @@ -274,6 +274,21 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) { } else if (strcmp(key_str, "brotli_enabled") == 0) { if (val.type == TOKEN_TRUE) config->brotli_enabled = true; 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, bool has_root_dir, bool has_port, bool has_workers, 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; /* 命令行显式指定的值覆盖配置文件 */ @@ -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_gzip_enabled) base->gzip_enabled = cmdline->gzip_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 参数,命令行指定了就用命令行的 */ if (cmdline->threaded) base->threaded = true; } diff --git a/config.h b/config.h index 801f9d3..f587008 100644 --- a/config.h +++ b/config.h @@ -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, bool has_root_dir, bool has_port, bool has_workers, 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 */ diff --git a/main.c b/main.c index 6c7e14f..ca5fd6b 100644 --- a/main.c +++ b/main.c @@ -51,6 +51,9 @@ static void print_usage(const char *prog) { printf(" -o 连接空闲超时毫秒(默认 30000)\n"); printf(" -l 日志级别: error, warn, info, debug(默认 info)\n"); printf(" -v 详细日志输出(等同于 -l debug)\n"); + printf(" --cert TLS 证书路径(启用 HTTPS)\n"); + printf(" --key TLS 私钥路径\n"); + printf(" --tls 显式启用 TLS(需同时指定 --cert 和 --key)\n"); printf(" --no-gzip 禁用 gzip 压缩\n"); printf(" --no-brotli 禁用 brotli 压缩\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->brotli_enabled = true; + config->tls_cert = NULL; + config->tls_key = NULL; + config->tls_enabled = false; + bool has_root_dir = false; bool has_port = 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_gzip_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; 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) { config->brotli_enabled = false; 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) { print_usage(argv[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, - 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) { @@ -205,11 +228,10 @@ int main(int argc, char *argv[]) { server_destroy(g_ctx); g_ctx = NULL; - /* 释放配置文件分配的 root_dir */ - if (config.root_dir) { - 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); return ret == COCOON_OK ? 0 : 1; } diff --git a/server.c b/server.c index 43dfc73..0437765 100644 --- a/server.c +++ b/server.c @@ -21,6 +21,7 @@ #include "cocoon.h" #include "log.h" #include "multipart.h" +#include "tls.h" #include #include #include @@ -88,6 +89,7 @@ static void set_nonblocking(int fd) { */ static void close_connection(connection_t *conn) { if (conn && conn->fd >= 0) { + tls_close(conn->fd); close(conn->fd); conn->fd = -1; conn->closed = true; @@ -112,8 +114,9 @@ static ssize_t conn_read(connection_t *conn) { ssize_t n; - /* 尝试使用 coco 的异步 I/O */ - if (coco_sched_get_current() != NULL) { + if (tls_has_connection(conn->fd)) { + 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); } else { /* 无调度器时直接 read */ @@ -164,7 +167,9 @@ static int conn_read_body(connection_t *conn, http_request_t *req, size_t need) /* 从 socket 读取剩余数据 */ while (got < need) { 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); } else { 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" "Content-Length: 0\r\n" "Connection: close\r\n\r\n"; - ssize_t wr = write(client_fd, resp, strlen(resp)); - (void)wr; - close(client_fd); + if (tls_has_context()) { + if (tls_accept(client_fd) == 0) { + 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; } } @@ -611,6 +624,15 @@ static void accept_loop(void *arg) { int opt = 1; 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)); if (!conn) { @@ -696,6 +718,16 @@ server_context_t *server_create(const cocoon_config_t *config) { 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); @@ -765,6 +797,8 @@ void server_stop(server_context_t *ctx) { void server_destroy(server_context_t *ctx) { if (!ctx) return; + tls_destroy_context(); + if (ctx->listen_fd >= 0) { close(ctx->listen_fd); ctx->listen_fd = -1; diff --git a/static.c b/static.c index 8e1e1e4..bb3f37b 100644 --- a/static.c +++ b/static.c @@ -10,6 +10,7 @@ #include "static.h" #include "cocoon.h" +#include "tls.h" #include #include #include @@ -239,6 +240,10 @@ static bool safe_path_join(char *dst, size_t dst_size, * @return 0 成功,-1 失败 */ 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; while (sent < len) { 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; } - if (use_gzip || use_brotli) { - /* 发送压缩后的数据 */ - send_all(fd, compress_buf, (size_t)compress_len); - free(compress_buf); + if (use_gzip || use_brotli || tls_has_connection(fd)) { + /* 发送压缩后的数据,或 TLS 模式下的文件内容 */ + if (use_gzip || use_brotli) { + 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); } else { /* 定位到起始位置 */ diff --git a/tests/integration_test.sh b/tests/integration_test.sh index 681ade2..8219657 100755 --- a/tests/integration_test.sh +++ b/tests/integration_test.sh @@ -18,7 +18,9 @@ TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"; kill_server' EXIT 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 desc="${3:-$url}" local body - body=$(curl -s "$url") + body=$(curl -s -k "$url") if echo "$body" | grep -q "$expect"; then echo " ✓ $desc — 响应体包含 '$expect'" pass @@ -411,6 +413,45 @@ echo "" echo "=== 文件上传测试 ===" 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 "通过: $PASS" diff --git a/tests/server.crt b/tests/server.crt new file mode 100644 index 0000000..6c520ec --- /dev/null +++ b/tests/server.crt @@ -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----- diff --git a/tests/server.key b/tests/server.key new file mode 100644 index 0000000..c1209a1 --- /dev/null +++ b/tests/server.key @@ -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----- diff --git a/tests/unit/test_config.c b/tests/unit/test_config.c index 693ec07..97622a1 100644 --- a/tests/unit/test_config.c +++ b/tests/unit/test_config.c @@ -126,7 +126,7 @@ void test_merge_override_all(void) { .brotli_enabled = false }; 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(9090, base.port); TEST_ASSERT_TRUE(base.threaded); @@ -155,7 +155,7 @@ void test_merge_no_override(void) { .log_level = LOG_LEVEL_DEBUG }; 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(8080, base.port); TEST_ASSERT_FALSE(base.threaded); @@ -178,7 +178,7 @@ void test_merge_partial_override(void) { .log_level = LOG_LEVEL_DEBUG }; 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(8080, base.port); /* 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 */ cocoon_config_t base = {.root_dir = strdup("/old"), .port = 8080}; 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(9090, base.port); /* port 覆盖 */ free((void *)base.root_dir); @@ -251,7 +251,7 @@ void test_merge_cmdline_null_root_dir(void) { void test_merge_null_safety(void) { /* 不应 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); } diff --git a/tls.c b/tls.c new file mode 100644 index 0000000..cd2d394 --- /dev/null +++ b/tls.c @@ -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 +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/tls.h b/tls.h new file mode 100644 index 0000000..07372ca --- /dev/null +++ b/tls.h @@ -0,0 +1,92 @@ +/** + * tls.h - TLS/SSL 模块接口 + * + * 基于 OpenSSL 实现 HTTPS 支持,使用 Memory BIO 与 coco 协程 I/O 集成。 + * + * 设计: + * - 全局 SSL_CTX 管理证书和私钥 + * - 每个连接独立的 SSL* + Memory BIO 对 + * - fd→SSL 映射表,使 send_all 等现有代码透明支持 TLS + * - SSL_read/write 的 WANT_READ/WANT_WRITE 通过 coco_read/write 协程化 + * + * @author xfy + */ + +#ifndef COCOON_TLS_H +#define COCOON_TLS_H + +#include +#include +#include + +/** + * 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 */