diff --git a/CMakeLists.txt b/CMakeLists.txt index ecd550440c835cd0a7f7b3745df423ff766491c0..2f65bc02a58eecd733be33aa64ce9d42bf3f0257 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/README.md b/README.md index cb7fe8728936a475f7292e8755fbf29f24874713..2c67a40e32371aa002c2d0f3c8ffadea1080f125 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 cf812a1dfbc6a07dcbce09d891c855e964f69d60..97d8dd1c84420ab5ec382774b9e7482cdce7f6cf 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 进行非阻塞处理事务的功能 | # 适用环境 diff --git a/examples/crypto/Makefile b/examples/crypto/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..cce046aeec54384311858c706464e8a8945a37f7 --- /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 0000000000000000000000000000000000000000..4b50f28ab4cac3ec28624e972ecf4133de9d9e17 --- /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 0000000000000000000000000000000000000000..18770d25a6a04ada0c4cc39e010d478565f4889f --- /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 0000000000000000000000000000000000000000..714200c7c35395188880d7bedbac8b1d87b3e330 --- /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 0000000000000000000000000000000000000000..7eed9ed0c0ef2fe1e23b47a9169ab5398462675f --- /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/examples/https/Makefile b/examples/https/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..a0b7a8a038cdfa8ba4bb40dd0b27740d24e96917 --- /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 0000000000000000000000000000000000000000..a0b7a8a038cdfa8ba4bb40dd0b27740d24e96917 --- /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 0000000000000000000000000000000000000000..7649f3113cbba366c8ccabd9e6e0a7337808b8c3 --- /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 0000000000000000000000000000000000000000..fb1acca9445fd9395830963a5861c620c0311ad6 --- /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 0000000000000000000000000000000000000000..6c8ade7dca718a96384c8cabe96d10625b7c6fa4 --- /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 0000000000000000000000000000000000000000..b0b7ee7bf5e9db5f80398057774549597cb8d41f --- /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 0000000000000000000000000000000000000000..a0b7a8a038cdfa8ba4bb40dd0b27740d24e96917 --- /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 0000000000000000000000000000000000000000..e9220b4d1c309878da0a31a8a6937e68e4ec3f5c --- /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 0000000000000000000000000000000000000000..f97c550ba29a03d4b64f441bf57590664f2ba1d6 --- /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 0000000000000000000000000000000000000000..727747c93cab022eb15a4330e0fb13a212d224a0 --- /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 0000000000000000000000000000000000000000..f3473eb49bd732c1762d58528dfbc4164357fd11 --- /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 0000000000000000000000000000000000000000..2bf92b282b5c265d7cef0e279207a71189f77891 --- /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 0000000000000000000000000000000000000000..df73cb57a971225e5a582ab9e0fa804ae712c80e --- /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 0000000000000000000000000000000000000000..a64aff7d519f38e3298870d5625308aa95f1ce7c --- /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 0000000000000000000000000000000000000000..7fef670885cee7f417ca8e2133bcdff4a1bbb310 --- /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 0000000000000000000000000000000000000000..b99b80736714a9774b559c999426067220889d58 --- /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 diff --git a/modules/crypto/CMakeLists.txt b/modules/crypto/CMakeLists.txt index ed2bc53bf24a96b3d865eace9a7d8688a175972d..9073fccafdea0e842a82eaf5ed9c3492b84a499c 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 3ba7066896e0ca99cc83aa16f8133f954de6bcd4..cca2b108095dfb3c54d882d7b6ba2bdeb7e68b09 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 0000000000000000000000000000000000000000..2967d51d444f6e0152ffd6aa15fbb2f38a1aa241 --- /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 0000000000000000000000000000000000000000..a48f230d84c653689db694a52e45b7b27c49c315 --- /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 0000000000000000000000000000000000000000..2e85f70ee2a860ffa0eee1866f7a5dbb111479bb --- /dev/null +++ b/modules/crypto/asym_ctx.cpp @@ -0,0 +1,613 @@ +/* + * .============. + * // 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 + +#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) +{ + + // 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; + } + + algo_ = cfg.algo; + 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; + } + + algo_ = cfg.algo; + pkey_ = pkey; + LogInfo("Public key loaded: algo=%s", getAlgoName(algo_)); + return true; + } + + LogErr("No private or public key provided"); + return false; +} + +//! 生成 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 { + 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 == nullptr) { + LogErr("BIO_new failed for private key"); + 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"); + 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); + + // Write public key + bio = BIO_new(BIO_s_mem()); + if (bio == nullptr) { + LogErr("BIO_new failed for public key"); + return false; + } + + SetScopeExitAction([bio] { BIO_free(bio); }); + if (PEM_write_bio_PUBKEY(bio, pkey) <= 0) { + LogErr("PEM_write_bio_PUBKEY failed"); + 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); + + LogInfo("Key pair generated: algo=%s", getAlgoName(algo)); + return true; + + } catch (...) { + 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 0000000000000000000000000000000000000000..872efd4e99d3176c596c85d3c241f8a3f11398c4 --- /dev/null +++ b/modules/crypto/asym_ctx.h @@ -0,0 +1,194 @@ +/* + * .============. + * // 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 0000000000000000000000000000000000000000..17ccf00ec576024ae3837beac27f86e419bf26b7 --- /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 0000000000000000000000000000000000000000..116e77bd2f9e2234ebcc600c184b87c7ff8acdc0 --- /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 0000000000000000000000000000000000000000..79e700432a9196f433510abd807a27acebc7897b --- /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 0000000000000000000000000000000000000000..261a027c6820dd34c7dc5bdcf88e38a2aa153f66 --- /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 0000000000000000000000000000000000000000..2a32ed62d94bf8930ed977ff84650e56556a8d29 --- /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 0000000000000000000000000000000000000000..adb3bfc4b56b45bb561ade3db44602d29a8b44ef --- /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 0000000000000000000000000000000000000000..b3426c131b5f91e992aabf4bfb27fd9181df8b40 --- /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 Binary files /dev/null and b/modules/crypto/images/image-1.png differ diff --git a/modules/crypto/images/image.png b/modules/crypto/images/image.png new file mode 100644 index 0000000000000000000000000000000000000000..2c32e1632dd6baa13fc139aeb806835ebb722fcb Binary files /dev/null and b/modules/crypto/images/image.png differ diff --git a/modules/http/Makefile b/modules/http/Makefile index 6fc3bb024bc8d5d8a3384f8b5bdd869051f6245a..ba974a7e63863cf1582f4e319c4bc275ee4233a0 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 c4530a62290fed0078fd5349014951ff864672b7..28ef50ec5beebf43472d2b164ab3da7631ca9977 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 14a2c013aaf55bfe7f94a684cc667fb99fb9cda7..1068ce32b400622fbaf61e8c9dc12bdcc5f30c65 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 7d408506674e4e413395810eefa86e561c292398..fe03525f3e18fb43b2f1730e7518c5aeba9268cb 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 0000000000000000000000000000000000000000..29406b59bcc7f580b8f51b656a6f9dcf89596c8d --- /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 0000000000000000000000000000000000000000..c0505d3c00828db32a99db1d6a29061fe52dc228 --- /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 0000000000000000000000000000000000000000..df1f77acdb75f3d324c5a5a3a58264016c31463e --- /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 0000000000000000000000000000000000000000..f59dfe82be0eca42714f6625a13a5a0203d1ccf6 --- /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 0000000000000000000000000000000000000000..91ecc97a535cfd3c029381992ce481232d0c6761 --- /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 0000000000000000000000000000000000000000..facc2409d4c179ade97f274f873dc94316a827f4 --- /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 0000000000000000000000000000000000000000..c7aa478267de830fff6b6c0ebf63c0482f1cd8be --- /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 0000000000000000000000000000000000000000..af1fd2c479b26d8edb8876736994a4a34ccccf5a --- /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 0000000000000000000000000000000000000000..5c97ab23f1d42711be2535fdaf4574719f3c60e3 --- /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 0000000000000000000000000000000000000000..47af4dd9e20edad54771190a311353c9d50d502a --- /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 0000000000000000000000000000000000000000..bdbd967adeadc58669bee57a8141aaa78dbaa6a9 --- /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 0000000000000000000000000000000000000000..57e8ce9b50e17bebbcd54f2cb4f70dc95ecfc559 --- /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 0000000000000000000000000000000000000000..2be54ff7868f086c2e1db9ce975cfc0fa1d9310d --- /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 0000000000000000000000000000000000000000..c080e696b9151942283a257afc03399ceb253f56 --- /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_ diff --git a/modules/network/CMakeLists.txt b/modules/network/CMakeLists.txt index a2fe0b8580ec2d145096afd27f5ad11bcb4badd4..e5262cc7292f766183f80314c8296dc51f89dfd0 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 e5823adf682292d7ce1d300096446e4b07ca2e69..39bf09e9d83fa9dc23a290e46d6f3132f511e376 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 0000000000000000000000000000000000000000..3b4b50a140d5c647a38f87f67ff2f281890f49a9 --- /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 0000000000000000000000000000000000000000..7d31b3c5fb20e6d037d2b346865764ed67125c78 --- /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 0000000000000000000000000000000000000000..dc6da4e3e08f453bdc08e5a27896d211ac4d2e3e --- /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 0000000000000000000000000000000000000000..ad25a3c290ebedb9a6e3348eda3c84dfd62a102d --- /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 0000000000000000000000000000000000000000..256528d783b389496ec02b47bb23040024ba0806 --- /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 0000000000000000000000000000000000000000..254448f8752e249328d9d279171cfe61fffdb33d --- /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 0000000000000000000000000000000000000000..a271bc0859fcd50398020139a0587314ba4e89a2 --- /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 0000000000000000000000000000000000000000..c428e2f711b4ee8e9aad63bc01ef4f4ef0d787a7 --- /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 0000000000000000000000000000000000000000..e8747df016b5610f82def491dffd68edd713aa33 --- /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 0000000000000000000000000000000000000000..9e6f573da1ff95f0d05612fa6269b82a8b4719e9 --- /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 0000000000000000000000000000000000000000..a790f5d51691092153dff22f6f559c871a8c4f78 --- /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 0000000000000000000000000000000000000000..127c2d86844b431e1a9d529c1c314373c7bc8266 --- /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