
1. 项目概述与核心价值最近在调试一个需要分析HTTPS流量的内部工具时我遇到了一个经典难题如何在不修改客户端和服务端代码的前提下透明地解密和查看HTTPS请求与响应的内容直接抓包看到的是加密的TLS数据而修改系统根证书信任库又过于侵入且影响全局。这让我想起了“中间人代理”这个老牌技术。它就像一个透明的“翻译官”坐在客户端和真实服务器之间既能与客户端建立安全的HTTPS连接又能与服务器建立另一个HTTPS连接从而有机会看到明文的通信内容。这个项目就是使用C和轻量级的cpp-httplib库从零开始构建一个支持HTTPS的中间人代理。我们不仅会实现代理转发更关键的是要动态生成和签发被客户端信任的SSL证书并完成请求与响应的拦截与修改。这不仅仅是写一个转发器更是对HTTPS/TLS协议、公钥基础设施PKI以及网络编程的一次深度实践。对于从事安全研究、API调试、流量分析或需要深度定制网络行为的开发者来说掌握这套技术栈非常实用。即使你只是对网络底层原理好奇跟着走一遍这个流程也能彻底弄明白浏览器那个“小锁头”背后到底发生了什么以及“中间人攻击”的基本原理和防御方法。2. 核心原理与架构设计2.1 HTTPS中间人代理的工作机制要理解我们构建什么首先得拆解HTTPS中间人代理的核心工作流程。它本质上扮演了两个角色对客户端来说它是“目标服务器”对真实服务器来说它是“客户端”。监听与连接代理服务器启动监听某个端口如8888。客户端如浏览器配置代理指向该地址。HTTPS连接建立客户端侧当客户端发起一个CONNECT请求这是HTTP代理用于建立隧道连接的标准方法常用于HTTPS时代理会与客户端协商建立TLS连接。这里的关键是代理需要向客户端出示一个针对目标域名例如www.example.com的SSL证书。为了让客户端信任这个证书这个证书必须要么由客户端信任的根证书签发要么客户端手动信任了我们的代理根证书。证书的动态签发代理不可能预先拥有互联网上所有域名的证书。因此它需要具备一个自签名的根证书CA Certificate并能在运行时针对客户端请求的任意域名动态生成一张由该根证书签名的“叶子证书”。这个过程模拟了正规CA的签发行为。HTTPS连接建立服务器侧代理使用客户端原本想要访问的真实域名与目标服务器建立标准的HTTPS连接。这里使用的是服务器真实的证书代理作为标准的TLS客户端验证该证书可选但建议。数据双向转发与拦截两个TLS连接建立后代理就拥有了两条安全通道。它可以从客户端TLS连接中解密出明文HTTP请求进行查看、记录或修改然后将这个请求通过服务器侧的TLS连接加密发送出去。反之将服务器的响应解密、处理后再加密返回给客户端。整个架构的难点和核心就在于第2步和第3步如何管理证书、如何动态签发、如何让客户端信任我们。cpp-httplib库内置了SSL支持大大简化了TLS通信的编程复杂度让我们能更专注于代理逻辑本身。2.2 技术选型为什么是cpp-httplibC实现网络服务有多种选择从底层的socket API到Boost.Asio再到各种HTTP库。我选择cpp-httplib主要基于以下几点考量轻量级与单头文件它是一个单头文件库只需包含httplib.h无需复杂的编译和链接第三方库的过程集成成本极低非常适合快速原型开发和嵌入式部署。内置SSL支持它封装了OpenSSL或mbed TLS的接口提供了简单的SSLServer和SSLClient类让我们可以直接在应用层处理HTTPS而无需深入啃食OpenSSL复杂的BIO和上下文管理。简洁的API其API设计直观建立服务器、定义路由、处理请求的代码看起来非常清晰降低了实现HTTP代理逻辑的心智负担。足够的性能对于中间人代理这种I/O密集型应用cpp-httplib基于线程池的模型能够提供不错的并发性能。虽然可能不及专门优化的异步框架但对于调试、分析及中小流量场景完全足够。当然它也有局限例如异步支持较弱、定制化程度不如底层库。但对于我们这个以演示原理和核心功能为主的项目它的优势非常突出。注意cpp-httplib的SSL客户端在验证服务器证书时行为比较基础。在生产级中间人代理中我们可能需要更精细的证书验证逻辑甚至需要忽略证书错误仅用于调试环境。本项目会展示如何处理这种情况。3. 核心组件实现详解3.1 自签名根证书的创建与管理一切始于信任的源头——我们自己的根证书。这个证书将用于为我们动态生成的所有站点证书签名。# 使用OpenSSL生成根证书私钥无密码 openssl genrsa -out ca.key 2048 # 使用私钥生成自签名的根证书CA Certificate openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \ -subj /CCN/STBeijing/LBeijing/OMyProxy CA/CNMyProxy Root Certificate关键参数解析genrsa -out ca.key 2048: 生成一个2048位的RSA私钥这是证书安全的基础。req -new -x509 ...:-x509表示直接生成一个自签名证书而不是证书签名请求CSR。-days 3650: 证书有效期10年避免频繁更换。-subj: 设置证书的主题信息。其中CNMyProxy Root Certificate是证书的通用名这个名称会显示在客户端的证书颁发者中。生成ca.crt和ca.key后我们需要将ca.crt导入到客户端的信任根证书存储区。这是让整个代理工作的前提。Windows: 双击ca.crt选择“安装证书”放入“受信任的根证书颁发机构”。macOS: 使用钥匙串访问将证书拖入“系统”钥匙串然后信任该证书。Linux: 拷贝到/usr/local/share/ca-certificates/然后执行sudo update-ca-certificates。实操心得在开发环境中可以为浏览器或系统单独创建一个证书信任配置文件避免污染全局环境。例如启动Chrome时使用--ignore-certificate-errors和--ignore-certificate-errors-spki-list参数仅忽略特定证书的错误是更安全的选择。3.2 动态证书签发引擎的实现这是代理的核心“黑魔法”。我们不能预知用户会访问哪个域名因此必须能实时生成证书。// 伪代码展示核心逻辑 #include openssl/x509.h #include openssl/x509v3.h #include openssl/pem.h class CertificateGenerator { private: X509* ca_cert; EVP_PKEY* ca_key; public: bool loadCA(const std::string cert_path, const std::string key_path) { // 加载CA证书和私钥到内存 FILE* fp fopen(cert_path.c_str(), r); ca_cert PEM_read_X509(fp, nullptr, nullptr, nullptr); fclose(fp); // ... 类似地加载 ca_key ... return (ca_cert ca_key); } std::pairX509*, EVP_PKEY* generateCertForDomain(const std::string domain) { // 1. 生成新的RSA密钥对 EVP_PKEY* pkey EVP_PKEY_new(); RSA* rsa RSA_generate_key(2048, RSA_F4, nullptr, nullptr); EVP_PKEY_assign_RSA(pkey, rsa); // 2. 创建X509证书结构并设置版本、序列号等 X509* cert X509_new(); X509_set_version(cert, 2); // X509v3 ASN1_INTEGER_set(X509_get_serialNumber(cert), generate_serial()); X509_gmtime_adj(X509_get_notBefore(cert), 0); X509_gmtime_adj(X509_get_notAfter(cert), 365 * 86400); // 1年有效期 // 3. 设置证书主题使用者这里CN设置为请求的域名 X509_NAME* name X509_get_subject_name(cert); X509_NAME_add_entry_by_txt(name, CN, MBSTRING_ASC, (const unsigned char*)domain.c_str(), -1, -1, 0); // 设置颁发者为我们的CA X509_set_issuer_name(cert, X509_get_subject_name(ca_cert)); // 4. 设置公钥 X509_set_pubkey(cert, pkey); // 5. 添加扩展项主题备用名称SAN这是现代浏览器必须的 X509_EXTENSION* ext nullptr; X509V3_CTX ctx; X509V3_set_ctx_nodb(ctx); X509V3_set_ctx(ctx, ca_cert, cert, nullptr, nullptr, 0); const char* san_str (DNS: domain).c_str(); ext X509V3_EXT_conf_nid(nullptr, ctx, NID_subject_alt_name, san_str); X509_add_ext(cert, ext, -1); X509_EXTENSION_free(ext); // 6. 使用CA私钥对证书进行签名 X509_sign(cert, ca_key, EVP_sha256()); return {cert, pkey}; } };关键点解析密钥与证书对象每个动态证书都需要自己独立的密钥对EVP_PKEY和证书结构X509。主题与颁发者证书的Subject.CN通常设置为请求的域名而Issuer必须设置为我们的CA证书的主题这样证书链才能建立。主题备用名称这是至关重要的一步。现代浏览器如Chrome 58主要依赖Subject Alternative Name扩展来验证域名而不是Subject.CN。如果不设置SAN即使证书被信任浏览器也会报错“证书与域名不匹配”。签名使用CA的私钥ca_key和摘要算法如SHA256对证书进行签名完成权威性背书。3.3 基于cpp-httplib的代理服务器框架有了证书签发能力我们就可以搭建代理服务器了。cpp-httplib使得搭建一个支持SSL的服务器变得异常简单。#include httplib.h #include certificate_generator.h // 我们上面实现的类 int main() { CertificateGenerator cert_gen; if (!cert_gen.loadCA(ca.crt, ca.key)) { std::cerr Failed to load CA certificate or key! std::endl; return -1; } // 创建SSL服务器需要服务器证书和私钥。 // 注意这里初始化的证书是用于代理服务器自身身份比如它的管理页面 // 不是用于动态签发的。我们可以用一个固定的域名证书或者临时生成一个。 httplib::SSLServer svr(server.crt, server.key); // 1. 处理HTTP代理请求非CONNECT的普通HTTP请求 svr.Get(.*, [](const httplib::Request req, httplib::Response res) { // 这里实现普通HTTP请求的转发和拦截 std::string target_host req.get_header_value(Host); // ... 使用httplib::Client转发请求处理响应 ... }); svr.Post(.*, [](const httplib::Request req, httplib::Response res) { // 类似地处理POST等其它方法 }); // 2. 处理HTTPS隧道请求CONNECT方法 svr.set_pre_routing_handler([](const httplib::Request req, httplib::Response res) - bool { if (req.method CONNECT) { // 这是建立HTTPS隧道的关键请求 std::string host_port req.path; // 例如 “www.example.com:443” // 解析出主机名和端口 // ... 解析逻辑 ... // 动态生成该主机名的证书 auto [cert, pkey] cert_gen.generateCertForDomain(hostname); // 将证书和私钥转换为内存BIO以便cpp-httplib使用 // ... 转换逻辑 ... // 这里需要“劫持”连接创建一个新的SSL连接给客户端使用动态生成的证书。 // cpp-httplib的默认流程不直接支持在CONNECT后动态切换证书。 // 因此我们需要更底层的处理接受CONNECT请求后发送200 Connection Established // 然后从原始socket中提升为SSL连接并指定新的证书上下文。 // 这涉及到对cpp-httplib内部socket和SSL上下文的管理是最大的难点。 // 伪代码示意关键步骤 // res.status 200; // 发送成功响应 // 从req中获取底层socket // 创建新的SSL_CTX并设置动态证书和私钥 // 将socket包装成SSL*连接 // 开始双向数据转发循环 // 由于篇幅此处不展开完整代码下文会详述。 return true; // 告诉框架我们已经处理了这个请求不再走默认路由。 } return false; // 其他请求继续由默认路由处理 }); svr.listen(0.0.0.0, 8888); return 0; }这个框架勾勒出了核心结构一个处理普通HTTP请求的路由和一个拦截CONNECT方法以建立HTTPS隧道的预处理句柄。难点在于CONNECT处理中如何将原始的TCP连接升级为使用我们动态证书的SSL连接。4. HTTPS隧道拦截的核心实现4.1 CONNECT请求的处理与隧道建立当浏览器发起CONNECT www.example.com:443 HTTP/1.1请求时代理需要做以下几件事解析目标从请求路径中提取主机名www.example.com和端口443。响应建立向客户端发送HTTP/1.1 200 Connection Established\r\n\r\n。发送完这个响应后后续的数据将不再是HTTP协议而是原始的TCP数据流。接管Socket此时与客户端的TCP连接Socket仍然开放。我们需要获取到这个原始的socket文件描述符。创建服务端SSL上下文使用OpenSSL API创建一个新的SSL_CTX并将动态为该域名生成的证书和私钥设置到这个上下文中。SSL握手基于原始的socket和新的SSL_CTX创建一个SSL*对象并调用SSL_accept()与客户端进行TLS握手。此时客户端验证的正是我们动态签发的证书。连接目标服务器同时使用另一个httplib::SSLClient或普通的Client如果目标是HTTP与真实的目标服务器www.example.com:443建立连接。这个连接使用服务器真实的证书。双向数据转发现在有两个活跃的连接client_ssl_conn(客户端-代理) 和server_conn(代理-服务器)。我们需要在两个连接之间进行非阻塞的、双向的数据转发。从client_ssl_ssl读取的数据解密后是明文HTTP请求我们可以进行记录或修改然后通过server_conn加密发送出去反之亦然。// 简化的核心转发循环伪代码 void tunnel_forward(SSL* client_ssl, httplib::Client server_client) { int client_fd SSL_get_fd(client_ssl); // 假设server_client能提供与服务端的连接socket fd int server_fd get_server_socket_from_client(server_client); fd_set readfds; char buffer[16 * 1024]; while (true) { FD_ZERO(readfds); FD_SET(client_fd, readfds); FD_SET(server_fd, readfds); int max_fd std::max(client_fd, server_fd) 1; int activity select(max_fd, readfds, nullptr, nullptr, nullptr); if (activity 0) { break; } // 客户端 - 服务器 if (FD_ISSET(client_fd, readfds)) { int bytes_read SSL_read(client_ssl, buffer, sizeof(buffer)); if (bytes_read 0) { break; } // 连接关闭或错误 // 此处buffer中是解密后的明文HTTP请求数据 log_request(buffer, bytes_read); // 可选修改buffer中的数据 server_client.send_raw_data(buffer, bytes_read); // 需要自定义发送方法 } // 服务器 - 客户端 if (FD_ISSET(server_fd, readfds)) { int bytes_read recv(server_fd, buffer, sizeof(buffer), 0); if (bytes_read 0) { break; } // 此处buffer中是来自服务器的原始TLS加密数据如果server_conn是SSL或明文HTTP响应如果是HTTP // 如果是明文响应可以记录或修改 log_response(buffer, bytes_read); SSL_write(client_ssl, buffer, bytes_read); } } }4.2 请求与响应的拦截与修改点在双向转发循环中我们获得了拦截和修改数据的绝佳机会请求拦截在SSL_read(client_ssl)之后我们得到的是完整的明文HTTP请求包括方法、URL、Headers、Body。我们可以记录将请求完整地记录到日志文件或数据库用于调试或审计。修改修改请求头如添加、删除、修改User-Agent,Cookie等甚至修改请求体对POST数据做处理。例如可以全局添加一个认证头。阻断根据规则如黑名单URL直接返回一个自定义的HTTP响应而不转发到真实服务器。响应拦截在从服务器连接recv到数据后如果是HTTPS连服务器则需要先解密这要求代理也作为客户端与服务器完成TLS握手并解密我们得到的是明文HTTP响应。我们可以记录记录状态码、响应头和响应体。修改修改响应内容例如注入JavaScript脚本、修改HTML内容、替换图片资源等。缓存实现响应缓存逻辑对相同的请求直接返回缓存内容。重要提示修改HTTPS响应的内容需要特别小心可能会破坏响应的完整性如Content-Length, Transfer-Encoding或导致数字签名失效。对于非文本内容如图片、视频通常只记录不修改。5. 项目集成、编译与测试5.1 工程组织与依赖管理一个完整的项目目录结构可能如下所示mitm_proxy/ ├── CMakeLists.txt ├── src/ │ ├── main.cpp │ ├── certificate_generator.cpp │ └── certificate_generator.h ├── certs/ │ ├── ca.crt │ ├── ca.key │ └── server.crt # 代理服务器自身静态证书 ├── third_party/ │ └── httplib.h └── build/CMakeLists.txt需要配置OpenSSL库cmake_minimum_required(VERSION 3.10) project(MITMProxy) set(CMAKE_CXX_STANDARD 11) # 查找OpenSSL find_package(OpenSSL REQUIRED) # 包含头文件 include_directories(${OPENSSL_INCLUDE_DIR} ./src ./third_party) # 添加可执行文件 add_executable(mitm_proxy src/main.cpp src/certificate_generator.cpp) # 链接OpenSSL库 target_link_libraries(mitm_proxy ${OPENSSL_LIBRARIES} pthread)5.2 编译与运行# 在项目根目录 mkdir build cd build cmake .. make -j4 # 生成可执行文件 mitm_proxy ./mitm_proxy代理服务器将在0.0.0.0:8888启动。5.3 客户端配置与测试验证配置系统代理将系统的HTTP/HTTPS代理设置为127.0.0.1:8888。或者只为特定浏览器配置。访问HTTPS网站用浏览器访问任何一个HTTPS网站例如https://httpbin.org/anything。观察证书首次访问时浏览器可能会提示证书不安全因为是我们签发的。由于我们已经将ca.crt导入为受信任的根证书这个警告应该消失或者点击“高级”-“继续访问”即可。查看证书详情你会发现颁发者是“MyProxy Root Certificate”。验证拦截查看代理服务器的控制台输出你应该能看到解密的HTTP请求和响应日志。例如[REQ] GET https://httpbin.org/anything Headers: Host: httpbin.org, User-Agent: Mozilla/5.0... [RES] 200 OK Headers: content-type: application/json... Body: {args:{}, headers:{Host:httpbin.org, ...}, url:https://httpbin.org/anything}测试修改功能你可以在代码的转发循环中添加逻辑例如修改所有请求添加一个X-Proxied-By: MyMitmProxy的请求头然后在httpbin.org的响应中查看headers字段确认该头已成功添加。6. 常见问题、安全考量与进阶方向6.1 开发与调试中的典型问题浏览器提示“证书无效”或“NET::ERR_CERT_AUTHORITY_INVALID”原因根证书ca.crt未正确导入或未受信任。排查检查证书是否导入到了“受信任的根证书颁发机构”。在macOS钥匙串中需要双击导入的证书展开“信任”选项将“使用此证书时”设置为“始终信任”。技巧使用浏览器开发者工具的“安全”标签页查看证书链确保证书路径指向你的CA证书。浏览器提示“证书与域名不匹配”原因动态生成的证书缺少Subject Alternative Name扩展或SAN中未包含请求的域名。解决确保在generateCertForDomain函数中正确添加了NID_subject_alt_name扩展并且值为DNS:example.com。代理服务器崩溃或内存泄漏原因OpenSSL对象X509,EVP_PKEY,SSL_CTX等未正确释放多线程环境下证书生成器竞争条件。解决使用RAII思想封装OpenSSL对象对CertificateGenerator的签发方法加锁如果多线程访问使用Valgrind等工具检测内存泄漏。连接速度慢或转发卡顿原因select循环效率问题证书生成耗时RSA 2048生成较慢日志写入阻塞。优化对于高并发考虑使用epoll或kqueue替代select可以预生成一批证书或使用ECC密钥生成更快将日志改为异步写入。6.2 安全警告与负责任的使用这是一个强大的工具但必须负责任地使用仅用于合法用途仅在你拥有完全控制权的网络、设备或明确获得授权的环境下使用。例如调试你自己的应用程序、分析公司内部API流量、安全教学研究。切勿用于非法拦截拦截他人的网络通信可能违反法律和隐私条例。理解风险导入自签名根证书会降低你设备的安全性。恶意软件可能利用这一点进行攻击。建议在虚拟机或专用测试设备上进行使用后及时移除根证书。不要处理敏感信息避免在日志中记录密码、银行卡号等敏感信息。如果必须记录确保日志文件被妥善加密和保护。6.3 性能优化与功能扩展证书缓存为每个域名首次访问生成证书后将其缓存到内存或磁盘。下次相同域名访问时直接复用避免重复的密钥生成和签名计算极大提升性能。连接池对于频繁访问的服务器可以维护一个到后端服务器的连接池避免为每个客户端请求都建立新的TCP/TLS连接。支持WebSocket扩展代理逻辑使其能够正确转发和拦截WebSocket流量这需要解析Upgrade: websocket头并处理二进制帧。规则引擎实现一个配置文件或规则引擎允许用户通过规则基于URL、内容类型、关键字来定义哪些请求需要被记录、修改或阻断。UI管理界面使用cpp-httplib再暴露一个管理用的HTTP API甚至集成一个简单的Web界面来实时查看流量日志、管理规则、清空缓存等。这个项目从原理到实现涵盖了网络、安全、密码学和应用开发的多个层面。亲手实现一遍你会对HTTPS的“安全”二字有更立体和深刻的认识——它既是保护用户的盾其原理也可以成为开发者手中的工具。关键在于使用工具的人。