From bf1f3201e11cf9ff4f80d3c412436265d7b0d00e Mon Sep 17 00:00:00 2001 From: duanyh <19517952495@139.com> Date: Tue, 14 Apr 2026 19:26:45 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(network):=20=E6=96=B0=E5=A2=9ETLS?= =?UTF-8?q?=E4=BC=A0=E8=BE=93=E5=B1=82=E4=B8=8E=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tls_ctx: SSL_CTX 初始化与安全策略配置(TlsMode / TlsConfig) - tls_conn: 单连接 TLS 状态机(握手/收发/内存 BIO) - tls_tcp_server: 服务端多连接管理封装 - tls_tcp_client: 客户端连接+握手,支持自动重连 - 单元测试: TlsCtx 初始化与 TlsTcpClient 收发验证 - 更新 network Makefile / CMakeLists.txt 支持 tls 子目录 - 新增 README / README_CN 说明 API 与构建方式 --- modules/network/CMakeLists.txt | 23 +- modules/network/Makefile | 11 + modules/network/tls/README.md | 173 ++++++++++ modules/network/tls/README_CN.md | 174 ++++++++++ modules/network/tls/tls_conn.cpp | 340 ++++++++++++++++++++ modules/network/tls/tls_conn.h | 157 +++++++++ modules/network/tls/tls_ctx.cpp | 233 ++++++++++++++ modules/network/tls/tls_ctx.h | 95 ++++++ modules/network/tls/tls_ctx_test.cpp | 60 ++++ modules/network/tls/tls_tcp_client.cpp | 238 ++++++++++++++ modules/network/tls/tls_tcp_client.h | 108 +++++++ modules/network/tls/tls_tcp_client_test.cpp | 191 +++++++++++ modules/network/tls/tls_tcp_server.cpp | 283 ++++++++++++++++ modules/network/tls/tls_tcp_server.h | 119 +++++++ 14 files changed, 2203 insertions(+), 2 deletions(-) create mode 100644 modules/network/tls/README.md create mode 100644 modules/network/tls/README_CN.md create mode 100644 modules/network/tls/tls_conn.cpp create mode 100644 modules/network/tls/tls_conn.h create mode 100644 modules/network/tls/tls_ctx.cpp create mode 100644 modules/network/tls/tls_ctx.h create mode 100644 modules/network/tls/tls_ctx_test.cpp create mode 100644 modules/network/tls/tls_tcp_client.cpp create mode 100644 modules/network/tls/tls_tcp_client.h create mode 100644 modules/network/tls/tls_tcp_client_test.cpp create mode 100644 modules/network/tls/tls_tcp_server.cpp create mode 100644 modules/network/tls/tls_tcp_server.h diff --git a/modules/network/CMakeLists.txt b/modules/network/CMakeLists.txt index a2fe0b85..e5262cc7 100644 --- a/modules/network/CMakeLists.txt +++ b/modules/network/CMakeLists.txt @@ -45,7 +45,11 @@ set(TBOX_NETWORK_HEADERS tcp_server.h net_if.h domain_name.h - dns_request.h) + dns_request.h + tls/tls_ctx.h + tls/tls_conn.h + tls/tls_tcp_server.h + tls/tls_tcp_client.h) set(TBOX_NETWORK_SOURCES buffered_fd.cpp @@ -61,9 +65,15 @@ set(TBOX_NETWORK_SOURCES tcp_client.cpp tcp_server.cpp net_if.cpp - dns_request.cpp) + dns_request.cpp + tls/tls_ctx.cpp + tls/tls_conn.cpp + tls/tls_tcp_server.cpp + tls/tls_tcp_client.cpp) set(TBOX_NETWORK_TEST_SOURCES + tls/tls_ctx_test.cpp + tls/tls_tcp_client_test.cpp buffered_fd_test.cpp uart_test.cpp ip_address_test.cpp @@ -72,8 +82,12 @@ set(TBOX_NETWORK_TEST_SOURCES net_if_test.cpp dns_request_test.cpp) +# OpenSSL is required for TLS support +find_package(OpenSSL REQUIRED) + add_library(${TBOX_LIBRARY_NAME} ${TBOX_BUILD_LIB_TYPE} ${TBOX_NETWORK_SOURCES}) add_library(tbox::${TBOX_LIBRARY_NAME} ALIAS ${TBOX_LIBRARY_NAME}) +target_link_libraries(${TBOX_LIBRARY_NAME} OpenSSL::SSL OpenSSL::Crypto) set_target_properties( ${TBOX_LIBRARY_NAME} PROPERTIES @@ -103,6 +117,11 @@ install( DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/tbox/network ) +install( + FILES tls/tls_ctx.h tls/tls_conn.h tls/tls_tcp_server.h tls/tls_tcp_client.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/tbox/network/tls +) + # generate and install export file install( EXPORT ${TBOX_LIBRARY_NAME}_targets diff --git a/modules/network/Makefile b/modules/network/Makefile index e5823adf..39bf09e9 100644 --- a/modules/network/Makefile +++ b/modules/network/Makefile @@ -41,6 +41,10 @@ HEAD_FILES = \ net_if.h \ domain_name.h \ dns_request.h \ + tls/tls_ctx.h \ + tls/tls_conn.h \ + tls/tls_tcp_client.h \ + tls/tls_tcp_server.h \ CPP_SRC_FILES = \ buffered_fd.cpp \ @@ -57,11 +61,18 @@ CPP_SRC_FILES = \ tcp_server.cpp \ net_if.cpp \ dns_request.cpp \ + tls/tls_ctx.cpp \ + tls/tls_conn.cpp \ + tls/tls_tcp_client.cpp \ + tls/tls_tcp_server.cpp \ CXXFLAGS := -DMODULE_ID='"tbox.network"' $(CXXFLAGS) +LDFLAGS := $(LDFLAGS) -lssl -lcrypto TEST_CPP_SRC_FILES = \ $(CPP_SRC_FILES) \ + tls/tls_ctx_test.cpp \ + tls/tls_tcp_client_test.cpp \ buffered_fd_test.cpp \ uart_test.cpp \ ip_address_test.cpp \ diff --git a/modules/network/tls/README.md b/modules/network/tls/README.md new file mode 100644 index 00000000..3b4b50a1 --- /dev/null +++ b/modules/network/tls/README.md @@ -0,0 +1,173 @@ +# tbox::network::tls + +TLS transport module for cpp-tbox network stack. + +This module provides non-blocking TLS wrappers on top of TCP, powered by OpenSSL. +It is used by upper-layer modules such as HTTPS, and can also be used directly for custom protocols. + +## Features + +- TLS 1.2+ (minimum version is enforced) +- Client and server mode (`TlsMode::kClient`, `TlsMode::kServer`) +- Optional peer verification with CA file +- mTLS support via `verify_peer` + certificate configuration +- Security presets (`kDefault`, `kCompatible`, `kStrict`) +- Custom cipher configuration (`cipher_list`, `ciphersuites`) +- ALPN `http/1.1` support +- Non-blocking I/O with event loop integration + +## Files + +- `tls_ctx.h/.cpp`: TLS context (`SSL_CTX`) setup and policy +- `tls_conn.h/.cpp`: Per-connection TLS state machine (`SSL` + memory BIO) +- `tls_tcp_server.h/.cpp`: TLS server wrapper for accepted TCP connections +- `tls_tcp_client.h/.cpp`: TLS client wrapper with optional auto-reconnect + +## Core APIs + +### 1) TlsCtx + +`TlsCtx` manages global TLS configuration and certificates. + +Important config fields in `TlsConfig`: + +- `cert_file`: PEM certificate path +- `key_file`: PEM private key path +- `ca_file`: CA certificate path for peer verification +- `verify_peer`: require and verify peer certificate +- `enable_session_cache`: server-side TLS 1.2 session cache +- `cipher_list`: OpenSSL cipher list for TLS <= 1.2 +- `ciphersuites`: OpenSSL ciphersuites for TLS 1.3 +- `security_level`: preset (`kDefault`, `kCompatible`, `kStrict`) + +Note: explicit `cipher_list` / `ciphersuites` override preset values. + +### 2) TlsConn + +`TlsConn` wraps a connected socket and performs TLS handshake + encrypted I/O. + +- `enable()`: start handshake and event handling +- `send()`: send plaintext; module encrypts before writing to socket +- `setReceiveCallback(...)`: receive decrypted plaintext +- `setDisconnectedCallback(...)`: connection-closed callback +- `setSendCompleteCallback(...)`: send queue drained callback +- `setEstablishedCallback(...)`: handshake established callback + +### 3) TlsTcpServer / TlsTcpClient + +- `TlsTcpServer`: drop-in TLS transport for server-side connection management +- `TlsTcpClient`: active connect + TLS handshake wrapper, supports auto reconnect + +## Minimal Usage + +### Server + +```cpp +#include +#include + +using namespace tbox::network; +using namespace tbox::network::tls; + +TlsCtx tls_ctx; +TlsConfig cfg; +cfg.cert_file = "server.crt"; +cfg.key_file = "server.key"; +cfg.verify_peer = false; + +if (!tls_ctx.initialize(TlsMode::kServer, cfg)) { + return false; +} + +TlsTcpServer server(loop); +server.initialize(SockAddr::FromString("0.0.0.0:8443"), 128, &tls_ctx); +server.setReceiveCallback([](const TlsTcpServer::ConnToken &, util::Buffer &buf) { + // buf is already decrypted plaintext +}, 0); +server.start(); +``` + +### Client + +```cpp +#include +#include + +using namespace tbox::network; +using namespace tbox::network::tls; + +TlsCtx tls_ctx; +TlsConfig cfg; +cfg.ca_file = "ca.crt"; +cfg.verify_peer = true; + +if (!tls_ctx.initialize(TlsMode::kClient, cfg)) { + return false; +} + +TlsTcpClient client(loop); +client.initialize(SockAddr::FromString("127.0.0.1:8443"), &tls_ctx); +client.setConnectedCallback([&]() { + const char msg[] = "hello"; + client.send(msg, sizeof(msg) - 1); +}); +client.start(); +``` + +## Security Presets + +- `kDefault`: OpenSSL defaults (no additional restrictions) +- `kCompatible`: broad interoperability, still modern ciphers +- `kStrict`: stronger policy, ECDHE-focused suites + +If your deployment environment is legacy-heavy, start with `kCompatible`. +For new deployments, prefer `kStrict` where possible. + +## Build & Unit Test + +### 1) Build (Make) + +```bash +# From the project root — build the network module only +make modules MODULES="network" +``` + +### 2) Unit Test (Make) + +```bash +# From the project root — build the network test binary +make test MODULES="network" + +# Run all network tests +./.build/network/test + +# Run only TLS-related tests +./.build/network/test --gtest_filter="TlsCtx.*:TlsTcpClientTest.*" +``` + +### 3) Build + Test (CMake) + +```bash +# Build network library and tests +cmake --build build --target tbox_network -j2 +cmake --build build --target tbox_network_test -j2 + +# Run all network tests +ctest --test-dir build --output-on-failure -R tbox_network_test + +# Run only TLS-related tests +./build/modules/network/tbox_network_test --gtest_filter="TlsCtx.*:TlsTcpClientTest.*" +``` + +Notes: +- TLS code lives inside the `network` module; all build/test entry points use `network`. +- `TlsCtx.*:TlsTcpClientTest.*` is the focused TLS filter to skip unrelated environment-dependent cases. + +## Related Modules + +- `modules/https/`: HTTP/1.1 over this TLS transport +- `modules/crypto/`: application-layer cryptography utilities + +## License + +Part of cpp-tbox, under MIT license. diff --git a/modules/network/tls/README_CN.md b/modules/network/tls/README_CN.md new file mode 100644 index 00000000..7d31b3c5 --- /dev/null +++ b/modules/network/tls/README_CN.md @@ -0,0 +1,174 @@ +# tbox::network::tls + +cpp-tbox 网络栈中的 TLS 传输模块。 + +该模块在 TCP 之上提供基于 OpenSSL 的非阻塞 TLS 封装。 +上层模块(如 HTTPS)可直接复用,也可用于自定义协议。 + +## 功能特性 + +- TLS 1.2+(内部强制最低版本) +- 支持客户端/服务端模式(`TlsMode::kClient`、`TlsMode::kServer`) +- 可选的对端证书校验(配合 CA 文件) +- 支持 mTLS(`verify_peer` + 证书配置) +- 安全级别预设(`kDefault`、`kCompatible`、`kStrict`) +- 自定义密码配置(`cipher_list`、`ciphersuites`) +- ALPN `http/1.1` 支持 +- 与事件循环深度集成的非阻塞 I/O + +## 文件说明 + +- `tls_ctx.h/.cpp`:TLS 上下文(`SSL_CTX`)初始化和策略配置 +- `tls_conn.h/.cpp`:单连接 TLS 状态机(`SSL` + 内存 BIO) +- `tls_tcp_server.h/.cpp`:服务端 TLS 连接管理封装 +- `tls_tcp_client.h/.cpp`:客户端主动连接 + TLS 握手封装(支持自动重连) + +## 核心 API + +### 1) TlsCtx + +`TlsCtx` 用于管理 TLS 全局配置、证书与密钥。 + +`TlsConfig` 关键字段: + +- `cert_file`:PEM 证书路径 +- `key_file`:PEM 私钥路径 +- `ca_file`:用于校验证书链的 CA 文件 +- `verify_peer`:是否要求并校验对端证书 +- `enable_session_cache`:服务端 TLS 1.2 会话缓存 +- `cipher_list`:TLS 1.2 及以下密码列表 +- `ciphersuites`:TLS 1.3 密码套件 +- `security_level`:安全级别预设 + +说明:显式配置 `cipher_list` / `ciphersuites` 会覆盖 `security_level` 预设。 + +### 2) TlsConn + +`TlsConn` 用于包装一个已连接 socket,负责 TLS 握手与加解密 I/O。 + +- `enable()`:启动握手并接入事件 +- `send()`:发送明文(模块自动加密后发出) +- `setReceiveCallback(...)`:接收已解密明文 +- `setDisconnectedCallback(...)`:连接断开回调 +- `setSendCompleteCallback(...)`:发送完成回调 +- `setEstablishedCallback(...)`:握手完成回调 + +### 3) TlsTcpServer / TlsTcpClient + +- `TlsTcpServer`:服务端 TLS 连接管理封装 +- `TlsTcpClient`:客户端连接 + TLS 握手封装,支持自动重连 + +## 最小示例 + +### 服务端 + +```cpp +#include +#include + +using namespace tbox::network; +using namespace tbox::network::tls; + +TlsCtx tls_ctx; +TlsConfig cfg; +cfg.cert_file = "server.crt"; +cfg.key_file = "server.key"; +cfg.verify_peer = false; + +if (!tls_ctx.initialize(TlsMode::kServer, cfg)) { + return false; +} + +TlsTcpServer server(loop); +server.initialize(SockAddr::FromString("0.0.0.0:8443"), 128, &tls_ctx); +server.setReceiveCallback([](const TlsTcpServer::ConnToken &, util::Buffer &buf) { + // buf 已经是解密后的明文 +}, 0); +server.start(); +``` + +### 客户端 + +```cpp +#include +#include + +using namespace tbox::network; +using namespace tbox::network::tls; + +TlsCtx tls_ctx; +TlsConfig cfg; +cfg.ca_file = "ca.crt"; +cfg.verify_peer = true; + +if (!tls_ctx.initialize(TlsMode::kClient, cfg)) { + return false; +} + +TlsTcpClient client(loop); +client.initialize(SockAddr::FromString("127.0.0.1:8443"), &tls_ctx); +client.setConnectedCallback([&]() { + const char msg[] = "hello"; + client.send(msg, sizeof(msg) - 1); +}); +client.start(); +``` + +## 安全级别说明 + +- `kDefault`:使用 OpenSSL 默认策略 +- `kCompatible`:兼顾互操作性与现代安全算法 +- `kStrict`:更严格策略,优先 ECDHE 与高强度套件 + +建议: +- 兼容历史环境优先用 `kCompatible` +- 新系统优先使用 `kStrict` + +## 编译与单元测试 + +### 1) 编译(Make) + +```bash +# 从项目根目录执行——仅构建 network 模块 +make modules MODULES="network" +``` + +### 2) 单元测试(Make) + +```bash +# 从项目根目录执行——构建 network 测试二进制 +make test MODULES="network" + +# 运行所有 network 测试 +./.build/network/test + +# 仅运行 TLS 相关测试 +./.build/network/test --gtest_filter="TlsCtx.*:TlsTcpClientTest.*" +``` + +### 3) 编译 + 测试(CMake) + +```bash +# 构建 network 库和测试目标 +cmake --build build --target tbox_network -j2 +cmake --build build --target tbox_network_test -j2 + +# 运行所有 network 测试 +ctest --test-dir build --output-on-failure -R tbox_network_test + +# 仅运行 TLS 相关测试 +./build/modules/network/tbox_network_test --gtest_filter="TlsCtx.*:TlsTcpClientTest.*" +``` + +说明: +- TLS 代码归属在 `network` 模块内,所有编译/测试入口都使用 `network`。 +- `TlsCtx.*:TlsTcpClientTest.*` 是聚焦 TLS 的测试过滤方式,可跳过与 TLS 无关的环境依赖测试。 + +## 相关模块 + +- `modules/https/`:基于本 TLS 传输层实现 HTTP/1.1 over TLS +- `modules/crypto/`:应用层加密、哈希、签名能力 + +## 许可证 + +本模块属于 cpp-tbox,遵循 MIT 许可证。 diff --git a/modules/network/tls/tls_conn.cpp b/modules/network/tls/tls_conn.cpp new file mode 100644 index 00000000..dc6da4e3 --- /dev/null +++ b/modules/network/tls/tls_conn.cpp @@ -0,0 +1,340 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#undef MODULE_ID +#define MODULE_ID "tbox.tls" + +#include "tls_conn.h" + +#include +#include + +// Platform portability: use recv()/send() on Windows, read()/write() on POSIX +#ifdef _WIN32 +# include +# define TBOX_TLS_READ(fd, buf, len) ::recv((fd), static_cast(buf), static_cast(len), 0) +# define TBOX_TLS_WRITE(fd, buf, len) ::send((fd), static_cast(buf), static_cast(len), 0) +# ifndef EAGAIN +# define EAGAIN WSAEWOULDBLOCK +# endif +# ifndef EWOULDBLOCK +# define EWOULDBLOCK WSAEWOULDBLOCK +# endif +#else +# include +# include +# define TBOX_TLS_READ(fd, buf, len) ::read((fd), (buf), (len)) +# define TBOX_TLS_WRITE(fd, buf, len) ::write((fd), (buf), (len)) +#endif + +#include +#include + +#include +#include +#include + +namespace tbox { +namespace network { +namespace tls { + +namespace { +//! Drain the OpenSSL error queue into the tbox log. +void LogSslErrors(const char *context) { + char buf[256]; + unsigned long e; + while ((e = ERR_get_error()) != 0) { + ERR_error_string_n(e, buf, sizeof(buf)); + LogWarn("[OpenSSL] %s: %s", context, buf); + } +} +} // namespace + +TlsConn::TlsConn(event::Loop *wp_loop, SocketFd sock_fd, + const SockAddr &peer_addr, ssl_ctx_st *ssl_ctx, + bool is_server) + : wp_loop_(wp_loop) + , sock_fd_(std::move(sock_fd)) + , peer_addr_(peer_addr) + , raw_send_buf_(0) + , is_server_(is_server) { + ssl_ = SSL_new(static_cast(ssl_ctx)); + TBOX_ASSERT(ssl_ != nullptr); + + rbio_ = BIO_new(BIO_s_mem()); + wbio_ = BIO_new(BIO_s_mem()); + TBOX_ASSERT(rbio_ != nullptr && wbio_ != nullptr); + + // SSL takes ownership of both BIOs; they are freed by SSL_free() + SSL_set_bio(ssl_, rbio_, wbio_); + if (is_server_) + SSL_set_accept_state(ssl_); // server-side handshake + else + SSL_set_connect_state(ssl_); // client-side handshake +} + +TlsConn::~TlsConn() { + TBOX_ASSERT(cb_level_ == 0); + + if (context_ && context_deleter_) + context_deleter_(context_); + + CHECK_DELETE_RESET_OBJ(sp_read_ev_); + CHECK_DELETE_RESET_OBJ(sp_write_ev_); + + if (ssl_) { + SSL_free(ssl_); // also calls BIO_free on rbio_ and wbio_ + ssl_ = nullptr; + } + // sock_fd_ destructor handles fd close via reference counting +} + +void TlsConn::setReceiveCallback(const ReceiveCallback &cb, size_t threshold) { + recv_cb_ = cb; + recv_threshold_ = threshold; +} + +void TlsConn::setDisconnectedCallback(const DisconnectedCallback &cb) { + disc_cb_ = cb; +} + +void TlsConn::setSendCompleteCallback(const SendCompleteCallback &cb) { + send_complete_cb_ = cb; +} + +void TlsConn::setEstablishedCallback(const EstablishedCallback &cb) { + established_cb_ = cb; +} + +void TlsConn::setContext(void *ctx, ContextDeleter &&deleter) { + if (context_ && context_deleter_) + context_deleter_(context_); + context_ = ctx; + context_deleter_ = std::move(deleter); +} + +bool TlsConn::enable() { + if (!sp_read_ev_) { + sp_read_ev_ = wp_loop_->newFdEvent("TlsConn::read"); + sp_read_ev_->initialize(sock_fd_.get(), event::FdEvent::kReadEvent, + event::Event::Mode::kPersist); + sp_read_ev_->setCallback([this](short ev) { onFdReadable(ev); }); + } + if (!sp_write_ev_) { + sp_write_ev_ = wp_loop_->newFdEvent("TlsConn::write"); + sp_write_ev_->initialize(sock_fd_.get(), event::FdEvent::kWriteEvent, + event::Event::Mode::kPersist); + sp_write_ev_->setCallback([this](short ev) { onFdWritable(ev); }); + } + + sp_read_ev_->enable(); + // Write event is enabled on demand only + + // Start the TLS handshake (server sends nothing until client hello arrives, + // but kick it once in case data already arrived synchronously) + doHandshake(); + return true; +} + +bool TlsConn::disable() { + if (sp_read_ev_) sp_read_ev_->disable(); + if (sp_write_ev_) sp_write_ev_->disable(); + return true; +} + +bool TlsConn::send(const void *data, size_t size) { + if (state_ != State::kEstablished) { + LogWarn("send() when not in Established state (state=%d) — dropped", + static_cast(state_)); + return false; + } + + int ret = SSL_write(ssl_, data, static_cast(size)); + if (ret <= 0) { + int err = SSL_get_error(ssl_, ret); + LogWarn("SSL_write error: %d", err); + ERR_clear_error(); + return false; + } + drainWbio(); + return true; +} + +bool TlsConn::shutdown(int howto) { + // Only block further sends when the write half is being closed. + // SHUT_RD (read half only) must not prevent send() from pushing + // already-buffered response data to the peer. + if (howto == SHUT_WR || howto == SHUT_RDWR) + state_ = State::kClosing; + return ::shutdown(sock_fd_.get(), howto) == 0; +} + +// ============================================================ +// Private helpers +// ============================================================ + +void TlsConn::onFdReadable(short) { + // Read raw (encrypted) bytes from the socket and feed into rbio_ + char buf[4096]; + while (true) { + ssize_t n = TBOX_TLS_READ(sock_fd_.get(), buf, sizeof(buf)); + if (n > 0) { + BIO_write(rbio_, buf, static_cast(n)); + } else if (n == 0) { + LogDbg("peer closed connection: %s", peer_addr_.toString().c_str()); + handleDisconnect("EOF"); + return; + } else { + if (errno == EAGAIN || errno == EWOULDBLOCK) + break; + LogWarn("socket read error: %s", strerror(errno)); + handleDisconnect("read error"); + return; + } + } + + if (state_ == State::kHandshaking) { + doHandshake(); + } else if (state_ == State::kEstablished) { + doRead(); + } + // kClosing: silently ignore incoming data +} + +void TlsConn::onFdWritable(short) { + flushRawSend(); +} + +void TlsConn::doHandshake() { + int ret = SSL_do_handshake(ssl_); + drainWbio(); // may have generated ServerHello / Finished / etc. + + if (ret == 1) { + state_ = State::kEstablished; + LogInfo("TLS handshake complete with %s", peer_addr_.toString().c_str()); + if (established_cb_) { + ++cb_level_; + established_cb_(); + --cb_level_; + } + // There might already be application data buffered in the SSL object + doRead(); + } else { + int err = SSL_get_error(ssl_, ret); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + // Wait for more network data — nothing to do here + return; + } + LogWarn("TLS handshake failed (err=%d) with %s", err, peer_addr_.toString().c_str()); + LogSslErrors("TLS handshake"); + ERR_clear_error(); + handleDisconnect("handshake failed"); + } +} + +void TlsConn::doRead() { + util::Buffer plain(0); + char buf[4096]; + while (true) { + int n = SSL_read(ssl_, buf, static_cast(sizeof(buf))); + if (n > 0) { + plain.append(buf, static_cast(n)); + } else { + int err = SSL_get_error(ssl_, n); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + break; // Wait for more data + } else if (err == SSL_ERROR_ZERO_RETURN) { + // Peer sent TLS close_notify — orderly shutdown + LogDbg("TLS close_notify from %s", peer_addr_.toString().c_str()); + handleDisconnect("close_notify"); + return; + } else { + LogWarn("SSL_read error: %d", err); + LogSslErrors("SSL_read"); + ERR_clear_error(); + handleDisconnect("SSL_read error"); + return; + } + } + } + + if (plain.readableSize() > 0 && recv_cb_) { + ++cb_level_; + recv_cb_(plain); + --cb_level_; + } +} + +void TlsConn::drainWbio() { + // Move all pending encrypted output from wbio_ into raw_send_buf_ + char buf[4096]; + while (true) { + int n = BIO_read(wbio_, buf, static_cast(sizeof(buf))); + if (n <= 0) break; + raw_send_buf_.append(buf, static_cast(n)); + } + if (raw_send_buf_.readableSize() > 0) + flushRawSend(); +} + +void TlsConn::flushRawSend() { + while (raw_send_buf_.readableSize() > 0) { + ssize_t n = TBOX_TLS_WRITE(sock_fd_.get(), + raw_send_buf_.readableBegin(), + raw_send_buf_.readableSize()); + if (n > 0) { + raw_send_buf_.hasRead(static_cast(n)); + } else { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // Cannot write right now — enable write event to retry + if (sp_write_ev_) sp_write_ev_->enable(); + return; + } + LogWarn("socket write error: %s", strerror(errno)); + handleDisconnect("write error"); + return; + } + } + + // All encrypted bytes flushed to the socket + if (sp_write_ev_) sp_write_ev_->disable(); + + if (send_complete_cb_) { + ++cb_level_; + send_complete_cb_(); + --cb_level_; + } +} + +void TlsConn::handleDisconnect(const char *reason) { + (void)reason; + LogDbg("TlsConn disconnect [%s]: %s", reason, peer_addr_.toString().c_str()); + state_ = State::kClosing; + if (sp_read_ev_) sp_read_ev_->disable(); + if (sp_write_ev_) sp_write_ev_->disable(); + if (disc_cb_) { + ++cb_level_; + disc_cb_(); + --cb_level_; + } +} + +} // namespace tls +} // namespace network +} // namespace tbox diff --git a/modules/network/tls/tls_conn.h b/modules/network/tls/tls_conn.h new file mode 100644 index 00000000..ad25a3c2 --- /dev/null +++ b/modules/network/tls/tls_conn.h @@ -0,0 +1,157 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_TLS_CONN_H_20260324 +#define TBOX_NETWORK_TLS_CONN_H_20260324 + +#include + +#include +#include +#include + +#include "../socket_fd.h" +#include "../sockaddr.h" + +// Forward-declare OpenSSL types to keep the header clean +struct ssl_st; +struct bio_st; +struct ssl_ctx_st; + +namespace tbox { + +namespace event { class FdEvent; } + +namespace network { +namespace tls { + +/** + * Per-connection TLS wrapper. + * + * Implements a non-blocking TLS state machine using OpenSSL memory BIOs. + * The raw encrypted bytes are shuttled between the socket fd (via FdEvent) + * and OpenSSL's pair of in-memory BIOs. All I/O callbacks deliver plaintext. + * + * Lifecycle: + * 1. Construct with a connected socket fd and a borrowed SSL_CTX. + * 2. Register callbacks (setReceiveCallback, setDisconnectedCallback, …). + * 3. Call enable() — this kicks off the TLS handshake automatically. + * 4. Use send() to deliver plaintext (encrypted before hitting the wire). + * 5. Destroying this object closes the socket and frees all SSL resources. + * + * Note: getReceiveBuffer() is not supported for TLS connections (always returns + * nullptr) because data must be decrypted before being buffered. + */ +class TlsConn { + public: + using ReceiveCallback = std::function; + using DisconnectedCallback = std::function; + using SendCompleteCallback = std::function; + using EstablishedCallback = std::function; ///< TLS 握手完成回调 + using ContextDeleter = std::function; + + /** + * @param wp_loop Event loop (borrowed) + * @param sock_fd Connected peer socket (ownership transferred) + * @param peer_addr Remote address (for logging / getClientAddress) + * @param ssl_ctx SSL_CTX (borrowed — must outlive this object) + * @param is_server true = server-side (SSL_set_accept_state); false = client-side + */ + TlsConn(event::Loop *wp_loop, SocketFd sock_fd, + const SockAddr &peer_addr, ssl_ctx_st *ssl_ctx, + bool is_server = true); + ~TlsConn(); + + NONCOPYABLE(TlsConn); + IMMOVABLE(TlsConn); + + // --- Callbacks (set before enable()) --- + + void setReceiveCallback(const ReceiveCallback &cb, size_t threshold = 0); + void setDisconnectedCallback(const DisconnectedCallback &cb); + void setSendCompleteCallback(const SendCompleteCallback &cb); + /** TLS 握手完成时触发,只对客户端模式有意义(服务端可忽略) */ + void setEstablishedCallback(const EstablishedCallback &cb); + + // --- Control --- + + bool enable(); //!< Start TLS handshake and enable I/O events + bool disable(); //!< Disable I/O events (connection stays alive for later re-enable) + + /** Encrypt and send plaintext data. */ + bool send(const void *data, size_t size); + + /** TCP half-close: SHUT_RD / SHUT_WR / SHUT_RDWR */ + bool shutdown(int howto); + + // --- Context (arbitrary user data, like TcpConnection) --- + + void setContext(void *ctx, ContextDeleter &&deleter = nullptr); + void *getContext() const { return context_; } + + /** Always returns nullptr — buffered plaintext is delivered via callback only */ + util::Buffer *getReceiveBuffer() { return nullptr; } + + SockAddr peerAddr() const { return peer_addr_; } + + private: + enum class State { kHandshaking, kEstablished, kClosing }; + + void onFdReadable(short events); + void onFdWritable(short events); + + void doHandshake(); //!< Drive SSL_do_handshake(), called after feeding data to rbio_ + void doRead(); //!< Drain plaintext from SSL_read() and deliver to recv_cb_ + void drainWbio(); //!< Read encrypted output from wbio_ into raw_send_buf_ + void flushRawSend(); //!< Write raw_send_buf_ to the socket fd + void handleDisconnect(const char *reason); //!< Shut down events and fire disc_cb_ + + event::Loop *wp_loop_; + SocketFd sock_fd_; + SockAddr peer_addr_; + + ssl_st *ssl_ = nullptr; + bio_st *rbio_ = nullptr; //!< Encrypted network data → SSL (app writes here) + bio_st *wbio_ = nullptr; //!< SSL encrypted output → network (app reads here) + // After SSL_set_bio(), both BIOs are owned by ssl_; freed via SSL_free(). + + event::FdEvent *sp_read_ev_ = nullptr; + event::FdEvent *sp_write_ev_ = nullptr; + + State state_ = State::kHandshaking; + util::Buffer raw_send_buf_; //!< Encrypted bytes queued for the socket + + ReceiveCallback recv_cb_; + size_t recv_threshold_ = 0; + DisconnectedCallback disc_cb_; + SendCompleteCallback send_complete_cb_; + EstablishedCallback established_cb_; + bool is_server_ = true; + + void *context_ = nullptr; + ContextDeleter context_deleter_; + + int cb_level_ = 0; +}; + +} // namespace tls +} // namespace network +} // namespace tbox + +#endif // TBOX_NETWORK_TLS_CONN_H_20260324 diff --git a/modules/network/tls/tls_ctx.cpp b/modules/network/tls/tls_ctx.cpp new file mode 100644 index 00000000..256528d7 --- /dev/null +++ b/modules/network/tls/tls_ctx.cpp @@ -0,0 +1,233 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#undef MODULE_ID +#define MODULE_ID "tbox.tls" + +#include "tls_ctx.h" + +#include +#include +#include + +#include + +namespace tbox { +namespace network { +namespace tls { + +namespace { + +// Cipher presets for TlsSecurityLevel +const char *kCompatibleCipherList = + "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" + "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" + "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" + "DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; + +const char *kCompatibleCiphersuites = + "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256"; + +const char *kStrictCipherList = + "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" + "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305"; + +const char *kStrictCiphersuites = + "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256"; + +void GlobalInitOpenSSL() { + static std::once_flag s_flag; + std::call_once(s_flag, [] { + SSL_library_init(); + SSL_load_error_strings(); + OpenSSL_add_all_algorithms(); + }); +} + +//! Drain the OpenSSL error queue into the tbox log, then clear it. +void LogOpenSslErrors(const char *context) { + char buf[256]; + unsigned long e; + while ((e = ERR_get_error()) != 0) { + ERR_error_string_n(e, buf, sizeof(buf)); + LogErr("[OpenSSL] %s: %s", context, buf); + } +} + +//! SNI callback: logs the requested hostname. Single-cert servers always accept. +int SniCb(SSL *ssl, int * /*al*/, void * /*arg*/) { + const char *name = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); + if (name && *name) + LogDbg("SNI: client requests host '%s'", name); + return SSL_TLSEXT_ERR_OK; +} + +//! ALPN callback: selects \"http/1.1\" from the client's offered list. +int AlpnSelectCb(SSL *, const unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int inlen, void *) { + // Wire format: ... + static const unsigned char kPrefs[] = "\x08http/1.1"; + int r = SSL_select_next_proto(const_cast(out), outlen, + kPrefs, sizeof(kPrefs) - 1, in, inlen); + // NOACK means no overlap — client can still connect without ALPN + return (r == OPENSSL_NPN_NEGOTIATED) ? SSL_TLSEXT_ERR_OK : SSL_TLSEXT_ERR_NOACK; +} + +} // namespace + +TlsCtx::TlsCtx() = default; + +TlsCtx::~TlsCtx() { + if (ctx_) { + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } +} + +bool TlsCtx::initialize(TlsMode mode, const TlsConfig &config) { + if (ctx_) { + LogWarn("TlsCtx already initialized"); + return false; + } + + GlobalInitOpenSSL(); + + const SSL_METHOD *method = (mode == TlsMode::kServer) + ? TLS_server_method() + : TLS_client_method(); + + ctx_ = SSL_CTX_new(method); + if (!ctx_) { + LogErr("SSL_CTX_new failed"); + LogOpenSslErrors("SSL_CTX_new"); + return false; + } + + // Require TLS 1.2 or above (TLS 1.0/1.1 are deprecated) + SSL_CTX_set_min_proto_version(ctx_, TLS1_2_VERSION); + + // Disable insecure compression + SSL_CTX_set_options(ctx_, SSL_OP_NO_COMPRESSION); + + // Resolve effective ciphers: explicit fields override security_level preset + const char *eff_cipher_list = config.cipher_list.empty() ? nullptr : config.cipher_list.c_str(); + const char *eff_ciphersuites = config.ciphersuites.empty() ? nullptr : config.ciphersuites.c_str(); + + if (config.security_level == TlsSecurityLevel::kCompatible) { + if (!eff_cipher_list) eff_cipher_list = kCompatibleCipherList; + if (!eff_ciphersuites) eff_ciphersuites = kCompatibleCiphersuites; + LogDbg("TLS security level: compatible"); + } else if (config.security_level == TlsSecurityLevel::kStrict) { + if (!eff_cipher_list) eff_cipher_list = kStrictCipherList; + if (!eff_ciphersuites) eff_ciphersuites = kStrictCiphersuites; + LogDbg("TLS security level: strict"); + } + + if (eff_cipher_list) { + if (SSL_CTX_set_cipher_list(ctx_, eff_cipher_list) != 1) { + LogErr("Failed to set cipher list: %s", eff_cipher_list); + LogOpenSslErrors("cipher_list"); + SSL_CTX_free(ctx_); + ctx_ = nullptr; + return false; + } + } + + if (eff_ciphersuites) { +#if defined(SSL_CTX_set_ciphersuites) + if (SSL_CTX_set_ciphersuites(ctx_, eff_ciphersuites) != 1) { + LogErr("Failed to set TLS1.3 ciphersuites: %s", eff_ciphersuites); + LogOpenSslErrors("ciphersuites"); + SSL_CTX_free(ctx_); + ctx_ = nullptr; + return false; + } +#else + LogWarn("TLS1.3 ciphersuites are not supported by this OpenSSL version"); +#endif + } + + if (!config.cert_file.empty()) { + if (SSL_CTX_use_certificate_file(ctx_, config.cert_file.c_str(), + SSL_FILETYPE_PEM) != 1) { + LogErr("Failed to load certificate: %s", config.cert_file.c_str()); + LogOpenSslErrors("certificate"); + SSL_CTX_free(ctx_); + ctx_ = nullptr; + return false; + } + } + + if (!config.key_file.empty()) { + if (SSL_CTX_use_PrivateKey_file(ctx_, config.key_file.c_str(), + SSL_FILETYPE_PEM) != 1) { + LogErr("Failed to load private key: %s", config.key_file.c_str()); + LogOpenSslErrors("private key"); + SSL_CTX_free(ctx_); + ctx_ = nullptr; + return false; + } + + if (SSL_CTX_check_private_key(ctx_) != 1) { + LogErr("Private key does not match certificate"); + LogOpenSslErrors("key/cert match"); + SSL_CTX_free(ctx_); + ctx_ = nullptr; + return false; + } + } + + if (!config.ca_file.empty()) { + if (SSL_CTX_load_verify_locations(ctx_, config.ca_file.c_str(), nullptr) != 1) { + LogWarn("Failed to load CA file: %s", config.ca_file.c_str()); + LogOpenSslErrors("CA file"); + } + } + + if (config.verify_peer) { + SSL_CTX_set_verify(ctx_, + SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + nullptr); + } + + // TLS session resumption (server-side cache avoids the full handshake cost) + if (mode == TlsMode::kServer && config.enable_session_cache) { + SSL_CTX_set_session_cache_mode(ctx_, SSL_SESS_CACHE_SERVER); + static const unsigned char kSidCtx[] = "tbox"; + SSL_CTX_set_session_id_context(ctx_, kSidCtx, sizeof(kSidCtx) - 1); + } + + // Server-side TLS extensions: SNI hostname logging + ALPN http/1.1 + if (mode == TlsMode::kServer) { + SSL_CTX_set_tlsext_servername_callback(ctx_, SniCb); + SSL_CTX_set_alpn_select_cb(ctx_, AlpnSelectCb, nullptr); + } + + // Client-side ALPN: advertise http/1.1 preference + if (mode == TlsMode::kClient) { + static const unsigned char kClientProtos[] = "\x08http/1.1"; + SSL_CTX_set_alpn_protos(ctx_, kClientProtos, sizeof(kClientProtos) - 1); + } + + return true; +} + +} // namespace tls +} // namespace network +} // namespace tbox diff --git a/modules/network/tls/tls_ctx.h b/modules/network/tls/tls_ctx.h new file mode 100644 index 00000000..254448f8 --- /dev/null +++ b/modules/network/tls/tls_ctx.h @@ -0,0 +1,95 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_TLS_CTX_H_20260324 +#define TBOX_NETWORK_TLS_CTX_H_20260324 + +#include +#include + +// Forward-declare to avoid pulling OpenSSL headers into user code +struct ssl_ctx_st; + +namespace tbox { +namespace network { +namespace tls { + +enum class TlsMode { + kServer, //!< TLS server (listens, accepts) + kClient, //!< TLS client (connects) +}; + +/** + * Convenience security preset that maps to a curated cipher list. + * Explicit cipher_list / ciphersuites fields always take precedence. + */ +enum class TlsSecurityLevel { + kDefault, //!< Use OpenSSL built-in defaults (no restriction) + kCompatible, //!< Broad compatibility: ECDHE/DHE + AESGCM/CHACHA20 + kStrict, //!< Max security: ECDHE-only + AES256GCM/CHACHA20, PFS enforced +}; + +/** TLS context configuration */ +struct TlsConfig { + std::string cert_file; //!< PEM certificate file path + std::string key_file; //!< PEM private key file path + std::string ca_file; //!< CA certificate for peer verification (optional) + bool verify_peer = false; //!< server: require client cert; client: verify server + bool enable_session_cache = true; //!< server: enable TLS 1.2 session resumption + std::string cipher_list; //!< OpenSSL cipher list for TLS <= 1.2 (overrides security_level) + std::string ciphersuites; //!< OpenSSL ciphersuites for TLS 1.3 (overrides security_level) + TlsSecurityLevel security_level = TlsSecurityLevel::kDefault; //!< cipher preset +}; + +/** + * TLS context wrapper (SSL_CTX). + * + * Loads certificates and keys once; shared across all connections in a server. + * Thread-safe for read-only usage (multiple TlsConn objects can share one TlsCtx). + */ +class TlsCtx { + public: + TlsCtx(); + ~TlsCtx(); + + NONCOPYABLE(TlsCtx); + IMMOVABLE(TlsCtx); + + /** + * Initialize the TLS context. + * @param mode kServer or kClient + * @param config Certificate/key paths and verification options + * @return true on success + */ + bool initialize(TlsMode mode, const TlsConfig &config); + + /** @return raw SSL_CTX pointer (borrowed, do not free) */ + ssl_ctx_st *get() const { return ctx_; } + + bool isValid() const { return ctx_ != nullptr; } + + private: + ssl_ctx_st *ctx_ = nullptr; +}; + +} // namespace tls +} // namespace network +} // namespace tbox + +#endif // TBOX_NETWORK_TLS_CTX_H_20260324 diff --git a/modules/network/tls/tls_ctx_test.cpp b/modules/network/tls/tls_ctx_test.cpp new file mode 100644 index 00000000..a271bc08 --- /dev/null +++ b/modules/network/tls/tls_ctx_test.cpp @@ -0,0 +1,60 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \\ \\ /\\ / + * \\ \\ \\ / + * \\ \\ \\ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +#include + +#include "tls_ctx.h" + +namespace tbox { +namespace network { +namespace tls { + +TEST(TlsCtx, ClientDefaultInit) { + TlsCtx ctx; + EXPECT_FALSE(ctx.isValid()); + + TlsConfig cfg; + cfg.verify_peer = false; + EXPECT_TRUE(ctx.initialize(TlsMode::kClient, cfg)); + EXPECT_TRUE(ctx.isValid()); +} + +TEST(TlsCtx, DoubleInitializeFails) { + TlsCtx ctx; + + TlsConfig cfg; + cfg.verify_peer = false; + ASSERT_TRUE(ctx.initialize(TlsMode::kClient, cfg)); + EXPECT_FALSE(ctx.initialize(TlsMode::kClient, cfg)); +} + +TEST(TlsCtx, InvalidCipherListFails) { + TlsCtx ctx; + + TlsConfig cfg; + cfg.verify_peer = false; + cfg.cipher_list = "THIS-IS-NOT-A-VALID-CIPHER"; + EXPECT_FALSE(ctx.initialize(TlsMode::kClient, cfg)); + EXPECT_FALSE(ctx.isValid()); +} + +} // namespace tls +} // namespace network +} // namespace tbox \ No newline at end of file diff --git a/modules/network/tls/tls_tcp_client.cpp b/modules/network/tls/tls_tcp_client.cpp new file mode 100644 index 00000000..c428e2f7 --- /dev/null +++ b/modules/network/tls/tls_tcp_client.cpp @@ -0,0 +1,238 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#undef MODULE_ID +#define MODULE_ID "tbox.tls" + +#include "tls_tcp_client.h" + +#include +#include +#include + +#include "../tcp_connector.h" +#include "../tcp_connection.h" + +#include "tls_ctx.h" +#include "tls_conn.h" + +namespace tbox { +namespace network { +namespace tls { + +using namespace std::placeholders; + +// ============================================================ + +struct TlsTcpClient::Data { + event::Loop *wp_loop = nullptr; + TcpConnector *connector = nullptr; + TlsCtx *tls_ctx = nullptr; // borrowed + TlsConn *tls_conn = nullptr; // owned; null when not connected + bool auto_reconnect = false; + size_t receive_threshold = 0; + + ConnectedCallback connected_cb; + DisconnectedCallback disconnected_cb; + ReceiveCallback receive_cb; + SendCompleteCallback send_complete_cb; + + State state = State::kNone; + int cb_level = 0; +}; + +// ============================================================ + +TlsTcpClient::TlsTcpClient(event::Loop *wp_loop) + : d_(new Data) { + d_->wp_loop = wp_loop; + d_->connector = new TcpConnector(wp_loop); +} + +TlsTcpClient::~TlsTcpClient() { + TBOX_ASSERT(d_->cb_level == 0); + cleanup(); + CHECK_DELETE_RESET_OBJ(d_->connector); + delete d_; +} + +bool TlsTcpClient::initialize(const SockAddr &server_addr, TlsCtx *tls_ctx) { + if (d_->state != State::kNone) { + LogWarn("already initialized"); + return false; + } + if (tls_ctx == nullptr || !tls_ctx->isValid()) { + LogErr("TlsCtx is null or invalid"); + return false; + } + + d_->tls_ctx = tls_ctx; + d_->connector->initialize(server_addr); + d_->connector->setConnectedCallback( + std::bind(&TlsTcpClient::onTcpConnected, this, _1)); + // try_times = 0 → unlimited retries (TcpConnector semantics: >0 limits) + + d_->state = State::kInited; + return true; +} + +void TlsTcpClient::setConnectedCallback(const ConnectedCallback &cb) { + d_->connected_cb = cb; +} + +void TlsTcpClient::setDisconnectedCallback(const DisconnectedCallback &cb) { + d_->disconnected_cb = cb; +} + +void TlsTcpClient::setReceiveCallback(const ReceiveCallback &cb, size_t threshold) { + d_->receive_cb = cb; + d_->receive_threshold = threshold; +} + +void TlsTcpClient::setSendCompleteCallback(const SendCompleteCallback &cb) { + d_->send_complete_cb = cb; +} + +void TlsTcpClient::setAutoReconnect(bool enable) { + d_->auto_reconnect = enable; + // 0 = unlimited; >0 = finite; auto_reconnect=false → try once only + d_->connector->setTryTimes(enable ? 0 : 1); +} + +bool TlsTcpClient::start() { + if (d_->state != State::kInited) { + LogWarn("start() called in wrong state"); + return false; + } + if (!d_->connector->start()) + return false; + d_->state = State::kConnecting; + return true; +} + +void TlsTcpClient::stop() { + if (d_->state == State::kNone || d_->state == State::kInited) + return; + + if (d_->tls_conn) { + // Prevent the disconnect callback from triggering reconnect logic + d_->tls_conn->setDisconnectedCallback(nullptr); + d_->tls_conn->setEstablishedCallback(nullptr); + d_->tls_conn->disable(); + TlsConn *c = d_->tls_conn; + d_->tls_conn = nullptr; + // Deferred delete: avoid deleting in a TlsConn callback context + d_->wp_loop->runNext([c] { delete c; }, "TlsTcpClient::stop"); + } + d_->connector->stop(); + d_->state = State::kInited; +} + +void TlsTcpClient::cleanup() { + if (d_->state == State::kNone) + return; + stop(); + d_->connected_cb = nullptr; + d_->disconnected_cb = nullptr; + d_->receive_cb = nullptr; + d_->send_complete_cb = nullptr; + d_->tls_ctx = nullptr; + d_->connector->cleanup(); + d_->state = State::kNone; +} + +bool TlsTcpClient::send(const void *data, size_t size) { + if (d_->tls_conn) + return d_->tls_conn->send(data, size); + LogWarn("send() when not connected"); + return false; +} + +bool TlsTcpClient::shutdown(int howto) { + if (d_->tls_conn) + return d_->tls_conn->shutdown(howto); + return false; +} + +TlsTcpClient::State TlsTcpClient::state() const { + return d_->state; +} + +// ============================================================ +// Private +// ============================================================ + +void TlsTcpClient::onTcpConnected(TcpConnection *raw_conn) { + RECORD_SCOPE(); + SocketFd fd = raw_conn->socketFd(); + SockAddr peer = raw_conn->peerAddr(); + delete raw_conn; // TlsConn takes over the fd + + TBOX_ASSERT(d_->tls_conn == nullptr); + d_->tls_conn = new TlsConn( + d_->wp_loop, std::move(fd), peer, + static_cast(d_->tls_ctx->get()), + false); // is_server = false → client-mode handshake + + d_->tls_conn->setEstablishedCallback( + std::bind(&TlsTcpClient::onTlsEstablished, this)); + d_->tls_conn->setDisconnectedCallback( + std::bind(&TlsTcpClient::onTlsDisconnected, this)); + if (d_->receive_cb) + d_->tls_conn->setReceiveCallback(d_->receive_cb, d_->receive_threshold); + if (d_->send_complete_cb) + d_->tls_conn->setSendCompleteCallback(d_->send_complete_cb); + + d_->state = State::kConnecting; // TCP ok, TLS handshake in progress + d_->tls_conn->enable(); +} + +void TlsTcpClient::onTlsEstablished() { + d_->state = State::kConnected; + if (d_->connected_cb) { + ++d_->cb_level; + d_->connected_cb(); + --d_->cb_level; + } +} + +void TlsTcpClient::onTlsDisconnected() { + // Null the pointer first; deferred delete to avoid deleting from own callback + TlsConn *c = d_->tls_conn; + d_->tls_conn = nullptr; + d_->wp_loop->runNext([c] { delete c; }, "TlsTcpClient::disconnect"); + + if (d_->auto_reconnect) { + d_->state = State::kConnecting; + d_->connector->start(); + LogDbg("TlsTcpClient: reconnecting…"); + } else { + d_->state = State::kInited; + } + + if (d_->disconnected_cb) { + ++d_->cb_level; + d_->disconnected_cb(); + --d_->cb_level; + } +} + +} // namespace tls +} // namespace network +} // namespace tbox diff --git a/modules/network/tls/tls_tcp_client.h b/modules/network/tls/tls_tcp_client.h new file mode 100644 index 00000000..e8747df0 --- /dev/null +++ b/modules/network/tls/tls_tcp_client.h @@ -0,0 +1,108 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_TLS_TCP_CLIENT_H_20260324 +#define TBOX_NETWORK_TLS_TCP_CLIENT_H_20260324 + +#include + +#include +#include +#include + +#include "../sockaddr.h" + +namespace tbox { +namespace network { + +class TcpConnection; + +namespace tls { + +class TlsCtx; +class TlsConn; + +/** + * TLS 客户端连接。 + * + * 封装 TcpConnector(负责 TCP 发起连接)+ TlsConn(客户端 TLS 握手)。 + * 握手完成后触发 ConnectedCallback,之后才可调用 send()。 + * + * auto-reconnect:断开后自动重连,重连成功后再次触发 ConnectedCallback。 + * + * Lifecycle: + * 1. Construct with event loop. + * 2. Set callbacks. + * 3. initialize(server_addr, tls_ctx). + * 4. start() — begins connection attempt. + * 5. ConnectedCallback fires when TLS handshake succeeds. + * 6. stop() / cleanup() to tear down. + */ +class TlsTcpClient { + public: + explicit TlsTcpClient(event::Loop *wp_loop); + virtual ~TlsTcpClient(); + + NONCOPYABLE(TlsTcpClient); + IMMOVABLE(TlsTcpClient); + + public: + enum class State { kNone, kInited, kConnecting, kConnected }; + + bool initialize(const SockAddr &server_addr, TlsCtx *tls_ctx); + + using ConnectedCallback = std::function; + using DisconnectedCallback = std::function; + using ReceiveCallback = std::function; + using SendCompleteCallback = std::function; + + void setConnectedCallback(const ConnectedCallback &cb); + void setDisconnectedCallback(const DisconnectedCallback &cb); + void setReceiveCallback(const ReceiveCallback &cb, size_t threshold = 0); + void setSendCompleteCallback(const SendCompleteCallback &cb); + + /** + * 开启自动重连。0 = 不限次数(默认)。 + * 须在 start() 之前调用。 + */ + void setAutoReconnect(bool enable); + + bool start(); ///< 开始连接(进入 kConnecting 状态) + void stop(); ///< 停止连接;若已连接则断开 + void cleanup(); ///< 全量清理,回到 kNone + + bool send(const void *data, size_t size); + bool shutdown(int howto); ///< TCP 半关闭 + + State state() const; + + private: + void onTcpConnected(TcpConnection *raw_conn); + void onTlsEstablished(); + void onTlsDisconnected(); + + struct Data; + Data *d_ = nullptr; +}; + +} // namespace tls +} // namespace network +} // namespace tbox + +#endif // TBOX_NETWORK_TLS_TCP_CLIENT_H_20260324 diff --git a/modules/network/tls/tls_tcp_client_test.cpp b/modules/network/tls/tls_tcp_client_test.cpp new file mode 100644 index 00000000..9e6f573d --- /dev/null +++ b/modules/network/tls/tls_tcp_client_test.cpp @@ -0,0 +1,191 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \\ \\ /\\ / + * \\ \\ \\ / + * \\ \\ \\ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +#include + +#include + +#include +#include +#include + +#include "tls_ctx.h" +#include "tls_tcp_client.h" +#include "tls_tcp_server.h" + +#include "../../https/test_certs.h" + +namespace tbox { +namespace network { +namespace tls { + +namespace { + +class TlsTcpClientTest : public ::testing::Test { + protected: + void SetUp() override { + loop_ = event::Loop::New(); + ASSERT_TRUE(loop_ != nullptr); + + timeout_ev_ = loop_->newTimerEvent(); + timeout_ev_->initialize(std::chrono::seconds(3), event::Event::Mode::kOneshot); + timeout_ev_->setCallback([this] { + timed_out_ = true; + loop_->exitLoop(); + }); + } + + void TearDown() override { + delete timeout_ev_; + delete loop_; + } + + bool InitServerTls(TlsCtx &ctx) { + cert_file_ = https::test::TempFile(https::test::kTestCertPem); + key_file_ = https::test::TempFile(https::test::kTestKeyPem); + if (cert_file_.path.empty() || key_file_.path.empty()) { + return false; + } + + TlsConfig cfg; + cfg.cert_file = cert_file_.path; + cfg.key_file = key_file_.path; + cfg.verify_peer = false; + return ctx.initialize(TlsMode::kServer, cfg); + } + + bool InitClientTls(TlsCtx &ctx) { + TlsConfig cfg; + cfg.verify_peer = false; + return ctx.initialize(TlsMode::kClient, cfg); + } + + void RunLoop() { + timeout_ev_->enable(); + loop_->runLoop(); + timeout_ev_->disable(); + } + + protected: + event::Loop *loop_ = nullptr; + event::TimerEvent *timeout_ev_ = nullptr; + bool timed_out_ = false; + + https::test::TempFile cert_file_; + https::test::TempFile key_file_; +}; + +TEST_F(TlsTcpClientTest, ClientServerRoundTrip) { + TlsCtx server_tls; + TlsCtx client_tls; + ASSERT_TRUE(InitServerTls(server_tls)); + ASSERT_TRUE(InitClientTls(client_tls)); + + TlsTcpServer server(loop_); + ASSERT_TRUE(server.initialize(SockAddr::FromString("127.0.0.1:21443"), 8, &server_tls)); + + bool server_received = false; + server.setReceiveCallback([&](const TlsTcpServer::ConnToken &token, util::Buffer &buf) { + std::string data(reinterpret_cast(buf.readableBegin()), buf.readableSize()); + buf.hasReadAll(); + server_received = (data == "ping"); + + const char reply[] = "pong"; + server.send(token, reply, sizeof(reply) - 1); + }, 0); + ASSERT_TRUE(server.start()); + + TlsTcpClient client(loop_); + ASSERT_TRUE(client.initialize(SockAddr::FromString("127.0.0.1:21443"), &client_tls)); + + bool client_received = false; + client.setConnectedCallback([&] { + const char msg[] = "ping"; + client.send(msg, sizeof(msg) - 1); + }); + client.setReceiveCallback([&](util::Buffer &buf) { + std::string data(reinterpret_cast(buf.readableBegin()), buf.readableSize()); + buf.hasReadAll(); + client_received = (data == "pong"); + loop_->exitLoop(); + }, 0); + + ASSERT_TRUE(client.start()); + RunLoop(); + + client.cleanup(); + server.cleanup(); + + EXPECT_FALSE(timed_out_); + EXPECT_TRUE(server_received); + EXPECT_TRUE(client_received); +} + +TEST_F(TlsTcpClientTest, AutoReconnectReconnectsAndReceivesAgain) { + TlsCtx server_tls; + TlsCtx client_tls; + ASSERT_TRUE(InitServerTls(server_tls)); + ASSERT_TRUE(InitClientTls(client_tls)); + + TlsTcpServer server(loop_); + ASSERT_TRUE(server.initialize(SockAddr::FromString("127.0.0.1:21444"), 8, &server_tls)); + + int server_rx_count = 0; + server.setReceiveCallback([&](const TlsTcpServer::ConnToken &token, util::Buffer &buf) { + std::string data(reinterpret_cast(buf.readableBegin()), buf.readableSize()); + buf.hasReadAll(); + if (data == "hello") { + ++server_rx_count; + if (server_rx_count == 1) { + server.disconnect(token); + } else if (server_rx_count == 2) { + loop_->exitLoop(); + } + } + }, 0); + ASSERT_TRUE(server.start()); + + TlsTcpClient client(loop_); + ASSERT_TRUE(client.initialize(SockAddr::FromString("127.0.0.1:21444"), &client_tls)); + client.setAutoReconnect(true); + + int connected_count = 0; + client.setConnectedCallback([&] { + ++connected_count; + const char msg[] = "hello"; + client.send(msg, sizeof(msg) - 1); + }); + + ASSERT_TRUE(client.start()); + RunLoop(); + + client.cleanup(); + server.cleanup(); + + EXPECT_FALSE(timed_out_); + EXPECT_GE(connected_count, 2); + EXPECT_EQ(server_rx_count, 2); +} + +} // namespace + +} // namespace tls +} // namespace network +} // namespace tbox diff --git a/modules/network/tls/tls_tcp_server.cpp b/modules/network/tls/tls_tcp_server.cpp new file mode 100644 index 00000000..a790f5d5 --- /dev/null +++ b/modules/network/tls/tls_tcp_server.cpp @@ -0,0 +1,283 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#undef MODULE_ID +#define MODULE_ID "tbox.tls" + +#include "tls_tcp_server.h" + +#include +#include +#include +#include + +#include "../tcp_acceptor.h" +#include "../tcp_connection.h" + +#include "tls_ctx.h" +#include "tls_conn.h" + +namespace tbox { +namespace network { +namespace tls { + +using namespace std::placeholders; +using TlsConns = cabinet::Cabinet; + +struct TlsTcpServer::Data { + event::Loop *wp_loop = nullptr; + + ConnectedCallback connected_cb; + DisconnectedCallback disconnected_cb; + ReceiveCallback receive_cb; + size_t receive_threshold = 0; + SendCompleteCallback send_complete_cb; + + TcpAcceptor *sp_acceptor = nullptr; + TlsCtx *tls_ctx = nullptr; // borrowed + TlsConns conns; + + State state = State::kNone; + int cb_level = 0; +}; + +TlsTcpServer::TlsTcpServer(event::Loop *wp_loop) + : d_(new Data) { + TBOX_ASSERT(d_ != nullptr); + d_->wp_loop = wp_loop; + d_->sp_acceptor = new TcpAcceptor(wp_loop); +} + +TlsTcpServer::~TlsTcpServer() { + TBOX_ASSERT(d_->cb_level == 0); + cleanup(); + CHECK_DELETE_RESET_OBJ(d_->sp_acceptor); + delete d_; +} + +bool TlsTcpServer::initialize(const SockAddr &bind_addr, int listen_backlog, + TlsCtx *tls_ctx) { + if (d_->state != State::kNone) + return false; + + if (tls_ctx == nullptr || !tls_ctx->isValid()) { + LogErr("TlsCtx is null or invalid"); + return false; + } + + d_->tls_ctx = tls_ctx; + + // We use TcpAcceptor to listen and accept raw TCP connections, + // then immediately wrap each in a TlsConn before reporting it up. + if (!d_->sp_acceptor->initialize(bind_addr, listen_backlog)) + return false; + + d_->sp_acceptor->setNewConnectionCallback( + [this](TcpConnection *raw_conn) { + onTcpConnected(static_cast(raw_conn)); + }); + + d_->state = State::kInited; + return true; +} + +void TlsTcpServer::setConnectedCallback(const ConnectedCallback &cb) { + d_->connected_cb = cb; +} + +void TlsTcpServer::setDisconnectedCallback(const DisconnectedCallback &cb) { + d_->disconnected_cb = cb; +} + +void TlsTcpServer::setReceiveCallback(const ReceiveCallback &cb, size_t threshold) { + d_->receive_cb = cb; + d_->receive_threshold = threshold; +} + +void TlsTcpServer::setSendCompleteCallback(const SendCompleteCallback &cb) { + d_->send_complete_cb = cb; +} + +bool TlsTcpServer::start() { + if (d_->state != State::kInited) + return false; + if (d_->sp_acceptor->start()) { + d_->state = State::kRunning; + return true; + } + return false; +} + +void TlsTcpServer::stop() { + if (d_->state != State::kRunning) + return; + + d_->conns.foreach([](TlsConn *conn) { + conn->disable(); + delete conn; + }); + d_->conns.clear(); + + d_->sp_acceptor->stop(); + d_->state = State::kInited; +} + +void TlsTcpServer::cleanup() { + if (d_->state <= State::kNone) + return; + + stop(); + + d_->sp_acceptor->cleanup(); + + d_->connected_cb = nullptr; + d_->disconnected_cb = nullptr; + d_->receive_cb = nullptr; + d_->receive_threshold = 0; + d_->send_complete_cb = nullptr; + d_->tls_ctx = nullptr; + + d_->state = State::kNone; +} + +bool TlsTcpServer::send(const ConnToken &client, const void *data_ptr, size_t data_size) { + auto conn = d_->conns.at(client); + if (conn) + return conn->send(data_ptr, data_size); + return false; +} + +bool TlsTcpServer::disconnect(const ConnToken &client) { + auto conn = d_->conns.free(client); + if (conn) { + conn->disable(); + d_->wp_loop->runNext([conn] { delete conn; }, + "TlsTcpServer::disconnect"); + return true; + } + return false; +} + +bool TlsTcpServer::shutdown(const ConnToken &client, int howto) { + auto conn = d_->conns.at(client); + if (conn) + return conn->shutdown(howto); + return false; +} + +bool TlsTcpServer::isClientValid(const ConnToken &client) const { + return d_->conns.at(client) != nullptr; +} + +SockAddr TlsTcpServer::getClientAddress(const ConnToken &client) const { + auto conn = d_->conns.at(client); + if (conn) + return conn->peerAddr(); + return SockAddr(); +} + +void TlsTcpServer::setContext(const ConnToken &client, void *context, + ContextDeleter &&deleter) { + auto conn = d_->conns.at(client); + if (conn) + conn->setContext(context, std::move(deleter)); +} + +void *TlsTcpServer::getContext(const ConnToken &client) const { + auto conn = d_->conns.at(client); + if (conn) + return conn->getContext(); + return nullptr; +} + +TlsTcpServer::State TlsTcpServer::state() const { + return d_->state; +} + +// ============================================================ +// Private +// ============================================================ + +void TlsTcpServer::onTcpConnected(void *raw_ptr) { + RECORD_SCOPE(); + auto *raw_conn = static_cast(raw_ptr); + + // Extract the socket fd and peer address from the raw TCP connection. + // SocketFd uses reference counting, so our copy keeps the fd alive + // even after we delete raw_conn below. + SocketFd sock_fd = raw_conn->socketFd(); + SockAddr peer_addr = raw_conn->peerAddr(); + + // Destroy TcpConnection before TlsConn takes over the fd. + // The underlying fd stays open because sock_fd holds a reference. + delete raw_conn; + + auto *tls_conn = new TlsConn(d_->wp_loop, std::move(sock_fd), peer_addr, + static_cast(d_->tls_ctx->get())); + + ConnToken client = d_->conns.alloc(tls_conn); + + tls_conn->setReceiveCallback( + [this, client](Buffer &b) { onTlsReceived(client, b); }, + d_->receive_threshold); + tls_conn->setDisconnectedCallback( + [this, client]() { onTlsDisconnected(client); }); + tls_conn->setSendCompleteCallback( + [this, client]() { onTlsSendCompleted(client); }); + + tls_conn->enable(); // begins TLS handshake + + ++d_->cb_level; + if (d_->connected_cb) + d_->connected_cb(client); + --d_->cb_level; +} + +void TlsTcpServer::onTlsDisconnected(const ConnToken &client) { + RECORD_SCOPE(); + ++d_->cb_level; + if (d_->disconnected_cb) + d_->disconnected_cb(client); + --d_->cb_level; + + TlsConn *conn = d_->conns.free(client); + d_->wp_loop->runNext( + [conn] { CHECK_DELETE_OBJ(conn); }, + "TlsTcpServer::onTlsDisconnected"); +} + +void TlsTcpServer::onTlsReceived(const ConnToken &client, Buffer &buff) { + RECORD_SCOPE(); + ++d_->cb_level; + if (d_->receive_cb) + d_->receive_cb(client, buff); + --d_->cb_level; +} + +void TlsTcpServer::onTlsSendCompleted(const ConnToken &client) { + RECORD_SCOPE(); + ++d_->cb_level; + if (d_->send_complete_cb) + d_->send_complete_cb(client); + --d_->cb_level; +} + +} // namespace tls +} // namespace network +} // namespace tbox diff --git a/modules/network/tls/tls_tcp_server.h b/modules/network/tls/tls_tcp_server.h new file mode 100644 index 00000000..127c2d86 --- /dev/null +++ b/modules/network/tls/tls_tcp_server.h @@ -0,0 +1,119 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_TLS_TCP_SERVER_H_20260324 +#define TBOX_NETWORK_TLS_TCP_SERVER_H_20260324 + +#include +#include +#include +#include + +#include "../sockaddr.h" + +namespace tbox { +namespace network { +namespace tls { + +class TlsCtx; +class TlsConn; + +using Buffer = util::Buffer; + +/** + * TLS-enabled TCP server. + * + * Drop-in replacement for network::TcpServer. The public interface is + * intentionally identical so that HttpsServer::Impl can use it in place + * of TcpServer without changing any HTTP-level logic. + * + * Lifecycle: + * 1. Construct with event loop. + * 2. Set callbacks. + * 3. Call initialize(addr, backlog, tls_ctx) — tls_ctx is borrowed. + * 4. Call start() / stop() to control accepting. + * 5. Call cleanup() before destruction if needed. + */ +class TlsTcpServer { + public: + explicit TlsTcpServer(event::Loop *wp_loop); + virtual ~TlsTcpServer(); + + NONCOPYABLE(TlsTcpServer); + IMMOVABLE(TlsTcpServer); + + public: + using ConnToken = cabinet::Token; + + enum class State { kNone, kInited, kRunning }; + + /** + * Initialize the server. + * @param bind_addr Local address to bind (IPv4 or Unix socket) + * @param listen_backlog Accept-queue depth + * @param tls_ctx TLS context (borrowed — must outlive this server) + */ + bool initialize(const SockAddr &bind_addr, int listen_backlog, TlsCtx *tls_ctx); + + using ConnectedCallback = std::function; + using DisconnectedCallback = std::function; + using ReceiveCallback = std::function; + using SendCompleteCallback = std::function; + + void setConnectedCallback(const ConnectedCallback &cb); + void setDisconnectedCallback(const DisconnectedCallback &cb); + void setReceiveCallback(const ReceiveCallback &cb, size_t threshold); + void setSendCompleteCallback(const SendCompleteCallback &cb); + + bool start(); //!< Start accepting connections + void stop(); //!< Stop accepting; disconnect all existing connections + void cleanup(); //!< Full teardown (implies stop) + + bool send(const ConnToken &client, const void *data_ptr, size_t data_size); + bool disconnect(const ConnToken &client); + bool shutdown(const ConnToken &client, int howto); + + bool isClientValid(const ConnToken &client) const; + SockAddr getClientAddress(const ConnToken &client) const; + + using ContextDeleter = std::function; + void setContext(const ConnToken &client, void *context, + ContextDeleter &&deleter = nullptr); + void *getContext(const ConnToken &client) const; + + /** Returns nullptr — TLS data must be decrypted before buffering */ + Buffer *getClientReceiveBuffer(const ConnToken &) { return nullptr; } + + State state() const; + + private: + void onTcpConnected(void *raw_tcp_conn); // TcpConnection* cast to void* + void onTlsDisconnected(const ConnToken &client); + void onTlsReceived(const ConnToken &client, Buffer &buff); + void onTlsSendCompleted(const ConnToken &client); + + struct Data; + Data *d_ = nullptr; +}; + +} // namespace tls +} // namespace network +} // namespace tbox + +#endif // TBOX_NETWORK_TLS_TCP_SERVER_H_20260324 -- Gitee From 99fbe2273ae7c7d8c4882ab5efea242aef42c6e7 Mon Sep 17 00:00:00 2001 From: duanyh <19517952495@139.com> Date: Tue, 14 Apr 2026 19:27:01 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(crypto):=20=E6=96=B0=E5=A2=9E=E9=9D=9E?= =?UTF-8?q?=E5=AF=B9=E7=A7=B0=E5=8A=A0=E5=AF=86API=E5=8F=8A=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - asym_ctx: RSA-2048/3072/4096、ECC-P256/P384/P521、Ed25519 支持密钥对生成、加解密(RSA)、签名验签(全算法) - 更新 Makefile / CMakeLists.txt 纳入新增源文件 - 新增 crypto_hash 示例:命令行计算 MD5/SHA 系列摘要 - 新增 crypto_asym 示例:生成密钥、签名、验签全流程 - 新增 README / README_CN 说明 API、安全建议与构建方式 --- examples/crypto/Makefile | 26 ++ examples/crypto/crypto_asym/Makefile | 14 + examples/crypto/crypto_asym/simple.cpp | 299 +++++++++++++ examples/crypto/crypto_hash/Makefile | 14 + examples/crypto/crypto_hash/simple.cpp | 59 +++ modules/crypto/CMakeLists.txt | 17 +- modules/crypto/Makefile | 10 + modules/crypto/README.md | 262 +++++++++++ modules/crypto/README_CN.md | 263 +++++++++++ modules/crypto/asym_ctx.cpp | 586 +++++++++++++++++++++++++ modules/crypto/asym_ctx.h | 197 +++++++++ modules/crypto/asym_ctx_test.cpp | 204 +++++++++ modules/crypto/crypto_ctx.cpp | 282 ++++++++++++ modules/crypto/crypto_ctx.h | 145 ++++++ modules/crypto/crypto_ctx_test.cpp | 136 ++++++ modules/crypto/digest_ctx.cpp | 200 +++++++++ modules/crypto/digest_ctx.h | 112 +++++ modules/crypto/digest_ctx_test.cpp | 59 +++ modules/crypto/images/image-1.png | Bin 0 -> 123181 bytes modules/crypto/images/image.png | Bin 0 -> 99750 bytes 20 files changed, 2882 insertions(+), 3 deletions(-) create mode 100644 examples/crypto/Makefile create mode 100644 examples/crypto/crypto_asym/Makefile create mode 100644 examples/crypto/crypto_asym/simple.cpp create mode 100644 examples/crypto/crypto_hash/Makefile create mode 100644 examples/crypto/crypto_hash/simple.cpp create mode 100644 modules/crypto/README.md create mode 100644 modules/crypto/README_CN.md create mode 100644 modules/crypto/asym_ctx.cpp create mode 100644 modules/crypto/asym_ctx.h create mode 100644 modules/crypto/asym_ctx_test.cpp create mode 100644 modules/crypto/crypto_ctx.cpp create mode 100644 modules/crypto/crypto_ctx.h create mode 100644 modules/crypto/crypto_ctx_test.cpp create mode 100644 modules/crypto/digest_ctx.cpp create mode 100644 modules/crypto/digest_ctx.h create mode 100644 modules/crypto/digest_ctx_test.cpp create mode 100644 modules/crypto/images/image-1.png create mode 100644 modules/crypto/images/image.png diff --git a/examples/crypto/Makefile b/examples/crypto/Makefile new file mode 100644 index 00000000..cce046ae --- /dev/null +++ b/examples/crypto/Makefile @@ -0,0 +1,26 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \\ \\ /\\ / +# \\ \\ \\ / +# \\ \\ \\ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +all test clean distclean: + @for i in $(shell ls) ; do \ + if [ -d $$i ]; then \ + $(MAKE) -C $$i $@ || exit $$? ; \ + fi \ + done diff --git a/examples/crypto/crypto_asym/Makefile b/examples/crypto/crypto_asym/Makefile new file mode 100644 index 00000000..4b50f28a --- /dev/null +++ b/examples/crypto/crypto_asym/Makefile @@ -0,0 +1,14 @@ +PROJECT := examples/crypto/crypto_asym +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := simple.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_crypto \ + -ltbox_util \ + -ltbox_base \ + -lssl -lcrypto \ + -lpthread -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/crypto/crypto_asym/simple.cpp b/examples/crypto/crypto_asym/simple.cpp new file mode 100644 index 00000000..a71989d0 --- /dev/null +++ b/examples/crypto/crypto_asym/simple.cpp @@ -0,0 +1,299 @@ +#include +#include +#include +#include +#include +#include + +using namespace tbox::crypto; + +void printUsage(const char *prog) { + LogInfo("Usage:"); + LogInfo(" %s genkey ", prog); + LogInfo(" %s sign ", prog); + LogInfo(" %s verify ", prog); + LogInfo(" %s encrypt ", prog); + LogInfo(" %s decrypt ", prog); + LogInfo("Algorithms:"); + LogInfo(" rsa2048, rsa3072, rsa4096 - RSA encryption/signing"); + LogInfo(" ecc256, ecc384, ecc521 - ECC signing (no encryption)"); + LogInfo(" ed25519 - EdDSA signing"); + LogInfo("Examples:"); + LogInfo(" # Generate key pair"); + LogInfo(" %s genkey rsa2048", prog); + LogInfo(" -> Generates rsa2048_private.pem and rsa2048_public.pem"); + LogInfo(" # Sign data"); + LogInfo(" %s sign rsa2048 rsa2048_private.pem 'Hello World'", prog); + LogInfo(" -> Outputs signature in rsa2048.sig"); + LogInfo(" # Verify signature"); + LogInfo(" %s verify rsa2048 rsa2048_public.pem 'Hello World' rsa2048.sig", prog); + LogInfo(" # Encrypt (RSA only)"); + LogInfo(" %s encrypt rsa2048 rsa2048_public.pem 'Secret Message'", prog); + LogInfo(" -> Outputs ciphertext in rsa2048_encrypted.bin"); + LogInfo(" # Decrypt (RSA only)"); + LogInfo(" %s decrypt rsa2048 rsa2048_private.pem rsa2048_encrypted.bin", prog); +} + +AsymAlgorithm parseAlgorithm(const std::string &algo_str) { + if (algo_str == "rsa2048") return AsymAlgorithm::kRSA2048; + if (algo_str == "rsa3072") return AsymAlgorithm::kRSA3072; + if (algo_str == "rsa4096") return AsymAlgorithm::kRSA4096; + if (algo_str == "ecc256") return AsymAlgorithm::kECC_P256; + if (algo_str == "ecc384") return AsymAlgorithm::kECC_P384; + if (algo_str == "ecc521") return AsymAlgorithm::kECC_P521; + if (algo_str == "ed25519") return AsymAlgorithm::kEd25519; + throw std::runtime_error("Unknown algorithm: " + algo_str); +} + +std::string readFile(const std::string &path) { + std::ifstream file(path, std::ios::binary); + if (!file) { + throw std::runtime_error("Cannot open file: " + path); + } + return std::string((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); +} + +void writeFile(const std::string &path, const std::string &content) { + std::ofstream file(path, std::ios::binary); + if (!file) { + throw std::runtime_error("Cannot open file for writing: " + path); + } + file.write(content.data(), content.size()); +} + +void writeBinaryFile(const std::string &path, const std::vector &data) { + std::ofstream file(path, std::ios::binary); + if (!file) { + throw std::runtime_error("Cannot open file for writing: " + path); + } + file.write(reinterpret_cast(data.data()), data.size()); +} + +std::vector readBinaryFile(const std::string &path) { + std::ifstream file(path, std::ios::binary); + if (!file) { + throw std::runtime_error("Cannot open file: " + path); + } + file.seekg(0, std::ios::end); + size_t size = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector data(size); + file.read(reinterpret_cast(data.data()), size); + return data; +} + +std::string bytesToHex(const std::vector &data) { + static const char hex_chars[] = "0123456789abcdef"; + std::string result; + for (auto byte : data) { + result += hex_chars[(byte >> 4) & 0x0F]; + result += hex_chars[byte & 0x0F]; + } + return result; +} + +int main(int argc, char *argv[]) { + try { + LogOutput_Enable(); + + if (argc < 2) { + printUsage(argv[0]); + return 1; + } + + std::string cmd = argv[1]; + + if (cmd == "genkey") { + if (argc < 3) { + LogErr("Usage: %s genkey ", argv[0]); + return 1; + } + + AsymAlgorithm algo = parseAlgorithm(argv[2]); + std::string private_key, public_key; + + LogInfo("Generating key pair for %s...", AsymCtx::getAlgoName(algo)); + + if (!AsymCtx::generateKeyPair(algo, private_key, public_key)) { + LogErr("Failed to generate key pair"); + return 1; + } + + std::string base_name = argv[2]; + std::string pri_file = base_name + "_private.pem"; + std::string pub_file = base_name + "_public.pem"; + + writeFile(pri_file, private_key); + writeFile(pub_file, public_key); + + LogInfo("Keys generated:"); + LogInfo(" Private key: %s (%zu bytes)", pri_file.c_str(), private_key.size()); + LogInfo(" Public key: %s (%zu bytes)", pub_file.c_str(), public_key.size()); + + } else if (cmd == "sign") { + if (argc < 5) { + LogErr("Usage: %s sign ", argv[0]); + return 1; + } + + AsymAlgorithm algo = parseAlgorithm(argv[2]); + std::string private_key_pem = readFile(argv[3]); + std::string text = argv[4]; + + AsymConfig cfg; + cfg.algo = algo; + cfg.private_key_pem = private_key_pem; + + AsymCtx ctx; + if (!ctx.initialize(cfg)) { + LogErr("Failed to initialize context"); + return 1; + } + + SignResult result = ctx.sign(text.data(), text.size()); + if (!result.success) { + LogErr("Signature failed: %s", result.error_message.c_str()); + return 1; + } + + std::string base_name = argv[2]; + std::string sig_file = base_name + ".sig"; + writeBinaryFile(sig_file, result.signature); + + LogInfo("Signature created:"); + LogInfo(" Algorithm: %s", AsymCtx::getAlgoName(algo)); + LogInfo(" Text: %s", text.c_str()); + LogInfo(" Signature: %s (%zu bytes)", sig_file.c_str(), result.signature.size()); + LogInfo(" Signature (hex): %s", bytesToHex(result.signature).c_str()); + + } else if (cmd == "verify") { + if (argc < 6) { + LogErr("Usage: %s verify ", argv[0]); + return 1; + } + + AsymAlgorithm algo = parseAlgorithm(argv[2]); + std::string public_key_pem = readFile(argv[3]); + std::string text = argv[4]; + std::vector signature = readBinaryFile(argv[5]); + + AsymConfig cfg; + cfg.algo = algo; + cfg.public_key_pem = public_key_pem; + + AsymCtx ctx; + if (!ctx.initialize(cfg)) { + LogErr("Failed to initialize context"); + return 1; + } + + VerifyResult result = + ctx.verify(text.data(), text.size(), signature.data(), signature.size()); + + if (!result.success) { + LogErr("Verification error: %s", result.error_message.c_str()); + return 1; + } + + LogInfo("Verification result:"); + LogInfo(" Algorithm: %s", AsymCtx::getAlgoName(algo)); + LogInfo(" Text: %s", text.c_str()); + LogInfo(" Valid: %s", result.valid ? "YES" : "NO"); + + return result.valid ? 0 : 1; + + } else if (cmd == "encrypt") { + if (argc < 5) { + LogErr("Usage: %s encrypt ", argv[0]); + return 1; + } + + AsymAlgorithm algo = parseAlgorithm(argv[2]); + std::string public_key_pem = readFile(argv[3]); + std::string plaintext = argv[4]; + + AsymConfig cfg; + cfg.algo = algo; + cfg.public_key_pem = public_key_pem; + + AsymCtx ctx; + if (!ctx.initialize(cfg)) { + LogErr("Failed to initialize context"); + return 1; + } + + if (algo != AsymAlgorithm::kRSA2048 && algo != AsymAlgorithm::kRSA3072 && + algo != AsymAlgorithm::kRSA4096) { + LogErr("Encryption only supported for RSA algorithms"); + return 1; + } + + EncryptResult result = ctx.encrypt(plaintext.data(), plaintext.size()); + if (!result.success) { + LogErr("Encryption failed: %s", result.error_message.c_str()); + return 1; + } + + std::string base_name = argv[2]; + std::string cipher_file = base_name + "_encrypted.bin"; + writeBinaryFile(cipher_file, result.ciphertext); + + LogInfo("Encryption successful:"); + LogInfo(" Algorithm: %s", AsymCtx::getAlgoName(algo)); + LogInfo(" Plaintext: %s (%zu bytes)", plaintext.c_str(), plaintext.size()); + LogInfo(" Ciphertext: %s (%zu bytes)", cipher_file.c_str(), result.ciphertext.size()); + + } else if (cmd == "decrypt") { + if (argc < 5) { + LogErr("Usage: %s decrypt ", argv[0]); + return 1; + } + + AsymAlgorithm algo = parseAlgorithm(argv[2]); + std::string private_key_pem = readFile(argv[3]); + std::vector ciphertext = readBinaryFile(argv[4]); + + AsymConfig cfg; + cfg.algo = algo; + cfg.private_key_pem = private_key_pem; + + AsymCtx ctx; + if (!ctx.initialize(cfg)) { + LogErr("Failed to initialize context"); + return 1; + } + + if (algo != AsymAlgorithm::kRSA2048 && algo != AsymAlgorithm::kRSA3072 && + algo != AsymAlgorithm::kRSA4096) { + LogErr("Decryption only supported for RSA algorithms"); + return 1; + } + + DecryptResult result = ctx.decrypt(ciphertext.data(), ciphertext.size()); + if (!result.success) { + LogErr("Decryption failed: %s", result.error_message.c_str()); + return 1; + } + + std::string plain_text(reinterpret_cast(result.plaintext.data()), + result.plaintext.size()); + LogInfo("Decryption successful:"); + LogInfo(" Algorithm: %s", AsymCtx::getAlgoName(algo)); + LogInfo(" Ciphertext: %s (%zu bytes)", argv[4], ciphertext.size()); + LogInfo(" Plaintext: %s (%zu bytes)", plain_text.c_str(), result.plaintext.size()); + + } else { + LogErr("Unknown command: %s", cmd.c_str()); + printUsage(argv[0]); + return 1; + } + + return 0; + + } catch (const std::exception &e) { + LogErr("Error: %s", e.what()); + return 1; + } +} diff --git a/examples/crypto/crypto_hash/Makefile b/examples/crypto/crypto_hash/Makefile new file mode 100644 index 00000000..714200c7 --- /dev/null +++ b/examples/crypto/crypto_hash/Makefile @@ -0,0 +1,14 @@ +PROJECT := examples/crypto/crypto_hash +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := simple.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_crypto \ + -ltbox_util \ + -ltbox_base \ + -lssl -lcrypto \ + -lpthread -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/crypto/crypto_hash/simple.cpp b/examples/crypto/crypto_hash/simple.cpp new file mode 100644 index 00000000..7eed9ed0 --- /dev/null +++ b/examples/crypto/crypto_hash/simple.cpp @@ -0,0 +1,59 @@ +#include +#include +#include +#include + +using namespace tbox::crypto; + +int main(int argc, char *argv[]) { + LogOutput_Enable(); + + if (argc < 3) { + LogInfo("Usage: %s ", argv[0]); + LogInfo("Algorithms:"); + LogInfo(" md5 - MD5 hash (128-bit)"); + LogInfo(" sha1 - SHA-1 hash (160-bit)"); + LogInfo(" sha256 - SHA-256 hash (256-bit, recommended)"); + LogInfo(" sha384 - SHA-384 hash (384-bit)"); + LogInfo(" sha512 - SHA-512 hash (512-bit)"); + LogInfo("Examples:"); + LogInfo(" %s sha256 'Hello, World!'", argv[0]); + LogInfo(" %s md5 'password'", argv[0]); + return 1; + } + + std::string algo_str = argv[1]; + std::string text = argv[2]; + + // Determine algorithm + DigestAlgorithm algo; + if (algo_str == "md5") { + algo = DigestAlgorithm::kMD5; + } else if (algo_str == "sha1") { + algo = DigestAlgorithm::kSHA1; + } else if (algo_str == "sha256") { + algo = DigestAlgorithm::kSHA256; + } else if (algo_str == "sha384") { + algo = DigestAlgorithm::kSHA384; + } else if (algo_str == "sha512") { + algo = DigestAlgorithm::kSHA512; + } else { + LogErr("Unknown algorithm: %s", algo_str.c_str()); + return 1; + } + + // Compute hash + DigestResult result = DigestCtx::hash(algo, text.data(), text.size()); + + if (!result.success) { + LogErr("Hash computation failed: %s", result.error_message.c_str()); + return 1; + } + + LogInfo("Algorithm: %s", DigestCtx::getAlgoName(algo)); + LogInfo("Text: %s", text.c_str()); + LogInfo("Hash: %s", result.digest_hex.c_str()); + LogInfo("Size: %zu bytes", result.digest.size()); + + return 0; +} diff --git a/modules/crypto/CMakeLists.txt b/modules/crypto/CMakeLists.txt index ed2bc53b..9073fcca 100644 --- a/modules/crypto/CMakeLists.txt +++ b/modules/crypto/CMakeLists.txt @@ -31,17 +31,28 @@ set(TBOX_LIBRARY_NAME tbox_crypto) set(TBOX_CRYPTO_HEADERS md5.h - aes.h) + aes.h + crypto_ctx.h + digest_ctx.h + asym_ctx.h) set(TBOX_CRYPTO_SOURCES md5.cpp - aes.cpp) + aes.cpp + crypto_ctx.cpp + digest_ctx.cpp + asym_ctx.cpp) set(TBOX_CRYPTO_TEST_SOURCES md5_test.cpp - aes_test.cpp) + aes_test.cpp + digest_ctx_test.cpp + crypto_ctx_test.cpp + asym_ctx_test.cpp) add_library(${TBOX_LIBRARY_NAME} ${TBOX_BUILD_LIB_TYPE} ${TBOX_CRYPTO_SOURCES}) +find_package(OpenSSL REQUIRED) +target_link_libraries(${TBOX_LIBRARY_NAME} OpenSSL::SSL OpenSSL::Crypto) set_target_properties(${TBOX_LIBRARY_NAME} PROPERTIES VERSION ${TBOX_CRYPTO_VERSION} diff --git a/modules/crypto/Makefile b/modules/crypto/Makefile index 3ba70668..cca2b108 100644 --- a/modules/crypto/Makefile +++ b/modules/crypto/Makefile @@ -27,17 +27,27 @@ LIB_VERSION_Z = 1 HEAD_FILES = \ md5.h \ aes.h \ + crypto_ctx.h \ + digest_ctx.h \ + asym_ctx.h \ CPP_SRC_FILES = \ md5.cpp \ aes.cpp \ + crypto_ctx.cpp \ + digest_ctx.cpp \ + asym_ctx.cpp \ CXXFLAGS := -DMODULE_ID='"tbox.crypto"' $(CXXFLAGS) +LDFLAGS := $(LDFLAGS) -lssl -lcrypto TEST_CPP_SRC_FILES = \ $(CPP_SRC_FILES) \ md5_test.cpp \ aes_test.cpp \ + digest_ctx_test.cpp \ + crypto_ctx_test.cpp \ + asym_ctx_test.cpp \ TEST_LDFLAGS := $(LDFLAGS) -ltbox_util -ltbox_base -ldl diff --git a/modules/crypto/README.md b/modules/crypto/README.md new file mode 100644 index 00000000..2967d51d --- /dev/null +++ b/modules/crypto/README.md @@ -0,0 +1,262 @@ +# Crypto Module + +## Overview + +The `crypto` module provides comprehensive cryptographic capabilities for the cpp-tbox framework. It includes the original AES and MD5 utilities as well as the newer OpenSSL-based APIs for modern symmetric encryption, hashing, and asymmetric cryptography. + +## Features + +### ✅ Symmetric Encryption +- **AES-256-GCM**: NIST-standard authenticated encryption with 256-bit keys +- **ChaCha20-Poly1305**: Modern high-performance authenticated encryption +- Support for AEAD (Authenticated Encryption with Associated Data) +- 16-byte authentication tags for integrity verification +- 96-bit nonce/IV (12 bytes) for both algorithms + +### ✅ Hash Algorithms +- **MD5**: 128-bit (legacy, compatibility only) +- **SHA-1**: 160-bit (legacy, not recommended) +- **SHA-256**: 256-bit (recommended) +- **SHA-384**: 384-bit (recommended) +- **SHA-512**: 512-bit (maximum security) +- Stream-based incremental hashing support + +### ✅ Asymmetric Cryptography +| Algorithm | Key Size | Signature | Encryption | Performance | +|-----------|----------|-----------|------------|-------------| +| RSA-2048 | 2048-bit | ✅ | ✅ | Baseline | +| RSA-3072 | 3072-bit | ✅ | ✅ | 2-3x slower | +| RSA-4096 | 4096-bit | ✅ | ✅ | 4-5x slower | +| ECC-P256 | 256-bit | ✅ | ❌ | 100x faster | +| ECC-P384 | 384-bit | ✅ | ❌ | 50x faster | +| ECC-P521 | 521-bit | ✅ | ❌ | 50x faster | +| Ed25519 | 256-bit | ✅ | ❌ | Fastest | + +Implementation notes: +- RSA encryption uses OAEP with SHA-256 (OpenSSL EVP API) +- RSA encryption input length is limited by key size and OAEP padding overhead + +### ✅ Integration with HTTPS +- Integrated with TLS/SSL (automatic at transport layer) +- Support for optional application-layer encryption +- mTLS (mutual TLS) with client certificates +- Custom cipher suite configuration +- Security level presets (kDefault, kCompatible, kStrict) + +## Encryption Methods and Use Cases (Quick Reference) + +This section answers two practical questions for product and engineering teams: + +1. Which encryption mechanism are we actually using? +2. Which business scenario is each mechanism for? + +![alt text](images/image-1.png) + +### Fast Selection Rules + +- If you only need channel security (anti-eavesdropping and anti-tamper), TLS is enough. +- If intermediates must not see business plaintext, use TLS + application-layer symmetric encryption. +- If you must prove sender identity and support non-repudiation, add digital signatures. +- If you need efficient protection for large payloads, use hybrid encryption (symmetric encryption for data + asymmetric encryption for the symmetric key). + +### Combination Examples + +**Scenario A: Standard HTTPS API** + +- Transport layer: TLS 1.3 +- Business layer: no extra encryption +- Suitable for: internal service calls, standard web APIs + +**Scenario B: Highly sensitive fields through third-party gateways** + +- Transport layer: TLS 1.3 +- Business layer: AES-256-GCM (field-level) +- Suitable for: IDs, key material, privacy fields + +**Scenario C: OTA / command dispatch** + +- Transport layer: TLS 1.3 +- Integrity/identity: Ed25519 signatures +- Optional: additional application-layer symmetric encryption for package payload +- Suitable for: firmware updates and remote control commands + +## Module Files + +### Legacy Utilities +- `aes.h` / `aes.cpp` - legacy AES block encryption helper +- `md5.h` / `md5.cpp` - legacy MD5 helper + +### Headers +- `crypto_ctx.h` - Symmetric encryption API +- `digest_ctx.h` - Hash algorithm API +- `asym_ctx.h` - Asymmetric encryption and signing API + +### Implementations +- `crypto_ctx.cpp` - AES-256-GCM, ChaCha20-Poly1305 +- `digest_ctx.cpp` - MD5, SHA-1, SHA-256/384/512 +- `asym_ctx.cpp` - RSA, ECC, Ed25519 + +## Usage Examples + +### Symmetric Encryption +```cpp +#include +using namespace tbox::crypto; + +CryptoCryptoCtx crypto; +CryptoConfig cfg; +cfg.algo = CryptoAlgorithm::kAES256GCM; +cfg.key_hex = ""; // 64 hex chars for 256-bit key +cfg.iv_hex = ""; // 24 hex chars for 96-bit IV +crypto.initialize(cfg); + +auto result = crypto.encrypt(plaintext, len); +if (result.success) { + // Use result.ciphertext and result.tag +} +``` + +### Hash Computation +```cpp +#include +using namespace tbox::crypto; + +DigestResult result = DigestCtx::hash(DigestAlgorithm::kSHA256, + data, len); +LogInfo("SHA-256: %s", result.digest_hex.c_str()); +``` + +### Asymmetric Encryption +```cpp +#include +using namespace tbox::crypto; + +// Generate key pair +std::string private_key, public_key; +AsymCtx::generateKeyPair(AsymAlgorithm::kRSA2048, + private_key, public_key); + +// Encrypt +AsymCtx ctx; +AsymConfig cfg; +cfg.algo = AsymAlgorithm::kRSA2048; +cfg.public_key_pem = public_key; +ctx.initialize(cfg); + +auto enc = ctx.encrypt(plaintext, len); +``` + +### Digital Signing +```cpp +// Sign with private key +cfg.private_key_pem = private_key; +ctx.initialize(cfg); +SignResult sig = ctx.sign(data, len); + +// Verify with public key +cfg.public_key_pem = public_key; +ctx.initialize(cfg); +VerifyResult ver = ctx.verify(data, len, + sig.signature.data(), + sig.signature.size()); +if (ver.success && ver.valid) { + LogInfo("Signature valid"); +} +``` + +Signing semantics: +- RSA and ECC signatures use SHA-256 internally on the original message +- Ed25519 signs and verifies the original message directly without a pre-hash step + +## Security Recommendations + +### ✅ Recommended Combinations + +**For Web APIs:** +``` +TLS 1.3 + ECDHE-P256 + AES-256-GCM + SHA-256 (automatic) +``` + +**For Data Encryption:** +``` +Application: AES-256-GCM +Integrity: SHA-256 +Key Exchange: RSA-2048 or ECC-P256 +``` + +**For Digital Signatures:** +``` +Hash: SHA-256 +Algorithm: Ed25519 (recommended) or ECC-P256 +``` + +### ❌ Avoid + +- ❌ MD5 or SHA-1 for security purposes +- ❌ DES or 3DES +- ❌ Re-implementing crypto (use OpenSSL) +- ❌ Storing keys in plaintext +- ❌ Direct RSA encryption for large data (use hybrid encryption) +- ❌ Reusing the same nonce/IV with the same key in AEAD modes (GCM/ChaCha20-Poly1305) + +## Examples + +See `examples/crypto/crypto_hash/` for hash computation examples +See `examples/crypto/crypto_asym/` for asymmetric encryption examples +See `examples/https/*/data_encrypt/` for application-layer encryption examples + +## Building + +```bash +# From the project root — build the crypto module only +make modules MODULES="crypto" + +# Build crypto examples (from the project root) +make examples MODULES="crypto" +``` + +## Testing + +```bash +# From the project root — build the crypto test binary +make test MODULES="crypto" + +# Run crypto module unit tests +./.build/crypto/test + +# CMake path +cmake --build build --target tbox_crypto_test -j2 +ctest --test-dir build --output-on-failure -R tbox_crypto + +# Hash example +./.build/examples/crypto/crypto_hash/examples/crypto/crypto_hash sha256 "test" + +# Asymmetric example +./.build/examples/crypto/crypto_asym/examples/crypto/crypto_asym genkey rsa2048 +./.build/examples/crypto/crypto_asym/examples/crypto/crypto_asym sign rsa2048 rsa2048_private.pem "data" +./.build/examples/crypto/crypto_asym/examples/crypto/crypto_asym verify rsa2048 rsa2048_public.pem "data" rsa2048.sig +``` + +Unit tests cover: +- digest one-shot and streaming paths +- AEAD encrypt/decrypt success and integrity failures +- RSA/Ed25519 sign-verify flows +- unsupported or invalid initialization paths +- OpenSSL interoperability verification for RSA and Ed25519 signatures + +Practical constraints: +- `CryptoCryptoCtx` currently binds key and IV at initialization; callers must ensure IV uniqueness per encrypted message. +- `AsymCtx::encrypt`/`decrypt` are RSA-only by design; ECC and Ed25519 are for signatures. + +Note: +- if the CMake build directory only built `tbox_crypto`, `ctest` may report `tbox_crypto_test` as not found +- build `tbox_crypto_test` explicitly before running the `ctest` command above + +## References + +- OpenSSL Documentation: https://www.openssl.org/docs/ +- NIST FIPS Standards: https://csrc.nist.gov/publications/fips/ + +## License + +This module is part of cpp-tbox and is licensed under the MIT License. See LICENSE file in the root directory. diff --git a/modules/crypto/README_CN.md b/modules/crypto/README_CN.md new file mode 100644 index 00000000..a48f230d --- /dev/null +++ b/modules/crypto/README_CN.md @@ -0,0 +1,263 @@ +# Crypto 模块 + +## 概述 + +`crypto` 模块为 cpp-tbox 框架提供完整的密码学能力。它既保留原有的 AES、MD5 工具,也包含基于 OpenSSL 的现代对称加密、哈希和非对称密码学接口。 + +## 核心特性 + +### ✅ 对称加密 +- **AES-256-GCM**: NIST 标准的认证加密,256 位密钥 +- **ChaCha20-Poly1305**: 现代高性能认证加密 +- 支持 AEAD(带认证的加密) +- 16 字节认证标签用于完整性验证 +- 两种算法都使用 96 位 nonce/IV(12 字节) + +### ✅ 哈希算法 +- **MD5**: 128 位(仅用于兼容性) +- **SHA-1**: 160 位(不推荐) +- **SHA-256**: 256 位(推荐) +- **SHA-384**: 384 位(推荐) +- **SHA-512**: 512 位(最高安全性) +- 支持流式增量哈希 + +### ✅ 非对称加密 + +| 算法 | 密钥长度 | 签名 | 加密 | 性能 | +|----------|---------|------|------|----------| +| RSA-2048 | 2048 位 | ✅ | ✅ | 基准 | +| RSA-3072 | 3072 位 | ✅ | ✅ | 慢 2-3 倍 | +| RSA-4096 | 4096 位 | ✅ | ✅ | 慢 4-5 倍 | +| ECC-P256 | 256 位 | ✅ | ❌ | 快 100 倍 | +| ECC-P384 | 384 位 | ✅ | ❌ | 快 50 倍 | +| ECC-P521 | 521 位 | ✅ | ❌ | 快 50 倍 | +| Ed25519 | 256 位 | ✅ | ❌ | 最快 | + +实现说明: +- RSA 加密使用 OAEP + SHA-256(OpenSSL EVP 接口) +- RSA 可加密明文长度受密钥位数与 OAEP 填充开销限制 + +### ✅ HTTPS 集成 +- 与 TLS/SSL 集成(传输层自动) +- 支持应用层可选加密 +- mTLS(双向 TLS)和客户端证书支持 +- 自定义密码套件配置 +- 安全级别预设(kDefault、kCompatible、kStrict) + +## 加密方式与使用场景(给业务方的速查) + +下面这张表用于回答两个问题: + +1. 你到底在用哪种加密方式? +2. 这种方式适合什么场景? + +![alt text](images/image.png) + +### 选型速记 + +- 只需要“链路安全”(防窃听、防篡改):用 TLS 即可。 +- 需要“中间链路看不到业务明文”:TLS + 应用层对称加密。 +- 需要“确认是谁发的,并且不可抵赖”:增加数字签名。 +- 需要“高效加密大体量数据”:使用混合加密(对称加密数据 + 非对称加密对称密钥)。 + +### 组合示例 + +**场景 A:普通 HTTPS API** + +- 传输层:TLS 1.3 +- 业务层:不额外加密 +- 适用:内部服务调用、标准 Web API + +**场景 B:高敏字段(经过第三方网关)** + +- 传输层:TLS 1.3 +- 业务层:AES-256-GCM(字段级) +- 适用:身份证号、密钥材料、隐私字段 + +**场景 C:固件升级 / 指令下发** + +- 传输层:TLS 1.3 +- 完整性/身份:Ed25519 签名 +- 可选:固件包再做应用层对称加密 +- 适用:OTA、远程控制命令 + +## 模块文件 + +### 历史工具接口 +- `aes.h` / `aes.cpp` - 历史 AES 分组加密工具 +- `md5.h` / `md5.cpp` - 历史 MD5 工具 + +### 头文件 +- `crypto_ctx.h` - 对称加密 API +- `digest_ctx.h` - 哈希算法 API +- `asym_ctx.h` - 非对称加密和签名 API + +### 实现文件 +- `crypto_ctx.cpp` - AES-256-GCM、ChaCha20-Poly1305 +- `digest_ctx.cpp` - MD5、SHA-1、SHA-256/384/512 +- `asym_ctx.cpp` - RSA、ECC、Ed25519 + +## 使用示例 + +### 对称加密 +```cpp +#include +using namespace tbox::crypto; + +CryptoCryptoCtx crypto; +CryptoConfig cfg; +cfg.algo = CryptoAlgorithm::kAES256GCM; +cfg.key_hex = ""; // 64 个十六进制字符(256 位密钥) +cfg.iv_hex = ""; // 24 个十六进制字符(96 位 IV) +crypto.initialize(cfg); + +auto result = crypto.encrypt(plaintext, len); +if (result.success) { + // 使用 result.ciphertext 和 result.tag +} +``` + +### 计算哈希 +```cpp +#include +using namespace tbox::crypto; + +DigestResult result = DigestCtx::hash(DigestAlgorithm::kSHA256, + data, len); +LogInfo("SHA-256: %s", result.digest_hex.c_str()); +``` + +### 非对称加密 +```cpp +#include +using namespace tbox::crypto; + +// 生成密钥对 +std::string private_key, public_key; +AsymCtx::generateKeyPair(AsymAlgorithm::kRSA2048, + private_key, public_key); + +// 加密 +AsymCtx ctx; +AsymConfig cfg; +cfg.algo = AsymAlgorithm::kRSA2048; +cfg.public_key_pem = public_key; +ctx.initialize(cfg); + +auto enc = ctx.encrypt(plaintext, len); +``` + +### 数字签名 +```cpp +// 用私钥签名 +cfg.private_key_pem = private_key; +ctx.initialize(cfg); +SignResult sig = ctx.sign(data, len); + +// 用公钥验证 +cfg.public_key_pem = public_key; +ctx.initialize(cfg); +VerifyResult ver = ctx.verify(data, len, + sig.signature.data(), + sig.signature.size()); +if (ver.success && ver.valid) { + LogInfo("签名有效"); +} +``` + +签名语义说明: +- RSA 和 ECC 会对原始消息在内部使用 SHA-256 后再签名与验签 +- Ed25519 直接对原始消息签名与验签,不执行预哈希 + +## 安全建议 + +### ✅ 推荐的加密组合 + +**Web API 安全通信:** +``` +TLS 1.3 + ECDHE-P256 + AES-256-GCM + SHA-256(自动) +``` + +**文件数据加密:** +``` +应用层: AES-256-GCM +完整性: SHA-256 +密钥交换: RSA-2048 或 ECC-P256 +``` + +**数字签名认证:** +``` +哈希: SHA-256 +算法: Ed25519(推荐)或 ECC-P256 +``` + +### ❌ 避免的做法 + +- ❌ 用 MD5 或 SHA-1 进行安全用途 +- ❌ 使用 DES 或 3DES +- ❌ 重新实现加密算法(使用 OpenSSL 库) +- ❌ 以明文方式存储密钥 +- ❌ 用 RSA 直接加密大量数据(使用混合加密) +- ❌ 在 AEAD 模式(GCM/ChaCha20-Poly1305)下对同一密钥重复使用同一个 nonce/IV + +## 示例程序 + +查看 `examples/crypto/crypto_hash/` 了解哈希计算示例 +查看 `examples/crypto/crypto_asym/` 了解非对称加密示例 +查看 `examples/https/*/data_encrypt/` 了解应用层加密示例 + +## 编译构建 + +```bash +# 从项目根目录执行——仅构建 crypto 模块 +make modules MODULES="crypto" + +# 构建 crypto 示例(从项目根目录执行) +make examples MODULES="crypto" +``` + +## 测试 + +```bash +# 从项目根目录执行——构建 crypto 测试二进制 +make test MODULES="crypto" + +# 运行 crypto 模块单元测试 +./.build/crypto/test + +# CMake 路径 +cmake --build build --target tbox_crypto_test -j2 +ctest --test-dir build --output-on-failure -R tbox_crypto + +# 哈希示例 +./.build/examples/crypto/crypto_hash/examples/crypto/crypto_hash sha256 "test" + +# 非对称示例 +./.build/examples/crypto/crypto_asym/examples/crypto/crypto_asym genkey rsa2048 +./.build/examples/crypto/crypto_asym/examples/crypto/crypto_asym sign rsa2048 rsa2048_private.pem "data" +./.build/examples/crypto/crypto_asym/examples/crypto/crypto_asym verify rsa2048 rsa2048_public.pem "data" rsa2048.sig +``` + +当前单元测试覆盖: +- 摘要算法的一次性与流式计算 +- AEAD 加解密成功路径与完整性失败路径 +- RSA 与 Ed25519 的签名验签流程 +- 非法初始化与不支持算法的失败路径 +- RSA 与 Ed25519 签名的 OpenSSL 互通验证 + +实践约束: +- `CryptoCryptoCtx` 当前在初始化时固定 key 与 IV,调用方需自行保证每条密文使用唯一 IV。 +- `AsymCtx::encrypt`/`decrypt` 仅支持 RSA;ECC 与 Ed25519 用于签名场景。 + +说明: +- 如果当前 CMake 构建目录只编译过 `tbox_crypto`,直接执行 `ctest` 可能会提示找不到 `tbox_crypto_test` +- 先显式构建 `tbox_crypto_test`,再执行上面的 `ctest` 命令即可 + +## 参考资源 + +- OpenSSL 文档: https://www.openssl.org/docs/ +- NIST FIPS 标准: https://csrc.nist.gov/publications/fips/ + +## 许可证 + +该模块是 cpp-tbox 的一部分,采用 MIT 许可证。详见根目录 LICENSE 文件。 \ No newline at end of file diff --git a/modules/crypto/asym_ctx.cpp b/modules/crypto/asym_ctx.cpp new file mode 100644 index 00000000..3b2517c9 --- /dev/null +++ b/modules/crypto/asym_ctx.cpp @@ -0,0 +1,586 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "asym_ctx.h" + +namespace tbox { +namespace crypto { + +AsymCtx::AsymCtx() : algo_(AsymAlgorithm::kRSA2048), pkey_(nullptr) { +} + +AsymCtx::~AsymCtx() { + if (pkey_) { + EVP_PKEY_free(static_cast(pkey_)); + pkey_ = nullptr; + } +} + +bool AsymCtx::initialize(const AsymConfig &cfg) { + algo_ = cfg.algo; + + // Free previous key + if (pkey_) { + EVP_PKEY_free(static_cast(pkey_)); + pkey_ = nullptr; + } + + // Load private key if provided + if (!cfg.private_key_pem.empty()) { + BIO *bio = BIO_new_mem_buf(cfg.private_key_pem.data(), + static_cast(cfg.private_key_pem.size())); + if (!bio) { + LogErr("Failed to create BIO for private key"); + return false; + } + + EVP_PKEY *pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + BIO_free(bio); + + if (!pkey) { + LogErr("Failed to load private key"); + return false; + } + + pkey_ = pkey; + LogInfo("Private key loaded: algo=%s", getAlgoName(algo_)); + return true; + } + + // Load public key if provided + if (!cfg.public_key_pem.empty()) { + BIO *bio = BIO_new_mem_buf(cfg.public_key_pem.data(), + static_cast(cfg.public_key_pem.size())); + if (!bio) { + LogErr("Failed to create BIO for public key"); + return false; + } + + EVP_PKEY *pkey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr); + BIO_free(bio); + + if (!pkey) { + LogErr("Failed to load public key"); + return false; + } + + pkey_ = pkey; + LogInfo("Public key loaded: algo=%s", getAlgoName(algo_)); + return true; + } + + LogErr("No private or public key provided"); + return false; +} + +bool AsymCtx::generateKeyPair(AsymAlgorithm algo, std::string &private_key_out, + std::string &public_key_out) { + EVP_PKEY_CTX *ctx = nullptr; + EVP_PKEY *pkey = nullptr; + BIO *bio = nullptr; + + try { + // Create context for key generation + if (algo == AsymAlgorithm::kRSA2048 || algo == AsymAlgorithm::kRSA3072 || + algo == AsymAlgorithm::kRSA4096) { + int bits = (algo == AsymAlgorithm::kRSA2048) ? 2048 : + (algo == AsymAlgorithm::kRSA3072) ? 3072 : 4096; + + ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr); + if (!ctx) { + LogErr("EVP_PKEY_CTX_new_id failed"); + return false; + } + + if (EVP_PKEY_keygen_init(ctx) <= 0) { + LogErr("EVP_PKEY_keygen_init failed"); + EVP_PKEY_CTX_free(ctx); + return false; + } + + if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) <= 0) { + LogErr("EVP_PKEY_CTX_set_rsa_keygen_bits failed"); + EVP_PKEY_CTX_free(ctx); + return false; + } + + if (EVP_PKEY_keygen(ctx, &pkey) <= 0) { + LogErr("EVP_PKEY_keygen failed"); + EVP_PKEY_CTX_free(ctx); + return false; + } + + EVP_PKEY_CTX_free(ctx); + } else if (algo == AsymAlgorithm::kECC_P256 || algo == AsymAlgorithm::kECC_P384 || + algo == AsymAlgorithm::kECC_P521) { + int nid = (algo == AsymAlgorithm::kECC_P256) ? NID_X9_62_prime256v1 : + (algo == AsymAlgorithm::kECC_P384) ? NID_secp384r1 : + NID_secp521r1; + + ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr); + if (!ctx) { + LogErr("EVP_PKEY_CTX_new_id failed for EC"); + return false; + } + + if (EVP_PKEY_paramgen_init(ctx) <= 0) { + LogErr("EVP_PKEY_paramgen_init failed"); + EVP_PKEY_CTX_free(ctx); + return false; + } + + if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, nid) <= 0) { + LogErr("EVP_PKEY_CTX_set_ec_paramgen_curve_nid failed"); + EVP_PKEY_CTX_free(ctx); + return false; + } + + EVP_PKEY *params = nullptr; + if (EVP_PKEY_paramgen(ctx, ¶ms) <= 0) { + LogErr("EVP_PKEY_paramgen failed"); + EVP_PKEY_CTX_free(ctx); + return false; + } + + EVP_PKEY_CTX_free(ctx); + ctx = EVP_PKEY_CTX_new(params, nullptr); + EVP_PKEY_free(params); + + if (!ctx) { + LogErr("EVP_PKEY_CTX_new failed for EC keygen"); + return false; + } + + if (EVP_PKEY_keygen_init(ctx) <= 0) { + LogErr("EVP_PKEY_keygen_init failed for EC"); + EVP_PKEY_CTX_free(ctx); + return false; + } + + if (EVP_PKEY_keygen(ctx, &pkey) <= 0) { + LogErr("EVP_PKEY_keygen failed for EC"); + EVP_PKEY_CTX_free(ctx); + return false; + } + + EVP_PKEY_CTX_free(ctx); + } else if (algo == AsymAlgorithm::kEd25519) { + ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, nullptr); + if (!ctx) { + LogErr("EVP_PKEY_CTX_new_id failed for Ed25519"); + return false; + } + + if (EVP_PKEY_keygen_init(ctx) <= 0) { + LogErr("EVP_PKEY_keygen_init failed for Ed25519"); + EVP_PKEY_CTX_free(ctx); + return false; + } + + if (EVP_PKEY_keygen(ctx, &pkey) <= 0) { + LogErr("EVP_PKEY_keygen failed for Ed25519"); + EVP_PKEY_CTX_free(ctx); + return false; + } + + EVP_PKEY_CTX_free(ctx); + } else { + LogErr("Unsupported algorithm for key generation"); + return false; + } + + // Write private key + bio = BIO_new(BIO_s_mem()); + if (!bio) { + LogErr("BIO_new failed for private key"); + EVP_PKEY_free(pkey); + return false; + } + + if (PEM_write_bio_PrivateKey(bio, pkey, nullptr, nullptr, 0, nullptr, nullptr) <= 0) { + LogErr("PEM_write_bio_PrivateKey failed"); + BIO_free(bio); + EVP_PKEY_free(pkey); + return false; + } + + // Extract private key string + size_t len = BIO_pending(bio); + private_key_out.resize(len); + BIO_read(bio, const_cast(private_key_out.data()), len); + BIO_free(bio); + + // Write public key + bio = BIO_new(BIO_s_mem()); + if (!bio) { + LogErr("BIO_new failed for public key"); + EVP_PKEY_free(pkey); + return false; + } + + if (PEM_write_bio_PUBKEY(bio, pkey) <= 0) { + LogErr("PEM_write_bio_PUBKEY failed"); + BIO_free(bio); + EVP_PKEY_free(pkey); + return false; + } + + // Extract public key string + len = BIO_pending(bio); + public_key_out.resize(len); + BIO_read(bio, const_cast(public_key_out.data()), len); + BIO_free(bio); + + EVP_PKEY_free(pkey); + LogInfo("Key pair generated: algo=%s", getAlgoName(algo)); + return true; + + } catch (...) { + if (ctx) EVP_PKEY_CTX_free(ctx); + if (pkey) EVP_PKEY_free(pkey); + if (bio) BIO_free(bio); + return false; + } +} + +EncryptResult AsymCtx::encrypt(const void *plaintext, size_t len) { + EncryptResult result; + + if (!pkey_) { + result.error_message = "Key not loaded"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + if (algo_ != AsymAlgorithm::kRSA2048 && algo_ != AsymAlgorithm::kRSA3072 && + algo_ != AsymAlgorithm::kRSA4096) { + result.error_message = "Encryption only supported for RSA algorithms"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(static_cast(pkey_), nullptr); + if (!ctx) { + result.error_message = "EVP_PKEY_CTX_new failed"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + if (EVP_PKEY_encrypt_init(ctx) <= 0) { + result.error_message = "EVP_PKEY_encrypt_init failed"; + LogErr("%s", result.error_message.c_str()); + EVP_PKEY_CTX_free(ctx); + return result; + } + + if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) { + result.error_message = "EVP_PKEY_CTX_set_rsa_padding failed"; + LogErr("%s", result.error_message.c_str()); + EVP_PKEY_CTX_free(ctx); + return result; + } + + if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()) <= 0) { + result.error_message = "EVP_PKEY_CTX_set_rsa_oaep_md failed"; + LogErr("%s", result.error_message.c_str()); + EVP_PKEY_CTX_free(ctx); + return result; + } + + // Get output buffer length + size_t out_len = 0; + if (EVP_PKEY_encrypt(ctx, nullptr, &out_len, + static_cast(plaintext), len) <= 0) { + result.error_message = "EVP_PKEY_encrypt (get size) failed"; + LogErr("%s", result.error_message.c_str()); + EVP_PKEY_CTX_free(ctx); + return result; + } + + result.ciphertext.resize(out_len); + + // Encrypt + if (EVP_PKEY_encrypt(ctx, result.ciphertext.data(), &out_len, + static_cast(plaintext), len) <= 0) { + result.error_message = "EVP_PKEY_encrypt failed"; + LogErr("%s", result.error_message.c_str()); + EVP_PKEY_CTX_free(ctx); + return result; + } + + result.ciphertext.resize(out_len); + result.success = true; + EVP_PKEY_CTX_free(ctx); + + LogInfo("RSA encryption successful: plaintext_len=%zu, ciphertext_len=%zu", + len, out_len); + return result; +} + +DecryptResult AsymCtx::decrypt(const void *ciphertext, size_t len) { + DecryptResult result; + + if (!pkey_) { + result.error_message = "Key not loaded"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + if (algo_ != AsymAlgorithm::kRSA2048 && algo_ != AsymAlgorithm::kRSA3072 && + algo_ != AsymAlgorithm::kRSA4096) { + result.error_message = "Decryption only supported for RSA algorithms"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(static_cast(pkey_), nullptr); + if (!ctx) { + result.error_message = "EVP_PKEY_CTX_new failed"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + if (EVP_PKEY_decrypt_init(ctx) <= 0) { + result.error_message = "EVP_PKEY_decrypt_init failed"; + LogErr("%s", result.error_message.c_str()); + EVP_PKEY_CTX_free(ctx); + return result; + } + + if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) { + result.error_message = "EVP_PKEY_CTX_set_rsa_padding failed"; + LogErr("%s", result.error_message.c_str()); + EVP_PKEY_CTX_free(ctx); + return result; + } + + if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()) <= 0) { + result.error_message = "EVP_PKEY_CTX_set_rsa_oaep_md failed"; + LogErr("%s", result.error_message.c_str()); + EVP_PKEY_CTX_free(ctx); + return result; + } + + // Get output buffer length + size_t out_len = 0; + if (EVP_PKEY_decrypt(ctx, nullptr, &out_len, + static_cast(ciphertext), len) <= 0) { + result.error_message = "EVP_PKEY_decrypt (get size) failed"; + LogErr("%s", result.error_message.c_str()); + EVP_PKEY_CTX_free(ctx); + return result; + } + + result.plaintext.resize(out_len); + + // Decrypt + if (EVP_PKEY_decrypt(ctx, result.plaintext.data(), &out_len, + static_cast(ciphertext), len) <= 0) { + result.error_message = "EVP_PKEY_decrypt failed"; + LogErr("%s", result.error_message.c_str()); + EVP_PKEY_CTX_free(ctx); + return result; + } + + result.plaintext.resize(out_len); + result.success = true; + EVP_PKEY_CTX_free(ctx); + + LogInfo("RSA decryption successful: ciphertext_len=%zu, plaintext_len=%zu", + len, out_len); + return result; +} + +SignResult AsymCtx::sign(const void *data, size_t len) { + return signInternal(data, len); +} + +SignResult AsymCtx::signInternal(const void *data, size_t len) { + SignResult result; + + if (!pkey_) { + result.error_message = "Key not loaded"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + EVP_MD_CTX *md_ctx = EVP_MD_CTX_new(); + if (!md_ctx) { + result.error_message = "EVP_MD_CTX_new failed"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + const EVP_MD *md = nullptr; + if (algo_ == AsymAlgorithm::kEd25519) { + // Ed25519 doesn't need explicit hash (uses context-based signing) + md = nullptr; + } else { + md = EVP_sha256(); + } + + if (EVP_DigestSignInit(md_ctx, nullptr, md, nullptr, + static_cast(pkey_)) <= 0) { + result.error_message = "EVP_DigestSignInit failed"; + LogErr("%s", result.error_message.c_str()); + EVP_MD_CTX_free(md_ctx); + return result; + } + + size_t sig_len = 0; + if (EVP_DigestSign(md_ctx, nullptr, &sig_len, + static_cast(data), len) <= 0) { + result.error_message = "EVP_DigestSign (get size) failed"; + LogErr("%s", result.error_message.c_str()); + EVP_MD_CTX_free(md_ctx); + return result; + } + + result.signature.resize(sig_len); + if (EVP_DigestSign(md_ctx, result.signature.data(), &sig_len, + static_cast(data), len) <= 0) { + result.error_message = "EVP_DigestSign failed"; + LogErr("%s", result.error_message.c_str()); + EVP_MD_CTX_free(md_ctx); + return result; + } + + result.signature.resize(sig_len); + result.success = true; + EVP_MD_CTX_free(md_ctx); + + LogInfo("Signature created: algo=%s, sig_len=%zu", getAlgoName(algo_), sig_len); + return result; +} + +VerifyResult AsymCtx::verify(const void *data, size_t len, + const void *signature, size_t sig_len) { + return verifyInternal(data, len, signature, sig_len); +} + +VerifyResult AsymCtx::verifyInternal(const void *data, size_t len, + const void *signature, size_t sig_len) { + VerifyResult result; + + if (!pkey_) { + result.error_message = "Key not loaded"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + EVP_MD_CTX *md_ctx = EVP_MD_CTX_new(); + if (!md_ctx) { + result.error_message = "EVP_MD_CTX_new failed"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + const EVP_MD *md = nullptr; + if (algo_ == AsymAlgorithm::kEd25519) { + md = nullptr; + } else { + md = EVP_sha256(); + } + + if (EVP_DigestVerifyInit(md_ctx, nullptr, md, nullptr, + static_cast(pkey_)) <= 0) { + result.error_message = "EVP_DigestVerifyInit failed"; + LogErr("%s", result.error_message.c_str()); + EVP_MD_CTX_free(md_ctx); + return result; + } + + int verify_result = EVP_DigestVerify(md_ctx, + static_cast(signature), + sig_len, + static_cast(data), + len); + + EVP_MD_CTX_free(md_ctx); + + if (verify_result == 1) { + result.success = true; + result.valid = true; + LogInfo("Signature verification successful: algo=%s", getAlgoName(algo_)); + } else if (verify_result == 0) { + result.success = true; + result.valid = false; + LogWarn("Signature verification failed: invalid signature"); + } else { + result.success = false; + result.error_message = "EVP_DigestVerify error"; + LogErr("%s", result.error_message.c_str()); + } + + return result; +} + +const char* AsymCtx::getAlgoName(AsymAlgorithm algo) { + switch (algo) { + case AsymAlgorithm::kRSA2048: + return "RSA-2048"; + case AsymAlgorithm::kRSA3072: + return "RSA-3072"; + case AsymAlgorithm::kRSA4096: + return "RSA-4096"; + case AsymAlgorithm::kECC_P256: + return "ECC-P256"; + case AsymAlgorithm::kECC_P384: + return "ECC-P384"; + case AsymAlgorithm::kECC_P521: + return "ECC-P521"; + case AsymAlgorithm::kEd25519: + return "Ed25519"; + default: + return "UNKNOWN"; + } +} + +size_t AsymCtx::getSignatureSize(AsymAlgorithm algo) { + switch (algo) { + case AsymAlgorithm::kRSA2048: + return 256; // 2048 bits / 8 + case AsymAlgorithm::kRSA3072: + return 384; // 3072 bits / 8 + case AsymAlgorithm::kRSA4096: + return 512; // 4096 bits / 8 + case AsymAlgorithm::kECC_P256: + return 64; // 2 * 32 bytes + case AsymAlgorithm::kECC_P384: + return 96; // 2 * 48 bytes + case AsymAlgorithm::kECC_P521: + return 132; // 2 * 66 bytes + case AsymAlgorithm::kEd25519: + return 64; // Fixed 64 bytes + default: + return 0; + } +} + +} // namespace crypto +} // namespace tbox diff --git a/modules/crypto/asym_ctx.h b/modules/crypto/asym_ctx.h new file mode 100644 index 00000000..6482b9f6 --- /dev/null +++ b/modules/crypto/asym_ctx.h @@ -0,0 +1,197 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_CRYPTO_ASYM_CTX_H_20260324 +#define TBOX_CRYPTO_ASYM_CTX_H_20260324 + +#include +#include +#include + +namespace tbox { +namespace crypto { + +/** + * 非对称加密/签名算法枚举 + */ +enum class AsymAlgorithm { + // RSA 算法 + kRSA2048, //!< RSA-2048 (推荐用于向后兼容) + kRSA3072, //!< RSA-3072 (良好安全性和性能平衡) + kRSA4096, //!< RSA-4096 (最大安全性,较慢) + + // ECC 算法 + kECC_P256, //!< NIST P-256 / secp256r1 (256-bit, 广泛支持) + kECC_P384, //!< NIST P-384 / secp384r1 (384-bit, 更安全) + kECC_P521, //!< NIST P-521 / secp521r1 (521-bit, 最高安全) + + // EdDSA 算法 (现代替代品,更快更安全) + kEd25519, //!< Ed25519 (128-bit 安全等级) +}; + +/** + * 签名/验证结果 + */ +struct SignResult { + bool success = false; + std::vector signature; //!< 签名数据 + std::string error_message; //!< 错误信息 +}; + +struct VerifyResult { + bool success = false; + bool valid = false; //!< 签名是否有效 + std::string error_message; //!< 错误信息 +}; + +/** + * 非对称加密操作结果 + */ +struct EncryptResult { + bool success = false; + std::vector ciphertext; //!< 密文 + std::string error_message; //!< 错误信息 +}; + +struct DecryptResult { + bool success = false; + std::vector plaintext; //!< 明文 + std::string error_message; //!< 错误信息 +}; + +/** + * 非对称加密上下文配置 + */ +struct AsymConfig { + AsymAlgorithm algo = AsymAlgorithm::kRSA2048; //!< 算法类型 + std::string private_key_pem; //!< 私钥 PEM 格式 + std::string public_key_pem; //!< 公钥 PEM 格式 +}; + +/** + * 非对称加密/解密上下文类 + * 支持 RSA 和 ECC 算法的加密、解密、签名、验证 + * + * RSA 使用场景: + * - 密钥长度: 2048/3072/4096 位 + * - 加密: 用公钥加密,私钥解密 + * - 签名: 用私钥签名,公钥验证 + * + * ECC 使用场景: + * - 密钥长度: 256/384/521 位 + * - 签名: 用私钥签名,公钥验证 (ECDSA) + * - 加密: 不直接支持,需要配合 ECDH 密钥交换 (用于 TLS) + * + * EdDSA 使用场景: + * - Ed25519: 现代优先选择(快速、安全、简洁) + * - 仅支持签名/验证 (不支持加密) + */ +class AsymCtx { + public: + AsymCtx(); + ~AsymCtx(); + + /** + * 初始化非对称加密上下文 + * @param cfg 配置参数 + * @return 成功返回 true + */ + bool initialize(const AsymConfig &cfg); + + /** + * 生成密钥对 (仅用于测试/演示) + * @param algo 算法类型 + * @param private_key_out 返回私钥 PEM + * @param public_key_out 返回公钥 PEM + * @return 成功返回 true + */ + static bool generateKeyPair( + AsymAlgorithm algo, + std::string &private_key_out, + std::string &public_key_out); + + // =========== RSA 加密/解密 =========== + + /** + * RSA 公钥加密 (用于 RSA 算法) + * @param plaintext 明文数据 + * @param len 明文长度 + * @return 加密结果 + */ + EncryptResult encrypt(const void *plaintext, size_t len); + + /** + * RSA 私钥解密 (用于 RSA 算法) + * @param ciphertext 密文数据 + * @param len 密文长度 + * @return 解密结果 + */ + DecryptResult decrypt(const void *ciphertext, size_t len); + + // =========== 签名/验证 =========== + + /** + * 使用私钥签名 (支持 RSA、ECC、Ed25519) + * @param data 要签名的数据 + * @param len 数据长度 + * @return 签名结果 + * + * 注: 对于 RSA/ECC,内部使用 SHA-256 作为摘要算法; + * 对于 Ed25519,直接对原始消息签名。 + */ + SignResult sign(const void *data, size_t len); + + /** + * 使用公钥验证签名 (支持 RSA、ECC、Ed25519) + * @param data 原始数据 + * @param len 数据长度 + * @param signature 签名数据 + * @param sig_len 签名长度 + * @return 验证结果 + */ + VerifyResult verify(const void *data, size_t len, + const void *signature, size_t sig_len); + + /** + * 获取算法名称 + * @param algo 算法类型 + * @return 算法名称字符串 + */ + static const char* getAlgoName(AsymAlgorithm algo); + + /** + * 获取签名大小 (字节数) + * @param algo 算法类型 + * @return 签名大小 + */ + static size_t getSignatureSize(AsymAlgorithm algo); + + private: + AsymAlgorithm algo_; + void *pkey_; // EVP_PKEY* (opaque) + + SignResult signInternal(const void *data, size_t len); + VerifyResult verifyInternal(const void *data, size_t len, + const void *signature, size_t sig_len); +}; + +} // namespace crypto +} // namespace tbox + +#endif // TBOX_CRYPTO_ASYM_CTX_H_20260324 diff --git a/modules/crypto/asym_ctx_test.cpp b/modules/crypto/asym_ctx_test.cpp new file mode 100644 index 00000000..721266ab --- /dev/null +++ b/modules/crypto/asym_ctx_test.cpp @@ -0,0 +1,204 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \\ \\ /\\ / + * \\ \\ \\ / + * \\ \\ \\ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +#include +#include +#include + +#include + +#include "asym_ctx.h" + +namespace tbox { +namespace crypto { + +namespace { + +bool VerifyWithOpenSsl(const std::string &public_key_pem, + AsymAlgorithm algo, + const void *data, + size_t len, + const std::vector &signature) { + BIO *bio = BIO_new_mem_buf(public_key_pem.data(), static_cast(public_key_pem.size())); + if (!bio) { + return false; + } + + EVP_PKEY *pkey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr); + BIO_free(bio); + if (!pkey) { + return false; + } + + EVP_MD_CTX *md_ctx = EVP_MD_CTX_new(); + if (!md_ctx) { + EVP_PKEY_free(pkey); + return false; + } + + const EVP_MD *md = algo == AsymAlgorithm::kEd25519 ? nullptr : EVP_sha256(); + bool ok = false; + if (EVP_DigestVerifyInit(md_ctx, nullptr, md, nullptr, pkey) > 0) { + ok = EVP_DigestVerify(md_ctx, + signature.data(), signature.size(), + static_cast(data), len) == 1; + } + + EVP_MD_CTX_free(md_ctx); + EVP_PKEY_free(pkey); + return ok; +} + +} // namespace + +TEST(AsymCtx, RsaEncryptDecrypt) { + std::string private_key; + std::string public_key; + ASSERT_TRUE(AsymCtx::generateKeyPair(AsymAlgorithm::kRSA2048, private_key, public_key)); + + AsymCtx enc_ctx; + AsymConfig enc_cfg; + enc_cfg.algo = AsymAlgorithm::kRSA2048; + enc_cfg.public_key_pem = public_key; + ASSERT_TRUE(enc_ctx.initialize(enc_cfg)); + + const char *plain = "secret"; + auto enc = enc_ctx.encrypt(plain, strlen(plain)); + ASSERT_TRUE(enc.success); + + AsymCtx dec_ctx; + AsymConfig dec_cfg; + dec_cfg.algo = AsymAlgorithm::kRSA2048; + dec_cfg.private_key_pem = private_key; + ASSERT_TRUE(dec_ctx.initialize(dec_cfg)); + + auto dec = dec_ctx.decrypt(enc.ciphertext.data(), enc.ciphertext.size()); + ASSERT_TRUE(dec.success); + EXPECT_EQ(std::string(reinterpret_cast(dec.plaintext.data()), dec.plaintext.size()), + plain); +} + +TEST(AsymCtx, RsaSignVerify) { + std::string private_key; + std::string public_key; + ASSERT_TRUE(AsymCtx::generateKeyPair(AsymAlgorithm::kRSA2048, private_key, public_key)); + + const char *message = "sign me"; + + AsymCtx sign_ctx; + AsymConfig sign_cfg; + sign_cfg.algo = AsymAlgorithm::kRSA2048; + sign_cfg.private_key_pem = private_key; + ASSERT_TRUE(sign_ctx.initialize(sign_cfg)); + auto sig = sign_ctx.sign(message, strlen(message)); + ASSERT_TRUE(sig.success); + + AsymCtx verify_ctx; + AsymConfig verify_cfg; + verify_cfg.algo = AsymAlgorithm::kRSA2048; + verify_cfg.public_key_pem = public_key; + ASSERT_TRUE(verify_ctx.initialize(verify_cfg)); + auto ver = verify_ctx.verify(message, strlen(message), sig.signature.data(), sig.signature.size()); + ASSERT_TRUE(ver.success); + EXPECT_TRUE(ver.valid); + EXPECT_TRUE(VerifyWithOpenSsl(public_key, AsymAlgorithm::kRSA2048, + message, strlen(message), sig.signature)); +} + +TEST(AsymCtx, Ed25519SignVerify) { + std::string private_key; + std::string public_key; + ASSERT_TRUE(AsymCtx::generateKeyPair(AsymAlgorithm::kEd25519, private_key, public_key)); + + const char *message = "eddsa"; + + AsymCtx sign_ctx; + AsymConfig sign_cfg; + sign_cfg.algo = AsymAlgorithm::kEd25519; + sign_cfg.private_key_pem = private_key; + ASSERT_TRUE(sign_ctx.initialize(sign_cfg)); + auto sig = sign_ctx.sign(message, strlen(message)); + ASSERT_TRUE(sig.success); + + AsymCtx verify_ctx; + AsymConfig verify_cfg; + verify_cfg.algo = AsymAlgorithm::kEd25519; + verify_cfg.public_key_pem = public_key; + ASSERT_TRUE(verify_ctx.initialize(verify_cfg)); + auto ver = verify_ctx.verify(message, strlen(message), sig.signature.data(), sig.signature.size()); + ASSERT_TRUE(ver.success); + EXPECT_TRUE(ver.valid); + EXPECT_TRUE(VerifyWithOpenSsl(public_key, AsymAlgorithm::kEd25519, + message, strlen(message), sig.signature)); +} + +TEST(AsymCtx, InitializeWithoutKeyFails) { + AsymCtx ctx; + AsymConfig cfg; + cfg.algo = AsymAlgorithm::kRSA2048; + + EXPECT_FALSE(ctx.initialize(cfg)); +} + +TEST(AsymCtx, TamperedSignatureIsRejected) { + std::string private_key; + std::string public_key; + ASSERT_TRUE(AsymCtx::generateKeyPair(AsymAlgorithm::kRSA2048, private_key, public_key)); + + const char *message = "sign me"; + + AsymCtx sign_ctx; + AsymConfig sign_cfg; + sign_cfg.algo = AsymAlgorithm::kRSA2048; + sign_cfg.private_key_pem = private_key; + ASSERT_TRUE(sign_ctx.initialize(sign_cfg)); + auto sig = sign_ctx.sign(message, strlen(message)); + ASSERT_TRUE(sig.success); + ASSERT_FALSE(sig.signature.empty()); + + sig.signature[0] ^= 0x01; + + AsymCtx verify_ctx; + AsymConfig verify_cfg; + verify_cfg.algo = AsymAlgorithm::kRSA2048; + verify_cfg.public_key_pem = public_key; + ASSERT_TRUE(verify_ctx.initialize(verify_cfg)); + auto ver = verify_ctx.verify(message, strlen(message), sig.signature.data(), sig.signature.size()); + ASSERT_TRUE(ver.success); + EXPECT_FALSE(ver.valid); +} + +TEST(AsymCtx, EncryptWithEd25519Fails) { + std::string private_key; + std::string public_key; + ASSERT_TRUE(AsymCtx::generateKeyPair(AsymAlgorithm::kEd25519, private_key, public_key)); + + AsymCtx ctx; + AsymConfig cfg; + cfg.algo = AsymAlgorithm::kEd25519; + cfg.public_key_pem = public_key; + ASSERT_TRUE(ctx.initialize(cfg)); + + auto result = ctx.encrypt("hello", 5); + EXPECT_FALSE(result.success); +} + +} // namespace crypto +} // namespace tbox \ No newline at end of file diff --git a/modules/crypto/crypto_ctx.cpp b/modules/crypto/crypto_ctx.cpp new file mode 100644 index 00000000..116e77bd --- /dev/null +++ b/modules/crypto/crypto_ctx.cpp @@ -0,0 +1,282 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "crypto_ctx.h" + +#include +#include +#include +#include + +#include + +#undef MODULE_ID +#define MODULE_ID "tbox.crypto" + +namespace tbox { +namespace crypto { + +CryptoCryptoCtx::CryptoCryptoCtx() = default; + +CryptoCryptoCtx::~CryptoCryptoCtx() { + // Securely clear sensitive data + std::fill(key_.begin(), key_.end(), 0); + std::fill(iv_.begin(), iv_.end(), 0); +} + +bool CryptoCryptoCtx::HexToBytes(const std::string &hex, std::vector &bytes) { + if (hex.length() % 2 != 0) { + LogErr("Hex string length must be even"); + return false; + } + + bytes.clear(); + bytes.reserve(hex.length() / 2); + + for (size_t i = 0; i < hex.length(); i += 2) { + char byte_str[3] = {hex[i], hex[i+1], '\0'}; + char *endptr = nullptr; + long byte_val = std::strtol(byte_str, &endptr, 16); + + if (endptr != &byte_str[2] || byte_val < 0 || byte_val > 255) { + LogErr("Invalid hex characters: %s", byte_str); + return false; + } + + bytes.push_back(static_cast(byte_val)); + } + + return true; +} + +bool CryptoCryptoCtx::initialize(const CryptoConfig &config) { + if (is_valid_) { + LogWarn("CryptoCryptoCtx already initialized"); + return false; + } + + algo_ = config.algo; + + // Parse hex key and IV + if (!HexToBytes(config.key_hex, key_)) { + LogErr("Failed to parse key hex string"); + return false; + } + + if (!HexToBytes(config.iv_hex, iv_)) { + LogErr("Failed to parse IV hex string"); + return false; + } + + // Validate key and IV sizes based on algorithm + if (config.algo == CryptoAlgorithm::kAES256GCM) { + if (key_.size() != 32) { // AES-256 = 256 bits = 32 bytes + LogErr("AES-256-GCM requires 32-byte (64 hex) key, got %zu bytes", key_.size()); + return false; + } + if (iv_.size() != 12) { // Standard IV size for GCM + LogErr("AES-256-GCM requires 12-byte (24 hex) IV, got %zu bytes", iv_.size()); + return false; + } + } else if (config.algo == CryptoAlgorithm::kChaCha20Poly1305) { + if (key_.size() != 32) { // ChaCha20 = 256 bits = 32 bytes + LogErr("ChaCha20-Poly1305 requires 32-byte (64 hex) key, got %zu bytes", key_.size()); + return false; + } + if (iv_.size() != 12) { // ChaCha20 nonce = 12 bytes + LogErr("ChaCha20-Poly1305 requires 12-byte (24 hex) nonce, got %zu bytes", iv_.size()); + return false; + } + } + + is_valid_ = true; + LogInfo("Crypto context initialized: algo=%d, key_size=%zu, iv_size=%zu", + static_cast(config.algo), key_.size(), iv_.size()); + return true; +} + +CryptoResult CryptoCryptoCtx::encrypt(const uint8_t *plaintext, size_t plen, + const uint8_t *aad, size_t alen) const { + CryptoResult result; + + if (!is_valid_) { + result.error_message = "CryptoCryptoCtx not initialized"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + // Select cipher based on algorithm + const EVP_CIPHER *cipher = nullptr; + if (algo_ == CryptoAlgorithm::kAES256GCM) { + cipher = EVP_aes_256_gcm(); + } else if (algo_ == CryptoAlgorithm::kChaCha20Poly1305) { + cipher = EVP_chacha20_poly1305(); + } else { + result.error_message = "Unknown cipher algorithm"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + if (!ctx) { + result.error_message = "Failed to create cipher context"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + // Initialize encryption + if (EVP_EncryptInit_ex(ctx, cipher, nullptr, key_.data(), iv_.data()) != 1) { + result.error_message = "EVP_EncryptInit_ex failed"; + LogErr("%s", result.error_message.c_str()); + EVP_CIPHER_CTX_free(ctx); + return result; + } + + // Process AAD if provided + if (aad && alen > 0) { + int len = 0; + if (EVP_EncryptUpdate(ctx, nullptr, &len, aad, alen) != 1) { + result.error_message = "EVP_EncryptUpdate (AAD) failed"; + LogErr("%s", result.error_message.c_str()); + EVP_CIPHER_CTX_free(ctx); + return result; + } + } + + // Encrypt plaintext + result.ciphertext.resize(plen); + int clen = 0; + if (EVP_EncryptUpdate(ctx, result.ciphertext.data(), &clen, plaintext, plen) != 1) { + result.error_message = "EVP_EncryptUpdate (plaintext) failed"; + LogErr("%s", result.error_message.c_str()); + EVP_CIPHER_CTX_free(ctx); + return result; + } + + int flen = 0; + if (EVP_EncryptFinal_ex(ctx, result.ciphertext.data() + clen, &flen) != 1) { + result.error_message = "EVP_EncryptFinal_ex failed"; + LogErr("%s", result.error_message.c_str()); + EVP_CIPHER_CTX_free(ctx); + return result; + } + + // Get authentication tag (16 bytes) + result.tag.resize(16); + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, result.tag.data()) != 1) { + result.error_message = "Failed to get authentication tag"; + LogErr("%s", result.error_message.c_str()); + EVP_CIPHER_CTX_free(ctx); + return result; + } + + EVP_CIPHER_CTX_free(ctx); + result.success = true; + return result; +} + +DecryptoResult CryptoCryptoCtx::decrypt(const uint8_t *ciphertext, size_t clen, + const uint8_t *tag, size_t tag_len, + const uint8_t *aad, size_t alen) const { + DecryptoResult result; + + if (!is_valid_) { + result.error_message = "CryptoCryptoCtx not initialized"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + if (tag_len != 16) { + result.error_message = "Tag must be 16 bytes"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + // Select cipher based on algorithm + const EVP_CIPHER *cipher = nullptr; + if (algo_ == CryptoAlgorithm::kAES256GCM) { + cipher = EVP_aes_256_gcm(); + } else if (algo_ == CryptoAlgorithm::kChaCha20Poly1305) { + cipher = EVP_chacha20_poly1305(); + } else { + result.error_message = "Unknown cipher algorithm"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + if (!ctx) { + result.error_message = "Failed to create cipher context"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + // Initialize decryption + if (EVP_DecryptInit_ex(ctx, cipher, nullptr, key_.data(), iv_.data()) != 1) { + result.error_message = "EVP_DecryptInit_ex failed"; + LogErr("%s", result.error_message.c_str()); + EVP_CIPHER_CTX_free(ctx); + return result; + } + + // Set authentication tag for verification + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, tag_len, + const_cast(tag)) != 1) { + result.error_message = "Failed to set authentication tag"; + LogErr("%s", result.error_message.c_str()); + EVP_CIPHER_CTX_free(ctx); + return result; + } + + // Process AAD if provided + if (aad && alen > 0) { + int len = 0; + if (EVP_DecryptUpdate(ctx, nullptr, &len, aad, alen) != 1) { + result.error_message = "EVP_DecryptUpdate (AAD) failed"; + LogErr("%s", result.error_message.c_str()); + EVP_CIPHER_CTX_free(ctx); + return result; + } + } + + // Decrypt ciphertext + result.plaintext.resize(clen); + int plen = 0; + if (EVP_DecryptUpdate(ctx, result.plaintext.data(), &plen, ciphertext, clen) != 1) { + result.error_message = "EVP_DecryptUpdate failed"; + LogErr("%s", result.error_message.c_str()); + EVP_CIPHER_CTX_free(ctx); + return result; + } + + // Verify and finalize (this also verifies the tag) + if (EVP_DecryptFinal_ex(ctx, result.plaintext.data() + plen, &plen) != 1) { + result.error_message = "Tag verification failed or decryption failed"; + LogErr("%s", result.error_message.c_str()); + EVP_CIPHER_CTX_free(ctx); + return result; + } + + EVP_CIPHER_CTX_free(ctx); + result.success = true; + return result; +} + +} // namespace crypto +} // namespace tbox diff --git a/modules/crypto/crypto_ctx.h b/modules/crypto/crypto_ctx.h new file mode 100644 index 00000000..79e70043 --- /dev/null +++ b/modules/crypto/crypto_ctx.h @@ -0,0 +1,145 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_CRYPTO_CTX_H_20260324 +#define TBOX_CRYPTO_CTX_H_20260324 + +#include +#include +#include +#include + +namespace tbox { +namespace crypto { + +/** + * 对称加密算法枚举 + */ +enum class CryptoAlgorithm { + kAES256GCM, //!< AES-256-GCM (NIST 标准, 12 字节 IV) + kChaCha20Poly1305, //!< ChaCha20-Poly1305 (12 字节 IV) +}; + +/** + * 数据加密常见参数 + */ +struct CryptoConfig { + CryptoAlgorithm algo = CryptoAlgorithm::kAES256GCM; + std::string key_hex; //!< 十六进制密钥 (AES-256 需 64 字符, ChaCha20 需 64 字符) + std::string iv_hex; //!< 十六进制初始化向量 (AES-256 需 24 字符, ChaCha20 需 24 字符) +}; + +/** + * 加密操作结果 + */ +struct CryptoResult { + bool success = false; + std::vector ciphertext; //!< 密文 + std::vector tag; //!< 认证标签 (16 字节) + std::string error_message; //!< 错误信息 (失败时) +}; + +/** + * 解密操作结果 + */ +struct DecryptoResult { + bool success = false; + std::vector plaintext; //!< 明文 + std::string error_message; //!< 错误信息 (失败时) +}; + +/** + * 对称加密上下文 - 支持 AES-256-GCM 和 ChaCha20-Poly1305 + * + * 用法示例: + * @code + * CryptoCryptoCtx crypto; + * CryptoConfig cfg; + * cfg.algo = CryptoAlgorithm::kAES256GCM; + * cfg.key_hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; // 64 hex chars + * cfg.iv_hex = "0123456789abcdef01234567"; // 24 hex chars (12 bytes) + * crypto.initialize(cfg); + * + * std::string plaintext = "Hello, World!"; + * auto result = crypto.encrypt( + * reinterpret_cast(plaintext.data()), + * plaintext.size(), + * nullptr, 0 // no AAD (Additional Authenticated Data) + * ); + * if (result.success) { + * // use result.ciphertext and result.tag + * } + * @endcode + */ +class CryptoCryptoCtx { + public: + CryptoCryptoCtx(); + ~CryptoCryptoCtx(); + + NONCOPYABLE(CryptoCryptoCtx); + IMMOVABLE(CryptoCryptoCtx); + + /** + * 初始化加密上下文 + * @param config 加密配置 (密钥、IV、算法) + * @return true 成功; false 失败 + */ + bool initialize(const CryptoConfig &config); + + /** + * 加密数据 + * @param plaintext 明文数据 + * @param plen 明文长度 + * @param aad 关联认证数据 (可选, 用于完整性校验但不加密) + * @param alen AAD 长度 + * @return CryptoResult 包含密文、认证标签、或错误信息 + */ + CryptoResult encrypt(const uint8_t *plaintext, size_t plen, + const uint8_t *aad = nullptr, size_t alen = 0) const; + + /** + * 解密数据 + * @param ciphertext 密文数据 + * @param clen 密文长度 + * @param tag 认证标签 (16 字节) + * @param tag_len 标签长度 + * @param aad 关联认证数据 (与加密时相同) + * @param alen AAD 长度 + * @return DecryptoResult 包含明文、或错误信息 + */ + DecryptoResult decrypt(const uint8_t *ciphertext, size_t clen, + const uint8_t *tag, size_t tag_len, + const uint8_t *aad = nullptr, size_t alen = 0) const; + + bool isValid() const { return is_valid_; } + + private: + CryptoAlgorithm algo_ = CryptoAlgorithm::kAES256GCM; + std::vector key_; // 原始密钥字节 + std::vector iv_; // 原始 IV 字节 + bool is_valid_ = false; + + // Helper: hex string to bytes + static bool HexToBytes(const std::string &hex, std::vector &bytes); +}; + +} // namespace crypto +} // namespace tbox + +#endif // TBOX_CRYPTO_CTX_H_20260324 diff --git a/modules/crypto/crypto_ctx_test.cpp b/modules/crypto/crypto_ctx_test.cpp new file mode 100644 index 00000000..261a027c --- /dev/null +++ b/modules/crypto/crypto_ctx_test.cpp @@ -0,0 +1,136 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \\ \\ /\\ / + * \\ \\ \\ / + * \\ \\ \\ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +#include + +#include "crypto_ctx.h" + +namespace tbox { +namespace crypto { + +namespace { + +const char *kKeyHex = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +const char *kIvHex = "0123456789abcdef01234567"; + +} // namespace + +TEST(CryptoCtx, Aes256GcmRoundTrip) { + CryptoCryptoCtx ctx; + CryptoConfig cfg; + cfg.algo = CryptoAlgorithm::kAES256GCM; + cfg.key_hex = kKeyHex; + cfg.iv_hex = kIvHex; + ASSERT_TRUE(ctx.initialize(cfg)); + + const char *plain = "hello encrypted world"; + const uint8_t aad[] = {1, 2, 3, 4}; + + auto enc = ctx.encrypt(reinterpret_cast(plain), strlen(plain), aad, sizeof(aad)); + ASSERT_TRUE(enc.success); + EXPECT_EQ(enc.tag.size(), 16u); + + auto dec = ctx.decrypt(enc.ciphertext.data(), enc.ciphertext.size(), + enc.tag.data(), enc.tag.size(), aad, sizeof(aad)); + ASSERT_TRUE(dec.success); + EXPECT_EQ(std::string(reinterpret_cast(dec.plaintext.data()), dec.plaintext.size()), + plain); +} + +TEST(CryptoCtx, Chacha20Poly1305RoundTrip) { + CryptoCryptoCtx ctx; + CryptoConfig cfg; + cfg.algo = CryptoAlgorithm::kChaCha20Poly1305; + cfg.key_hex = kKeyHex; + cfg.iv_hex = kIvHex; + ASSERT_TRUE(ctx.initialize(cfg)); + + const char *plain = "chacha20-poly1305"; + auto enc = ctx.encrypt(reinterpret_cast(plain), strlen(plain)); + ASSERT_TRUE(enc.success); + + auto dec = ctx.decrypt(enc.ciphertext.data(), enc.ciphertext.size(), + enc.tag.data(), enc.tag.size()); + ASSERT_TRUE(dec.success); + EXPECT_EQ(std::string(reinterpret_cast(dec.plaintext.data()), dec.plaintext.size()), + plain); +} + +TEST(CryptoCtx, TamperedTagFails) { + CryptoCryptoCtx ctx; + CryptoConfig cfg; + cfg.algo = CryptoAlgorithm::kAES256GCM; + cfg.key_hex = kKeyHex; + cfg.iv_hex = kIvHex; + ASSERT_TRUE(ctx.initialize(cfg)); + + const char *plain = "integrity check"; + auto enc = ctx.encrypt(reinterpret_cast(plain), strlen(plain)); + ASSERT_TRUE(enc.success); + + auto tag = enc.tag; + tag[0] ^= 0x01; + auto dec = ctx.decrypt(enc.ciphertext.data(), enc.ciphertext.size(), tag.data(), tag.size()); + EXPECT_FALSE(dec.success); +} + +TEST(CryptoCtx, InvalidKeyLengthFails) { + CryptoCryptoCtx ctx; + CryptoConfig cfg; + cfg.algo = CryptoAlgorithm::kAES256GCM; + cfg.key_hex = "00112233"; + cfg.iv_hex = kIvHex; + + EXPECT_FALSE(ctx.initialize(cfg)); +} + +TEST(CryptoCtx, InvalidHexFails) { + CryptoCryptoCtx ctx; + CryptoConfig cfg; + cfg.algo = CryptoAlgorithm::kChaCha20Poly1305; + cfg.key_hex = "zz23456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + cfg.iv_hex = kIvHex; + + EXPECT_FALSE(ctx.initialize(cfg)); +} + +TEST(CryptoCtx, WrongAadFails) { + CryptoCryptoCtx ctx; + CryptoConfig cfg; + cfg.algo = CryptoAlgorithm::kAES256GCM; + cfg.key_hex = kKeyHex; + cfg.iv_hex = kIvHex; + ASSERT_TRUE(ctx.initialize(cfg)); + + const char *plain = "aad protected payload"; + const uint8_t aad[] = {1, 2, 3, 4}; + const uint8_t wrong_aad[] = {4, 3, 2, 1}; + + auto enc = ctx.encrypt(reinterpret_cast(plain), strlen(plain), aad, sizeof(aad)); + ASSERT_TRUE(enc.success); + + auto dec = ctx.decrypt(enc.ciphertext.data(), enc.ciphertext.size(), + enc.tag.data(), enc.tag.size(), wrong_aad, sizeof(wrong_aad)); + EXPECT_FALSE(dec.success); +} + +} // namespace crypto +} // namespace tbox \ No newline at end of file diff --git a/modules/crypto/digest_ctx.cpp b/modules/crypto/digest_ctx.cpp new file mode 100644 index 00000000..2a32ed62 --- /dev/null +++ b/modules/crypto/digest_ctx.cpp @@ -0,0 +1,200 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +#include +#include +#include +#include + +#include "digest_ctx.h" + +namespace tbox { +namespace crypto { + +DigestCtx::DigestCtx() : algo_(DigestAlgorithm::kSHA256), md_ctx_(nullptr) { +} + +DigestCtx::~DigestCtx() { + if (md_ctx_) { + EVP_MD_CTX_free(static_cast(md_ctx_)); + md_ctx_ = nullptr; + } +} + +bool DigestCtx::initialize(DigestAlgorithm algo) { + algo_ = algo; + + // Free previous context + if (md_ctx_) { + EVP_MD_CTX_free(static_cast(md_ctx_)); + } + + // Create new context + md_ctx_ = EVP_MD_CTX_new(); + if (!md_ctx_) { + LogErr("Failed to create EVP_MD_CTX"); + return false; + } + + // Select message digest algorithm + const EVP_MD *md = nullptr; + switch (algo) { + case DigestAlgorithm::kMD5: + md = EVP_md5(); + break; + case DigestAlgorithm::kSHA1: + md = EVP_sha1(); + break; + case DigestAlgorithm::kSHA256: + md = EVP_sha256(); + break; + case DigestAlgorithm::kSHA384: + md = EVP_sha384(); + break; + case DigestAlgorithm::kSHA512: + md = EVP_sha512(); + break; + default: + LogErr("Unknown digest algorithm"); + EVP_MD_CTX_free(static_cast(md_ctx_)); + md_ctx_ = nullptr; + return false; + } + + // Initialize context + if (EVP_DigestInit_ex(static_cast(md_ctx_), md, nullptr) != 1) { + LogErr("EVP_DigestInit_ex failed"); + EVP_MD_CTX_free(static_cast(md_ctx_)); + md_ctx_ = nullptr; + return false; + } + + LogInfo("Digest context initialized: algo=%s", getAlgoName(algo)); + return true; +} + +bool DigestCtx::update(const void *data, size_t len) { + if (!md_ctx_) { + LogErr("Digest context not initialized"); + return false; + } + + if (EVP_DigestUpdate(static_cast(md_ctx_), data, len) != 1) { + LogErr("EVP_DigestUpdate failed"); + return false; + } + + return true; +} + +DigestResult DigestCtx::finish() { + DigestResult result; + + if (!md_ctx_) { + result.error_message = "Digest context not initialized"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + size_t digest_len = getDigestSize(algo_); + result.digest.resize(digest_len); + unsigned int out_len = 0; + + if (EVP_DigestFinal_ex(static_cast(md_ctx_), + result.digest.data(), &out_len) != 1) { + result.error_message = "EVP_DigestFinal_ex failed"; + LogErr("%s", result.error_message.c_str()); + return result; + } + + if (out_len != digest_len) { + result.error_message = "Digest length mismatch"; + LogErr("%s: expected %zu, got %u", result.error_message.c_str(), + digest_len, out_len); + return result; + } + + // Convert to hex string + static const char hex_chars[] = "0123456789abcdef"; + result.digest_hex.clear(); + for (auto byte : result.digest) { + result.digest_hex += hex_chars[(byte >> 4) & 0x0F]; + result.digest_hex += hex_chars[byte & 0x0F]; + } + + result.success = true; + LogInfo("Digest computed: %s", result.digest_hex.c_str()); + return result; +} + +DigestResult DigestCtx::hash(DigestAlgorithm algo, const void *data, + size_t len) { + DigestCtx ctx; + if (!ctx.initialize(algo)) { + DigestResult result; + result.error_message = "Failed to initialize digest context"; + return result; + } + + if (!ctx.update(data, len)) { + DigestResult result; + result.error_message = "Failed to update digest"; + return result; + } + + return ctx.finish(); +} + +size_t DigestCtx::getDigestSize(DigestAlgorithm algo) { + switch (algo) { + case DigestAlgorithm::kMD5: + return 16; // 128-bit + case DigestAlgorithm::kSHA1: + return 20; // 160-bit + case DigestAlgorithm::kSHA256: + return 32; // 256-bit + case DigestAlgorithm::kSHA384: + return 48; // 384-bit + case DigestAlgorithm::kSHA512: + return 64; // 512-bit + default: + return 0; + } +} + +const char* DigestCtx::getAlgoName(DigestAlgorithm algo) { + switch (algo) { + case DigestAlgorithm::kMD5: + return "MD5"; + case DigestAlgorithm::kSHA1: + return "SHA-1"; + case DigestAlgorithm::kSHA256: + return "SHA-256"; + case DigestAlgorithm::kSHA384: + return "SHA-384"; + case DigestAlgorithm::kSHA512: + return "SHA-512"; + default: + return "UNKNOWN"; + } +} + +} // namespace crypto +} // namespace tbox diff --git a/modules/crypto/digest_ctx.h b/modules/crypto/digest_ctx.h new file mode 100644 index 00000000..adb3bfc4 --- /dev/null +++ b/modules/crypto/digest_ctx.h @@ -0,0 +1,112 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_CRYPTO_DIGEST_CTX_H_20260324 +#define TBOX_CRYPTO_DIGEST_CTX_H_20260324 + +#include +#include +#include + +namespace tbox { +namespace crypto { + +/** + * 哈希算法枚举 + */ +enum class DigestAlgorithm { + kMD5, //!< MD5 (128-bit, 16 字节) + kSHA1, //!< SHA-1 (160-bit, 20 字节) + kSHA256, //!< SHA-256 (256-bit, 32 字节) + kSHA384, //!< SHA-384 (384-bit, 48 字节) + kSHA512, //!< SHA-512 (512-bit, 64 字节) +}; + +/** + * 哈希计算结果 + */ +struct DigestResult { + bool success = false; + std::vector digest; //!< 哈希摘要 + std::string digest_hex; //!< 十六进制哈希值 + std::string error_message; //!< 错误信息 (失败时) +}; + +/** + * 哈希上下文类 + * 支持流式更新哈希 + */ +class DigestCtx { + public: + DigestCtx(); + ~DigestCtx(); + + /** + * 初始化哈希上下文 + * @param algo 哈希算法 + * @return 成功返回 true + */ + bool initialize(DigestAlgorithm algo); + + /** + * 更新哈希数据 (可多次调用) + * @param data 要哈希的数据 + * @param len 数据长度 + * @return 成功返回 true + */ + bool update(const void *data, size_t len); + + /** + * 完成哈希计算并获取结果 + * @return 哈希结果 + */ + DigestResult finish(); + + /** + * 一次性计算哈希 (简单使用) + * @param algo 哈希算法 + * @param data 要哈希的数据 + * @param len 数据长度 + * @return 哈希结果 + */ + static DigestResult hash(DigestAlgorithm algo, const void *data, size_t len); + + /** + * 获取指定算法的摘要大小 + * @param algo 哈希算法 + * @return 摘要大小(字节数) + */ + static size_t getDigestSize(DigestAlgorithm algo); + + /** + * 获取算法名称 + * @param algo 哈希算法 + * @return 算法名称字符串 + */ + static const char* getAlgoName(DigestAlgorithm algo); + + private: + DigestAlgorithm algo_; + void *md_ctx_; // EVP_MD_CTX* (opaque) +}; + +} // namespace crypto +} // namespace tbox + +#endif // TBOX_CRYPTO_DIGEST_CTX_H_20260324 diff --git a/modules/crypto/digest_ctx_test.cpp b/modules/crypto/digest_ctx_test.cpp new file mode 100644 index 00000000..b3426c13 --- /dev/null +++ b/modules/crypto/digest_ctx_test.cpp @@ -0,0 +1,59 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \\ \\ /\\ / + * \\ \\ \\ / + * \\ \\ \\ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +#include + +#include "digest_ctx.h" + +namespace tbox { +namespace crypto { + +TEST(DigestCtx, Sha256OneShot) { + const char *text = "hello"; + auto result = DigestCtx::hash(DigestAlgorithm::kSHA256, text, 5); + + ASSERT_TRUE(result.success); + EXPECT_EQ(result.digest_hex, + "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"); + EXPECT_EQ(result.digest.size(), 32u); +} + +TEST(DigestCtx, Sha512Streaming) { + DigestCtx ctx; + ASSERT_TRUE(ctx.initialize(DigestAlgorithm::kSHA512)); + ASSERT_TRUE(ctx.update("he", 2)); + ASSERT_TRUE(ctx.update("llo", 3)); + + auto result = ctx.finish(); + ASSERT_TRUE(result.success); + EXPECT_EQ(result.digest_hex, + "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043"); +} + +TEST(DigestCtx, InvalidUsageFails) { + DigestCtx ctx; + EXPECT_FALSE(ctx.update("x", 1)); + + auto result = ctx.finish(); + EXPECT_FALSE(result.success); +} + +} // namespace crypto +} // namespace tbox \ No newline at end of file diff --git a/modules/crypto/images/image-1.png b/modules/crypto/images/image-1.png new file mode 100644 index 0000000000000000000000000000000000000000..d2b25f695d67a22f83483551efa792b0df60c07f GIT binary patch literal 123181 zcmc$`Wk6Nk+V3q0xagGb66ufz=>|c%K}zXv=|&n^lu`l;NSAbjlyrA@cb|dx-urpZ zyU+Lc8!jhvjXCDH>VN$%!j#@fp&}6?J$dp3RYqD|`NKld^Xni%*`A zKamj^QFVja^$O8Zk-0fPV7VGZg+q&hgD8ZPOXOwIxiD&$_*oiD2*Z^cwG?TVK5=>5 z5@iH~`IfN&f?JYdr1f&k^!4(}A5X3|JAZQO2)v2?Rg*t`92c_#eX}FSKB2c%5DYj7 zEF6sg6A@o>VGQ!W|C|eNesH$jr*{0mKK<84zM+g1)c;)ffBjl50Su*FZ4IpS`TzCt zKR1u134HqB9}9C+37g1pYdq;r`_Fg(&sAlCcK@@a|9re@1{cb>wT?tSh5g?PpDtTf zo6|X#`qy*(=a(Lr%ZKUb;+#mTHN4MH4J*=22d6*Qz)Al)Odi89;K|Z+qR-OA#+c;w z?6_=L>8P-Q8dge__I*S!T>8Q$VeXC@QlFzxF+Bq_+ad9yzK1>8`Mo9)H1+Js`Rp&n z5R2vWYeP%*{GSl9ke6l4WxNO*#xxWY_xOlK3n>xtJq$;crH(3vVCWNk!VeojfN5pm zqKoBOkNkUmdp++Um26jVs9>v-QJ@Tku$!s!m??WFmVUClTJq=2Kn+?oz4l3F5#LBW z46R|B!Fcg4NC^d{;A=!|xJhzhw+;jvtS(JBNO6QjMEm=HKg@~b-|LO$i8nEGjK$hX z-_fzPW@CUS%-d!sFa~SR&6n>{YGsJeXEw4*Bya9S1{;gDDU;;{H;EBTnjVMTj5~1z~!YPVeM?VU_*>Csg9=p{V?^I|6C9I zEL&OYMYd6+ObJES6*TShPe+WwGxpYkKO)fVV*@x3j$)^V-KNPHMG~nD?vwnLAIU4J z4O6|7ZZNSlBqj~og;42irot@s*k5YY(C$?d`5$=oox!0EPY{&FzhN4lv$ryQgP~z5 z5zeSS3ZLrLd?J}uBKr9AbGimvtRwax7JuJArp?goFnz+EE`Dr}31z8fp?ZO;MY1k4 zu3Uulk3(v-FQvD!nTXgJy~wx$-4_4L^8M!w}s&M&&Wa( zx0L_Bm#j~z=ke=M^-Gn!(QTh5tLI^BNbn!yx>p-mWkElQc*U%N8OVuh!pp*0V3j?nrlsCbDI8b|ObWX$+dYd_U<#%fS)L40{^iMgnRP(eYjB;eh zR^&zm5y~i*w>UN!c}CN`c%85to}mbM+*04kJZr^c*2As!X!#_u!Qy>?3{Rn|=>#LR zE1a^|=umv@J^JH=M@2|9sg51pvkmY3hhu8Cmt+27Uj@{Jqx)!yF>t&O?MPFTQ0$?( z#YxYFqA6MruJVobJNp>?2Iu_6TFAOmHiTGTZ5S5W*r%kVF!HGGeM+n~aGt}4RJ+K?nM2i=L}zPy}lh9=^t#!TxA(421dPkuS;PFgyLJhe4eV=!88#*&Yw!> z!^3lfs*h?aj^#EftB9U(_-;WMalLChL;m0I)(+x;X$C`cq(Gd?0LQ~Py1v84E{ zJ-r?ocY4-X<#@vTo!xPY>4bvycL8m|q(b)BE>m9%XUYrrh&j}Y-Z6(682&CsXHG8P z{A}BSB_n59AWf}Kucm2fKHAlEEXIJBAUifpAJ=2j&r}$9{zYkLPX8$>;CDA1by5OcD&>DbcK7EZ z%d#)EyQdr_Ces!$HpN+t*As*{8((>K^y}ns+a}-|uxOQ96vBt_eN)lQQ%$L1F0igz znvN?mN(if^^oyZLKp$!_i~4WL?=OOR3ee4_6!fo&|qj8NNw(@u?|&g2ioi-W%q$ zsk)0fW+r}*+w4R|VUtmOhrl>5rkzfc#pj}Q)>E>6gMywD^9&lrBs`yYmiB~(@;d4Rb^Yo_g*?l#)wbmq-s>pwEjkH>=D0*!*-WTCP=7hX& z#iNDXr~8ufI5rhEgU_PswHjx_hLYUj2}lIJhdbxl zHZKVj6}A;jI8Wx+)_O#77H?dvOfHh*8$Oi9%mhS7^Td zSa=k#oX5!Lg9&vlAbm~9>Ea*RfITt(PTr|+#`OSGluA;n&B(Pi%Or5Hq30$bBnUcZvogG2j0Oq3ZkwV0sggr%xLbi#aHSSW@SE50pOFLwr(@L@0 zBob+M31E+@JSX(izRlbQg}Gtl%{$p2tHS(*?)J@7fiF$9~!|M7v(+r880P&Zby*ZyJD0M3Om{_FUIdG)&_fvJ}Mwa%%)L4jt`!VF+ z5Y+wqFJez&{g2ub3MPs)E1}GC$I-+W$EX!mj>LqVgo?a!re&tR8oV`b;H@mL_l0!a z`DPrh6|hT1L{(*70v~!v8KKY3$XLq?_7B{IkLSYC;YjyOJx*WSjSLQwgtBbPw@)Ep zFHhTFIy;}0j9DKJVp>kvuJYC%5zhgy;=Hz9dG8%yb=6vfe6LKO@Qd8zsotJxU;Qx2 zb#(fx=Po~oh+=FP8e3?KiVZmZRK5?molop4eSI!eeC^&p_+x0?C8|v->gI5X>hsZR zo_}+OogC5Yg>J!jZ`mtI!({Rh!@A#Lwt*d~vYKmgwCgDBsodede6xS-v%yJ@i7>M% zb9Z+-brT#snS3N5#SMIb=@$Fsd~?~`{(k=H1zU_sgS-0Nil4-F!eVXDbk87}w^Uuv z;NTozihO7Mu z1=$LEd+<9o?f3wZ;{?fSFpkEfF0(ZYStL^o_XW$@5wgR{X@@mD$<=Y)bIe$(nkqD4 zPa7<{{_Rr%j)!*oLbg7Ijk{g6MI;uL*KHxuD=!q*_-kt1QKhBjaX)noCX)OSB*YMy z92&E{{PE#~NyT*;rZKIkkHWKCk8XuceiGU?Enw!@?OM?vgEQd{g$T zvb6$mV!$ggdf(}DJqj@=75K6-wXhJr{45BqM&BVmEw&~&*?nz$sobKU&tu9qan3I` zK_6wSOxzYOa4-a&?E9N^g`>bf`|}{&Sw?nkrv5s;&dbyfurc}L*Cy~xW6oNml10DO z#nj`)8x0aSYz;>_x!EsQIEtHVM87Ntyphr1^9aUbDFRPx<1%4hG(fCc_2v>wOIrk% zSv_Gs4?(n!Gqy9~Lp<~@nh0?CTa8{<7Y!^h6fZ1;tJ5aJ8CiL@F8#>mrNoT2yh z^XDtkz4o~>>cz8}F1vHqo4Z0z1aEzVMPCv^($l}eTdP;^w5D}-!ZyT)ON?Y9bDNI* z@h(8NwY9}yTnE@bXgae^*OO^r=k?qKLU!*1YKZZxO4UVY3U$|X`S z7`wzORFB1H9#X!aj18jdeI5?H!N=Q7zE@PNIkGhIy+@8Y$})+R^a&~Vm@gz?_pt0& zla4r6)+qy_#XTV0w?Flm%_8W)h@lY+j-;=1ybu`^W0ae3^t5fs)UW@TqnwBIiCNR` z$5DK)y5L8rqQm+e)Q5JN{;jCbqo=Q-RU1eHy=%u3GIBrZ?z6IKj7l#@w>W(4_K1jt z3RLMqfZEmbTxj$(&~)-M?yplR64YgA)Mq#EaVqo(!M*BFtp?W`GCFkZZ~?P79}>n2Kmvb>t3-{@B@*PEX{Aub*j0S=NV9XZ|X^q-^OrPzx1 z-hAMWEDOScF%;Z^$IPs5#{w|OKzz`y1;j+kT zi%KC-v>Or$zKoS#T_fT#?>T)xHej0wb-zNOWs!M5^&^c}qrK9Z$7OkRr?$2U_xtI| zaeVLEi#NF7hX8#t4KJJdx8W}Y-18SZdNsg}*QWWJH#xw*wbU#G)_odtP0V| z7vO-+28nZ%jhqGOT>{g`DF1-^m?baOAbc?&oRwLDeZ`FdxUi(p9vs)zMG zeh_4P#w@|>^P=Nk7!Ed2QL)6~d4{%D>weW56!VMagb2UZyf>=qt@&=><?8$` z#h^bbePa!KaNL2=0$<~&HW3DnzISz2ZZ6mT06fJ~EXY+sB1Yu-eTRm7q24R8hPfBY zf)nO_Ud4lVg3Ma20am4L_oGEj%4)vsPJhkoIy?Gb4kSmJ03U}`(7tEn{TMi5z!R;l zljKBq*^Rvat%!O?c$y!UUT3$ws(G7TxX^C>s;EYGWP=gDOlm{@76ubbE80=A*gRoy1|eF?4l7Xj5cUehx}%egejV3EnHFn) z7gSO{*yH9;!9-j(nYM|uW!5il& zWhTHYv3`foiv;g{E?7Z1@3ZBO-BO%nGgV;P?H$gWLO8e}V;0|+0qam)F&hYpd~Xx7 z2b;=!MxDlRX1QSwbuH>ZLywGi$%bh4` zRPV8DyDoJ=MjPuIcDZjAPinT*DQO*CY9uh}2C+Fq3B+rWr{sa4${-PRp%mQ&ulGvo zRV%`MWcn-Tj{Hh;IfNkb+Z%jvAh}&z3^K^v(28Z;tMZZqQ#bRf$s$&9KUv zvNO{(-zIjiR()LBE8|aOHmJnrX>z+l`ncNJKXbY66%l*|pESd;sQ|Jz5bG){EB@Hb zgAnca3F*`*)HIADrgwjKP4mR=!s%QWv+sO4_bInm@a?Elu}E=>JvObF&NFnW=+r%ulU|?F@q)fzje$RM%D#LY(oB<3!b6&2ys z%6aONr_~;gNy(w0AhiGK&N``FFvfg$-SsE*>8}h44k!h=&>_Kdv9k7-8nU9#mo>mY%Fow7;`J7_3Y7Jf8;cy2+p{JFlUI5agRwwj!d7Ub(T%o7_tru&=fgvFK)3&2y zT-icDvXGIJvs}B&@3~Q{yIA*Sd{<(?5|PUwcj_V*B!RanCl8cQW;wIY{vbj4Q@-Kl zq=s)Z$7+^3+7gJL;_#SdhqsN;Hb^?Sk(L|#xXDd+&h>!DZAHc_q-zW)S=i-8F6qnQ zW*2zj%KvO)N@#!i+l49GAJV~i3jzeP1lhHk))U|Y-h?J2z!Ie(&jn+CFt*+Zop}Wu zJRP?IWo!g7$GpE-P@q)NOo>dj{YYwYo{))6vE7y`{n*_zwG%vA5XXkg9K`UAw2bIl z{lscYVuDnHpV;vICt$(7?-=-23>_VB^pzwhr~4Ag=$D5oumJOwWE1xrAA3VlwrjHt zH)%*FRX`-GNqgwKt#F{|vWh~Re=^+(Lm21K$awcHj?O?b1XQD7$w(0NrH(4&B2%lE z8q_9HL7F}gk$O{Gg!V8t(eyS?)jF{!VfI%L1<-AP^-##S4Vua*ZDPcbJ=S-eiI7ZC zsUo10yw%XKwLZ99-@AdT$13NvCSLWJI27jIb_F#FJr8~S;6vV+MUFDf^-;lI99h?_ z!~^O6_wR$l^sRLfpXn1c8Czy*?J%W%qn>bJwowM_*!^f!cR8FGx7C_HeD@^KH0&)3 zjF!JE5n&w7FMcUV$ADf^(r2VHID;h56ZO33IZ{yaKnn{CZrdqROy08SoWgMhb%RE4 zkkRDgk+qh1&y9|r?%&;jn=UloaX4Ml4f1M2KiC*eHK2QTffNgD$jPFhb$0 zEj+k~&oq(gs5P16{lvhyPUbevmqi`qjrOv)%RZ(a;VS(JozCOGnA&<#`5!a->(|a3$h=-G^NYSRAbweR1w`98-E4q*l)k@m_nF1tu%SV0T0Dk6v+-1!g+ZbgGh z_CNz}YPp|##Gr^+{2(`?5%E~mA};ekHQfAGsam8&2Pf)!J@i_7b^45mT<&jtiHFhv z#}p)i;&Zaj)RpqAKT#5jg3{5P2IQWhj(~k9$8{RUTLwfaU$v>Y)asvfEk7^a>(19_ z=^Hrgk55f|IW5(Jp=`Wh%)JnYWR`EdB2s#|nx{_awlnB-bmDj8LBB4$u0C);TYr*aEZcA$GPy^J!BG8Jj672z!lc*vF zOyFQ?;AED^%jR^tnA8@*rQ$n3`?6^25F~nLtoV9XLtHZA*K#di^CCQtC6x(c#5`<3 z>lU{?3BNtk`Od)r@9J)m*0paocqUN1wk*ga^M&o;UYHE0*In!VpxXux9Ld zAcmJ$Ea81&y1mycul-&bhFn#)_%=wpP@N9vV4i#OFc4fEn#}&~tP58yRx?ZKRv=o= zfT*Rz%rQ*8KsthD(c*d7b0m0^XI-=Q%G86*uqfgnM=Tfgx|sj6y}^0tdL?T=15Lf- z4cXjjl4NVoU}XC!!s@AY+?e^!{2Hlm=@;+G_kk2KsYRfM3jE9#>MF~&E+rkmJ1li$ zlmp%%H(gA6K-2xYkGDqGM&jtGv+4#tzy9M>Q}!BtEu-J4sLrgK&{Zplhju=6kqFfmA_BRjDK7;^COCoAI2&mT^vZ?K1ydX z-Xg3;*D;6E*R(|-CoQ@8^oF&e3=S;-2u|AEY&HuuL?&SrH>fQihRar0x)i+%cgKIu zv9hgisC)L)GRq#`-3-(PBxajNghgfc#{E>#v1iFsFZezXvCzQsUUGI86fn)budF35 zSsCSU8D%?nZiUM=ig)hGLTZ(D{aFI*T@A|1_d)$!soel{ib=XLdMvWO{*bdJOyd`f zvfBPP>>o^6b0}gGeIn!#He5jW%8l8x#`gF6+Rz23h9?h^8yN# zQbsnMr8n;I>c3N_X7oHot``4SdiA-pbD-*D=J;ywMDfD4`;{N#5gAT!^{aHAJl+iH zL>eIN9$1=>&wHR}{{epS_mB9IiX!SHlhZCK?YAWo_$bB{EgRX_RYpqZ%S2@VA_& zbKW7aG+zYkTnSE{u;+VyLeUhmaipy!i%Tp=v;IzOSLV7meeL~X{PX?Jj> z<TQXt1fw(A6hm%lXXN+v1m0~2CHzuQ8E?59rsjsp_FB|1_;LBYdm9-E+X4t$6|!G~I?FJFU-eoGGA z{8NN&cE2eu4`rmFRv*L7(XDbzOM}k~g&=N2@=2>u*tqlYSoF;6{5Eu#uDER%XmYaM z(eWXvJ<~-=bsuvyDPtG&g`BS=m<%-Ss!AH~71nDR8NwgxbJaf?2AhP5QxdRS;S97q z%BmYkIMh8%|7HOOK>v$~U*GY4H;8GhhyMr*+R$b{22e^Qsag7lWfpy9RGI`PE# z(s6=Oj&fKmc#5IvKlUr@*0%GFy+RJU9%(Z*a1JYVUL7$LAfc`f*kD zdkDWJWb$em@FAI?C-POdzRG6cwdelr_}-NM>!(LBy+CWT_Tl8L^bzuWO<^|u3o#y3 z6rA$uN&YqBo&xtH|M}m8Vfm``-y|iF^%~B1du#MuUVx^aT+=JX5Hxy7PO%u%Ci%Bf%ixueq`NB|Eu@p_nzhpNb3&dOy8ZaPp z@S?6fZ0sNdc@TMLt+8ZK=iTOG_yfvlKI0(d^Ug?E=@RHXpznBf{Y>Q1#+fwAN?r5w z^2YcVal+ck<=U}co|=|0HF@5!GfDSfQh`=qOJqC^BoiEoe!as)up_`$t3I)#udTQB zl`7}S%gU_q)=nYoN=AQm1I^MztY&CS{jD*MA|$@2x_3WN0tGaT>O4^=>(v}}Ao3_C zMZH*%IUuto@01bi2`tu^NmI+pzQL~bm_gJv>*y~i@G02Z{w#a_L-d<`dz@H})MeCu zdeaAQY=v_%I1aMRZ|MplL=`3Mv9hMJIc5rqKd zoxAoN$aw2wvb{FbUayUu+c8C{HzK_rA;S0@4AWn8g3`*!dV|jCEQN!G!nk8Q^X<2# z-*w|y$=Zc3C#U_{Bs5*8W>t~Qa*IhVfl21VRX}NZr;k7}YV$1|V;VFs(-YGxzpiZB z#~Y5<=hbM5>B%0hcB=^R1b@Z9tG@-ZZOprGhes-WEhybsJY`=REN$&3m54lFc>47M z8)36WeN-J(5@|rE2s8!UzI}W*1pfkW05n{b1v20JJk(QlWjf0AYI5ET1PvyF7AGJrkWL7X(+UIN@4nA1t zCFk^VZ)*~wQqj0DJFZ9us{J#(;=I!GqqTYuA_6|4ccxU1dg?w+B0ynX!;UF+K%F;t z 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

SHAUG2oqD@o}&FbxcUT33(3_A&qp%2WDLM|6d~ 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 RK2R1!@1l_wQNuRuKLBGBP00WN literal 0 HcmV?d00001 diff --git a/modules/crypto/images/image.png b/modules/crypto/images/image.png new file mode 100644 index 0000000000000000000000000000000000000000..2c32e1632dd6baa13fc139aeb806835ebb722fcb GIT binary patch literal 99750 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

lKdr0aIOOwt&vc4e88gW^x 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

nWKe5Ff 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ɺcfc`U^MUO<;+8h0##!H?RzdJmSmN? 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

tfUGr<$Q_q9fWr{Bv#Mzht)gKSxR0E3?G1 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 Lq6>kACtv Date: Tue, 14 Apr 2026 19:27:22 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat(https):=20=E6=96=B0=E5=A2=9EHTTPS?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF/=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E4=B8=8E=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心模块: - HttpsServer: 基于 TlsTcpServer 的 HTTP/1.1 over TLS 服务端 支持中间件链、Keep-Alive、并发连接、Pimpl 隔离 - HttpsContext: 请求上下文,RAII 自动提交,支持异步响应 - HttpsClient: 基于 TlsTcpClient 的 HTTPS 客户端 支持请求队列、自动重连、成功/错误回调 - RespondParser: HTTP/1.1 响应解析状态机(Content-Length / 无正文状态码) Bug 修复: - respond_parser: Content-Length 改用大小写不敏感比较(RFC 7230) - https_client: 解析失败时通知所有 pending 请求并断开连接 - https_server.h: 修正文档注释中的命名空间错误 单元测试: - respond_parser_test: 17 个用例(分片、流水线、大小写 Header 等) - https_server_client_test: 7 个集成测试(含中间件、ErrorCallback) - http common/request/respond 测试补充 isValid 失败与反向转换用例 构建: - 新增 modules/https Makefile / CMakeLists.txt - 根 CMakeLists.txt 新增 TBOX_ENABLE_HTTPS 选项 - 新增 README / README_CN --- CMakeLists.txt | 6 + modules/http/Makefile | 1 + modules/http/common_test.cpp | 34 + modules/http/request_test.cpp | 27 + modules/http/respond_test.cpp | 43 + modules/https/CMakeLists.txt | 88 ++ modules/https/Makefile | 52 + modules/https/README.md | 970 +++++++++++++++++++ modules/https/README_CN.md | 945 ++++++++++++++++++ modules/https/client/https_client.cpp | 172 ++++ modules/https/client/https_client.h | 120 +++ modules/https/client/respond_parser.h | 179 ++++ modules/https/client/respond_parser_test.cpp | 325 +++++++ modules/https/https_server_client_test.cpp | 358 +++++++ modules/https/server/https_context.cpp | 66 ++ modules/https/server/https_context.h | 83 ++ modules/https/server/https_server.cpp | 352 +++++++ modules/https/server/https_server.h | 92 ++ modules/https/test_certs.h | 140 +++ 19 files changed, 4053 insertions(+) create mode 100644 modules/https/CMakeLists.txt create mode 100644 modules/https/Makefile create mode 100644 modules/https/README.md create mode 100644 modules/https/README_CN.md create mode 100644 modules/https/client/https_client.cpp create mode 100644 modules/https/client/https_client.h create mode 100644 modules/https/client/respond_parser.h create mode 100644 modules/https/client/respond_parser_test.cpp create mode 100644 modules/https/https_server_client_test.cpp create mode 100644 modules/https/server/https_context.cpp create mode 100644 modules/https/server/https_context.h create mode 100644 modules/https/server/https_server.cpp create mode 100644 modules/https/server/https_server.h create mode 100644 modules/https/test_certs.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ecd55044..2f65bc02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,6 +81,7 @@ option(TBOX_ENABLE_MAIN "build main" ON) option(TBOX_ENABLE_COROUTINE "build coroutine" ON) option(TBOX_ENABLE_HTTP "build http" ON) +option(TBOX_ENABLE_HTTPS "build https (requires http and network)" ON) option(TBOX_ENABLE_MQTT "build mqtt" ON) option(TBOX_ENABLE_FLOW "build flow" ON) option(TBOX_ENABLE_ALARM "build alarm" ON) @@ -178,6 +179,11 @@ if(TBOX_ENABLE_HTTP) list(APPEND TBOX_COMPONENTS http) endif() +if(TBOX_ENABLE_HTTPS AND TBOX_ENABLE_HTTP AND TBOX_ENABLE_NETWORK) + message(STATUS "https module enabled") + list(APPEND TBOX_COMPONENTS https) +endif() + if(TBOX_ENABLE_MQTT) message(STATUS "mqtt module enabled") list(APPEND TBOX_COMPONENTS mqtt) diff --git a/modules/http/Makefile b/modules/http/Makefile index 6fc3bb02..ba974a7e 100644 --- a/modules/http/Makefile +++ b/modules/http/Makefile @@ -31,6 +31,7 @@ HEAD_FILES = \ url.h \ server/types.h \ server/server.h \ + server/request_parser.h \ server/context.h \ server/middleware.h \ server/middlewares/router_middleware.h \ diff --git a/modules/http/common_test.cpp b/modules/http/common_test.cpp index c4530a62..28ef50ec 100644 --- a/modules/http/common_test.cpp +++ b/modules/http/common_test.cpp @@ -51,6 +51,40 @@ TEST(common, StateCodeToStream) EXPECT_EQ(StatusCodeToString(StatusCode::k505_HTTPVersionNotSupported), "505 HTTP Version Not Supported"); } +TEST(common, StringToMethod) +{ + EXPECT_EQ(StringToMethod("GET"), Method::kGet); + EXPECT_EQ(StringToMethod("HEAD"), Method::kHead); + EXPECT_EQ(StringToMethod("PUT"), Method::kPut); + EXPECT_EQ(StringToMethod("POST"), Method::kPost); + EXPECT_EQ(StringToMethod("TRACE"), Method::kTrace); + EXPECT_EQ(StringToMethod("OPTIONS"), Method::kOptions); + EXPECT_EQ(StringToMethod("DELETE"), Method::kDelete); + EXPECT_EQ(StringToMethod("INVALID"), Method::kUnset); + EXPECT_EQ(StringToMethod(""), Method::kUnset); +} + +TEST(common, StringToHttpVer) +{ + EXPECT_EQ(StringToHttpVer("HTTP/1.0"), HttpVer::k1_0); + EXPECT_EQ(StringToHttpVer("HTTP/1.1"), HttpVer::k1_1); + EXPECT_EQ(StringToHttpVer("HTTP/2.0"), HttpVer::k2_0); + EXPECT_EQ(StringToHttpVer("INVALID"), HttpVer::kUnset); + EXPECT_EQ(StringToHttpVer(""), HttpVer::kUnset); +} + +TEST(common, MethodToString_AllValues) +{ + EXPECT_EQ(MethodToString(Method::kGet), "GET"); + EXPECT_EQ(MethodToString(Method::kHead), "HEAD"); + EXPECT_EQ(MethodToString(Method::kPut), "PUT"); + EXPECT_EQ(MethodToString(Method::kPost), "POST"); + EXPECT_EQ(MethodToString(Method::kTrace), "TRACE"); + EXPECT_EQ(MethodToString(Method::kOptions), "OPTIONS"); + EXPECT_EQ(MethodToString(Method::kDelete), "DELETE"); + EXPECT_EQ(MethodToString(Method::kUnset), ""); +} + } } } diff --git a/modules/http/request_test.cpp b/modules/http/request_test.cpp index 14a2c013..1068ce32 100644 --- a/modules/http/request_test.cpp +++ b/modules/http/request_test.cpp @@ -70,6 +70,33 @@ TEST(Request, ToString_Post) EXPECT_EQ(req.toString(), target_str); } +TEST(Request, IsValid_Fail_NoMethod) +{ + Request req; + req.http_ver = HttpVer::k1_1; + req.url.path = "/index.html"; + // method 未赋值(kUnset),isValid() 应返回 false + EXPECT_FALSE(req.isValid()); +} + +TEST(Request, IsValid_Fail_NoUrl) +{ + Request req; + req.method = Method::kGet; + req.http_ver = HttpVer::k1_1; + // url.path 为空,isValid() 应返回 false + EXPECT_FALSE(req.isValid()); +} + +TEST(Request, IsValid_Fail_NoHttpVer) +{ + Request req; + req.method = Method::kGet; + req.url.path = "/index.html"; + // http_ver 未赋值(kUnset),isValid() 应返回 false + EXPECT_FALSE(req.isValid()); +} + } } } diff --git a/modules/http/respond_test.cpp b/modules/http/respond_test.cpp index 7d408506..fe03525f 100644 --- a/modules/http/respond_test.cpp +++ b/modules/http/respond_test.cpp @@ -46,6 +46,49 @@ TEST(Respond, ToString) EXPECT_EQ(rsp.toString(), target_str); } +TEST(Respond, ToString_NoBody204) +{ + Respond rsp; + rsp.status_code = StatusCode::k204_NoContent; + rsp.http_ver = HttpVer::k1_1; + + EXPECT_TRUE(rsp.isValid()); + + std::string s = rsp.toString(); + EXPECT_NE(s.find("204 No Content"), std::string::npos); + EXPECT_NE(s.find("Content-Length: 0"), std::string::npos); +} + +TEST(Respond, ToString_404NotFound) +{ + Respond rsp; + rsp.status_code = StatusCode::k404_NotFound; + rsp.http_ver = HttpVer::k1_1; + rsp.body = "Not Found"; + + EXPECT_TRUE(rsp.isValid()); + + std::string s = rsp.toString(); + EXPECT_NE(s.find("404 Not Found"), std::string::npos); + EXPECT_NE(s.find("Not Found"), std::string::npos); +} + +TEST(Respond, IsValid_Fail_NoStatusCode) +{ + Respond rsp; + rsp.http_ver = HttpVer::k1_1; + // status_code 未赋值(kUnset),isValid() 应返回 false + EXPECT_FALSE(rsp.isValid()); +} + +TEST(Respond, IsValid_Fail_NoHttpVer) +{ + Respond rsp; + rsp.status_code = StatusCode::k200_OK; + // http_ver 未赋值(kUnset),isValid() 应返回 false + EXPECT_FALSE(rsp.isValid()); +} + } } } diff --git a/modules/https/CMakeLists.txt b/modules/https/CMakeLists.txt new file mode 100644 index 00000000..29406b59 --- /dev/null +++ b/modules/https/CMakeLists.txt @@ -0,0 +1,88 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +cmake_minimum_required(VERSION 3.15) + +set(TBOX_HTTPS_VERSION_MAJOR 0) +set(TBOX_HTTPS_VERSION_MINOR 0) +set(TBOX_HTTPS_VERSION_PATCH 1) +set(TBOX_HTTPS_VERSION ${TBOX_HTTPS_VERSION_MAJOR}.${TBOX_HTTPS_VERSION_MINOR}.${TBOX_HTTPS_VERSION_PATCH}) + +add_definitions(-DMODULE_ID="tbox.https") + +set(TBOX_LIBRARY_NAME tbox_https) + +set(TBOX_HTTPS_SOURCES + server/https_context.cpp + server/https_server.cpp + client/https_client.cpp) + +set(TBOX_HTTPS_TEST_SOURCES + client/respond_parser_test.cpp + https_server_client_test.cpp) + +add_library(${TBOX_LIBRARY_NAME} ${TBOX_BUILD_LIB_TYPE} ${TBOX_HTTPS_SOURCES}) +add_library(tbox::${TBOX_LIBRARY_NAME} ALIAS ${TBOX_LIBRARY_NAME}) +target_link_libraries(${TBOX_LIBRARY_NAME} tbox_network tbox_http) + +set_target_properties( + ${TBOX_LIBRARY_NAME} PROPERTIES + VERSION ${TBOX_HTTPS_VERSION} + SOVERSION ${TBOX_HTTPS_VERSION_MAJOR} +) + +if(${TBOX_ENABLE_TEST}) + add_executable(${TBOX_LIBRARY_NAME}_test ${TBOX_HTTPS_TEST_SOURCES}) + target_include_directories(${TBOX_LIBRARY_NAME}_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(${TBOX_LIBRARY_NAME}_test + gmock_main gmock gtest pthread + ${TBOX_LIBRARY_NAME} tbox_http tbox_network tbox_event tbox_log tbox_base tbox_util + ssl crypto rt dl) + add_test(NAME ${TBOX_LIBRARY_NAME}_test COMMAND ${TBOX_LIBRARY_NAME}_test) +endif() + +# install the target and create export-set +install( + TARGETS ${TBOX_LIBRARY_NAME} + EXPORT ${TBOX_LIBRARY_NAME}_targets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +# install header files +install( + FILES server/https_context.h server/https_server.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/tbox/https/server +) + +install( + FILES client/https_client.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/tbox/https/client +) + +# generate and install export file +install( + EXPORT ${TBOX_LIBRARY_NAME}_targets + FILE ${TBOX_LIBRARY_NAME}_targets.cmake + NAMESPACE tbox:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/tbox +) diff --git a/modules/https/Makefile b/modules/https/Makefile new file mode 100644 index 00000000..c0505d3c --- /dev/null +++ b/modules/https/Makefile @@ -0,0 +1,52 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT = https +LIB_NAME = https +LIB_VERSION_X = 0 +LIB_VERSION_Y = 0 +LIB_VERSION_Z = 1 + +HEAD_FILES = \ + server/https_server.h \ + server/https_context.h \ + client/https_client.h \ + client/respond_parser.h \ + +CPP_SRC_FILES = \ + server/https_context.cpp \ + server/https_server.cpp \ + client/https_client.cpp \ + +TEST_CPP_SRC_FILES = \ + $(CPP_SRC_FILES) \ + client/respond_parser_test.cpp \ + https_server_client_test.cpp +TEST_LDFLAGS := $(LDFLAGS) \ + -ltbox_http \ + -ltbox_network \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_log \ + -ltbox_util \ + -ltbox_base \ + -lssl -lcrypto -ldl + +include $(TOP_DIR)/mk/lib_tbox_common.mk diff --git a/modules/https/README.md b/modules/https/README.md new file mode 100644 index 00000000..df1f77ac --- /dev/null +++ b/modules/https/README.md @@ -0,0 +1,970 @@ +# tbox::https — HTTPS Module + +> **HTTP/1.1 over TLS** server and client, part of the [cpp-tbox](https://github.com/cpp-main/cpp-tbox) framework. + +--- + +## Table of Contents + +- [Feature Overview](#feature-overview) +- [Dependencies](#dependencies) +- [Module Layout](#module-layout) +- [Quick Start](#quick-start) + - [Server Example](#server-example) + - [Client Example](#client-example) +- [Core Components](#core-components) + - [HttpsServer](#httpsserver) + - [HttpsContext](#httpscontext) + - [HttpsClient](#httpsclient) + - [RespondParser](#respondparser) +- [Architecture](#architecture) + - [Layered Structure](#layered-structure) + - [Data Flow](#data-flow) + - [Design Patterns](#design-patterns) +- [Lifecycle Management](#lifecycle-management) +- [TLS Configuration Reference](#tls-configuration-reference) +- [Build & Test](#build--test) +- [FAQ](#faq) +- [Known Limitations](#known-limitations) + +--- + +## Feature Overview + +| Feature | Server | Client | +|---------|:------:|:------:| +| TLS 1.2 / 1.3 encryption | ✓ | ✓ | +| HTTP/1.1 protocol | ✓ | ✓ | +| HTTP/1.0 protocol | ✓ | — | +| Keep-Alive persistent connections | ✓ | ✓ | +| Middleware chain | ✓ | — | +| Concurrent connections | ✓ | — | +| Request queuing | — | ✓ | +| Auto-reconnect | — | ✓ | +| Async non-blocking I/O | ✓ | ✓ | +| Pimpl implementation isolation | ✓ | ✓ | + +--- + +## Dependencies + +``` +tbox_https + ├── tbox_http — HTTP data structures (Request / Respond / method / status) + ├── tbox_network — TLS TCP transport (TlsTcpServer / TlsTcpClient / TlsCtx) + ├── tbox_eventx — Extended events (TimerEvent, etc.) + ├── tbox_event — Core event loop (Loop / FdEvent) + ├── tbox_log — Logging + ├── tbox_util — Utilities (Buffer / WrappedRecorder) + ├── tbox_base — Fundamentals (NONCOPYABLE / ASSERT / cabinet) + ├── OpenSSL — TLS encryption (libssl / libcrypto) + └── libevent — Low-level event driver +``` + +**Link flags** (Makefile): +```makefile +TEST_LDFLAGS := $(LDFLAGS) \ + -ltbox_http -ltbox_network -ltbox_eventx \ + -ltbox_event -ltbox_log -ltbox_util -ltbox_base \ + -lssl -lcrypto -ldl +``` + +--- + +## Module Layout + +``` +modules/https/ +├── Makefile # GNU Make build config +├── CMakeLists.txt # CMake build config +├── README.md # This file (English) +├── README_CN.md # Chinese documentation +├── test_certs.h # Embedded test certificate (self-signed CN=localhost) +├── https_server_client_test.cpp # Integration tests (server + client) +│ +├── server/ +│ ├── https_server.h # HttpsServer public API +│ ├── https_server.cpp # HttpsServer::Impl internal implementation +│ ├── https_context.h # HttpsContext / Middleware definitions +│ └── https_context.cpp # HttpsContext implementation +│ +└── client/ + ├── https_client.h # HttpsClient public API + ├── https_client.cpp # HttpsClient::Impl internal implementation + ├── respond_parser.h # HTTP/1.1 response parser (header-only) + └── respond_parser_test.cpp # RespondParser unit tests +``` + +--- + +## Quick Start + +### Generate a Test Certificate + +```bash +openssl req -x509 -newkey rsa:2048 \ + -keyout server.key -out server.crt \ + -days 365 -nodes \ + -subj "/CN=localhost" \ + -addext "subjectAltName=IP:127.0.0.1,DNS:localhost" +``` + +### Server Example + +```cpp +#include +#include +#include +#include + +#include "server/https_server.h" // HttpsServer, HttpsContext + +using namespace tbox; +using namespace tbox::network; +using namespace tbox::network::tls; +using namespace tbox::http; +using namespace tbox::https::server; + +int main(int argc, char *argv[]) { + // ── 1. Create event loop ─────────────────────────────────────── + auto sp_loop = event::Loop::New(); + + // ── 2. Configure TLS (server needs cert + private key) ──────── + TlsCtx tls_ctx; + TlsConfig cfg; + cfg.cert_file = "server.crt"; + cfg.key_file = "server.key"; + if (!tls_ctx.initialize(TlsMode::kServer, cfg)) { + LogErr("TLS init failed"); + return 1; + } + + // ── 3. Create and initialize HttpsServer ────────────────────── + HttpsServer srv(sp_loop.get()); + + if (!srv.initializeTls(&tls_ctx)) return 1; + if (!srv.initialize(SockAddr::FromString("0.0.0.0:443"), 10)) return 1; + + // ── 4. Register middleware ──────────────────────────────────── + + // Access log middleware (calls next() to continue the chain) + srv.use([](HttpsContextSptr ctx, const HttpsNextFunc &next) { + LogInfo("[%s] %s", + Method_Name(ctx->req().method).c_str(), + ctx->req().url.path.c_str()); + next(); + }); + + // Business logic middleware (no next() = terminate chain) + srv.use([](HttpsContextSptr ctx, const HttpsNextFunc &) { + if (ctx->req().url.path == "/health") { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().body = "OK"; + } else { + ctx->res().status_code = StatusCode::k404_NotFound; + ctx->res().body = "Not Found"; + } + ctx->res().headers["Content-Type"] = "text/plain"; + }); + + // ── 5. Start ────────────────────────────────────────────────── + if (!srv.start()) return 1; + + LogInfo("HTTPS server listening on https://0.0.0.0:443"); + sp_loop->runLoop(); // blocks until exitLoop() + + srv.stop(); + srv.cleanup(); + return 0; +} +``` + +### Client Example + +```cpp +#include +#include +#include +#include +#include + +#include "client/https_client.h" + +using namespace tbox; +using namespace tbox::network; +using namespace tbox::network::tls; +using namespace tbox::http; +using namespace tbox::https::client; + +int main() { + auto sp_loop = event::Loop::New(); + + // ── 1. Configure TLS for client ─────────────────────────────── + TlsCtx tls_ctx; + TlsConfig cfg; + cfg.verify_peer = false; // disable for self-signed certs in testing + // set to true in production with ca_file + if (!tls_ctx.initialize(TlsMode::kClient, cfg)) return 1; + + // ── 2. Create and initialize HttpsClient ────────────────────── + HttpsClient cli(sp_loop.get()); + if (!cli.initialize(SockAddr::FromString("127.0.0.1:443"), &tls_ctx)) return 1; + cli.setAutoReconnect(true); // automatically reconnect after disconnection + + // ── 3. Build an HTTP request ────────────────────────────────── + Request req; + req.method = Method::kGet; + req.url.path = "/health"; + req.http_ver = HttpVer::k1_1; + req.headers["Host"] = "127.0.0.1"; + req.headers["Connection"] = "close"; + + // ── 4. Send request asynchronously (two callbacks) ──────────── + bool done = false; + cli.request(req, + [&](const Respond &res) { + LogInfo("Status : %d", static_cast(res.status_code)); + LogInfo("Body : %s", res.body.c_str()); + done = true; + sp_loop->exitLoop(); + }, + [&](const std::string &err) { + LogErr("Error: %s", err.c_str()); + done = true; + sp_loop->exitLoop(); + } + ); + + sp_loop->runLoop(); // wait for callbacks + + cli.cleanup(); + return done ? 0 : 1; +} +``` + +--- + +## Core Components + +### HttpsServer + +**Header**: `server/https_server.h` +**Namespace**: `tbox::https::server` + +#### Class Interface + +```cpp +class HttpsServer { + public: + explicit HttpsServer(event::Loop *wp_loop); + virtual ~HttpsServer(); + + // ── Initialization (order is strict) ───────────────────────── + bool initializeTls(network::tls::TlsCtx *tls_ctx); + bool initialize(const network::SockAddr &bind_addr, int listen_backlog); + + // ── Start / stop ────────────────────────────────────────────── + bool start(); + void stop(); + void cleanup(); + + // ── State query ─────────────────────────────────────────────── + enum class State { kNone, kInited, kRunning }; + State state() const; + + // ── Debugging ───────────────────────────────────────────────── + void setContextLogEnable(bool enable); // logs raw request/response + + // ── Middleware registration ─────────────────────────────────── + void use(HttpsRequestHandler &&handler); + void use(HttpsMiddleware *wp_middleware); +}; +``` + +#### State Machine + +``` + ┌─────────────────┐ + │ kNone │ ← initial state after construction + └────────┬────────┘ + │ initializeTls() + initialize() + ↓ + ┌─────────────────┐ + │ kInited │ ← not yet listening; handlers can be registered + └────────┬────────┘ + │ start() + ↓ + ┌─────────────────┐ + │ kRunning │ ← accepting and processing connections + └────────┬────────┘ + │ stop() + ↓ + ┌─────────────────┐ + │ kInited │ ← can call start() again + └────────┬────────┘ + │ cleanup() + ↓ + ┌─────────────────┐ + │ kNone │ + └─────────────────┘ +``` + +#### Internal Connection Processing + +``` +1. TlsTcpServer accepts the TCP connection +2. TLS handshake completes (handled automatically by OpenSSL) +3. Per-connection Connection struct is created (contains a RequestParser) +4. onNetReceived(buff): + a. Decrypted bytes fed to RequestParser + b. On complete request, inspect Connection header: + - Connection: close → mark close_index + - Default keep-alive → continue accepting requests + c. Create HttpsContext(CommitFn, ct, req_index, req) + d. Execute middleware chain: handle(ctx, 0) +5. When ~HttpsContext() runs, CommitFn commits the response +6. Response serialized → TLS encrypted → sent +7. If res_index > close_index → disconnect +``` + +> **Important**: The `TlsCtx *` is a borrowed pointer. `HttpsServer` does **not** own it. The caller must keep `TlsCtx` alive for the entire lifetime of `HttpsServer`. + +--- + +### HttpsContext + +**Header**: `server/https_context.h` +**Namespace**: `tbox::https::server` + +```cpp +class HttpsContext { + public: + using CommitFn = std::function; + + HttpsContext(CommitFn fn, const cabinet::Token &ct, + int req_index, http::Request *req); + ~HttpsContext(); // commits the response via CommitFn on destruction + + http::Request& req() const; // read-only (received from client) + http::Respond& res() const; // writable (will be sent back to client) +}; + +// Middleware-related type aliases +using HttpsContextSptr = std::shared_ptr; +using HttpsNextFunc = std::function; +using HttpsRequestHandler = std::function; + +class HttpsMiddleware { + public: + virtual void handle(HttpsContextSptr sp_ctx, const HttpsNextFunc &next) = 0; +}; +``` + +#### Key Design Points + +1. **Default response status**: The constructor initializes `status_code` to `404 Not Found`. Handlers must explicitly set the correct status code. +2. **Ownership semantics**: + - `sp_req` is heap-allocated by `HttpsServer::Impl` and **transferred** to `HttpsContext` + - `sp_res` is heap-allocated by `HttpsContext` at construction and **transferred** to `Impl` via `CommitFn` at destruction +3. **Commit timing**: The response is not committed when a handler returns — it is committed when the **last `shared_ptr` reference to `HttpsContext` is destroyed**. + +--- + +### HttpsClient + +**Header**: `client/https_client.h` +**Namespace**: `tbox::https::client` + +#### Class Interface + +```cpp +class HttpsClient { + public: + explicit HttpsClient(event::Loop *wp_loop); + virtual ~HttpsClient(); + + NONCOPYABLE(HttpsClient); + IMMOVABLE(HttpsClient); + + bool initialize(const network::SockAddr &server_addr, + network::tls::TlsCtx *tls_ctx); + + void setAutoReconnect(bool enable); + + using RespondCallback = std::function; + using ErrorCallback = std::function; + + void request(const http::Request &req, + RespondCallback res_cb, + ErrorCallback err_cb = nullptr); + + void cleanup(); +}; +``` + +#### Internal Key Structures + +```cpp +struct PendingReq { + std::string data; // serialized HTTP request bytes (from toString()) + RespondCallback res_cb; + ErrorCallback err_cb; +}; + +struct HttpsClient::Impl { + TlsTcpClient tls_client; + RespondParser parser; + std::deque pending; // request queue + bool in_flight; // true if a request is outstanding + bool started; // true after tls_client.start() called +}; +``` + +#### Request Queuing Logic + +``` +request() is called: + └── serialize req → PendingReq.data + pending.push_back(pending_req) + + Not started? → tls_client.start() (begin TCP + TLS handshake) + Already connected? → sendNext() + Handshaking? (wait for onConnected() to call sendNext()) + +onConnected(): + └── sendNext() + +sendNext(): + └── if in_flight || pending.empty(): return + tls_client.send(pending.front().data) + in_flight = true + +onData() (decrypted bytes received): + └── parser.feed(data, len) + while parser.state() == kFinished: + res = parser.takeRespond() + cb = pending.front().res_cb + pending.pop_front() + in_flight = false + cb(res) ← invoke user callback + sendNext() ← send next queued request + +onDisconnected(): + └── in_flight = false + for each pending request: + if err_cb: err_cb("disconnected") + pending.clear() +``` + +--- + +### RespondParser + +**Header**: `client/respond_parser.h` (header-only) +**Namespace**: `tbox::https::client` + +#### Interface + +```cpp +class RespondParser { + public: + enum class State { kHeaders, kBody, kFinished, kFail }; + + void feed(const char *data, size_t len); // incrementally feed raw bytes + State state() const; + + // Take the completed response and reset parser for next response + http::Respond takeRespond(); +}; +``` + +#### Parsing State Machine + +``` + feed(data) + │ + ┌──────▼──────┐ + │ kHeaders │ ← initial state + └──────┬──────┘ + │ "\r\n\r\n" found + │ parseStatusLine() + parseHeaders() + ▼ + ┌──────────────┐ + │ kBody │ + └──────┬───────┘ + │ + ┌──────▼───────────────────────────────────────────────────┐ + │ Decide body length: │ + │ content_length_ == 0 → kFinished │ (body-less status) + │ content_length_ > 0 and enough data in buf → kFinished │ + │ content_length_ == -1 (no Content-Length) → kFinished │ (empty body) + │ content_length_ > 0 but insufficient data → wait │ + └───────────────────────────────────────────────────────────┘ + │ + ┌──────▼──────┐ + │ kFinished │ ← takeRespond() resets parser, process() runs again + └─────────────┘ + + └── parseStatusLine() fails → kFail +``` + +#### Body-less Status Codes + +`noBodyStatus()` returns `true` (body is skipped) for: +- `1xx` (100–199) — informational +- `204 No Content` +- `304 Not Modified` + +--- + +## Architecture + +### Layered Structure + +``` +┌─────────────────────────────────────────────────────────────┐ +│ User Application │ +│ (calls HttpsServer::use() / HttpsClient::request()) │ +└─────────────────────────┬───────────────────────────────────┘ + │ +┌─────────────────────────┴───────────────────────────────────┐ +│ HTTPS Layer (tbox::https::*) │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Server: HttpsServer / HttpsContext / Middleware │ │ +│ │ Client: HttpsClient / RespondParser │ │ +│ └─────────────────────────┬───────────────────────────┘ │ +└─────────────────────────────┼───────────────────────────────┘ + │ depends on +┌─────────────────────────────┴───────────────────────────────┐ +│ HTTP Layer (tbox::http::*) │ +│ Request / Respond / Method / StatusCode / RequestParser │ +└─────────────────────────────┬───────────────────────────────┘ + │ depends on +┌─────────────────────────────┴───────────────────────────────┐ +│ TLS/Network Layer (tbox::network::tls::*) │ +│ TlsCtx / TlsTcpServer / TlsTcpClient / SockAddr │ +└─────────────────────────────┬───────────────────────────────┘ + │ built on +┌─────────────────────────────┴───────────────────────────────┐ +│ Event Layer (tbox::event::* + libevent + OpenSSL) │ +│ Loop / FdEvent / TimerEvent │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Data Flow + +#### Server: receiving a request → sending a response + +``` +Network → TLS decrypt → onNetReceived(buff) + ↓ + RequestParser::parse(buff) + state == kFinishedAll? + ↓ Yes + new Request (produced by parser) + new HttpsContext(CommitFn, ct, req_idx, req) + ↓ + handle(ctx, 0) // middleware chain starts + ├── handler[0](ctx, next) + │ ...next()... + ├── handler[1](ctx, next) + │ ctx->res().body = "Hello" + └── (chain ends) + ↓ + ~HttpsContext() // shared_ptr ref count reaches zero + ↓ + CommitFn → commitRespond(ct, idx, res) + ↓ + res->toString() → TLS encrypt → send() +``` + +#### Client: sending a request → receiving a response + +``` +request(req, res_cb, err_cb) + ↓ serialize +PendingReq{data, res_cb, err_cb} → pending.push_back() + ↓ (first call, or already connected) +tls_client.send(data) → TLS encrypt → transmit +in_flight = true + ↓ +[event loop runs...] + ↓ +onData(buff) — bytes after TLS decryption + ↓ +RespondParser::feed(data, len) +parser.state() == kFinished? + ↓ Yes +res = parser.takeRespond() +cb = pending.front().res_cb +pending.pop_front() +in_flight = false +cb(res) ← user callback fires +sendNext() ← next queued request is sent +``` + +### Design Patterns + +#### 1. Pimpl (Pointer to Implementation) + +Both `HttpsServer` and `HttpsClient` hide all internals behind a `struct Impl`: + +```cpp +class HttpsServer { + private: + struct Impl; // forward declaration only + Impl *impl_; // opaque pointer +}; +``` + +**Benefits**: +- Public headers have zero internal dependencies (no OpenSSL, libevent headers needed) +- ABI stability: implementation can change without recompiling users + +#### 2. Middleware Chain (Chain of Responsibility) + +```cpp +// Registered in order, executed in order +srv.use(jwt_auth_middleware); // authenticate first +srv.use(rate_limiter); // then rate-limit +srv.use([](auto ctx, auto) { // then handle business logic + ctx->res().body = "OK"; +}); + +// Internal recursive dispatch +void handle(ctx, index) { + auto func = req_handler_[index]; + func(ctx, [this, ctx, index]() { + handle(ctx, index + 1); // this is what next() does + }); +} +``` + +#### 3. RAII Auto-commit (via destructor) + +```cpp +HttpsContext::~HttpsContext() { + // Response is always committed regardless of how the + // handler exits (normal, exception, early return) + d_->commit_fn(d_->conn_token, d_->req_index, d_->sp_res); +} +``` + +#### 4. Ordered Queue Guarantee (client) + +`pending` is a `std::deque`. The `in_flight` flag ensures only one request is outstanding at a time, guaranteeing strict request-response pairing. + +--- + +## Lifecycle Management + +### HttpsServer + +```cpp +// ❶ Must outlive HttpsServer +network::tls::TlsCtx tls_ctx; +tls_ctx.initialize(TlsMode::kServer, cfg); + +// ❷ Strict initialization order +HttpsServer srv(loop); +srv.initializeTls(&tls_ctx); // step 1 — must come first +srv.initialize(bind_addr, 10); // step 2 + +// ❸ Register handlers (any time between initialize and cleanup) +srv.use(handler); + +// ❹ Start +srv.start(); +loop->runLoop(); + +// ❺ Cleanup (stop() is optional; cleanup() calls it internally) +srv.stop(); +srv.cleanup(); +// ❻ loop and tls_ctx may now be safely destroyed +``` + +### HttpsClient + +```cpp +// ❶ TlsCtx must outlive HttpsClient +network::tls::TlsCtx tls_ctx; +tls_ctx.initialize(TlsMode::kClient, cfg); + +// ❷ Initialize +HttpsClient cli(loop); +cli.initialize(server_addr, &tls_ctx); +cli.setAutoReconnect(true); // optional + +// ❸ Queue requests asynchronously +cli.request(req, res_cb, err_cb); + +// ❹ Run event loop +loop->runLoop(); + +// ❺ Cleanup (triggers err_cb("cleanup") for all remaining pending requests) +cli.cleanup(); +``` + +### Common Lifetime Mistakes + +```cpp +// ✗ WRONG: TlsCtx destroyed before HttpsServer +{ + TlsCtx tls_ctx; + srv.initializeTls(&tls_ctx); +} // ← tls_ctx destroyed here; srv holds a dangling pointer! + +// ✗ WRONG: Reversed initialization order +srv.initialize(addr, 10); +srv.initializeTls(&tls_ctx); // ← initializeTls() will warn and return false + +// ✗ WRONG: cleanup() called while event loop is still running handlers +// If the loop is dispatching a callback, cleanup() can race with it. +// Always exit the loop first, then cleanup. +``` + +--- + +## TLS Configuration Reference + +### TlsConfig Fields + +```cpp +struct TlsConfig { + // Server-side: certificate and private key (required for servers) + std::string cert_file; // path to PEM certificate file + std::string key_file; // path to PEM private key file + + // Peer verification + std::string ca_file; // CA certificate for verifying peer + bool verify_peer = true; // false = skip verification (test only!) + + // Security preset (overridden by explicit cipher_list / ciphersuites) + TlsSecurityLevel security_level = TlsSecurityLevel::kDefault; + + // Custom cipher configuration (both optional; override security_level) + std::string cipher_list; // OpenSSL cipher list for TLS <= 1.2 + std::string ciphersuites; // OpenSSL ciphersuites for TLS 1.3 + + bool enable_session_cache = false; // server-side TLS 1.2 session cache +}; +``` + +### Production Client Configuration + +```cpp +TlsConfig cfg; +cfg.ca_file = "/etc/ssl/certs/ca-certificates.crt"; // system CA bundle +cfg.verify_peer = true; +tls_ctx.initialize(TlsMode::kClient, cfg); +``` + +### Self-signed Certificate Client (Testing) + +```cpp +TlsConfig cfg; +cfg.verify_peer = false; // skip certificate verification +tls_ctx.initialize(TlsMode::kClient, cfg); +``` + +### Mutual TLS (mTLS) + +```cpp +// Server: require and verify client certificate +cfg.ca_file = "client_ca.crt"; +cfg.verify_peer = true; + +// Client: present its own certificate +cfg.cert_file = "client.crt"; +cfg.key_file = "client.key"; +``` + +--- + +## Build & Test + +### GNU Make + +```bash +# From the project root +cd /path/to/cpp-tbox + +# Step 1: build all default modules (network, http, and other dependencies) +make modules + +# Step 2: build the https module + test binary (https is not in config.mk by default) +make modules MODULES="https" +make test MODULES="https" + +# Run the https module tests +./.build/https/test + +# Build examples +make examples MODULES="https" +``` + +### CMake + +```bash +mkdir build && cd build +cmake .. # TBOX_ENABLE_HTTPS is ON by default +cmake --build . --target tbox_https_test -j2 +ctest --output-on-failure -R tbox_https +``` + +### Test Coverage + +#### Unit Tests: RespondParser + +```bash +./.build/https/test --gtest_filter="RespondParser.*" +``` + +| Test | What is verified | +|------|-----------------| +| `SimpleOk200` | 200 OK with Content-Length body | +| `Http10` | HTTP/1.0 response | +| `StatusCode201` | 201 Created | +| `NoBody204` | 204 No Content (body skipped) | +| `NoBody304` | 304 Not Modified (body skipped) | +| `NoBody101` | 101 Switching Protocols (body skipped) | +| `FragmentedByByte` | Response split byte-by-byte across feeds | +| `HeaderAndBodySplitAcrossFeeds` | Header/body boundary spans two feeds | +| `TwoResponsesPipelined` | Two complete responses in a single feed | +| `HeaderValueTrimSpaces` | Leading/trailing spaces in header values | +| `MultipleHeaders` | Multiple distinct headers parsed correctly | +| `InvalidStatusLineFail` | Garbage input reaches `kFail` state | +| `EmptyInputStaysInHeaders` | Empty feed keeps state at `kHeaders` | +| `NoContentLengthHeader` | Missing Content-Length treated as empty body | +| `ContentLengthCaseInsensitive` | `content-length` (lowercase) recognized | +| `ContentLengthUpperCase` | `CONTENT-LENGTH` (uppercase) recognized | +| `FailStateIsolated` | Feeding after `kFail` does not crash | + +#### Integration Tests: HTTPS Server + Client + +```bash +./.build/https/test --gtest_filter="HttpsServerClient.*" +``` + +| Test | What is verified | +|------|-----------------| +| `GetHelloWorld` | GET returns 200 + fixed body | +| `PostEcho` | POST body echoed back by server | +| `TwoSequentialRequests` | Two sequential requests, responses match | +| `Returns404` | Non-existent path returns 404 | +| `MiddlewareAuthReject` | Auth middleware rejects unauthorized request | +| `MiddlewareAuthPass` | Auth middleware passes authorized request | +| `ErrorCallbackCalledOnCleanup` | `err_cb` fires for pending requests on cleanup | + +The integration test suite creates an isolated `event::Loop`, `TlsCtx`, `HttpsServer`, and `HttpsClient` per `TEST_F`, using the embedded self-signed certificate from `test_certs.h`. A 3-second timer prevents tests from hanging indefinitely. + +--- + +## FAQ + +### Q: `initialize()` returns false — server won't start + +**Possible causes**: +1. `initializeTls()` was not called, or cert/key file paths are wrong +2. Port is already in use (`bind: Address already in use`) +3. Insufficient privileges (binding ports < 1024 requires root or `CAP_NET_BIND_SERVICE`) + +**Diagnosis**: +```bash +# Check for port conflicts +ss -tlnp | grep :443 + +# Validate certificate and key +openssl verify -CAfile ca.crt server.crt +openssl rsa -in server.key -check -noout +``` + +--- + +### Q: Client TLS handshake fails (ErrorCallback fired with "disconnected") + +**Possible causes**: +1. `verify_peer = true` but server uses a self-signed certificate +2. Server certificate has expired +3. SNI mismatch — certificate CN or SAN does not match the connected hostname/IP + +**Diagnosis**: +```bash +# Test the handshake directly with OpenSSL +openssl s_client -connect 127.0.0.1:443 -servername localhost +# Look for "Verify return code: 0 (ok)" +``` + +--- + +### Q: Response callback never fires + +**Possible causes**: +1. Event loop is not running (`runLoop()` / `runOnce()` not called) +2. Server `start()` was not called +3. Network unreachable (firewall, wrong address) + +--- + +### Q: Valgrind reports a leak related to HttpsContext + +**Check**: +- Does a middleware throw an unhandled exception, preventing `shared_ptr` ref count from reaching zero? +- Is an `HttpsContextSptr` stored somewhere outside the middleware (e.g., in a lambda capture that outlives the request)? + +--- + +### Q: The second request's callback never fires when sending multiple requests + +**Cause**: The client only calls `sendNext()` after the previous response is complete (ordered queue). Ensure: +1. The server sends a proper response for the first request +2. `RespondParser` successfully completes parsing the first response +3. The first request does **not** use `Connection: close` (which causes the server to close the connection) + +--- + +### Q: How do I add a per-request timeout on the client side? + +Use a `TimerEvent` to call `exitLoop()` or execute cleanup logic if the response does not arrive within the desired window: + +```cpp +auto *timer = loop->newTimerEvent(); +timer->initialize(std::chrono::seconds(5), Event::Mode::kOneshot); +timer->setCallback([&] { + LogWarn("Request timed out"); + sp_loop->exitLoop(); +}); +timer->enable(); + +cli.request(req, + [&](const Respond &res) { + timer->disable(); + // handle response... + sp_loop->exitLoop(); + } +); +``` + +--- + +## Known Limitations + +| Limitation | Details | +|------------|---------| +| No chunked transfer encoding | `RespondParser` only handles fixed `Content-Length` or no body | +| No built-in request timeout (client) | Must be implemented by the caller using `TimerEvent` | +| HTTP/2 not supported | HTTP/1.1 only (HTTP/1.0 supported on server side) | +| No WebSocket upgrade | The upgrade handshake must be handled at a higher layer | +| Serial requests only (client) | Only one request in-flight at a time; use multiple `HttpsClient` instances for concurrency | +| Certificate hot-reload not supported | A server restart is required to switch certificates | + +--- + +## References + +- [cpp-tbox project home](https://github.com/cpp-main/cpp-tbox) +- [OpenSSL documentation](https://www.openssl.org/docs/) +- [RFC 7230 — HTTP/1.1 Message Syntax](https://datatracker.ietf.org/doc/html/rfc7230) +- [RFC 8446 — TLS 1.3](https://datatracker.ietf.org/doc/html/rfc8446) +- [tbox::network::tls module](../../network/README.md) (if available) +- [tbox::http module](../../http/README.md) (if available) diff --git a/modules/https/README_CN.md b/modules/https/README_CN.md new file mode 100644 index 00000000..f59dfe82 --- /dev/null +++ b/modules/https/README_CN.md @@ -0,0 +1,945 @@ +# tbox::https — HTTPS 模块 + +> **HTTP/1.1 over TLS** 的服务端与客户端封装,隶属于 [cpp-tbox](https://github.com/cpp-main/cpp-tbox) 框架。 + +--- + +## 目录 + +- [特性概述](#特性概述) +- [依赖关系](#依赖关系) +- [模块结构](#模块结构) +- [快速上手](#快速上手) + - [服务端示例](#服务端示例) + - [客户端示例](#客户端示例) +- [核心组件详解](#核心组件详解) + - [HttpsServer](#httpsserver) + - [HttpsContext](#httpscontext) + - [HttpsClient](#httpsclient) + - [RespondParser](#respondparser) +- [架构设计](#架构设计) + - [分层结构](#分层结构) + - [数据流](#数据流) + - [设计模式](#设计模式) +- [生命周期管理](#生命周期管理) +- [TLS 配置参考](#tls-配置参考) +- [构建与测试](#构建与测试) +- [常见问题](#常见问题) +- [已知限制](#已知限制) + +--- + +## 特性概述 + +| 特性 | 服务端 | 客户端 | +|------|:------:|:------:| +| TLS 1.2 / 1.3 加密 | ✓ | ✓ | +| HTTP/1.1 协议 | ✓ | ✓ | +| HTTP/1.0 协议 | ✓ | — | +| Keep-Alive 持久连接 | ✓ | ✓ | +| 中间件链(Middleware) | ✓ | — | +| 多并发连接 | ✓ | — | +| 请求队列化 | — | ✓ | +| 自动重连 | — | ✓ | +| 异步非阻塞 I/O | ✓ | ✓ | +| Pimpl 隔离实现 | ✓ | ✓ | + +--- + +## 依赖关系 + +``` +tbox_https + ├── tbox_http — HTTP 数据结构(Request / Respond / method / status) + ├── tbox_network — TLS TCP 传输(TlsTcpServer / TlsTcpClient / TlsCtx) + ├── tbox_eventx — 扩展事件(TimerEvent 等) + ├── tbox_event — 基础事件循环(Loop / FdEvent) + ├── tbox_log — 日志 + ├── tbox_util — 工具(Buffer / WrappedRecorder) + ├── tbox_base — 基础定义(NONCOPYABLE / ASSERT / cabinet) + ├── OpenSSL — TLS 加密(libssl / libcrypto) + └── libevent — 底层事件驱动 +``` + +**编译选项**(Makefile): +```makefile +TEST_LDFLAGS := $(LDFLAGS) \ + -ltbox_http -ltbox_network -ltbox_eventx \ + -ltbox_event -ltbox_log -ltbox_util -ltbox_base \ + -lssl -lcrypto -ldl +``` + +--- + +## 模块结构 + +``` +modules/https/ +├── Makefile # GNU Make 编译配置 +├── CMakeLists.txt # CMake 编译配置 +├── README_CN.md # 本文件(中文) +├── README.md # 英文说明 +├── test_certs.h # 内嵌测试证书(自签 CN=localhost) +├── https_server_client_test.cpp # 集成测试(服务端+客户端联调) +│ +├── server/ +│ ├── https_server.h # HttpsServer 公开接口 +│ ├── https_server.cpp # HttpsServer::Impl 内部实现 +│ ├── https_context.h # HttpsContext / Middleware 定义 +│ └── https_context.cpp # HttpsContext 实现 +│ +└── client/ + ├── https_client.h # HttpsClient 公开接口 + ├── https_client.cpp # HttpsClient::Impl 内部实现 + ├── respond_parser.h # HTTP/1.1 响应解析器(header-only) + └── respond_parser_test.cpp # RespondParser 单元测试 +``` + +--- + +## 快速上手 + +### 生成测试证书 + +```bash +openssl req -x509 -newkey rsa:2048 \ + -keyout server.key -out server.crt \ + -days 365 -nodes \ + -subj "/CN=localhost" \ + -addext "subjectAltName=IP:127.0.0.1,DNS:localhost" +``` + +### 服务端示例 + +```cpp +#include +#include +#include +#include + +#include "server/https_server.h" // HttpsServer、HttpsContext + +using namespace tbox; +using namespace tbox::network; +using namespace tbox::network::tls; +using namespace tbox::http; +using namespace tbox::https::server; + +int main(int argc, char *argv[]) { + // ── 1. 创建事件循环 ──────────────────────────────────────────── + auto sp_loop = event::Loop::New(); + + // ── 2. 配置 TLS(服务端需要证书+私钥) ───────────────────────── + TlsCtx tls_ctx; + TlsConfig cfg; + cfg.cert_file = "server.crt"; + cfg.key_file = "server.key"; + if (!tls_ctx.initialize(TlsMode::kServer, cfg)) { + LogErr("TLS init failed"); + return 1; + } + + // ── 3. 创建并初始化 HttpsServer ──────────────────────────────── + HttpsServer srv(sp_loop.get()); + + if (!srv.initializeTls(&tls_ctx)) return 1; + if (!srv.initialize(SockAddr::FromString("0.0.0.0:443"), 10)) return 1; + + // ── 4. 注册中间件 ────────────────────────────────────────────── + + // 访问日志中间件(调用 next() 继续处理) + srv.use([](HttpsContextSptr ctx, const HttpsNextFunc &next) { + LogInfo("[%s] %s", + Method_Name(ctx->req().method).c_str(), + ctx->req().url.path.c_str()); + next(); + }); + + // 业务处理中间件(不调用 next() = 终止链) + srv.use([](HttpsContextSptr ctx, const HttpsNextFunc &) { + if (ctx->req().url.path == "/health") { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().body = "OK"; + } else { + ctx->res().status_code = StatusCode::k404_NotFound; + ctx->res().body = "Not Found"; + } + ctx->res().headers["Content-Type"] = "text/plain"; + }); + + // ── 5. 启动 ──────────────────────────────────────────────────── + if (!srv.start()) return 1; + + LogInfo("HTTPS server started at https://0.0.0.0:443"); + sp_loop->runLoop(); // 阻塞直到 exitLoop() + + srv.stop(); + srv.cleanup(); + return 0; +} +``` + +### 客户端示例 + +```cpp +#include +#include +#include +#include +#include + +#include "client/https_client.h" + +using namespace tbox; +using namespace tbox::network; +using namespace tbox::network::tls; +using namespace tbox::http; +using namespace tbox::https::client; + +int main() { + auto sp_loop = event::Loop::New(); + + // ── 1. 配置 TLS(客户端,关闭证书验证供测试) ────────────────── + TlsCtx tls_ctx; + TlsConfig cfg; + cfg.verify_peer = false; // 生产环境应设为 true 并提供 ca_file + if (!tls_ctx.initialize(TlsMode::kClient, cfg)) return 1; + + // ── 2. 创建并初始化 HttpsClient ──────────────────────────────── + HttpsClient cli(sp_loop.get()); + if (!cli.initialize(SockAddr::FromString("127.0.0.1:443"), &tls_ctx)) return 1; + cli.setAutoReconnect(true); // 自动在断开后重连 + + // ── 3. 构造 HTTP 请求 ────────────────────────────────────────── + Request req; + req.method = Method::kGet; + req.url.path = "/health"; + req.http_ver = HttpVer::k1_1; + req.headers["Host"] = "127.0.0.1"; + req.headers["Connection"] = "close"; + + // ── 4. 发起请求(异步,两个回调) ───────────────────────────── + bool done = false; + cli.request(req, + [&](const Respond &res) { + LogInfo("Status : %d", static_cast(res.status_code)); + LogInfo("Body : %s", res.body.c_str()); + done = true; + sp_loop->exitLoop(); + }, + [&](const std::string &err) { + LogErr("Error: %s", err.c_str()); + done = true; + sp_loop->exitLoop(); + } + ); + + sp_loop->runLoop(); // 等待回调 + + cli.cleanup(); + return done ? 0 : 1; +} +``` + +--- + +## 核心组件详解 + +### HttpsServer + +**头文件**: `server/https_server.h` +**命名空间**: `tbox::https::server` + +#### 类接口 + +```cpp +class HttpsServer { + public: + explicit HttpsServer(event::Loop *wp_loop); + virtual ~HttpsServer(); + + // ── 初始化(顺序不可颠倒) ──────────────────────────────────── + bool initializeTls(network::tls::TlsCtx *tls_ctx); + bool initialize(const network::SockAddr &bind_addr, int listen_backlog); + + // ── 启停 ───────────────────────────────────────────────────── + bool start(); + void stop(); + void cleanup(); + + // ── 状态查询 ───────────────────────────────────────────────── + enum class State { kNone, kInited, kRunning }; + State state() const; + + // ── 调试 ───────────────────────────────────────────────────── + void setContextLogEnable(bool enable); // 打印请求/响应原文 + + // ── 中间件注册 ──────────────────────────────────────────────── + void use(HttpsRequestHandler &&handler); + void use(HttpsMiddleware *wp_middleware); +}; +``` + +#### 状态机 + +``` + ┌─────────────────┐ + │ kNone │ ← 构造后初始状态 + └────────┬────────┘ + │ initializeTls() + initialize() + ↓ + ┌─────────────────┐ + │ kInited │ ← 监听未就绪,可注册处理器 + └────────┬────────┘ + │ start() + ↓ + ┌─────────────────┐ + │ kRunning │ ← 正在监听并处理连接 + └────────┬────────┘ + │ stop() + ↓ + ┌─────────────────┐ + │ kInited │ ← 停止后可再次 start() + └────────┬────────┘ + │ cleanup() + ↓ + ┌─────────────────┐ + │ kNone │ + └─────────────────┘ +``` + +#### 连接处理流程(内部) + +``` +1. TlsTcpServer 接受 TCP 连接 +2. 完成 TLS 握手(OpenSSL 自动处理) +3. 创建 per-connection 的 Connection 结构(含 RequestParser) +4. 在 onNetReceived() 中: + a. 将解密数据喂给 RequestParser + b. 解析完整请求后,检查 Connection: 头: + - Connection: close → 标记 close_index + - 默认 keep-alive → 继续接收下一请求 + c. 创建 HttpsContext(req_index++) + d. 执行中间件链 handle() +5. HttpsContext 析构时,通过 CommitFn 提交响应 +6. 序列化响应 → TLS 加密 → 发送 +7. 若 res_index > close_index → 关闭连接 +``` + +> **注意**: `TlsCtx *` 是借用指针,`HttpsServer` 不拥有其生命期,调用者必须保证 `TlsCtx` 在 `HttpsServer` 析构之前有效。 + +--- + +### HttpsContext + +**头文件**: `server/https_context.h` +**命名空间**: `tbox::https::server` + +```cpp +class HttpsContext { + public: + using CommitFn = std::function; + + HttpsContext(CommitFn fn, const cabinet::Token &ct, + int req_index, http::Request *req); + ~HttpsContext(); // 析构时提交响应(通过 CommitFn) + + http::Request& req() const; // 只读(来自客户端) + http::Respond& res() const; // 可写(即将发回客户端) +}; + +// 中间件相关类型别名 +using HttpsContextSptr = std::shared_ptr; +using HttpsNextFunc = std::function; +using HttpsRequestHandler = std::function; + +class HttpsMiddleware { + public: + virtual void handle(HttpsContextSptr sp_ctx, const HttpsNextFunc &next) = 0; +}; +``` + +#### 关键设计点 + +1. **响应默认状态**: 构造时 `status_code` 初始化为 `404 Not Found`,必须在处理器中显式设置正确状态码。 +2. **生命期所有权**: + - `sp_req` 由 `HttpsServer::Impl` 通过 `new Request` 创建并 **移交** 给 `HttpsContext` + - `sp_res` 由 `HttpsContext` 构造时 `new Respond` 创建,析构时通过 `CommitFn` **移交**给 `Impl` +3. **提交时机**: 不是在处理器返回时提交,而是在 `HttpsContextSptr` 最后一个引用消失(析构)时通过 `CommitFn` 自动提交。 + +--- + +### HttpsClient + +**头文件**: `client/https_client.h` +**命名空间**: `tbox::https::client` + +#### 类接口 + +```cpp +class HttpsClient { + public: + explicit HttpsClient(event::Loop *wp_loop); + virtual ~HttpsClient(); + + NONCOPYABLE(HttpsClient); + IMMOVABLE(HttpsClient); + + bool initialize(const network::SockAddr &server_addr, + network::tls::TlsCtx *tls_ctx); + + void setAutoReconnect(bool enable); + + using RespondCallback = std::function; + using ErrorCallback = std::function; + + void request(const http::Request &req, + RespondCallback res_cb, + ErrorCallback err_cb = nullptr); + + void cleanup(); +}; +``` + +#### 内部关键结构 + +```cpp +struct PendingReq { + std::string data; // 序列化后的 HTTP 请求字节(已 toString()) + RespondCallback res_cb; + ErrorCallback err_cb; +}; + +struct HttpsClient::Impl { + TlsTcpClient tls_client; + RespondParser parser; + std::deque pending; // 请求队列 + bool in_flight; // 是否有请求在途 + bool started; // 是否已调用 tls_client.start() +}; +``` + +#### 请求排队逻辑 + +``` +request() 被调用: + └── 序列化 req → PendingReq.data + pending.push_back(pending_req) + + 未启动? → tls_client.start() (开始 TCP 连接+TLS 握手) + 已连接? → sendNext() + 握手中? (等待 onConnected() 触发 sendNext()) + +onConnected(): + └── sendNext() + +sendNext(): + └── if in_flight || pending.empty(): 返回 + tls_client.send(pending.front().data) + in_flight = true + +onData() (收到解密数据): + └── parser.feed(data, len) + while parser.state() == kFinished: + res = parser.takeRespond() + cb = pending.front().res_cb + pending.pop_front() + in_flight = false + cb(res) ← 触发用户回调 + sendNext() ← 发送下一个请求 + +onDisconnected(): + └── in_flight = false + for 所有 pending: + if err_cb: err_cb("disconnected") + pending.clear() +``` + +--- + +### RespondParser + +**头文件**: `client/respond_parser.h`(header-only) +**命名空间**: `tbox::https::client` + +#### 接口 + +```cpp +class RespondParser { + public: + enum class State { kHeaders, kBody, kFinished, kFail }; + + void feed(const char *data, size_t len); // 增量喂入原始字节 + State state() const; + + // 取走完整响应,解析器重置准备下一个 + http::Respond takeRespond(); +}; +``` + +#### 解析状态机 + +``` + feed(data) + │ + ┌──────▼──────┐ + │ kHeaders │ ← 初始状态 + └──────┬──────┘ + │ 找到 "\r\n\r\n" + │ parseStatusLine() + parseHeaders() + ▼ + ┌──────────────┐ + │ kBody │ + └──────┬───────┘ + │ + ┌──────▼───────────────────────────────────────┐ + │ 判断正文长度 │ + │ content_length_ == 0 │ → kFinished (无正文状态码) + │ content_length_ > 0 且数据足够 │ → kFinished + │ content_length_ == -1 (无 Content-Length) │ → kFinished (空正文) + │ content_length_ > 0 但数据不足 │ → 等待更多 feed + └──────────────────────────────────────────────┘ + │ + ┌──────▼──────┐ + │ kFinished │ ← takeRespond() 后重置,立即再次 process() + └─────────────┘ + + └── parseStatusLine() 失败 → kFail +``` + +#### 支持的无正文状态码 + +`noBodyStatus()` 对以下状态码返回 `true`(不读正文直接完成): +- `1xx`(100–199):信息响应 +- `204 No Content` +- `304 Not Modified` + +--- + +## 架构设计 + +### 分层结构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 用户代码 / 应用程序 │ +│ (使用 HttpsServer::use() / HttpsClient::request()) │ +└─────────────────────────┬───────────────────────────────────┘ + │ +┌─────────────────────────┴───────────────────────────────────┐ +│ HTTPS 层(tbox::https::*) │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 服务端: HttpsServer / HttpsContext / Middleware │ │ +│ │ 客户端: HttpsClient / RespondParser │ │ +│ └─────────────────────────┬───────────────────────────┘ │ +└─────────────────────────────┼───────────────────────────────┘ + │ 使用 +┌─────────────────────────────┴───────────────────────────────┐ +│ HTTP 层(tbox::http::*) │ +│ Request / Respond / Method / StatusCode / RequestParser │ +└─────────────────────────────┬───────────────────────────────┘ + │ 使用 +┌─────────────────────────────┴───────────────────────────────┐ +│ TLS/Network 层(tbox::network::tls::*) │ +│ TlsCtx / TlsTcpServer / TlsTcpClient / SockAddr │ +└─────────────────────────────┬───────────────────────────────┘ + │ 基于 +┌─────────────────────────────┴───────────────────────────────┐ +│ 事件层(tbox::event::* + libevent + OpenSSL) │ +│ Loop / FdEvent / TimerEvent │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 数据流 + +#### 服务端:收到请求 → 发送响应 + +``` +网络 → TLS 解密 → onNetReceived(buff) + ↓ + RequestParser::parse(buff) + state == kFinishedAll? + ↓ Yes + new Request (parser 产生) + new HttpsContext(CommitFn, ct, req_idx, req) + ↓ + handle(ctx, 0) // 中间件链 + ├── handler[0](ctx, next) + │ ...next()... + ├── handler[1](ctx, next) + │ ctx->res().body = "Hello" + └── (链末) + ↓ + ~HttpsContext() // ctx 引用计数归零 + ↓ + CommitFn → commitRespond(ct, idx, res) + ↓ + res->toString() → TLS 加密 → send() +``` + +#### 客户端:发请求 → 收响应 + +``` +request(req, res_cb, err_cb) + ↓ 序列化 +PendingReq{data, res_cb, err_cb} → pending.push_back() + ↓ (首次或已连接) +tls_client.send(data) → TLS 加密 → 发送 +in_flight = true + ↓ +[等待事件循环...] + ↓ +onData(buff) — TLS 解密后的字节 + ↓ +RespondParser::feed(data, len) +parser.state() == kFinished? + ↓ Yes +res = parser.takeRespond() +cb = pending.front().res_cb +pending.pop_front() +in_flight = false +cb(res) ← 用户回调触发 +sendNext() ← 发送队列中下一个 +``` + +### 设计模式 + +#### 1. Pimpl(指向实现的指针) + +`HttpsServer` 和 `HttpsClient` 均使用 `struct Impl` 隐藏所有实现细节: + +```cpp +class HttpsServer { + private: + struct Impl; // 仅声明 + Impl *impl_; // 仅指针 +}; +``` + +**好处**: +- 公开头文件零内部依赖(不需要 include OpenSSL、libevent 等) +- ABI 稳定:内部变化不影响接口 + +#### 2. 中间件链(Chain of Responsibility) + +```cpp +// 注册顺序即执行顺序 +srv.use(jwt_auth_middleware); // 先验证 +srv.use(rate_limiter); // 再限流 +srv.use([](auto ctx, auto) { // 最后处理业务 + ctx->res().body = "OK"; +}); + +// 内部递归调用 +void handle(ctx, index) { + auto func = req_handler_[index]; + func(ctx, [this, ctx, index]() { + handle(ctx, index + 1); // next() 的实现 + }); +} +``` + +#### 3. RAII 自动提交(通过析构函数) + +```cpp +HttpsContext::~HttpsContext() { + // 不管处理器正常调用还是异常退出,响应总会被提交 + d_->commit_fn(d_->conn_token, d_->req_index, d_->sp_res); +} +``` + +#### 4. 顺序队列保证(客户端) + +`pending` 是 `std::deque`,`in_flight` 标志保证同时只有一个请求在途,响应与请求严格一一对应。 + +--- + +## 生命周期管理 + +### HttpsServer + +```cpp +// ❶ 必须比 HttpsServer 更长期 +network::tls::TlsCtx tls_ctx; +tls_ctx.initialize(TlsMode::kServer, cfg); + +// ❷ 初始化顺序严格要求 +HttpsServer srv(loop); +srv.initializeTls(&tls_ctx); // 必须第一步 +srv.initialize(bind_addr, 10); + +// ❸ 注册处理器(initialize 后,start 前或 start 后均可) +srv.use(handler); + +// ❹ 启动 +srv.start(); +loop->runLoop(); + +// ❺ 清理(stop() 可省略,cleanup() 内部会调用) +srv.stop(); +srv.cleanup(); +// ❻ loop 和 tls_ctx 在此之后才可安全销毁 +``` + +### HttpsClient + +```cpp +// ❶ TlsCtx 生命期必须覆盖 HttpsClient +network::tls::TlsCtx tls_ctx; +tls_ctx.initialize(TlsMode::kClient, cfg); + +// ❷ 初始化 +HttpsClient cli(loop); +cli.initialize(server_addr, &tls_ctx); +cli.setAutoReconnect(true); // 可选 + +// ❸ 异步请求(可在 loop 运行前后均可调用) +cli.request(req, res_cb, err_cb); + +// ❹ 运行事件循环 +loop->runLoop(); + +// ❺ 清理(会对所有剩余 pending 触发 err_cb("cleanup")) +cli.cleanup(); +``` + +### 常见生命期错误 + +```cpp +// ✗ 错误:TlsCtx 比 HttpsServer 先销毁 +{ + TlsCtx tls_ctx; + srv.initializeTls(&tls_ctx); +} // ← tls_ctx 析构,srv 持有悬空指针! + +// ✗ 错误:顺序颠倒 +srv.initialize(addr, 10); +srv.initializeTls(&tls_ctx); // ← 此时 initialize 已完成,initializeTls 会报 warn 并失败 + +// ✗ 错误:cleanup() 前未停止事件循环 +// 若 loop 仍在处理事件,cleanup() 可能在回调中被调用导致崩溃 +``` + +--- + +## TLS 配置参考 + +### TlsConfig 字段 + +```cpp +struct TlsConfig { + // 服务端:证书和私钥(服务端必填) + std::string cert_file; // PEM 格式证书文件路径 + std::string key_file; // PEM 格式私钥文件路径 + + // 对端证书验证 + std::string ca_file; // CA 证书路径(用于验证对端证书) + bool verify_peer = true; // false = 关闭验证(仅测试用!) + + // 安全预设(被显式设置的 cipher_list/ciphersuites 覆盖) + TlsSecurityLevel security_level = TlsSecurityLevel::kDefault; + + // 自定义密码配置(可选,覆盖 security_level 预设) + std::string cipher_list; // OpenSSL TLS 1.2 及以下密码列表 + std::string ciphersuites; // OpenSSL TLS 1.3 密码套件 + + bool enable_session_cache = false; // 服务端 TLS 1.2 会话缓存 +}; +``` + +### 生产环境客户端配置 + +```cpp +TlsConfig cfg; +cfg.ca_file = "/etc/ssl/certs/ca-certificates.crt"; // 系统 CA 束 +cfg.verify_peer = true; +tls_ctx.initialize(TlsMode::kClient, cfg); +``` + +### 自签证书客户端(测试) + +```cpp +TlsConfig cfg; +cfg.verify_peer = false; // 跳过证书验证 +tls_ctx.initialize(TlsMode::kClient, cfg); +``` + +### 双向 TLS(mTLS) + +```cpp +// 服务端额外配置 +cfg.ca_file = "client_ca.crt"; // 用于验证客户端证书 +cfg.verify_peer = true; // 要求客户端提供证书 + +// 客户端额外配置 +cfg.cert_file = "client.crt"; +cfg.key_file = "client.key"; +``` + +--- + +## 构建与测试 + +### GNU Make + +```bash +# 进入项目根目录 +cd /path/to/cpp-tbox + +# 第一步:构建所有默认模块(network、http 等依赖) +make modules + +# 第二步:构建 https 模块及测试二进制(默认不在 config.mk 中) +make modules MODULES="https" +make test MODULES="https" + +# 运行 https 模块测试 +./.build/https/test + +# 构建示例 +make examples MODULES="https" +``` + +### CMake + +```bash +mkdir build && cd build +cmake .. # TBOX_ENABLE_HTTPS 默认为 ON +cmake --build . --target tbox_https_test -j2 +ctest --output-on-failure -R tbox_https +``` + +### 测试说明 + +#### 单元测试:RespondParser + +```bash +./.build/https/test --gtest_filter="RespondParser.*" +``` + +测试覆盖: + +| 测试名 | 验证内容 | +|--------|---------| +| `SimpleOk200` | 带 Content-Length 的 200 OK 响应 | +| `Http10` | HTTP/1.0 响应 | +| `StatusCode201` | 201 Created | +| `NoBody204` | 204 No Content(无正文,直接完成) | +| `NoBody304` | 304 Not Modified(无正文) | +| `NoBody101` | 101 Switching Protocols(无正文) | +| `FragmentedByByte` | 逐字节分片 feed | +| `HeaderAndBodySplitAcrossFeeds` | Header/Body 边界横跨两次 feed | +| `TwoResponsesPipelined` | 单次 feed 中含两个完整响应 | +| `HeaderValueTrimSpaces` | Header 值两端空格被正确去除 | +| `MultipleHeaders` | 多个 header 字段均正确解析 | +| `InvalidStatusLineFail` | 垃圾输入进入 `kFail` 状态 | +| `EmptyInputStaysInHeaders` | 空 feed 保持 `kHeaders` 状态 | +| `NoContentLengthHeader` | 无 Content-Length 时视为空正文 | +| `ContentLengthCaseInsensitive` | `content-length`(小写)可正常识别 | +| `ContentLengthUpperCase` | `CONTENT-LENGTH`(大写)可正常识别 | +| `FailStateIsolated` | 进入 `kFail` 后继续 feed 不崩溃 | + +#### 集成测试:HTTPS 服务端+客户端联调 + +```bash +./.build/https/test --gtest_filter="HttpsServerClient.*" +``` + +测试覆盖: + +| 测试名 | 验证内容 | +|--------|---------| +| `GetHelloWorld` | GET 请求,检查 200 + 响应正文 | +| `PostEcho` | POST 请求,服务端 echo 正文 | +| `TwoSequentialRequests` | 顺序发送两个请求,响应一一对应 | +| `Returns404` | 路径不匹配时返回 404 | +| `MiddlewareAuthReject` | 认证中间件拒绝未授权请求 | +| `MiddlewareAuthPass` | 认证中间件放行已授权请求 | +| `ErrorCallbackCalledOnCleanup` | cleanup 时对所有 pending 请求触发 err_cb | + +--- + +## 常见问题 + +### Q: 服务器无法启动,initialize() 返回 false + +**可能原因**: +1. `initializeTls()` 未调用,或证书/私钥路径错误 +2. 端口被占用(`bind: Address already in use`) +3. 权限不足(绑定 1024 以下端口需要 root 或 `CAP_NET_BIND_SERVICE`) + +**排查**: +```bash +# 检查端口占用 +ss -tlnp | grep :443 + +# 检查证书有效性 +openssl verify -CAfile ca.crt server.crt +openssl rsa -in server.key -check -noout +``` + +--- + +### Q: 客户端 TLS 握手失败(ErrorCallback 收到 "disconnected") + +**可能原因**: +1. 客户端 `verify_peer = true` 但服务器使用自签证书 +2. 服务器证书已过期 +3. Server Name Indication (SNI) 不匹配(证书 CN/SAN 与连接地址不符) + +**排查**: +```bash +# 使用 OpenSSL 直接测试握手 +openssl s_client -connect 127.0.0.1:443 -servername localhost +# 查看输出中的 Verify return code(0 = 成功) +``` + +--- + +### Q: 响应回调一直不触发 + +**可能原因**: +1. 事件循环未运行(未调用 `loop->runLoop()` 或 `loop->runOnce()`) +2. 服务端未调用 `start()` +3. 网络不通(防火墙、地址错误等) + +--- + +### Q: 内存泄漏,Valgrind 报告 HttpsContext 相关 + +**检查**: +- 是否在中间件中抛出异常而未捕获,导致 `shared_ptr` 引用计数无法归零? +- 是否将 `HttpsContextSptr` 保存在回调之外,延长了其生命期? + +--- + +### Q: 多个请求时第二个请求的回调不触发 + +**原因**:客户端的 `sendNext()` 只在收到前一个响应后才发送下一个请求(顺序队列)。确保: +1. 服务端正确发回了第一个响应 +2. `RespondParser` 正确完成了第一个响应的解析 +3. `Connection: keep-alive`(不要在首个请求中带 `Connection: close`) + +--- + +## 已知限制 + +| 限制 | 说明 | +|------|------| +| 不支持 chunked 编码 | `RespondParser` 仅处理固定 `Content-Length` 或无正文 | +| 客户端无请求超时 | 需由用户通过 `TimerEvent` 实现超时取消 | +| 不支持 HTTP/2 | 仅 HTTP/1.1(及 HTTP/1.0 服务端)| +| 不支持 WebSocket 升级 | 升级握手需要上层自行处理 | +| 一次只有一个在途请求(客户端)| 并发场景需创建多个 `HttpsClient` 实例 | +| TlsCtx 不能热更新证书 | 需重启服务器才能更换证书 | + +--- + +## 参考资料 + +- [cpp-tbox 项目主页](https://github.com/cpp-main/cpp-tbox) +- [OpenSSL 文档](https://www.openssl.org/docs/) +- [RFC 7230 — HTTP/1.1 消息语法](https://datatracker.ietf.org/doc/html/rfc7230) +- [RFC 8446 — TLS 1.3](https://datatracker.ietf.org/doc/html/rfc8446) +- [tbox::network::tls 模块文档](../../network/README.md)(如有) +- [tbox::http 模块文档](../../http/README.md)(如有) diff --git a/modules/https/client/https_client.cpp b/modules/https/client/https_client.cpp new file mode 100644 index 00000000..91ecc97a --- /dev/null +++ b/modules/https/client/https_client.cpp @@ -0,0 +1,172 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#undef MODULE_ID +#define MODULE_ID "tbox.https" + +#include "https_client.h" +#include "respond_parser.h" + +#include +#include + +#include +#include +#include +#include + +namespace tbox { +namespace https { +namespace client { + +using namespace network::tls; +using namespace std::placeholders; +using namespace http; + +// =========================================================================== +// HttpsClient::Impl +// =========================================================================== + +struct PendingReq { + std::string data; ///< 序列化后的 HTTP 请求字节 + HttpsClient::RespondCallback res_cb; + HttpsClient::ErrorCallback err_cb; +}; + +struct HttpsClient::Impl { + event::Loop *wp_loop = nullptr; + TlsTcpClient tls_client; + RespondParser parser; + std::deque pending; + bool in_flight = false; + bool started = false; + + explicit Impl(event::Loop *lp) + : wp_loop(lp), tls_client(lp) {} + + // ----------------------------------------------------------------------- + + void onConnected() { + LogDbg("HTTPS: TLS established — %zu request(s) waiting", pending.size()); + sendNext(); + } + + void onDisconnected() { + in_flight = false; + for (auto &p : pending) { + if (p.err_cb) p.err_cb("disconnected"); + } + pending.clear(); + LogDbg("HTTPS: disconnected"); + } + + void onData(util::Buffer &buff) { + parser.feed(reinterpret_cast(buff.readableBegin()), + buff.readableSize()); + buff.hasReadAll(); + + while (parser.state() == RespondParser::State::kFinished) { + Respond res = parser.takeRespond(); + if (!pending.empty()) { + auto cb = std::move(pending.front().res_cb); + pending.pop_front(); + in_flight = false; + if (cb) cb(res); + sendNext(); + } + } + + if (parser.state() == RespondParser::State::kFail) { + LogWarn("HTTPS: HTTP response parse error"); + // 通知所有等待中的请求(不只是队首),然后断开连接 + for (auto &p : pending) + if (p.err_cb) p.err_cb("parse error"); + pending.clear(); + in_flight = false; + tls_client.stop(); + } + } + + // ----------------------------------------------------------------------- + + void sendNext() { + if (in_flight || pending.empty()) return; + const auto &p = pending.front(); + if (tls_client.send(p.data.data(), p.data.size())) + in_flight = true; + } + + void enqueue(PendingReq &&req) { + pending.push_back(std::move(req)); + if (!started) { + tls_client.start(); + started = true; + } else if (tls_client.state() == TlsTcpClient::State::kConnected) { + sendNext(); + } + // 若仍在握手中,onConnected() 会调用 sendNext() + } +}; + +// =========================================================================== +// HttpsClient 公开接口 +// =========================================================================== + +HttpsClient::HttpsClient(event::Loop *wp_loop) + : impl_(new Impl(wp_loop)) +{ + impl_->tls_client.setConnectedCallback(std::bind(&Impl::onConnected, impl_)); + impl_->tls_client.setDisconnectedCallback(std::bind(&Impl::onDisconnected, impl_)); + impl_->tls_client.setReceiveCallback(std::bind(&Impl::onData, impl_, std::placeholders::_1)); +} + +HttpsClient::~HttpsClient() { + delete impl_; +} + +bool HttpsClient::initialize(const network::SockAddr &server_addr, network::tls::TlsCtx *tls_ctx) { + return impl_->tls_client.initialize(server_addr, tls_ctx); +} + +void HttpsClient::setAutoReconnect(bool enable) { + impl_->tls_client.setAutoReconnect(enable); +} + +void HttpsClient::request(const http::Request &req, + RespondCallback res_cb, + ErrorCallback err_cb) { + PendingReq p; + p.data = req.toString(); + p.res_cb = std::move(res_cb); + p.err_cb = std::move(err_cb); + impl_->enqueue(std::move(p)); +} + +void HttpsClient::cleanup() { + impl_->tls_client.cleanup(); + for (auto &p : impl_->pending) + if (p.err_cb) p.err_cb("cleanup"); + impl_->pending.clear(); + impl_->in_flight = false; + impl_->started = false; +} + +} // namespace client +} // namespace https +} // namespace tbox diff --git a/modules/https/client/https_client.h b/modules/https/client/https_client.h new file mode 100644 index 00000000..facc2409 --- /dev/null +++ b/modules/https/client/https_client.h @@ -0,0 +1,120 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_HTTPS_CLIENT_H_20260324 +#define TBOX_HTTPS_CLIENT_H_20260324 + +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace tbox { +namespace https { +namespace client { + +/** + * HTTPS 客户端。 + * + * 封装 TlsTcpClient + HTTP/1.1 响应解析器,对外提供与 + * http::client::Client 一致的接口(额外需要 TlsCtx*)。 + * + * 特性: + * - 持久连接(Connection: keep-alive) + * - 顺序请求队列:一次只有一个请求在途,按序返回 + * - 自动重连(需调用 setAutoReconnect(true)) + * - 断开时对所有排队请求触发 ErrorCallback + * + * 用法: + * @code + * network::tls::TlsCtx tls_ctx; + * network::tls::TlsConfig cfg; + * cfg.ca_file = "/etc/ssl/certs/ca-certificates.crt"; + * cfg.verify_peer = true; + * tls_ctx.initialize(network::tls::TlsMode::kClient, cfg); + * + * http::client::HttpsClient client(wp_loop); + * client.initialize(network::SockAddr::MakeIPv4("192.168.1.1", 443), &tls_ctx); + * client.setAutoReconnect(true); + * + * http::Request req; + * req.method = http::Method::kGet; + * req.url = http::UrlFromPath("/api/status"); + * req.http_ver = http::HttpVer::k1_1; + * req.headers["Host"] = "192.168.1.1"; + * req.headers["Connection"] = "keep-alive"; + * + * client.request(req, + * [](const http::Respond &res) { + * LogInfo("HTTPS status: %d", static_cast(res.status_code)); + * }, + * [](const std::string &err) { + * LogErr("HTTPS error: %s", err.c_str()); + * }); + * @endcode + */ +class HttpsClient { + public: + explicit HttpsClient(event::Loop *wp_loop); + virtual ~HttpsClient(); + + NONCOPYABLE(HttpsClient); + IMMOVABLE(HttpsClient); + + public: + bool initialize(const network::SockAddr &server_addr, + network::tls::TlsCtx *tls_ctx); + + /** 开启全自动重连(须在 initialize() 之后调用)*/ + void setAutoReconnect(bool enable); + + using RespondCallback = std::function; + using ErrorCallback = std::function; + + /** + * 发送 HTTPS 请求。 + * + * 若当前未连接则自动发起连接;请求进入队列,按序发送。 + * + * @param req HTTP 请求(需自行填写 Host 头) + * @param res_cb 请求成功时的回调 + * @param err_cb 连接断开或解析失败时的回调(可为 nullptr) + */ + void request(const http::Request &req, + RespondCallback res_cb, + ErrorCallback err_cb = nullptr); + + void cleanup(); + + private: + struct Impl; + Impl *impl_ = nullptr; +}; + +} // namespace client +} // namespace https +} // namespace tbox + +#endif // TBOX_HTTPS_CLIENT_H_20260324 diff --git a/modules/https/client/respond_parser.h b/modules/https/client/respond_parser.h new file mode 100644 index 00000000..c7aa4782 --- /dev/null +++ b/modules/https/client/respond_parser.h @@ -0,0 +1,179 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_HTTPS_RESPOND_PARSER_H_ +#define TBOX_HTTPS_RESPOND_PARSER_H_ + +#include +#include +#include +#include +#include +#include + +namespace tbox { +namespace https { +namespace client { + +/** + * HTTP/1.1 响应解析器(内部使用)。 + * + * 使用方法: + * RespondParser p; + * p.feed(data, len); + * if (p.state() == RespondParser::State::kFinished) + * Respond r = p.takeRespond(); + */ +class RespondParser { + public: + enum class State { kHeaders, kBody, kFinished, kFail }; + + /** 喂入原始字节,内部缓冲并尝试推进解析。*/ + void feed(const char *data, size_t len) { + buf_.append(data, len); + process(); + } + + State state() const { return state_; } + + /** + * 取走已完成的 Respond 并重置解析器。 + * 多余字节(属于下一响应)保留在缓冲区,重置后立即重新尝试解析。 + */ + http::Respond takeRespond() { + http::Respond r = std::move(respond_); + respond_ = http::Respond{}; + content_length_ = -1; + state_ = State::kHeaders; + process(); + return r; + } + + private: + void process() { + if (state_ == State::kHeaders) { + auto pos = buf_.find("\r\n\r\n"); + if (pos == std::string::npos) return; + + std::string hdr = buf_.substr(0, pos); + buf_.erase(0, pos + 4); + + if (!parseHeaders(hdr)) { + state_ = State::kFail; + return; + } + state_ = State::kBody; + } + + if (state_ == State::kBody) { + if (content_length_ == 0 || noBodyStatus(respond_.status_code)) { + state_ = State::kFinished; + } else if (content_length_ > 0) { + auto need = static_cast(content_length_); + if (buf_.size() >= need) { + respond_.body = buf_.substr(0, need); + buf_.erase(0, need); + state_ = State::kFinished; + } + } else { + // content_length_ == -1: 无 Content-Length,视为空正文 + respond_.body.clear(); + state_ = State::kFinished; + } + } + } + + bool parseHeaders(const std::string &hdr) { + size_t pos = 0; + bool first = true; + while (pos < hdr.size()) { + size_t end = hdr.find("\r\n", pos); + if (end == std::string::npos) end = hdr.size(); + std::string line = hdr.substr(pos, end - pos); + pos = end + 2; + if (line.empty()) break; + + if (first) { + if (!parseStatusLine(line)) return false; + first = false; + } else { + parseHeaderLine(line); + } + } + return !first; + } + + bool parseStatusLine(const std::string &line) { + size_t p1 = line.find(' '); + if (p1 == std::string::npos) return false; + + const std::string ver = line.substr(0, p1); + respond_.http_ver = (ver == "HTTP/1.0") ? http::HttpVer::k1_0 : http::HttpVer::k1_1; + + size_t p2 = line.find(' ', p1 + 1); + std::string code = (p2 == std::string::npos) + ? line.substr(p1 + 1) + : line.substr(p1 + 1, p2 - p1 - 1); + + int c = 0; + try { c = std::stoi(code); } catch (...) { return false; } + respond_.status_code = static_cast(c); + return true; + } + + void parseHeaderLine(const std::string &line) { + size_t colon = line.find(':'); + if (colon == std::string::npos) return; + + std::string key = line.substr(0, colon); + std::string val = line.substr(colon + 1); + + auto vs = val.find_first_not_of(" \t"); + if (vs != std::string::npos) val = val.substr(vs); + while (!val.empty() && + (val.back() == ' ' || val.back() == '\t' || val.back() == '\r')) + val.pop_back(); + + respond_.headers[key] = val; + + // RFC 7230 §3.2: Header 字段名大小写不敏感 + std::string lower_key = key; + std::transform(lower_key.begin(), lower_key.end(), lower_key.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (lower_key == "content-length") { + try { content_length_ = std::stoll(val); } catch (...) {} + } + } + + static bool noBodyStatus(http::StatusCode sc) { + int c = static_cast(sc); + return (c >= 100 && c < 200) || c == 204 || c == 304; + } + + State state_ = State::kHeaders; + std::string buf_; + http::Respond respond_; + int64_t content_length_ = -1; +}; + +} // namespace client +} // namespace https +} // namespace tbox + +#endif // TBOX_HTTPS_RESPOND_PARSER_H_ diff --git a/modules/https/client/respond_parser_test.cpp b/modules/https/client/respond_parser_test.cpp new file mode 100644 index 00000000..af1fd2c4 --- /dev/null +++ b/modules/https/client/respond_parser_test.cpp @@ -0,0 +1,325 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include +#include + +#include "respond_parser.h" + +using namespace tbox::http; +using namespace tbox::https::client; + +namespace { + +// --------------------------------------------------------------------------- +// 辅助:按字节逐字节喂入,模拟分片到达 +// --------------------------------------------------------------------------- +void FeedByte(RespondParser &p, const char *data) { + for (size_t i = 0; data[i]; ++i) + p.feed(data + i, 1); +} + +// =========================================================================== +// 正常响应 +// =========================================================================== + +TEST(RespondParser, SimpleOk200) +{ + const char *raw = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"; + + RespondParser p; + p.feed(raw, ::strlen(raw)); + ASSERT_EQ(p.state(), RespondParser::State::kFinished); + + Respond r = p.takeRespond(); + EXPECT_EQ(r.http_ver, HttpVer::k1_1); + EXPECT_EQ(static_cast(r.status_code), 200); + EXPECT_EQ(r.headers["Content-Type"], "text/plain"); + EXPECT_EQ(r.headers["Content-Length"], "5"); + EXPECT_EQ(r.body, "hello"); +} + +TEST(RespondParser, Http10) +{ + const char *raw = + "HTTP/1.0 404 Not Found\r\n" + "Content-Length: 0\r\n" + "\r\n"; + + RespondParser p; + p.feed(raw, ::strlen(raw)); + ASSERT_EQ(p.state(), RespondParser::State::kFinished); + + Respond r = p.takeRespond(); + EXPECT_EQ(r.http_ver, HttpVer::k1_0); + EXPECT_EQ(static_cast(r.status_code), 404); + EXPECT_TRUE(r.body.empty()); +} + +TEST(RespondParser, StatusCode201) +{ + const char *raw = + "HTTP/1.1 201 Created\r\n" + "Content-Length: 0\r\n" + "\r\n"; + + RespondParser p; + p.feed(raw, ::strlen(raw)); + ASSERT_EQ(p.state(), RespondParser::State::kFinished); + EXPECT_EQ(static_cast(p.takeRespond().status_code), 201); +} + +// =========================================================================== +// 无正文状态码 +// =========================================================================== + +TEST(RespondParser, NoBody204) +{ + const char *raw = "HTTP/1.1 204 No Content\r\n\r\n"; + RespondParser p; + p.feed(raw, ::strlen(raw)); + ASSERT_EQ(p.state(), RespondParser::State::kFinished); + Respond r = p.takeRespond(); + EXPECT_EQ(static_cast(r.status_code), 204); + EXPECT_TRUE(r.body.empty()); +} + +TEST(RespondParser, NoBody304) +{ + const char *raw = + "HTTP/1.1 304 Not Modified\r\n" + "ETag: \"abc123\"\r\n" + "\r\n"; + RespondParser p; + p.feed(raw, ::strlen(raw)); + ASSERT_EQ(p.state(), RespondParser::State::kFinished); + Respond r = p.takeRespond(); + EXPECT_EQ(static_cast(r.status_code), 304); + EXPECT_TRUE(r.body.empty()); +} + +TEST(RespondParser, NoBody101) +{ + const char *raw = "HTTP/1.1 101 Switching Protocols\r\n\r\n"; + RespondParser p; + p.feed(raw, ::strlen(raw)); + ASSERT_EQ(p.state(), RespondParser::State::kFinished); + EXPECT_EQ(static_cast(p.takeRespond().status_code), 101); +} + +// =========================================================================== +// 分片到达 +// =========================================================================== + +TEST(RespondParser, FragmentedByByte) +{ + const char *raw = + "HTTP/1.1 200 OK\r\n" + "Content-Length: 4\r\n" + "\r\n" + "data"; + + RespondParser p; + FeedByte(p, raw); + ASSERT_EQ(p.state(), RespondParser::State::kFinished); + Respond r = p.takeRespond(); + EXPECT_EQ(static_cast(r.status_code), 200); + EXPECT_EQ(r.body, "data"); +} + +TEST(RespondParser, HeaderAndBodySplitAcrossFeeds) +{ + const char *part1 = "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n"; + const char *part2 = "\r\nab"; + const char *part3 = "c"; + + RespondParser p; + p.feed(part1, ::strlen(part1)); + EXPECT_EQ(p.state(), RespondParser::State::kHeaders); + p.feed(part2, ::strlen(part2)); + // body 只收到 "ab",还缺 1 字节 + EXPECT_EQ(p.state(), RespondParser::State::kBody); + p.feed(part3, ::strlen(part3)); + ASSERT_EQ(p.state(), RespondParser::State::kFinished); + EXPECT_EQ(p.takeRespond().body, "abc"); +} + +// =========================================================================== +// 多响应流水线 (pipelining) +// =========================================================================== + +TEST(RespondParser, TwoResponsesPipelined) +{ + const char *raw = + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "first" + "HTTP/1.1 200 OK\r\n" + "Content-Length: 6\r\n" + "\r\n" + "second"; + + RespondParser p; + p.feed(raw, ::strlen(raw)); + ASSERT_EQ(p.state(), RespondParser::State::kFinished); + + Respond r1 = p.takeRespond(); + EXPECT_EQ(r1.body, "first"); + + ASSERT_EQ(p.state(), RespondParser::State::kFinished); + Respond r2 = p.takeRespond(); + EXPECT_EQ(r2.body, "second"); + + EXPECT_NE(p.state(), RespondParser::State::kFinished); +} + +// =========================================================================== +// Header 解析细节 +// =========================================================================== + +TEST(RespondParser, HeaderValueTrimSpaces) +{ + const char *raw = + "HTTP/1.1 200 OK\r\n" + "X-Foo: bar baz \r\n" + "Content-Length: 0\r\n" + "\r\n"; + + RespondParser p; + p.feed(raw, ::strlen(raw)); + ASSERT_EQ(p.state(), RespondParser::State::kFinished); + Respond r = p.takeRespond(); + EXPECT_EQ(r.headers["X-Foo"], "bar baz"); +} + +TEST(RespondParser, MultipleHeaders) +{ + const char *raw = + "HTTP/1.1 200 OK\r\n" + "Content-Type: application/json\r\n" + "X-Request-Id: 12345\r\n" + "Content-Length: 2\r\n" + "\r\n" + "{}"; + + RespondParser p; + p.feed(raw, ::strlen(raw)); + ASSERT_EQ(p.state(), RespondParser::State::kFinished); + Respond r = p.takeRespond(); + EXPECT_EQ(r.headers["Content-Type"], "application/json"); + EXPECT_EQ(r.headers["X-Request-Id"], "12345"); + EXPECT_EQ(r.body, "{}"); +} + +// =========================================================================== +// 错误处理 +// =========================================================================== + +TEST(RespondParser, InvalidStatusLineFail) +{ + const char *raw = "GARBAGE\r\n\r\n"; + RespondParser p; + p.feed(raw, ::strlen(raw)); + EXPECT_EQ(p.state(), RespondParser::State::kFail); +} + +TEST(RespondParser, EmptyInputStaysInHeaders) +{ + RespondParser p; + p.feed("", 0); + EXPECT_EQ(p.state(), RespondParser::State::kHeaders); +} + +// =========================================================================== +// 无 Content-Length(视为空正文) +// =========================================================================== + +TEST(RespondParser, NoContentLengthHeader) +{ + const char *raw = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "\r\n"; + + RespondParser p; + p.feed(raw, ::strlen(raw)); + ASSERT_EQ(p.state(), RespondParser::State::kFinished); + Respond r = p.takeRespond(); + EXPECT_TRUE(r.body.empty()); +} + +// =========================================================================== +// Header 名大小写不敏感(RFC 7230 §3.2) +// =========================================================================== + +TEST(RespondParser, ContentLengthCaseInsensitive) +{ + // 全小写 content-length 应被正确识别 + const char *raw = + "HTTP/1.1 200 OK\r\n" + "content-type: text/plain\r\n" + "content-length: 5\r\n" + "\r\n" + "hello"; + + RespondParser p; + p.feed(raw, ::strlen(raw)); + ASSERT_EQ(p.state(), RespondParser::State::kFinished); + Respond r = p.takeRespond(); + EXPECT_EQ(r.body, "hello"); + EXPECT_EQ(r.headers["content-length"], "5"); +} + +TEST(RespondParser, ContentLengthUpperCase) +{ + // 全大写 CONTENT-LENGTH 也应被正确识别 + const char *raw = + "HTTP/1.1 200 OK\r\n" + "CONTENT-LENGTH: 4\r\n" + "\r\n" + "data"; + + RespondParser p; + p.feed(raw, ::strlen(raw)); + ASSERT_EQ(p.state(), RespondParser::State::kFinished); + EXPECT_EQ(p.takeRespond().body, "data"); +} + +// =========================================================================== +// kFail 状态隔离——后续喂数据不应崩溃 +// =========================================================================== + +TEST(RespondParser, FailStateIsolated) +{ + RespondParser p; + p.feed("GARBAGE\r\n\r\n", 11); + ASSERT_EQ(p.state(), RespondParser::State::kFail); + + // 在 kFail 状态下继续喂数据不应崩溃,状态保持 kFail + p.feed("more data", 9); + EXPECT_EQ(p.state(), RespondParser::State::kFail); +} + +} // namespace diff --git a/modules/https/https_server_client_test.cpp b/modules/https/https_server_client_test.cpp new file mode 100644 index 00000000..5c97ab23 --- /dev/null +++ b/modules/https/https_server_client_test.cpp @@ -0,0 +1,358 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +/** + * HTTPS 服务端 + 客户端集成测试。 + * + * 测试结构: + * 每个 TEST 在函数内创建独立的事件循环 (event::Loop)、TlsCtx、 + * HttpsServer、HttpsClient,完成交互后销毁,实现完全隔离。 + * + * TLS 证书: + * 服务端使用内嵌的自签证书(CN=localhost / SAN=127.0.0.1)。 + * 客户端关闭 verify_peer,避免 CA 验证依赖,仅测试 TLS 连通性。 + */ +#include + +#include +#include +#include +#include +#include +#include + +#include "server/https_server.h" +#include "client/https_client.h" +#include "test_certs.h" + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::network; +using namespace tbox::network::tls; +using namespace tbox::http; +using namespace tbox::https::server; +using namespace tbox::https::client; +using namespace tbox::https::test; + +namespace { + +// =========================================================================== +// 测试套件:HttpsServerClient +// =========================================================================== + +class HttpsServerClient : public ::testing::Test { + protected: + bool InitServerTls() { + srv_cert_ = TempFile(kTestCertPem); + srv_key_ = TempFile(kTestKeyPem); + if (srv_cert_.path.empty() || srv_key_.path.empty()) return false; + + TlsConfig cfg; + cfg.cert_file = srv_cert_.path; + cfg.key_file = srv_key_.path; + return srv_tls_.initialize(TlsMode::kServer, cfg); + } + + bool InitClientTls() { + TlsConfig cfg; + cfg.verify_peer = false; + return cli_tls_.initialize(TlsMode::kClient, cfg); + } + + Request MakeGet(const std::string &path) { + Request req; + req.method = Method::kGet; + req.url.path = path; + req.http_ver = HttpVer::k1_1; + req.headers["Host"] = "127.0.0.1"; + req.headers["Connection"] = "close"; + return req; + } + + Request MakePost(const std::string &path, const std::string &body) { + Request req; + req.method = Method::kPost; + req.url.path = path; + req.http_ver = HttpVer::k1_1; + req.body = body; + req.headers["Host"] = "127.0.0.1"; + req.headers["Content-Length"] = std::to_string(body.size()); + req.headers["Connection"] = "close"; + return req; + } + + void SetUp() override { + loop_ = Loop::New(); + ASSERT_TRUE(loop_); + + ASSERT_TRUE(InitServerTls()); + ASSERT_TRUE(InitClientTls()); + + server_ = new HttpsServer(loop_); + ASSERT_TRUE(server_->initializeTls(&srv_tls_)); + ASSERT_TRUE(server_->initialize(SockAddr::FromString("127.0.0.1:19443"), 5)); + + client_ = new HttpsClient(loop_); + ASSERT_TRUE(client_->initialize(SockAddr::FromString("127.0.0.1:19443"), &cli_tls_)); + + timeout_ev_ = loop_->newTimerEvent(); + timeout_ev_->initialize(std::chrono::seconds(3), Event::Mode::kOneshot); + timeout_ev_->setCallback([this] { + timed_out_ = true; + loop_->exitLoop(); + }); + } + + void TearDown() override { + if (client_) { client_->cleanup(); delete client_; client_ = nullptr; } + if (server_) { server_->stop(); server_->cleanup(); delete server_; server_ = nullptr; } + delete timeout_ev_; + delete loop_; + } + + void Run() { + server_->start(); + timeout_ev_->enable(); + loop_->runLoop(); + } + + Loop *loop_ = nullptr; + TlsCtx srv_tls_; + TlsCtx cli_tls_; + TempFile srv_cert_; + TempFile srv_key_; + HttpsServer *server_ = nullptr; + HttpsClient *client_ = nullptr; + TimerEvent *timeout_ev_ = nullptr; + bool timed_out_ = false; +}; + +// =========================================================================== +// 测试:GET 返回 200 + 固定正文 +// =========================================================================== + +TEST_F(HttpsServerClient, GetHelloWorld) +{ + server_->use([](HttpsContextSptr ctx, const HttpsNextFunc &) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().body = "Hello HTTPS!"; + ctx->res().headers["Content-Type"] = "text/plain"; + }); + + bool ok = false; + client_->request(MakeGet("/"), + [&](const Respond &res) { + EXPECT_EQ(static_cast(res.status_code), 200); + EXPECT_EQ(res.body, "Hello HTTPS!"); + ok = true; + loop_->exitLoop(); + } + ); + + Run(); + EXPECT_FALSE(timed_out_) << "Test timed out — TLS handshake or response failed"; + EXPECT_TRUE(ok); +} + +// =========================================================================== +// 测试:POST echo 请求正文 +// =========================================================================== + +TEST_F(HttpsServerClient, PostEcho) +{ + server_->use([](HttpsContextSptr ctx, const HttpsNextFunc &) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().body = ctx->req().body; + ctx->res().headers["Content-Type"] = "application/octet-stream"; + }); + + const std::string payload = "Echo me back!"; + bool ok = false; + + client_->request(MakePost("/echo", payload), + [&](const Respond &res) { + EXPECT_EQ(static_cast(res.status_code), 200); + EXPECT_EQ(res.body, payload); + ok = true; + loop_->exitLoop(); + } + ); + + Run(); + EXPECT_FALSE(timed_out_); + EXPECT_TRUE(ok); +} + +// =========================================================================== +// 测试:服务端返回 404 +// =========================================================================== + +TEST_F(HttpsServerClient, Returns404) +{ + server_->use([](HttpsContextSptr ctx, const HttpsNextFunc &) { + ctx->res().status_code = StatusCode::k404_NotFound; + ctx->res().body = "not found"; + }); + + bool ok = false; + client_->request(MakeGet("/missing"), + [&](const Respond &res) { + EXPECT_EQ(static_cast(res.status_code), 404); + ok = true; + loop_->exitLoop(); + } + ); + + Run(); + EXPECT_FALSE(timed_out_); + EXPECT_TRUE(ok); +} + +// =========================================================================== +// 测试:顺序发两个请求,按序返回 +// =========================================================================== + +TEST_F(HttpsServerClient, TwoSequentialRequests) +{ + server_->use([](HttpsContextSptr ctx, const HttpsNextFunc &) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().body = ctx->req().url.path; + }); + + int count = 0; + + // 第一个请求使用 keep-alive,保持连接供第二个请求复用 + Request req1; + req1.method = Method::kGet; + req1.url.path = "/first"; + req1.http_ver = HttpVer::k1_1; + req1.headers["Host"] = "127.0.0.1"; + // 不设置 Connection: close,允许连接复用 + + client_->request(req1, + [&](const Respond &res) { + EXPECT_EQ(res.body, "/first"); + ++count; + } + ); + + client_->request(MakeGet("/second"), // 最后一个请求才用 Connection: close + [&](const Respond &res) { + EXPECT_EQ(res.body, "/second"); + ++count; + loop_->exitLoop(); + } + ); + + Run(); + EXPECT_FALSE(timed_out_); + EXPECT_EQ(count, 2); +} + +// =========================================================================== +// 测试:Middleware 链(鉴权中间件演示) +// =========================================================================== + +class AuthMiddleware : public HttpsMiddleware { + public: + void handle(HttpsContextSptr ctx, const HttpsNextFunc &next) override { + if (ctx->req().headers.count("X-Token") && + ctx->req().headers.at("X-Token") == "secret") { + next(); + } else { + ctx->res().status_code = StatusCode::k401_Unauthorized; + ctx->res().body = "Unauthorized"; + } + } +}; + +TEST_F(HttpsServerClient, MiddlewareAuthReject) +{ + AuthMiddleware auth; + server_->use(&auth); + server_->use([](HttpsContextSptr ctx, const HttpsNextFunc &) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().body = "protected"; + }); + + bool ok = false; + client_->request(MakeGet("/protected"), // 无 X-Token 头 + [&](const Respond &res) { + EXPECT_EQ(static_cast(res.status_code), 401); + ok = true; + loop_->exitLoop(); + } + ); + + Run(); + EXPECT_FALSE(timed_out_); + EXPECT_TRUE(ok); +} + +TEST_F(HttpsServerClient, MiddlewareAuthPass) +{ + AuthMiddleware auth; + server_->use(&auth); + server_->use([](HttpsContextSptr ctx, const HttpsNextFunc &) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().body = "protected"; + }); + + Request req = MakeGet("/protected"); + req.headers["X-Token"] = "secret"; + + bool ok = false; + client_->request(req, + [&](const Respond &res) { + EXPECT_EQ(static_cast(res.status_code), 200); + EXPECT_EQ(res.body, "protected"); + ok = true; + loop_->exitLoop(); + } + ); + + Run(); + EXPECT_FALSE(timed_out_); + EXPECT_TRUE(ok); +} + +// =========================================================================== +// 测试:cleanup() 时正确触发 ErrorCallback +// =========================================================================== + +TEST_F(HttpsServerClient, ErrorCallbackCalledOnCleanup) +{ + // 不启动服务端,仅入队一个请求后立即 cleanup + // cleanup() 应对所有 pending 请求调用 ErrorCallback + bool error_called = false; + client_->request(MakeGet("/"), + /*res_cb=*/nullptr, + [&](const std::string &) { error_called = true; } + ); + + // 手动 cleanup,不需要跑事件循环 + client_->cleanup(); + delete client_; + client_ = nullptr; // 防止 TearDown 重复 cleanup + + EXPECT_TRUE(error_called); +} + +} // namespace diff --git a/modules/https/server/https_context.cpp b/modules/https/server/https_context.cpp new file mode 100644 index 00000000..47af4dd9 --- /dev/null +++ b/modules/https/server/https_context.cpp @@ -0,0 +1,66 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#undef MODULE_ID +#define MODULE_ID "tbox.https" + +#include "https_context.h" + +#include + +namespace tbox { +namespace https { +namespace server { + +using namespace http; + +struct HttpsContext::Data { + CommitFn commit_fn; + cabinet::Token conn_token; + int req_index; + + Request *sp_req; + Respond *sp_res; + /** + * 重点说明一下 sp_req 与 sp_res 的生命期: + * sp_req 由 HttpsServer::Impl 创建并通过构造函数传入,生命期移交给 HttpsContext。 + * sp_res 由 HttpsContext 在构造时创建,析构时通过 commit_fn 移交给 HttpsServer::Impl。 + */ +}; + +HttpsContext::HttpsContext(CommitFn fn, const cabinet::Token &ct, int req_index, Request *req) + : d_(new Data{ std::move(fn), ct, req_index, req, new Respond }) +{ + d_->sp_res->status_code = StatusCode::k404_NotFound; + d_->sp_res->http_ver = HttpVer::k1_1; +} + +HttpsContext::~HttpsContext() +{ + d_->commit_fn(d_->conn_token, d_->req_index, d_->sp_res); + CHECK_DELETE_RESET_OBJ(d_->sp_req); + CHECK_DELETE_RESET_OBJ(d_); +} + +Request& HttpsContext::req() const { return *(d_->sp_req); } +Respond& HttpsContext::res() const { return *(d_->sp_res); } + +} // namespace server +} // namespace https +} // namespace tbox diff --git a/modules/https/server/https_context.h b/modules/https/server/https_context.h new file mode 100644 index 00000000..bdbd967a --- /dev/null +++ b/modules/https/server/https_context.h @@ -0,0 +1,83 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_HTTPS_CONTEXT_H_20260324 +#define TBOX_HTTPS_CONTEXT_H_20260324 + +#include +#include + +#include +#include + +#include +#include +#include + +namespace tbox { +namespace https { +namespace server { + +/** + * HTTPS 请求上下文。 + * + * 与 http::server::Context 的公开接口完全一致(req() / res()), + * 但完全独立实现,不依赖 Server*:通过 CommitFn 回调提交响应, + * 由 HttpsServer::Impl 在创建时注入。 + */ +class HttpsContext { + public: + using CommitFn = std::function; + + HttpsContext(CommitFn commit_fn, + const cabinet::Token &ct, + int req_index, + http::Request *req); + ~HttpsContext(); + + NONCOPYABLE(HttpsContext); + + public: + http::Request& req() const; + http::Respond& res() const; + + private: + struct Data; + Data *d_; +}; + +using HttpsContextSptr = std::shared_ptr; +using HttpsNextFunc = std::function; +using HttpsRequestHandler = std::function; + +/** + * HTTPS 中间件基类。 + * 接口与 http::server::Middleware 完全对应,但使用 HttpsContextSptr。 + */ +class HttpsMiddleware { + public: + virtual ~HttpsMiddleware() {} + virtual void handle(HttpsContextSptr sp_ctx, const HttpsNextFunc &next) = 0; +}; + +} // namespace server +} // namespace https +} // namespace tbox + +#endif // TBOX_HTTPS_CONTEXT_H_20260324 diff --git a/modules/https/server/https_server.cpp b/modules/https/server/https_server.cpp new file mode 100644 index 00000000..57e8ce9b --- /dev/null +++ b/modules/https/server/https_server.cpp @@ -0,0 +1,352 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#undef MODULE_ID +#define MODULE_ID "tbox.https" + +#include "https_server.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace tbox { +namespace https { +namespace server { + +using namespace std; +using namespace std::placeholders; +using namespace network; +using namespace network::tls; +using namespace http; +using namespace http::server; + +// --------------------------------------------------------------------------- +// Internal implementation class +// --------------------------------------------------------------------------- + +struct HttpsServer::Impl { + explicit Impl(event::Loop *wp_loop) + : tls_server_(wp_loop) {} + + ~Impl() { + TBOX_ASSERT(cb_level_ == 0); + cleanup(); + } + + bool initializeTls(TlsCtx *tls_ctx) { + if (state_ != State::kNone) { + LogWarn("must call initializeTls() before initialize()"); + return false; + } + tls_ctx_ = tls_ctx; + return true; + } + + bool initialize(const network::SockAddr &bind_addr, int listen_backlog) { + if (tls_ctx_ == nullptr || !tls_ctx_->isValid()) { + LogErr("TLS context not configured — call initializeTls() first"); + return false; + } + + if (!tls_server_.initialize(bind_addr, listen_backlog, tls_ctx_)) + return false; + + tls_server_.setConnectedCallback(bind(&Impl::onNetConnected, this, _1)); + tls_server_.setReceiveCallback(bind(&Impl::onNetReceived, this, _1, _2), 0); + tls_server_.setSendCompleteCallback(bind(&Impl::onNetSendCompleted, this, _1)); + + state_ = State::kInited; + return true; + } + + bool start() { + if (tls_server_.start()) { + state_ = State::kRunning; + return true; + } + return false; + } + + void stop() { + if (state_ == State::kRunning) { + tls_server_.stop(); + state_ = State::kInited; + } + } + + void cleanup() { + if (state_ == State::kNone) + return; + + stop(); + req_handler_.clear(); + tls_server_.cleanup(); + state_ = State::kNone; + } + + State state() const { return state_; } + void setContextLogEnable(bool e) { context_log_enable_ = e; } + + void use(HttpsRequestHandler &&handler) { + req_handler_.push_back(std::move(handler)); + } + + void use(HttpsMiddleware *wp_middleware) { + req_handler_.push_back(bind(&HttpsMiddleware::handle, wp_middleware, _1, _2)); + } + + // ----------------------------------------------------------------------- + // commitRespond — called by Context::~Context() via CommitFn + // Mirrors Server::Impl::commitRespond() exactly, but uses tls_server_. + // ----------------------------------------------------------------------- + void commitRespond(const TlsTcpServer::ConnToken &ct, int index, Respond *res) { + RECORD_SCOPE(); + if (!tls_server_.isClientValid(ct)) { + delete res; + return; + } + + Connection *conn = static_cast(tls_server_.getContext(ct)); + TBOX_ASSERT(conn != nullptr); + + if (index == conn->res_index) { + { + const string &content = res->toString(); + tls_server_.send(ct, content.data(), content.size()); + delete res; + if (context_log_enable_) + LogDbg("RES: [%s]", content.c_str()); + } + + ++conn->res_index; + + if (conn->res_index > conn->close_index) + return; + + auto &res_buff = conn->res_buff; + auto iter = res_buff.find(conn->res_index); + + while (iter != res_buff.end()) { + Respond *queued = iter->second; + { + const string &content = queued->toString(); + tls_server_.send(ct, content.data(), content.size()); + delete queued; + if (context_log_enable_) + LogDbg("RES: [%s]", content.c_str()); + } + + res_buff.erase(iter); + ++conn->res_index; + + if (conn->res_index > conn->close_index) + return; + + iter = res_buff.find(conn->res_index); + } + } else { + conn->res_buff[index] = res; + } + } + + private: + // ----------------------------------------------------------------------- + // Per-connection HTTP parse state + // ----------------------------------------------------------------------- + struct Connection { + RequestParser req_parser; + int req_index = 0; + int res_index = 0; + int close_index = numeric_limits::max(); + map res_buff; + + ~Connection() { + for (auto &item : res_buff) + delete item.second; + } + }; + + // ----------------------------------------------------------------------- + // Network callbacks + // ----------------------------------------------------------------------- + + void onNetConnected(const TlsTcpServer::ConnToken &ct) { + RECORD_SCOPE(); + auto *conn = new Connection; + tls_server_.setContext(ct, conn, [](void *ptr) { + delete static_cast(ptr); + }); + } + + static bool IsLastRequest(const Request *req) { + auto iter = req->headers.find("Connection"); + if (req->http_ver == HttpVer::k1_0) { + if (iter == req->headers.end()) + return true; + return iter->second.find("keep-alive") == string::npos; + } else { + if (iter == req->headers.end()) + return false; + return iter->second.find("close") != string::npos; + } + } + + void onNetReceived(const TlsTcpServer::ConnToken &ct, util::Buffer &buff) { + RECORD_SCOPE(); + Connection *conn = static_cast(tls_server_.getContext(ct)); + TBOX_ASSERT(conn != nullptr); + + if (conn->close_index != numeric_limits::max()) { + buff.hasReadAll(); + LogWarn("should not recv any data after close mark"); + return; + } + + while (buff.readableSize() > 0) { + size_t rsize = conn->req_parser.parse(buff.readableBegin(), buff.readableSize()); + buff.hasRead(rsize); + + if (conn->req_parser.state() == RequestParser::State::kFinishedAll) { + Request *req = conn->req_parser.getRequest(); + + if (context_log_enable_) + LogDbg("REQ: [%s]", req->toString().c_str()); + + if (IsLastRequest(req)) { + conn->close_index = conn->req_index; + LogDbg("mark close at %d", conn->close_index); + tls_server_.shutdown(ct, SHUT_RD); + } + + // CommitFn constructor decouples HttpsServer from Server* + int req_idx = conn->req_index++; + auto commit_fn = [this, ct](const cabinet::Token &ct2, int idx, Respond *res) { + commitRespond(ct2, idx, res); + }; + auto sp_ctx = make_shared(std::move(commit_fn), ct, req_idx, req); + + handle(sp_ctx, 0); + + } else if (conn->req_parser.state() == RequestParser::State::kFail) { + LogNotice("HTTP parse from %s failed", + tls_server_.getClientAddress(ct).toString().c_str()); + tls_server_.disconnect(ct); + break; + } else { + break; + } + } + } + + void onNetSendCompleted(const TlsTcpServer::ConnToken &ct) { + RECORD_SCOPE(); + if (!tls_server_.isClientValid(ct)) + return; + + Connection *conn = static_cast(tls_server_.getContext(ct)); + TBOX_ASSERT(conn != nullptr); + + if (conn->res_index > conn->close_index) + tls_server_.disconnect(ct); + } + + void handle(HttpsContextSptr sp_ctx, size_t cb_index) { + RECORD_SCOPE(); + if (cb_index >= req_handler_.size()) + return; + + auto func = req_handler_.at(cb_index); + ++cb_level_; + if (func) + func(sp_ctx, bind(&Impl::handle, this, sp_ctx, cb_index + 1)); + --cb_level_; + } + + // ----------------------------------------------------------------------- + // Members + // ----------------------------------------------------------------------- + + TlsTcpServer tls_server_; + TlsCtx *tls_ctx_ = nullptr; // borrowed + vector req_handler_; + State state_ = State::kNone; + bool context_log_enable_ = false; + int cb_level_ = 0; +}; + +// --------------------------------------------------------------------------- +// HttpsServer public API +// --------------------------------------------------------------------------- + +HttpsServer::HttpsServer(event::Loop *wp_loop) + : impl_(new Impl(wp_loop)) {} + +HttpsServer::~HttpsServer() { + delete impl_; +} + +bool HttpsServer::initializeTls(network::tls::TlsCtx *tls_ctx) { + return impl_->initializeTls(tls_ctx); +} + +bool HttpsServer::initialize(const network::SockAddr &bind_addr, int listen_backlog) { + return impl_->initialize(bind_addr, listen_backlog); +} + +bool HttpsServer::start() { + return impl_->start(); +} + +void HttpsServer::stop() { + impl_->stop(); +} + +void HttpsServer::cleanup() { + impl_->cleanup(); +} + +HttpsServer::State HttpsServer::state() const { + return impl_->state(); +} + +void HttpsServer::setContextLogEnable(bool enable) { + impl_->setContextLogEnable(enable); +} + +void HttpsServer::use(HttpsRequestHandler &&handler) { + impl_->use(std::move(handler)); +} + +void HttpsServer::use(HttpsMiddleware *wp_middleware) { + impl_->use(wp_middleware); +} + +} // namespace server +} // namespace https +} // namespace tbox diff --git a/modules/https/server/https_server.h b/modules/https/server/https_server.h new file mode 100644 index 00000000..2be54ff7 --- /dev/null +++ b/modules/https/server/https_server.h @@ -0,0 +1,92 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_HTTPS_SERVER_H_20260324 +#define TBOX_HTTPS_SERVER_H_20260324 + +#include +#include +#include + +#include +#include +#include +#include "https_context.h" + +namespace tbox { +namespace https { +namespace server { + +/** + * HTTPS server — HTTP over TLS. + * + * The public interface is identical to Server, with the addition of + * initializeTls() which must be called before initialize(). + * + * Usage: + * @code + * network::tls::TlsCtx tls_ctx; + * network::tls::TlsConfig cfg; + * cfg.cert_file = "/etc/ssl/server.crt"; + * cfg.key_file = "/etc/ssl/server.key"; + * tls_ctx.initialize(network::tls::TlsMode::kServer, cfg); + * + * tbox::https::server::HttpsServer server(wp_loop); + * server.initializeTls(&tls_ctx); + * server.initialize(SockAddr::MakeIPv4("0.0.0.0", 443), 10); + * server.use([](auto ctx, auto next) { + * ctx->res().body = "Hello HTTPS!"; + * }); + * server.start(); + * @endcode + */ +class HttpsServer { + public: + explicit HttpsServer(event::Loop *wp_loop); + virtual ~HttpsServer(); + + /** + * Configure TLS before calling initialize(). + * @param tls_ctx TLS context (borrowed — must outlive this server) + */ + bool initializeTls(network::tls::TlsCtx *tls_ctx); + + bool initialize(const network::SockAddr &bind_addr, int listen_backlog); + bool start(); + void stop(); + void cleanup(); + + enum class State { kNone, kInited, kRunning }; + State state() const; + + void setContextLogEnable(bool enable); + + void use(HttpsRequestHandler &&handler); + void use(HttpsMiddleware *wp_middleware); + + private: + struct Impl; + Impl *impl_ = nullptr; +}; + +} // namespace server +} // namespace https +} // namespace tbox + +#endif // TBOX_HTTPS_SERVER_H_20260324 diff --git a/modules/https/test_certs.h b/modules/https/test_certs.h new file mode 100644 index 00000000..c080e696 --- /dev/null +++ b/modules/https/test_certs.h @@ -0,0 +1,140 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_HTTPS_TEST_CERTS_H_ +#define TBOX_HTTPS_TEST_CERTS_H_ + +/** + * 自签 RSA-2048 证书,CN=localhost / SAN=127.0.0.1,有效期 10 年。 + * 仅供单元测试使用,不要用于生产环境。 + * + * 生成命令(供参考,已嵌入此文件): + * openssl req -x509 -newkey rsa:2048 -keyout k.pem -out c.pem \ + * -days 3650 -nodes -subj "/CN=localhost" \ + * -addext "subjectAltName=IP:127.0.0.1" + */ + +#include +#include +#include +#include + +namespace tbox { +namespace https { +namespace test { + +// clang-format off + +static const char kTestCertPem[] = +"-----BEGIN CERTIFICATE-----\n" +"MIIDGjCCAgKgAwIBAgIUMh7qt6WtWAIyi0COEOMpbiNuZzowDQYJKoZIhvcNAQEL\n" +"BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDMyMzA4MDY0N1oXDTM2MDMy\n" +"MDA4MDY0N1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF\n" +"AAOCAQ8AMIIBCgKCAQEAyTbcU0UXzferuTMekYrYIxveWcu9L3sHKA2PHDePhW5M\n" +"asrLRlcSvCAhsvmDxvQq5+omZAnI2v4qqZ+lAaBPXYV61Et2K1B+4gJ5BFz95ll1\n" +"ZDWYubsvzdMzIwmddSyedXEtkvcm6PO5PvD4c8k9bGlswyPn22nISJRZi3Vn/L4a\n" +"7KuIH7mvJ63s6XoRRbKfIgRs/s11VqxcnCVd4fPILXrZ5HlOizvJNssN7IV82ZDU\n" +"j6N/bVDqnuYTBWaKQyJjJoHtHk99Nfvqtbje5ie1vq2bmXPwPo6kh78ZFfZIpXZV\n" +"0360T6TkdHH66vx5ZKmKpKyA+GZujxZWqsfC91APrwIDAQABo2QwYjAdBgNVHQ4E\n" +"FgQUSTTStD7J7Rma2t2alkCxDxjJYscwHwYDVR0jBBgwFoAUSTTStD7J7Rma2t2a\n" +"lkCxDxjJYscwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHREECDAGhwR/AAABMA0GCSqG\n" +"SIb3DQEBCwUAA4IBAQCxFMmAvpAvoFE3Is1TowNvY17HLzt+0/BAVGT4EvikeoQR\n" +"z5vLoBhA5ohzrSjlW2GZ2fBYCh9yOHkXpMAbiDOcRnawMjyYVQzQy0PzGrvAZW9+\n" +"0/h0XU+2HJkmxFx/sQCxx7tmCnOfanFn+ml+vCvEXUbXXC8ozPPm2AOPjRjXYliK\n" +"e/0V6zfoIUv5nDzY0npqnu+4rAFiGTRV175sWCqEEEYJkgObvFjHDboyNOmTQqIz\n" +"9uWheYHCTOGAvektun3p11yob1RB0WEKo9+8KYl+hvgjMLNunZcq38HNUOhB8jh2\n" +"ke7HM3eqSCNrs+4CQYqmauepYc92WGFpEda7L2zx\n" +"-----END CERTIFICATE-----\n"; + +static const char kTestKeyPem[] = +"-----BEGIN PRIVATE KEY-----\n" +"MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJNtxTRRfN96u5\n" +"Mx6RitgjG95Zy70vewcoDY8cN4+FbkxqystGVxK8ICGy+YPG9Crn6iZkCcja/iqp\n" +"n6UBoE9dhXrUS3YrUH7iAnkEXP3mWXVkNZi5uy/N0zMjCZ11LJ51cS2S9ybo87k+\n" +"8PhzyT1saWzDI+fbachIlFmLdWf8vhrsq4gfua8nrezpehFFsp8iBGz+zXVWrFyc\n" +"JV3h88gtetnkeU6LO8k2yw3shXzZkNSPo39tUOqe5hMFZopDImMmge0eT301++q1\n" +"uN7mJ7W+rZuZc/A+jqSHvxkV9kildlXTfrRPpOR0cfrq/HlkqYqkrID4Zm6PFlaq\n" +"x8L3UA+vAgMBAAECggEAEO0dgZ+5Tek8t9qWArZEUkfA35rk8j6OLo9db4k4+Id7\n" +"xCwFB4jBmbO2QgD9VdoqVdH7osSz8cAZxKUaU1Wx93MIDx299gzSb563oWdVMHBs\n" +"EJ71lwIpFk5i6dqgNUAooEaCB+/khQojlijdLZhLI3iG3q+BjJKMd5aLJdEdi0Qo\n" +"brYTdK/dDx3yzQxyK64kc8s7R74dIys5nKu4+cYoqgmjisY2PwqW2xNdqMbX0b9e\n" +"2QhFHy5+xdNyFhuGUARgD37eUU/qhg4OWfir203nCNLSgz2WVsDFPSVWlqsOekxH\n" +"uzRurE4RvkQc3LtegW4KRppULUQrjf7JnftvDAaaMQKBgQDwMvVJlNCpECGCovtb\n" +"/G7r+bmjrPjfPpLGUyGWrY6BnFl6wvCT6qciCjjOkQE7H4+nysXWr5KelOjF/58y\n" +"g494wZZ6VBY0hT+YPWr9yPyq8+2teQcD9F3pLrN8YA3ObAMGDbX3CqNmy1CYaTxm\n" +"4KmeHmtNgDI0N+m7qfno2YipiQKBgQDWc2J3b/Z0nB8/pcUAac0kshTfAZ4fcBQq\n" +"DGaQM6V50qMWBaM+Kvd5FDqK8rv/kyEMblnGTLyUvg6UOq/fjC8fJrL9w3rP9Xrg\n" +"GPh9+ZAp0uHzsi63/MbnvobpSGH+LZ7d76lavsWt8C8H6wJfGPCpBgSFE2wlELck\n" +"I3FZywv5dwKBgQCCYcbPmB3jh0QJW1rBxbaYFMf11pCI7bhSOxHCbpcqN6pCfsqE\n" +"IB101sObLQ7T/v/FfsYBEPCvb/kicO0DSHJ6g+qgoEAlZibtBnmrJIwyZ5IeVdG/\n" +"DchkKNt4qdMUt4C0qoCZhobH55jqAkWtOkoX8D8ipHGb8rXHDi7/fAU4sQKBgHWO\n" +"rw22TK5D30Vuw/2kAhb5oENXiazGLeeXAKpQBYgwlcI+uOwddafkFOuSgMhriRRd\n" +"cc0ox7/qJ+fN/BdZq4MyHbDKdgqGESPDzISSSBsFRWPn64Bki00CvsYnLcC+lXYo\n" +"KPhb19Wv8rgudhBXhaXCbLvel8wBy8N9wmdszVWlAoGAZEOhzRT55yv0AGGR2G4+\n" +"tZ7wWnkLys7Wk1SlAiWv4H2k26X/uGHnH5ZxFcDlN16VXancdvcTLRIvybunsCl6\n" +"J7l1dKDySU0YIW9NXogMNX1/iERq1VZMrxQrrBYf2APuyyyaOrb5ZlgBLlPe+QhB\n" +"y9P9rlAp4jNuoiMEX/2XlJA=\n" +"-----END PRIVATE KEY-----\n"; + +// clang-format on + +/** + * 将 PEM 字符串写入临时文件,返回路径。 + * 测试结束后应 unlink 该路径(或使用 RAII TempFile 辅助类)。 + */ + +struct TempFile { + std::string path; + TempFile() = default; + explicit TempFile(const char *content) { + if (content == nullptr) return; + char tmpl[] = "/tmp/tbox_tls_test_XXXXXX"; + int fd = ::mkstemp(tmpl); + if (fd >= 0) { + path = tmpl; + ssize_t n = ::write(fd, content, ::strlen(content)); + (void)n; // best-effort write to temp file + ::close(fd); + } + } + + ~TempFile() { + if (!path.empty()) ::unlink(path.c_str()); + } + + // movable, non-copyable + TempFile(TempFile &&o) noexcept : path(std::move(o.path)) { o.path.clear(); } + TempFile &operator=(TempFile &&o) noexcept { + if (this != &o) { + if (!path.empty()) ::unlink(path.c_str()); + path = std::move(o.path); + o.path.clear(); + } + return *this; + } + + TempFile(const TempFile &) = delete; + TempFile &operator=(const TempFile &) = delete; +}; + +} // namespace test +} // namespace https +} // namespace tbox + +#endif // TBOX_HTTPS_TEST_CERTS_H_ -- Gitee From 46affece48ac035b01c348745bb7b496c888cc46 Mon Sep 17 00:00:00 2001 From: duanyh <19517952495@139.com> Date: Tue, 14 Apr 2026 19:27:34 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat(https):=20=E6=96=B0=E5=A2=9EHTTPS?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF/=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=E7=A8=8B=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 基础示例: - server/simple: 最简 HTTPS 服务端(支持 mTLS 与自定义加密套件) - client/simple: 最简 HTTPS 客户端(支持 CA 校验与 mTLS) - server/data_encrypt: 服务端应用层加密响应(AES-256-GCM / ChaCha20-Poly1305) - client/data_encrypt: 客户端解密应用层加密响应,演示双重加密 进阶示例: - server/async_respond: 异步响应三种模式(立即 / 定时器 / 线程池) - server/router: 路由注册 + 中间件(AccessLogMiddleware)演示 --- examples/https/Makefile | 26 ++ examples/https/client/Makefile | 26 ++ examples/https/client/data_encrypt/Makefile | 40 +++ examples/https/client/data_encrypt/simple.cpp | 215 ++++++++++++++ examples/https/client/simple/Makefile | 39 +++ examples/https/client/simple/simple.cpp | 183 ++++++++++++ examples/https/server/Makefile | 26 ++ examples/https/server/async_respond/Makefile | 39 +++ .../server/async_respond/async_respond.cpp | 236 ++++++++++++++++ examples/https/server/data_encrypt/Makefile | 40 +++ examples/https/server/data_encrypt/simple.cpp | 208 ++++++++++++++ examples/https/server/router/Makefile | 39 +++ examples/https/server/router/router.cpp | 192 +++++++++++++ examples/https/server/router/webpage.cpp | 263 ++++++++++++++++++ examples/https/server/simple/Makefile | 39 +++ examples/https/server/simple/simple.cpp | 162 +++++++++++ 16 files changed, 1773 insertions(+) create mode 100644 examples/https/Makefile create mode 100644 examples/https/client/Makefile create mode 100644 examples/https/client/data_encrypt/Makefile create mode 100644 examples/https/client/data_encrypt/simple.cpp create mode 100644 examples/https/client/simple/Makefile create mode 100644 examples/https/client/simple/simple.cpp create mode 100644 examples/https/server/Makefile create mode 100644 examples/https/server/async_respond/Makefile create mode 100644 examples/https/server/async_respond/async_respond.cpp create mode 100644 examples/https/server/data_encrypt/Makefile create mode 100644 examples/https/server/data_encrypt/simple.cpp create mode 100644 examples/https/server/router/Makefile create mode 100644 examples/https/server/router/router.cpp create mode 100644 examples/https/server/router/webpage.cpp create mode 100644 examples/https/server/simple/Makefile create mode 100644 examples/https/server/simple/simple.cpp diff --git a/examples/https/Makefile b/examples/https/Makefile new file mode 100644 index 00000000..a0b7a8a0 --- /dev/null +++ b/examples/https/Makefile @@ -0,0 +1,26 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +all test clean distclean: + @for i in $(shell ls) ; do \ + if [ -d $$i ]; then \ + $(MAKE) -C $$i $@ || exit $$? ; \ + fi \ + done diff --git a/examples/https/client/Makefile b/examples/https/client/Makefile new file mode 100644 index 00000000..a0b7a8a0 --- /dev/null +++ b/examples/https/client/Makefile @@ -0,0 +1,26 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +all test clean distclean: + @for i in $(shell ls) ; do \ + if [ -d $$i ]; then \ + $(MAKE) -C $$i $@ || exit $$? ; \ + fi \ + done diff --git a/examples/https/client/data_encrypt/Makefile b/examples/https/client/data_encrypt/Makefile new file mode 100644 index 00000000..7649f311 --- /dev/null +++ b/examples/https/client/data_encrypt/Makefile @@ -0,0 +1,40 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT := examples/https/client/data_encrypt +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := simple.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_https \ + -ltbox_http \ + -ltbox_network \ + -ltbox_crypto \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_log \ + -ltbox_util \ + -ltbox_base \ + -lssl -lcrypto \ + -lpthread -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/https/client/data_encrypt/simple.cpp b/examples/https/client/data_encrypt/simple.cpp new file mode 100644 index 00000000..fb1acca9 --- /dev/null +++ b/examples/https/client/data_encrypt/simple.cpp @@ -0,0 +1,215 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +/** + * HTTPS 客户端 —— 应用层数据加密示例 + * + * 演示向 data_encrypt/server 发起 HTTPS 请求,并解密响应正文: + * 外层:TLS(传输层)负责通信链路加密 + * 内层:AES-256-GCM / ChaCha20-Poly1305(应用层)负责业务数据解密 + * + * 响应正文格式(二进制): + * [ "[ENCRYPTED]" 标记 (11 bytes) ][ 密文 ][ GCM/Poly1305 认证标签 (16 bytes) ] + * + * 用法: + * client [path] + * + * 示例(与 server/data_encrypt 配套使用): + * KEY_HEX=$(openssl rand -hex 32) + * IV_HEX=$(openssl rand -hex 12) + * # 先启动服务端 + * ./server 127.0.0.1:18443 server.crt server.key aes256gcm $KEY_HEX $IV_HEX + * # 再发起客户端请求 + * ./client 127.0.0.1:18443 aes256gcm $KEY_HEX $IV_HEX / + */ +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::network; +using namespace tbox::network::tls; +using namespace tbox::crypto; +using namespace tbox::http; +using namespace tbox::https::client; + +// clang-format off +int main(int argc, char **argv) +{ + LogOutput_Enable(); + LogInfo("HTTPS client with data encryption example"); + LogInfo("Usage: %s [path]", argv[0]); + LogInfo(" server_ip:port - HTTPS server address"); + LogInfo(" algo - aes256gcm | chacha20poly1305"); + LogInfo(" key_hex - 256-bit key as 64 hex chars"); + LogInfo(" iv_hex - 96-bit IV as 24 hex chars"); + LogInfo(" path - request path (default: /)"); + + if (argc < 5) { + LogErr("Too few arguments, need at least 4"); + return 1; + } + + std::string server_addr = argv[1]; + std::string algo_str = argv[2]; + std::string key_hex = argv[3]; + std::string iv_hex = argv[4]; + std::string req_path = (argc >= 6) ? argv[5] : "/"; + + LogInfo("server: %s, algo: %s, path: %s", + server_addr.c_str(), algo_str.c_str(), req_path.c_str()); + + auto sp_loop = Loop::New(); + auto sp_sig_event = sp_loop->newSignalEvent(); + + SetScopeExitAction([=] { + delete sp_sig_event; + delete sp_loop; + }); + + // ------------------------------------------------------------------------- + // TLS 配置(跳过证书校验,便于测试自签名证书) + // ------------------------------------------------------------------------- + TlsCtx tls_ctx; + TlsConfig tls_cfg; + tls_cfg.verify_peer = false; + + if (!tls_ctx.initialize(TlsMode::kClient, tls_cfg)) { + LogErr("init client tls fail"); + return 1; + } + + // ------------------------------------------------------------------------- + // 应用层对称解密配置 + // ------------------------------------------------------------------------- + CryptoCryptoCtx crypto; + CryptoConfig crypto_cfg; + + if (algo_str == "aes256gcm") + crypto_cfg.algo = CryptoAlgorithm::kAES256GCM; + else if (algo_str == "chacha20poly1305") + crypto_cfg.algo = CryptoAlgorithm::kChaCha20Poly1305; + else { + LogErr("unsupported algo: %s (aes256gcm | chacha20poly1305)", algo_str.c_str()); + return 1; + } + + crypto_cfg.key_hex = key_hex; + crypto_cfg.iv_hex = iv_hex; + + if (!crypto.initialize(crypto_cfg)) { + LogErr("init crypto fail"); + return 1; + } + + // ------------------------------------------------------------------------- + // HTTPS 客户端初始化 + // ------------------------------------------------------------------------- + HttpsClient cli(sp_loop); + if (!cli.initialize(SockAddr::FromString(server_addr), &tls_ctx)) { + LogErr("init https client fail"); + return 1; + } + + // ------------------------------------------------------------------------- + // 构造请求 + // ------------------------------------------------------------------------- + Request req; + req.method = Method::kGet; + req.url.path = req_path; + req.http_ver = HttpVer::k1_1; + req.headers["Host"] = server_addr; + req.headers["Connection"] = "close"; + + bool done = false; + + cli.request( + req, + // ----------------------------------------------------------------------- + // 响应回调:解析双层加密正文 + // + // 响应正文格式: + // [ "[ENCRYPTED]" (11 bytes) ][ 密文 ][ 认证标签 (16 bytes) ] + // ----------------------------------------------------------------------- + [&](const Respond &res) { + LogInfo("status: %d", static_cast(res.status_code)); + LogInfo("body size: %zu bytes", res.body.size()); + + const std::string marker = "[ENCRYPTED]"; + if (res.body.compare(0, marker.size(), marker) == 0) { + // 有效的加密正文 + const size_t cipher_start = marker.size(); + const size_t tag_offset = res.body.size() - 16; + + if (tag_offset > cipher_start) { + const auto *ciphertext = reinterpret_cast(res.body.data() + cipher_start); + const size_t ciphertext_len = tag_offset - cipher_start; + const auto *tag = reinterpret_cast(res.body.data() + tag_offset); + + auto dec = crypto.decrypt(ciphertext, ciphertext_len, tag, 16); + if (dec.success) { + LogInfo("decrypted: %.*s", + static_cast(dec.plaintext.size()), + reinterpret_cast(dec.plaintext.data())); + } else { + LogErr("decrypt fail: %s", dec.error_message.c_str()); + } + } else { + LogErr("invalid encrypted body: too short (size=%zu)", res.body.size()); + } + } else { + // 未加密的普通响应 + LogInfo("response (plaintext): %s", res.body.c_str()); + } + + done = true; + sp_loop->exitLoop(); + }, + // ----------------------------------------------------------------------- + // 错误回调 + // ----------------------------------------------------------------------- + [&](const std::string &reason) { + LogErr("request fail: %s", reason.c_str()); + done = true; + sp_loop->exitLoop(); + } + ); + + sp_sig_event->initialize(SIGINT, Event::Mode::kPersist); + sp_sig_event->setCallback([&](int) { + LogInfo("SIGINT received, stopping..."); + done = true; + sp_loop->exitLoop(); + }); + sp_sig_event->enable(); + + sp_loop->runLoop(); + cli.cleanup(); + + LogInfo("HTTPS client with data encryption example %s", done ? "exit" : "aborted"); + return 0; +} +// clang-format on diff --git a/examples/https/client/simple/Makefile b/examples/https/client/simple/Makefile new file mode 100644 index 00000000..6c8ade7d --- /dev/null +++ b/examples/https/client/simple/Makefile @@ -0,0 +1,39 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT := examples/https/client/simple +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := simple.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_https \ + -ltbox_http \ + -ltbox_network \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_log \ + -ltbox_util \ + -ltbox_base \ + -lssl -lcrypto \ + -lpthread -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/https/client/simple/simple.cpp b/examples/https/client/simple/simple.cpp new file mode 100644 index 00000000..b0b7ee7b --- /dev/null +++ b/examples/https/client/simple/simple.cpp @@ -0,0 +1,183 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +/** + * HTTPS 客户端 —— 最简示例 + * + * 演示最基本的 HTTPS 请求:TLS 握手 + GET 请求 + 打印响应。 + * 支持 CA 校验、mTLS(双向证书认证)、自定义安全等级和加密套件。 + * + * 用法: + * client [server_ip:port] [path] [ca_file] [client_cert] [client_key] [level] [cipher_list] [ciphersuites] + * + * 快速测试(-k 等效于跳过证书校验,本示例默认也跳过): + * ./client 127.0.0.1:12345 / + */ +#include +#include +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::network; +using namespace tbox::network::tls; +using namespace tbox::http; +using namespace tbox::https::client; + +// clang-format off +int main(int argc, char **argv) +{ + std::string server_addr = "127.0.0.1:12345"; + std::string path = "/"; + std::string ca_file; // 若指定则校验服务端证书 + std::string client_cert; // mTLS: 客户端证书 + std::string client_key; // mTLS: 客户端私钥 + std::string level_str; // 安全等级: default / compatible / strict + std::string cipher_list; // TLS1.2 及以下加密套件(覆盖 level) + std::string ciphersuites; // TLS1.3 加密套件(覆盖 level) + + if (argc >= 2) server_addr = argv[1]; + if (argc >= 3) path = argv[2]; + if (argc >= 4) ca_file = argv[3]; + if (argc >= 5) client_cert = argv[4]; + if (argc >= 6) client_key = argv[5]; + if (argc >= 7) level_str = argv[6]; + if (argc >= 8) cipher_list = argv[7]; + if (argc >= 9) ciphersuites = argv[8]; + + TlsSecurityLevel sec_level = TlsSecurityLevel::kDefault; + if (level_str == "compatible") + sec_level = TlsSecurityLevel::kCompatible; + else if (level_str == "strict") + sec_level = TlsSecurityLevel::kStrict; + + LogOutput_Enable(); + LogInfo("HTTPS client example enter"); + LogInfo("Usage: %s [server_ip:port] [path] [ca_file] [client_cert] [client_key] [level] [cipher_list] [ciphersuites]", argv[0]); + LogInfo(" server_ip:port - server address (default: 127.0.0.1:12345)"); + LogInfo(" path - HTTP request path (default: /)"); + LogInfo(" ca_file - CA cert to verify server (enables verify_peer)"); + LogInfo(" client_cert - client PEM certificate for mTLS"); + LogInfo(" client_key - client PEM private key for mTLS"); + LogInfo(" level - cipher preset: default / compatible / strict (optional)"); + LogInfo(" cipher_list - OpenSSL TLS<=1.2 cipher list, overrides level (optional)"); + LogInfo(" ciphersuites - OpenSSL TLS1.3 ciphersuites, overrides level (optional)"); + + if (!ca_file.empty()) + LogInfo("server verification enabled, CA: %s", ca_file.c_str()); + + if (!client_cert.empty()) + LogInfo("mTLS mode: client cert: %s, key: %s", client_cert.c_str(), client_key.c_str()); + + if (sec_level != TlsSecurityLevel::kDefault) + LogInfo("security level preset: %s", level_str.c_str()); + + if (!cipher_list.empty()) + LogInfo("TLS cipher_list override: %s", cipher_list.c_str()); + + if (!ciphersuites.empty()) + LogInfo("TLS ciphersuites override: %s", ciphersuites.c_str()); + + auto sp_loop = Loop::New(); + auto sp_sig_event = sp_loop->newSignalEvent(); + + SetScopeExitAction([=] { + delete sp_sig_event; + delete sp_loop; + }); + + TlsCtx tls_ctx; + TlsConfig tls_cfg; + if (!ca_file.empty()) { + tls_cfg.ca_file = ca_file; + tls_cfg.verify_peer = true; // 校验服务端证书 + } else { + tls_cfg.verify_peer = false; // 未指定 CA 时跳过校验(兼容自签名证书) + } + + // mTLS: 若提供客户端证书和私钥则加载 + if (!client_cert.empty()) + tls_cfg.cert_file = client_cert; + + if (!client_key.empty()) + tls_cfg.key_file = client_key; + + tls_cfg.security_level = sec_level; + + if (!cipher_list.empty()) + tls_cfg.cipher_list = cipher_list; + + if (!ciphersuites.empty()) + tls_cfg.ciphersuites = ciphersuites; + + if (!tls_ctx.initialize(TlsMode::kClient, tls_cfg)) { + LogErr("init client tls fail"); + return 1; + } + + HttpsClient cli(sp_loop); + if (!cli.initialize(SockAddr::FromString(server_addr), &tls_ctx)) { + LogErr("init https client fail, server: %s", server_addr.c_str()); + return 1; + } + + Request req; + req.method = Method::kGet; + req.url.path = path; + req.http_ver = HttpVer::k1_1; + req.headers["Host"] = server_addr; + req.headers["Connection"] = "close"; + + bool done = false; + + cli.request( + req, + [&](const Respond &res) { + LogInfo("status: %d", static_cast(res.status_code)); + LogInfo("body: %s", res.body.c_str()); + done = true; + sp_loop->exitLoop(); + }, + [&](const std::string &reason) { + LogErr("request fail: %s", reason.c_str()); + done = true; + sp_loop->exitLoop(); + } + ); + + sp_sig_event->initialize(SIGINT, Event::Mode::kPersist); + sp_sig_event->setCallback([&](int) { + LogInfo("SIGINT received, stopping..."); + done = true; + sp_loop->exitLoop(); + }); + sp_sig_event->enable(); + + sp_loop->runLoop(); + cli.cleanup(); + + LogInfo("HTTPS client example %s", done ? "exit" : "aborted"); + return 0; +} +// clang-format on diff --git a/examples/https/server/Makefile b/examples/https/server/Makefile new file mode 100644 index 00000000..a0b7a8a0 --- /dev/null +++ b/examples/https/server/Makefile @@ -0,0 +1,26 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +all test clean distclean: + @for i in $(shell ls) ; do \ + if [ -d $$i ]; then \ + $(MAKE) -C $$i $@ || exit $$? ; \ + fi \ + done diff --git a/examples/https/server/async_respond/Makefile b/examples/https/server/async_respond/Makefile new file mode 100644 index 00000000..e9220b4d --- /dev/null +++ b/examples/https/server/async_respond/Makefile @@ -0,0 +1,39 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT := examples/https/server/async_respond +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := async_respond.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_https \ + -ltbox_http \ + -ltbox_network \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_log \ + -ltbox_util \ + -ltbox_base \ + -lssl -lcrypto \ + -lpthread -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/https/server/async_respond/async_respond.cpp b/examples/https/server/async_respond/async_respond.cpp new file mode 100644 index 00000000..f97c550b --- /dev/null +++ b/examples/https/server/async_respond/async_respond.cpp @@ -0,0 +1,236 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +/** + * HTTPS 服务端 —— 异步响应示例 + * + * 展示三种响应模式,演示 HttpsContext 可被安全地跨事件传递: + * + * GET / → HTML 导航页(链接到下面三个端点) + * GET /instant → 立即响应(同步) + * GET /delayed → 延迟 3 秒后响应(使用 TimerPool) + * GET /compute → 在工作线程执行耗时操作后响应(使用 ThreadPool) + * + * 关键设计: + * HttpsContext 由 shared_ptr 管理。将 ctx 捕获到 lambda 中可 + * 延长其生命期,直到 lambda 执行完毕、ctx 引用计数归零时, + * 析构函数自动提交响应。因此不必担心生命期问题。 + * + * 用法: + * async_respond [bind_ip:port] [cert_file] [key_file] + * + * 生成测试证书(仅供测试,请勿用于生产): + * openssl req -x509 -newkey rsa:2048 -keyout server.key \ + * -out server.crt -days 365 -nodes -subj "/CN=localhost" + * + * 测试(需 curl >= 7.x,-k 跳过证书校验): + * curl -k https://127.0.0.1:12345/instant + * curl -k https://127.0.0.1:12345/delayed + * curl -k https://127.0.0.1:12345/compute + */ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::eventx; +using namespace tbox::network; +using namespace tbox::network::tls; +using namespace tbox::https::server; +using namespace tbox::http; + +// clang-format off +int main(int argc, char **argv) +{ + std::string bind_addr = "0.0.0.0:12345"; + std::string cert_file = "./server.crt"; + std::string key_file = "./server.key"; + + if (argc >= 2) bind_addr = argv[1]; + if (argc >= 3) cert_file = argv[2]; + if (argc >= 4) key_file = argv[3]; + + LogOutput_Enable(); + LogInfo("HTTPS async_respond example"); + LogInfo("Usage: %s [bind_ip:port] [cert_file] [key_file]", argv[0]); + LogInfo(" bind_ip:port - listen address (default: 0.0.0.0:12345)"); + LogInfo(" cert_file - server PEM cert (default: ./server.crt)"); + LogInfo(" key_file - server PEM key (default: ./server.key)"); + + auto sp_loop = Loop::New(); + auto sp_sig_event = sp_loop->newSignalEvent(); + + TimerPool timer_pool(sp_loop); + ThreadPool thread_pool(sp_loop); + thread_pool.initialize(2); // 2 个工作线程 + + SetScopeExitAction([=] { + delete sp_sig_event; + delete sp_loop; + }); + + // ------------------------------------------------------------------------- + // TLS 配置 + // ------------------------------------------------------------------------- + TlsCtx tls_ctx; + TlsConfig tls_cfg; + tls_cfg.cert_file = cert_file; + tls_cfg.key_file = key_file; + + if (!tls_ctx.initialize(TlsMode::kServer, tls_cfg)) { + LogErr("init tls fail, cert: %s, key: %s", cert_file.c_str(), key_file.c_str()); + return 1; + } + + // ------------------------------------------------------------------------- + // HTTPS 服务器初始化 + // ------------------------------------------------------------------------- + HttpsServer srv(sp_loop); + + if (!srv.initializeTls(&tls_ctx)) { + LogErr("initializeTls fail"); + return 1; + } + if (!srv.initialize(SockAddr::FromString(bind_addr), 8)) { + LogErr("init server fail, addr: %s", bind_addr.c_str()); + return 1; + } + //srv.setContextLogEnable(true); // 调试时可打开,查看原始收发内容 + + // ------------------------------------------------------------------------- + // 请求处理 + // ------------------------------------------------------------------------- + srv.use([&](HttpsContextSptr ctx, const HttpsNextFunc &) { + const std::string &path = ctx->req().url.path; + auto &res = ctx->res(); + res.http_ver = HttpVer::k1_1; + + // --- 导航页 ----------------------------------------------------------- + if (path == "/") { + res.status_code = StatusCode::k200_OK; + res.headers["Content-Type"] = "text/html; charset=UTF-8"; + res.body = +R"( + + + + HTTPS 异步响应示例 + + + +

🔒 HTTPS 异步响应示例

+
    +
  • /instant — 立即响应(同步)
  • +
  • /delayed — 延迟 3 秒后响应(TimerPool)
  • +
  • /compute — 工作线程计算后响应(ThreadPool)
  • +
+

可同时打开多个标签页,验证异步不阻塞其他请求。

+ +)"; + + // --- 立即响应 ---------------------------------------------------------- + } else if (path == "/instant") { + res.status_code = StatusCode::k200_OK; + res.headers["Content-Type"] = "text/plain"; + res.body = "instant response"; + + // --- 延迟响应(TimerPool)---------------------------------------------- + // + // 将 ctx 按值捕获进 lambda,延长其生命期。 + // 3 秒后事件触发时填写响应;lambda 销毁时 ctx 引用计数归零, + // HttpsContext 析构函数自动向客户端提交响应。 + } else if (path == "/delayed") { + timer_pool.doAfter(std::chrono::seconds(3), [ctx]() mutable { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().headers["Content-Type"] = "text/plain"; + ctx->res().body = "delayed 3 seconds"; + }); + + // --- 线程池异步响应(ThreadPool)-------------------------------------- + // + // execute(work, done): + // work() 在工作线程执行(可阻塞,不可操作 Loop 对象) + // done() 在事件循环线程执行(可安全填写响应) + } else if (path == "/compute") { + struct Result { int wait_ms; }; + auto result = std::make_shared(); + + thread_pool.execute( + [result] { + // 模拟 0~5 秒的随机耗时计算 + result->wait_ms = ::rand() % 5000; + std::this_thread::sleep_for(std::chrono::milliseconds(result->wait_ms)); + }, + [ctx, result] { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().headers["Content-Type"] = "text/plain"; + ctx->res().body = "computed in " + std::to_string(result->wait_ms) + " ms"; + } + ); + + // --- 404 --------------------------------------------------------------- + } else { + res.status_code = StatusCode::k404_NotFound; + res.headers["Content-Type"] = "text/plain"; + res.body = "not found"; + } + }); + + // ------------------------------------------------------------------------- + // 信号处理与启动 + // ------------------------------------------------------------------------- + sp_sig_event->initialize(SIGINT, Event::Mode::kPersist); + sp_sig_event->setCallback([&](int) { + LogInfo("SIGINT received, stopping..."); + srv.stop(); + sp_loop->exitLoop(); + }); + sp_sig_event->enable(); + + if (!srv.start()) { + LogErr("start server fail"); + srv.cleanup(); + return 1; + } + + LogInfo("HTTPS async_respond server started at %s", bind_addr.c_str()); + sp_loop->runLoop(); + + srv.cleanup(); + thread_pool.cleanup(); + timer_pool.cleanup(); + + LogInfo("HTTPS async_respond example exit"); + return 0; +} +// clang-format on diff --git a/examples/https/server/data_encrypt/Makefile b/examples/https/server/data_encrypt/Makefile new file mode 100644 index 00000000..727747c9 --- /dev/null +++ b/examples/https/server/data_encrypt/Makefile @@ -0,0 +1,40 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT := examples/https/server/data_encrypt +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := simple.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_https \ + -ltbox_http \ + -ltbox_network \ + -ltbox_crypto \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_log \ + -ltbox_util \ + -ltbox_base \ + -lssl -lcrypto \ + -lpthread -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/https/server/data_encrypt/simple.cpp b/examples/https/server/data_encrypt/simple.cpp new file mode 100644 index 00000000..f3473eb4 --- /dev/null +++ b/examples/https/server/data_encrypt/simple.cpp @@ -0,0 +1,208 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +/** + * HTTPS 服务端 —— 应用层数据加密示例 + * + * 演示在 TLS 传输层之上再进行一次对称加密,实现"双重加密": + * 外层:TLS(传输层)负责通信链路加密 + * 内层:AES-256-GCM / ChaCha20-Poly1305(应用层)负责业务数据加密 + * + * 这在零信任架构或端对端加密场景中非常有用:即便 TLS 被中间人截获, + * 内层加密数据在没有应用层密钥时仍然无法被解读。 + * + * 响应正文格式(二进制): + * [ "[ENCRYPTED]" 标记 (11 bytes) ][ 密文 ][ GCM/Poly1305 认证标签 (16 bytes) ] + * + * 用法: + * server + * + * 生成测试证书与对称密钥(仅供测试): + * openssl req -x509 -newkey rsa:2048 -keyout server.key \ + * -out server.crt -days 365 -nodes -subj "/CN=localhost" + * KEY_HEX=$(openssl rand -hex 32) # 256-bit 密钥 → 64 个十六进制字符 + * IV_HEX=$(openssl rand -hex 12) # 96-bit IV → 24 个十六进制字符 + * ./server 127.0.0.1:18443 server.crt server.key aes256gcm $KEY_HEX $IV_HEX + * + * 配套使用 client/data_encrypt 示例发送请求并解密响应。 + */ +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::network; +using namespace tbox::network::tls; +using namespace tbox::crypto; +using namespace tbox::http; +using namespace tbox::https::server; + +// clang-format off +int main(int argc, char **argv) +{ + LogOutput_Enable(); + LogInfo("HTTPS server with data encryption example"); + LogInfo("Usage: %s ", argv[0]); + LogInfo(" bind_ip:port - listen address"); + LogInfo(" cert_file - server PEM certificate"); + LogInfo(" key_file - server PEM private key"); + LogInfo(" algo - aes256gcm | chacha20poly1305"); + LogInfo(" key_hex - 256-bit key as 64 hex chars"); + LogInfo(" iv_hex - 96-bit IV as 24 hex chars"); + + if (argc < 7) { + LogErr("Too few arguments, need 6"); + return 1; + } + + std::string bind_addr = argv[1]; + std::string cert_file = argv[2]; + std::string key_file = argv[3]; + std::string algo_str = argv[4]; + std::string key_hex = argv[5]; + std::string iv_hex = argv[6]; + + LogInfo("bind: %s, cert: %s, algo: %s", + bind_addr.c_str(), cert_file.c_str(), algo_str.c_str()); + + auto sp_loop = Loop::New(); + auto sp_sig_event = sp_loop->newSignalEvent(); + + SetScopeExitAction([=] { + delete sp_sig_event; + delete sp_loop; + }); + + // ------------------------------------------------------------------------- + // TLS 配置(传输层加密) + // ------------------------------------------------------------------------- + TlsCtx tls_ctx; + TlsConfig tls_cfg; + tls_cfg.cert_file = cert_file; + tls_cfg.key_file = key_file; + + if (!tls_ctx.initialize(TlsMode::kServer, tls_cfg)) { + LogErr("init tls fail, cert: %s, key: %s", cert_file.c_str(), key_file.c_str()); + return 1; + } + + // ------------------------------------------------------------------------- + // 应用层对称加密配置 + // ------------------------------------------------------------------------- + CryptoCryptoCtx crypto; + CryptoConfig crypto_cfg; + + if (algo_str == "aes256gcm") + crypto_cfg.algo = CryptoAlgorithm::kAES256GCM; + else if (algo_str == "chacha20poly1305") + crypto_cfg.algo = CryptoAlgorithm::kChaCha20Poly1305; + else { + LogErr("unsupported algo: %s (aes256gcm | chacha20poly1305)", algo_str.c_str()); + return 1; + } + + crypto_cfg.key_hex = key_hex; + crypto_cfg.iv_hex = iv_hex; + + if (!crypto.initialize(crypto_cfg)) { + LogErr("init crypto fail"); + return 1; + } + + // ------------------------------------------------------------------------- + // HTTPS 服务器初始化 + // ------------------------------------------------------------------------- + HttpsServer srv(sp_loop); + + if (!srv.initializeTls(&tls_ctx)) { + LogErr("initializeTls fail"); + return 1; + } + + if (!srv.initialize(SockAddr::FromString(bind_addr), 8)) { + LogErr("init server fail, addr: %s", bind_addr.c_str()); + return 1; + } + //srv.setContextLogEnable(true); // 调试时可打开,查看原始收发内容 + + // ------------------------------------------------------------------------- + // 请求处理:加密业务数据后放入响应正文 + // + // 响应正文格式(二进制): + // [ "[ENCRYPTED]" ][ ciphertext ][ tag (16 bytes) ] + // ------------------------------------------------------------------------- + srv.use([&](HttpsContextSptr ctx, const HttpsNextFunc &) { + const std::string plaintext = "Hello Encrypted HTTPS!"; + + auto enc = crypto.encrypt( + reinterpret_cast(plaintext.data()), + plaintext.size()); + + if (!enc.success) { + LogErr("encrypt fail: %s", enc.error_message.c_str()); + ctx->res().status_code = StatusCode::k500_InternalServerError; + ctx->res().http_ver = HttpVer::k1_1; + ctx->res().body = "encryption error"; + return; + } + + // 组装响应正文:标记 + 密文 + 认证标签 + std::string &body = ctx->res().body; + body = "[ENCRYPTED]"; + body.append(reinterpret_cast(enc.ciphertext.data()), enc.ciphertext.size()); + body.append(reinterpret_cast(enc.tag.data()), enc.tag.size()); + + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().http_ver = HttpVer::k1_1; + ctx->res().headers["Content-Type"] = "application/octet-stream"; + ctx->res().headers["X-Encrypt-Algo"] = algo_str; + + LogInfo("encrypted: plaintext=%zu bytes -> ciphertext=%zu + tag=%zu bytes", + plaintext.size(), enc.ciphertext.size(), enc.tag.size()); + }); + + sp_sig_event->initialize(SIGINT, Event::Mode::kPersist); + sp_sig_event->setCallback([&](int) { + LogInfo("SIGINT received, stopping..."); + srv.stop(); + sp_loop->exitLoop(); + }); + sp_sig_event->enable(); + + if (!srv.start()) { + LogErr("start server fail"); + srv.cleanup(); + return 1; + } + + LogInfo("HTTPS server with data encryption started at %s", bind_addr.c_str()); + sp_loop->runLoop(); + + srv.cleanup(); + LogInfo("HTTPS server with data encryption example exit"); + return 0; +} +// clang-format on diff --git a/examples/https/server/router/Makefile b/examples/https/server/router/Makefile new file mode 100644 index 00000000..2bf92b28 --- /dev/null +++ b/examples/https/server/router/Makefile @@ -0,0 +1,39 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT := examples/https/server/router +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := router.cpp webpage.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_https \ + -ltbox_http \ + -ltbox_network \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_log \ + -ltbox_util \ + -ltbox_base \ + -lssl -lcrypto \ + -lpthread -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/https/server/router/router.cpp b/examples/https/server/router/router.cpp new file mode 100644 index 00000000..df73cb57 --- /dev/null +++ b/examples/https/server/router/router.cpp @@ -0,0 +1,192 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +/** + * HTTPS 服务端 —— 路由示例 + * + * 展示如何在 HttpsServer 中实现多路由处理: + * GET / → HTML 首页(含跳转链接) + * GET /1 → HTML 页面一 + * GET /2 → HTML 页面二 + * 其他路径 → 404 JSON 错误 + * + * 同时演示自定义 HttpsMiddleware 类的用法(访问日志中间件)。 + * + * 用法: + * router [bind_ip:port] [cert_file] [key_file] + * + * 生成测试证书(仅供测试,请勿用于生产): + * openssl req -x509 -newkey rsa:2048 -keyout server.key \ + * -out server.crt -days 365 -nodes -subj "/CN=localhost" + * + * 浏览器访问: + * https://127.0.0.1:12345/ + */ +#include +#include +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::network; +using namespace tbox::network::tls; +using namespace tbox::https::server; +using namespace tbox::http; + +// --------------------------------------------------------------------------- +// 访问日志中间件:记录每次请求的方法和路径,及最终响应状态码 +// --------------------------------------------------------------------------- +class AccessLogMiddleware : public HttpsMiddleware { + public: + void handle(HttpsContextSptr ctx, const HttpsNextFunc &next) override { + LogInfo("[access] %s %s", + MethodToString(ctx->req().method).c_str(), + ctx->req().url.path.c_str()); + next(); + LogInfo("[access] -> %d", static_cast(ctx->res().status_code)); + } +}; + +// --------------------------------------------------------------------------- +// 网页内容(定义于 webpage.cpp) +// --------------------------------------------------------------------------- +namespace webpage { +extern const char *kHtmlHome; +extern const char *kHtmlPage1; +extern const char *kHtmlPage2; +} + +// clang-format off +int main(int argc, char **argv) +{ + std::string bind_addr = "0.0.0.0:12345"; + std::string cert_file = "./server.crt"; + std::string key_file = "./server.key"; + + if (argc >= 2) bind_addr = argv[1]; + if (argc >= 3) cert_file = argv[2]; + if (argc >= 4) key_file = argv[3]; + + LogOutput_Enable(); + LogInfo("HTTPS router example"); + LogInfo("Usage: %s [bind_ip:port] [cert_file] [key_file]", argv[0]); + LogInfo(" bind_ip:port - listen address (default: 0.0.0.0:12345)"); + LogInfo(" cert_file - server PEM cert (default: ./server.crt)"); + LogInfo(" key_file - server PEM key (default: ./server.key)"); + + auto sp_loop = Loop::New(); + auto sp_sig_event = sp_loop->newSignalEvent(); + + SetScopeExitAction([=] { + delete sp_sig_event; + delete sp_loop; + }); + + // ------------------------------------------------------------------------- + // TLS 配置 + // ------------------------------------------------------------------------- + TlsCtx tls_ctx; + TlsConfig tls_cfg; + tls_cfg.cert_file = cert_file; + tls_cfg.key_file = key_file; + + if (!tls_ctx.initialize(TlsMode::kServer, tls_cfg)) { + LogErr("init tls fail, cert: %s, key: %s", cert_file.c_str(), key_file.c_str()); + return 1; + } + + // ------------------------------------------------------------------------- + // HTTPS 服务器初始化 + // ------------------------------------------------------------------------- + HttpsServer srv(sp_loop); + + if (!srv.initializeTls(&tls_ctx)) { + LogErr("initializeTls fail"); + return 1; + } + if (!srv.initialize(SockAddr::FromString(bind_addr), 8)) { + LogErr("init server fail, addr: %s", bind_addr.c_str()); + return 1; + } + //srv.setContextLogEnable(true); // 调试时可打开,查看原始收发内容 + + // ------------------------------------------------------------------------- + // 中间件链:访问日志 → 路由分发 + // ------------------------------------------------------------------------- + AccessLogMiddleware access_log; + srv.use(&access_log); + + // 路由分发:根据请求路径返回不同内容 + srv.use([](HttpsContextSptr ctx, const HttpsNextFunc &) { + const std::string &path = ctx->req().url.path; + auto &res = ctx->res(); + res.http_ver = HttpVer::k1_1; + + if (path == "/") { + res.status_code = StatusCode::k200_OK; + res.headers["Content-Type"] = "text/html; charset=UTF-8"; + res.body = webpage::kHtmlHome; + + } else if (path == "/1") { + res.status_code = StatusCode::k200_OK; + res.headers["Content-Type"] = "text/html; charset=UTF-8"; + res.body = webpage::kHtmlPage1; + + } else if (path == "/2") { + res.status_code = StatusCode::k200_OK; + res.headers["Content-Type"] = "text/html; charset=UTF-8"; + res.body = webpage::kHtmlPage2; + + } else { + res.status_code = StatusCode::k404_NotFound; + res.headers["Content-Type"] = "application/json"; + res.body = R"({"error":"not found","path":")" + path + R"("})"; + } + }); + + // ------------------------------------------------------------------------- + // 信号处理与启动 + // ------------------------------------------------------------------------- + sp_sig_event->initialize(SIGINT, Event::Mode::kPersist); + sp_sig_event->setCallback([&](int) { + LogInfo("SIGINT received, stopping..."); + srv.stop(); + sp_loop->exitLoop(); + }); + sp_sig_event->enable(); + + if (!srv.start()) { + LogErr("start server fail"); + srv.cleanup(); + return 1; + } + + LogInfo("HTTPS router server started at %s", bind_addr.c_str()); + sp_loop->runLoop(); + + srv.cleanup(); + LogInfo("HTTPS router example exit"); + return 0; +} +// clang-format on diff --git a/examples/https/server/router/webpage.cpp b/examples/https/server/router/webpage.cpp new file mode 100644 index 00000000..a64aff7d --- /dev/null +++ b/examples/https/server/router/webpage.cpp @@ -0,0 +1,263 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +namespace webpage { + +const char* kHtmlHome = +R"( + + + + + TBOX HTTPS 示例 + + + +
+

TBOX HTTPS 示例服务器 🔒 TLS

+

这是一个简单的C++ HTTPS服务器演示,连接已加密

+
+ +
+
+

页面一

+

演示第一个示例页面

+ 访问 +
+ +
+

页面二

+

演示第二个示例页面

+ 访问 +
+
+ + + + +)"; + +const char* kHtmlPage1 = +R"( + + + + + 页面一 - TBOX HTTPS 示例 + + + +
+

页面一

+
+

这是第一个示例页面,展示了 TBOX HTTPS 服务器的基本路由功能。

+

本页面通过 TLS 加密传输,浏览器地址栏会显示🔒图标。

+
+ 返回首页 +
+ +)"; + +const char* kHtmlPage2 = +R"( + + + + + 页面二 - TBOX HTTPS 示例 + + + +
+

页面二

+
+

欢迎来到第二个示例页面!这里展示了 TBOX HTTPS 服务器的特性。

+ +
+

TBOX HTTPS 模块特点:

+
    +
  • TLS 1.2 / TLS 1.3 加密传输
  • +
  • 零依赖的 Pimpl 公开接口
  • +
  • Express 风格中间件链
  • +
  • 非阻塞异步 I/O,支持高并发
  • +
  • Keep-Alive 持久连接复用
  • +
  • 可选 mTLS 双向证书认证
  • +
+
+
+ 返回首页 +
+ +)"; + +} diff --git a/examples/https/server/simple/Makefile b/examples/https/server/simple/Makefile new file mode 100644 index 00000000..7fef6708 --- /dev/null +++ b/examples/https/server/simple/Makefile @@ -0,0 +1,39 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT := examples/https/server/simple +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := simple.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_https \ + -ltbox_http \ + -ltbox_network \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_log \ + -ltbox_util \ + -ltbox_base \ + -lssl -lcrypto \ + -lpthread -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/https/server/simple/simple.cpp b/examples/https/server/simple/simple.cpp new file mode 100644 index 00000000..b99b8073 --- /dev/null +++ b/examples/https/server/simple/simple.cpp @@ -0,0 +1,162 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +/** + * HTTPS 服务端 —— 最简示例 + * + * 演示最基本的 HTTPS 服务器:TLS 握手 + 返回一行文字。 + * 支持 mTLS(双向证书认证)、自定义安全等级和加密套件。 + * + * 用法: + * server [bind_ip:port] [cert_file] [key_file] [ca_file] [level] [cipher_list] [ciphersuites] + * + * 生成测试证书(仅供测试): + * openssl req -x509 -newkey rsa:2048 -keyout server.key \ + * -out server.crt -days 365 -nodes -subj "/CN=localhost" + * + * 快速测试(-k 跳过证书校验): + * curl -k https://127.0.0.1:12345/ + */ +#include +#include +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::network; +using namespace tbox::network::tls; +using namespace tbox::http; +using namespace tbox::https::server; + +// clang-format off +int main(int argc, char **argv) +{ + std::string bind_addr = "0.0.0.0:12345"; + std::string cert_file = "./server.crt"; + std::string key_file = "./server.key"; + std::string ca_file; // 若指定则开启 mTLS(要求客户端提供证书) + std::string level_str; // 安全等级: default / compatible / strict + std::string cipher_list; // TLS1.2 及以下加密套件(覆盖 level) + std::string ciphersuites; // TLS1.3 加密套件(覆盖 level) + + if (argc >= 2) bind_addr = argv[1]; + if (argc >= 3) cert_file = argv[2]; + if (argc >= 4) key_file = argv[3]; + if (argc >= 5) ca_file = argv[4]; + if (argc >= 6) level_str = argv[5]; + if (argc >= 7) cipher_list = argv[6]; + if (argc >= 8) ciphersuites = argv[7]; + + TlsSecurityLevel sec_level = TlsSecurityLevel::kDefault; + if (level_str == "compatible") + sec_level = TlsSecurityLevel::kCompatible; + else if (level_str == "strict") + sec_level = TlsSecurityLevel::kStrict; + + LogOutput_Enable(); + + LogInfo("HTTPS server example enter"); + LogInfo("Usage: %s [bind_ip:port] [cert_file] [key_file] [ca_file] [level] [cipher_list] [ciphersuites]", argv[0]); + LogInfo(" bind_ip:port - listen address (default: 0.0.0.0:12345)"); + LogInfo(" cert_file - server PEM certificate (default: ./server.crt)"); + LogInfo(" key_file - server PEM private key (default: ./server.key)"); + LogInfo(" ca_file - CA certificate for mTLS client verification (optional)"); + LogInfo(" level - cipher preset: default / compatible / strict (optional)"); + LogInfo(" cipher_list - OpenSSL TLS<=1.2 cipher list, overrides level (optional)"); + LogInfo(" ciphersuites - OpenSSL TLS1.3 ciphersuites, overrides level (optional)"); + + if (!ca_file.empty()) + LogInfo("mTLS mode: require client certificate, CA: %s", ca_file.c_str()); + if (sec_level != TlsSecurityLevel::kDefault) + LogInfo("Security level preset: %s", level_str.c_str()); + if (!cipher_list.empty()) + LogInfo("TLS cipher_list override: %s", cipher_list.c_str()); + if (!ciphersuites.empty()) + LogInfo("TLS ciphersuites override: %s", ciphersuites.c_str()); + + auto sp_loop = Loop::New(); + auto sp_sig_event = sp_loop->newSignalEvent(); + + SetScopeExitAction([=] { + delete sp_sig_event; + delete sp_loop; + }); + + TlsCtx tls_ctx; + TlsConfig tls_cfg; + tls_cfg.cert_file = cert_file; + tls_cfg.key_file = key_file; + tls_cfg.security_level = sec_level; + if (!ca_file.empty()) { + tls_cfg.ca_file = ca_file; + tls_cfg.verify_peer = true; // mTLS: 强制校验客户端证书 + } + if (!cipher_list.empty()) tls_cfg.cipher_list = cipher_list; + if (!ciphersuites.empty()) tls_cfg.ciphersuites = ciphersuites; + + if (!tls_ctx.initialize(TlsMode::kServer, tls_cfg)) { + LogErr("init tls fail, cert: %s, key: %s", cert_file.c_str(), key_file.c_str()); + return 1; + } + + HttpsServer srv(sp_loop); + if (!srv.initializeTls(&tls_ctx)) { + LogErr("initializeTls fail"); + return 1; + } + if (!srv.initialize(SockAddr::FromString(bind_addr), 8)) { + LogErr("init https server fail, bind: %s", bind_addr.c_str()); + return 1; + } + //srv.setContextLogEnable(true); // 调试时可打开,查看原始收发内容 + + srv.use([&](HttpsContextSptr ctx, const HttpsNextFunc &) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().http_ver = HttpVer::k1_1; + ctx->res().headers["Content-Type"] = "text/plain"; + ctx->res().body = "Hello HTTPS!"; + }); + + sp_sig_event->initialize(SIGINT, Event::Mode::kPersist); + sp_sig_event->setCallback([&](int) { + LogInfo("SIGINT received, stopping..."); + srv.stop(); + sp_loop->exitLoop(); + }); + sp_sig_event->enable(); + + if (!srv.start()) { + LogErr("start https server fail"); + srv.cleanup(); + return 1; + } + + LogInfo("HTTPS server started at %s", bind_addr.c_str()); + sp_loop->runLoop(); + + srv.cleanup(); + LogInfo("HTTPS server example exit"); + return 0; +} +// clang-format on -- Gitee From 8dc7570820547cb5c67ca89ce773cabc2cecafc4 Mon Sep 17 00:00:00 2001 From: duanyh <19517952495@139.com> Date: Tue, 14 Apr 2026 19:28:03 +0800 Subject: [PATCH 5/6] =?UTF-8?q?tidy:=20=E6=9B=B4=E6=96=B0=E9=A1=B6?= =?UTF-8?q?=E5=B1=82README=EF=BC=8C=E8=A1=A5=E5=85=85https=E4=B8=8Ecrypto?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +++- README_CN.md | 12 +++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cb7fe872..2c67a40e 100644 --- a/README.md +++ b/README.md @@ -110,9 +110,11 @@ It contains an event-driven behavior tree that can realize sequential, branching | mqtt | MQTT Client | | coroutine | coroutine function | | http | Implemented HTTP Server and Client modules on the basis of network | +| https | Implemented HTTPS with TLS/SSL support, client certificates, and custom cipher suites | +| network/tls | TLS transport module: non-blocking TLS client/server wrappers, mTLS, security presets, custom cipher lists, and ALPN support. See [modules/network/tls/README.md](modules/network/tls/README.md) | | alarm | Realized 4 commonly used alarm clocks: CRON alarm clock, single alarm clock, weekly cycle alarm clock, weekday alarm clock | | flow | Contains multi-level state machine and behavior tree to solve the problem of action flow in asynchronous mode | -| crypto | Implemented the commonly used AES and MD5 encryption and decryption calculations | +| crypto | Comprehensive cryptographic module: legacy AES/MD5 utilities plus modern symmetric encryption (AES-256-GCM, ChaCha20-Poly1305), hashing (MD5/SHA-*), and asymmetric cryptography (RSA/ECC/Ed25519). See [modules/crypto/README.md](modules/crypto/README.md) | | dbus | Implemented the function of integrating dbus into event to process transactions in a non-blocking manner | # Environment diff --git a/README_CN.md b/README_CN.md index cf812a1d..97d8dd1c 100644 --- a/README_CN.md +++ b/README_CN.md @@ -111,11 +111,13 @@ trace模块能记录被标记的函数每次执行的时间点与时长,可导 | run | 执行器 | 是个可执行程序,可加载多个由参数`-l xxx`指定的动态库,并运行其中的Module | | mqtt | MQTT客户端库 | | | coroutine | 协程库 | 众所周知,异步框架不方便处理顺序性业务,协程弥补之 | -| http | HTTP库 | 在network的基础上实现了HTTP的Server与Client模块 | -| alarm | 闹钟库 | 实现了4种常用的闹钟:CRON闹钟、单次闹钟、星期循环闹钟、工作日闹钟 | -| flow | 流程库| 含多层级状态机与行为树,解决异步模式下动行流程问题 | -| crypto | 加密工具库 | 实现了常用的AES、MD5运算 | -| dbus | DBUS适配库 | 实现了将dbus集成到event进行非阻塞处理事务的功能 | +| http | HTTP库 | 在 network 的基础上实现了 HTTP 的 Server 与 Client 模块 | +| https | HTTPS库 | 实现了 HTTPS 与 TLS/SSL 支持、客户端证书和自定义密码套件 | +| network/tls | TLS 传输模块 | 提供非阻塞 TLS 客户端/服务端封装、mTLS、安全级别预设、自定义密码套件与 ALPN 支持。详见 [modules/network/tls/README_CN.md](modules/network/tls/README_CN.md) | +| alarm | 闹钟库 | 实现了 4 种常用的闹钟:CRON 闹钟、单次闹钟、星期循环闹钟、工作日闹钟 | +| flow | 流程库| 含多层级状态机与行为树,解决异步模式下动行流程问题 | +| crypto | 完整加密模块 | 包含历史 AES、MD5 工具,以及现代对称加密(AES-256-GCM、ChaCha20-Poly1305)、哈希(MD5/SHA-*)和非对称密码学(RSA/ECC/Ed25519)。详见 [modules/crypto/README_CN.md](modules/crypto/README_CN.md) | +| dbus | DBUS 适配库 | 实现了将 dbus 集成到 event 进行非阻塞处理事务的功能 | # 适用环境 -- Gitee From 29282e62c265f5b6906147b508a94e89ab0242d0 Mon Sep 17 00:00:00 2001 From: "chunjun.li" Date: Tue, 14 Apr 2026 23:15:56 +0800 Subject: [PATCH 6/6] =?UTF-8?q?tidy:=20=E4=BF=AE=E6=95=B4=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/crypto/crypto_asym/simple.cpp | 2 +- modules/crypto/asym_ctx.cpp | 289 ++++++++++++++----------- modules/crypto/asym_ctx.h | 5 +- modules/crypto/asym_ctx_test.cpp | 10 +- 4 files changed, 165 insertions(+), 141 deletions(-) diff --git a/examples/crypto/crypto_asym/simple.cpp b/examples/crypto/crypto_asym/simple.cpp index a71989d0..18770d25 100644 --- a/examples/crypto/crypto_asym/simple.cpp +++ b/examples/crypto/crypto_asym/simple.cpp @@ -116,7 +116,7 @@ int main(int argc, char *argv[]) { LogInfo("Generating key pair for %s...", AsymCtx::getAlgoName(algo)); - if (!AsymCtx::generateKeyPair(algo, private_key, public_key)) { + if (!AsymCtx::GenerateKeyPair(algo, private_key, public_key)) { LogErr("Failed to generate key pair"); return 1; } diff --git a/modules/crypto/asym_ctx.cpp b/modules/crypto/asym_ctx.cpp index 3b2517c9..2e85f70e 100644 --- a/modules/crypto/asym_ctx.cpp +++ b/modules/crypto/asym_ctx.cpp @@ -19,6 +19,9 @@ */ #include + +#include + #include #include #include @@ -31,18 +34,21 @@ namespace tbox { namespace crypto { -AsymCtx::AsymCtx() : algo_(AsymAlgorithm::kRSA2048), pkey_(nullptr) { -} +AsymCtx::AsymCtx() + : algo_(AsymAlgorithm::kRSA2048) + , pkey_(nullptr) +{ } -AsymCtx::~AsymCtx() { +AsymCtx::~AsymCtx() +{ if (pkey_) { EVP_PKEY_free(static_cast(pkey_)); pkey_ = nullptr; } } -bool AsymCtx::initialize(const AsymConfig &cfg) { - algo_ = cfg.algo; +bool AsymCtx::initialize(const AsymConfig &cfg) +{ // Free previous key if (pkey_) { @@ -67,7 +73,9 @@ bool AsymCtx::initialize(const AsymConfig &cfg) { return false; } + algo_ = cfg.algo; pkey_ = pkey; + LogInfo("Private key loaded: algo=%s", getAlgoName(algo_)); return true; } @@ -89,6 +97,7 @@ bool AsymCtx::initialize(const AsymConfig &cfg) { return false; } + algo_ = cfg.algo; pkey_ = pkey; LogInfo("Public key loaded: algo=%s", getAlgoName(algo_)); return true; @@ -98,134 +107,158 @@ bool AsymCtx::initialize(const AsymConfig &cfg) { return false; } -bool AsymCtx::generateKeyPair(AsymAlgorithm algo, std::string &private_key_out, - std::string &public_key_out) { - EVP_PKEY_CTX *ctx = nullptr; +//! 生成 RSA 私钥 +EVP_PKEY* GenerateRsaPrivateKey(AsymAlgorithm algo) +{ + int bits = (algo == AsymAlgorithm::kRSA2048) ? 2048 : + (algo == AsymAlgorithm::kRSA3072) ? 3072 : + 4096; + + EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr); + if (ctx == nullptr) { + LogErr("EVP_PKEY_CTX_new_id failed"); + return nullptr; + } + + SetScopeExitAction([ctx] { EVP_PKEY_CTX_free(ctx); }); + if (EVP_PKEY_keygen_init(ctx) <= 0) { + LogErr("EVP_PKEY_keygen_init failed"); + return nullptr; + } + + if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) <= 0) { + LogErr("EVP_PKEY_CTX_set_rsa_keygen_bits failed"); + return nullptr; + } + + EVP_PKEY *pkey = nullptr; + if (EVP_PKEY_keygen(ctx, &pkey) <= 0) { + LogErr("EVP_PKEY_keygen failed"); + return nullptr; + } + + return pkey; +} + +//! 生成 ECC 私钥 +EVP_PKEY* GenerateEccPrivateKey(AsymAlgorithm algo) +{ + int nid = (algo == AsymAlgorithm::kECC_P256) ? NID_X9_62_prime256v1 : + (algo == AsymAlgorithm::kECC_P384) ? NID_secp384r1 : + NID_secp521r1; + + EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr); + if (ctx == nullptr) { + LogErr("EVP_PKEY_CTX_new_id failed for EC"); + return nullptr; + } + + SetScopeExitAction([ctx] { EVP_PKEY_CTX_free(ctx); }); + if (EVP_PKEY_paramgen_init(ctx) <= 0) { + LogErr("EVP_PKEY_paramgen_init failed"); + return nullptr; + } + + if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, nid) <= 0) { + LogErr("EVP_PKEY_CTX_set_ec_paramgen_curve_nid failed"); + return nullptr; + } + + EVP_PKEY *params = nullptr; + if (EVP_PKEY_paramgen(ctx, ¶ms) <= 0) { + LogErr("EVP_PKEY_paramgen failed"); + return nullptr; + } + + SetScopeExitAction([params] { EVP_PKEY_free(params); }); + ctx = EVP_PKEY_CTX_new(params, nullptr); + if (ctx == nullptr) { + LogErr("EVP_PKEY_CTX_new failed for EC keygen"); + return nullptr; + } + + SetScopeExitAction([ctx] { EVP_PKEY_CTX_free(ctx); }); + if (EVP_PKEY_keygen_init(ctx) <= 0) { + LogErr("EVP_PKEY_keygen_init failed for EC"); + return nullptr; + } + + EVP_PKEY *pkey = nullptr; + if (EVP_PKEY_keygen(ctx, &pkey) <= 0) { + LogErr("EVP_PKEY_keygen failed for EC"); + return nullptr; + } + + return pkey; +} + +EVP_PKEY* GenerateEd25519PrivateKey() +{ + EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, nullptr); + if (ctx == nullptr) { + LogErr("EVP_PKEY_CTX_new_id failed for Ed25519"); + return nullptr; + } + + SetScopeExitAction([ctx] { EVP_PKEY_CTX_free(ctx); }); + if (EVP_PKEY_keygen_init(ctx) <= 0) { + LogErr("EVP_PKEY_keygen_init failed for Ed25519"); + return nullptr; + } + + EVP_PKEY *pkey = nullptr; + if (EVP_PKEY_keygen(ctx, &pkey) <= 0) { + LogErr("EVP_PKEY_keygen failed for Ed25519"); + return nullptr; + } + + return pkey; +} + +//! 根据不同的算法生成对应的私钥key对象 +EVP_PKEY* GeneratePrivateKey(AsymAlgorithm algo) +{ + // Create context for key generation + if (algo == AsymAlgorithm::kRSA2048 || + algo == AsymAlgorithm::kRSA3072 || + algo == AsymAlgorithm::kRSA4096) { + return GenerateRsaPrivateKey(algo); + + } else if (algo == AsymAlgorithm::kECC_P256 || + algo == AsymAlgorithm::kECC_P384 || + algo == AsymAlgorithm::kECC_P521) { + return GenerateEccPrivateKey(algo); + + } else if (algo == AsymAlgorithm::kEd25519) { + return GenerateEd25519PrivateKey(); + + } else { + LogErr("Unsupported algorithm for key generation"); + return nullptr; + } +} + +bool AsymCtx::GenerateKeyPair(AsymAlgorithm algo, std::string &private_key_out, std::string &public_key_out) +{ EVP_PKEY *pkey = nullptr; BIO *bio = nullptr; try { - // Create context for key generation - if (algo == AsymAlgorithm::kRSA2048 || algo == AsymAlgorithm::kRSA3072 || - algo == AsymAlgorithm::kRSA4096) { - int bits = (algo == AsymAlgorithm::kRSA2048) ? 2048 : - (algo == AsymAlgorithm::kRSA3072) ? 3072 : 4096; - - ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr); - if (!ctx) { - LogErr("EVP_PKEY_CTX_new_id failed"); - return false; - } - - if (EVP_PKEY_keygen_init(ctx) <= 0) { - LogErr("EVP_PKEY_keygen_init failed"); - EVP_PKEY_CTX_free(ctx); - return false; - } - - if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) <= 0) { - LogErr("EVP_PKEY_CTX_set_rsa_keygen_bits failed"); - EVP_PKEY_CTX_free(ctx); - return false; - } - - if (EVP_PKEY_keygen(ctx, &pkey) <= 0) { - LogErr("EVP_PKEY_keygen failed"); - EVP_PKEY_CTX_free(ctx); - return false; - } - - EVP_PKEY_CTX_free(ctx); - } else if (algo == AsymAlgorithm::kECC_P256 || algo == AsymAlgorithm::kECC_P384 || - algo == AsymAlgorithm::kECC_P521) { - int nid = (algo == AsymAlgorithm::kECC_P256) ? NID_X9_62_prime256v1 : - (algo == AsymAlgorithm::kECC_P384) ? NID_secp384r1 : - NID_secp521r1; - - ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr); - if (!ctx) { - LogErr("EVP_PKEY_CTX_new_id failed for EC"); - return false; - } - - if (EVP_PKEY_paramgen_init(ctx) <= 0) { - LogErr("EVP_PKEY_paramgen_init failed"); - EVP_PKEY_CTX_free(ctx); - return false; - } - - if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, nid) <= 0) { - LogErr("EVP_PKEY_CTX_set_ec_paramgen_curve_nid failed"); - EVP_PKEY_CTX_free(ctx); - return false; - } - - EVP_PKEY *params = nullptr; - if (EVP_PKEY_paramgen(ctx, ¶ms) <= 0) { - LogErr("EVP_PKEY_paramgen failed"); - EVP_PKEY_CTX_free(ctx); - return false; - } - - EVP_PKEY_CTX_free(ctx); - ctx = EVP_PKEY_CTX_new(params, nullptr); - EVP_PKEY_free(params); - - if (!ctx) { - LogErr("EVP_PKEY_CTX_new failed for EC keygen"); - return false; - } - - if (EVP_PKEY_keygen_init(ctx) <= 0) { - LogErr("EVP_PKEY_keygen_init failed for EC"); - EVP_PKEY_CTX_free(ctx); - return false; - } - - if (EVP_PKEY_keygen(ctx, &pkey) <= 0) { - LogErr("EVP_PKEY_keygen failed for EC"); - EVP_PKEY_CTX_free(ctx); - return false; - } - - EVP_PKEY_CTX_free(ctx); - } else if (algo == AsymAlgorithm::kEd25519) { - ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, nullptr); - if (!ctx) { - LogErr("EVP_PKEY_CTX_new_id failed for Ed25519"); - return false; - } - - if (EVP_PKEY_keygen_init(ctx) <= 0) { - LogErr("EVP_PKEY_keygen_init failed for Ed25519"); - EVP_PKEY_CTX_free(ctx); - return false; - } - - if (EVP_PKEY_keygen(ctx, &pkey) <= 0) { - LogErr("EVP_PKEY_keygen failed for Ed25519"); - EVP_PKEY_CTX_free(ctx); - return false; - } - - EVP_PKEY_CTX_free(ctx); - } else { - LogErr("Unsupported algorithm for key generation"); + pkey = GeneratePrivateKey(algo); + if (pkey == nullptr) return false; - } + SetScopeExitAction([pkey] { EVP_PKEY_free(pkey); }); // Write private key bio = BIO_new(BIO_s_mem()); - if (!bio) { + if (bio == nullptr) { LogErr("BIO_new failed for private key"); - EVP_PKEY_free(pkey); return false; } + SetScopeExitAction([bio] { BIO_free(bio); }); if (PEM_write_bio_PrivateKey(bio, pkey, nullptr, nullptr, 0, nullptr, nullptr) <= 0) { LogErr("PEM_write_bio_PrivateKey failed"); - BIO_free(bio); - EVP_PKEY_free(pkey); return false; } @@ -233,20 +266,17 @@ bool AsymCtx::generateKeyPair(AsymAlgorithm algo, std::string &private_key_out, size_t len = BIO_pending(bio); private_key_out.resize(len); BIO_read(bio, const_cast(private_key_out.data()), len); - BIO_free(bio); // Write public key bio = BIO_new(BIO_s_mem()); - if (!bio) { + if (bio == nullptr) { LogErr("BIO_new failed for public key"); - EVP_PKEY_free(pkey); return false; } + SetScopeExitAction([bio] { BIO_free(bio); }); if (PEM_write_bio_PUBKEY(bio, pkey) <= 0) { LogErr("PEM_write_bio_PUBKEY failed"); - BIO_free(bio); - EVP_PKEY_free(pkey); return false; } @@ -254,21 +284,17 @@ bool AsymCtx::generateKeyPair(AsymAlgorithm algo, std::string &private_key_out, len = BIO_pending(bio); public_key_out.resize(len); BIO_read(bio, const_cast(public_key_out.data()), len); - BIO_free(bio); - EVP_PKEY_free(pkey); LogInfo("Key pair generated: algo=%s", getAlgoName(algo)); return true; } catch (...) { - if (ctx) EVP_PKEY_CTX_free(ctx); - if (pkey) EVP_PKEY_free(pkey); - if (bio) BIO_free(bio); return false; } } -EncryptResult AsymCtx::encrypt(const void *plaintext, size_t len) { +EncryptResult AsymCtx::encrypt(const void *plaintext, size_t len) +{ EncryptResult result; if (!pkey_) { @@ -342,7 +368,8 @@ EncryptResult AsymCtx::encrypt(const void *plaintext, size_t len) { return result; } -DecryptResult AsymCtx::decrypt(const void *ciphertext, size_t len) { +DecryptResult AsymCtx::decrypt(const void *ciphertext, size_t len) +{ DecryptResult result; if (!pkey_) { diff --git a/modules/crypto/asym_ctx.h b/modules/crypto/asym_ctx.h index 6482b9f6..872efd4e 100644 --- a/modules/crypto/asym_ctx.h +++ b/modules/crypto/asym_ctx.h @@ -121,10 +121,7 @@ class AsymCtx { * @param public_key_out 返回公钥 PEM * @return 成功返回 true */ - static bool generateKeyPair( - AsymAlgorithm algo, - std::string &private_key_out, - std::string &public_key_out); + static bool GenerateKeyPair(AsymAlgorithm algo, std::string &private_key_out, std::string &public_key_out); // =========== RSA 加密/解密 =========== diff --git a/modules/crypto/asym_ctx_test.cpp b/modules/crypto/asym_ctx_test.cpp index 721266ab..17ccf00e 100644 --- a/modules/crypto/asym_ctx_test.cpp +++ b/modules/crypto/asym_ctx_test.cpp @@ -71,7 +71,7 @@ bool VerifyWithOpenSsl(const std::string &public_key_pem, TEST(AsymCtx, RsaEncryptDecrypt) { std::string private_key; std::string public_key; - ASSERT_TRUE(AsymCtx::generateKeyPair(AsymAlgorithm::kRSA2048, private_key, public_key)); + ASSERT_TRUE(AsymCtx::GenerateKeyPair(AsymAlgorithm::kRSA2048, private_key, public_key)); AsymCtx enc_ctx; AsymConfig enc_cfg; @@ -98,7 +98,7 @@ TEST(AsymCtx, RsaEncryptDecrypt) { TEST(AsymCtx, RsaSignVerify) { std::string private_key; std::string public_key; - ASSERT_TRUE(AsymCtx::generateKeyPair(AsymAlgorithm::kRSA2048, private_key, public_key)); + ASSERT_TRUE(AsymCtx::GenerateKeyPair(AsymAlgorithm::kRSA2048, private_key, public_key)); const char *message = "sign me"; @@ -125,7 +125,7 @@ TEST(AsymCtx, RsaSignVerify) { TEST(AsymCtx, Ed25519SignVerify) { std::string private_key; std::string public_key; - ASSERT_TRUE(AsymCtx::generateKeyPair(AsymAlgorithm::kEd25519, private_key, public_key)); + ASSERT_TRUE(AsymCtx::GenerateKeyPair(AsymAlgorithm::kEd25519, private_key, public_key)); const char *message = "eddsa"; @@ -160,7 +160,7 @@ TEST(AsymCtx, InitializeWithoutKeyFails) { TEST(AsymCtx, TamperedSignatureIsRejected) { std::string private_key; std::string public_key; - ASSERT_TRUE(AsymCtx::generateKeyPair(AsymAlgorithm::kRSA2048, private_key, public_key)); + ASSERT_TRUE(AsymCtx::GenerateKeyPair(AsymAlgorithm::kRSA2048, private_key, public_key)); const char *message = "sign me"; @@ -188,7 +188,7 @@ TEST(AsymCtx, TamperedSignatureIsRejected) { TEST(AsymCtx, EncryptWithEd25519Fails) { std::string private_key; std::string public_key; - ASSERT_TRUE(AsymCtx::generateKeyPair(AsymAlgorithm::kEd25519, private_key, public_key)); + ASSERT_TRUE(AsymCtx::GenerateKeyPair(AsymAlgorithm::kEd25519, private_key, public_key)); AsymCtx ctx; AsymConfig cfg; -- Gitee