cpp-httplib 客户端 HTTP 代理实战:set_proxy 配置、代理认证与 NO_PROXY 直连白名单
cpp-httplib 客户端 HTTP 代理实战:set_proxy 配置、代理认证与 NO_PROXY 直连白名单
在公司内网或需要经过特定出口网络访问公网接口时,cpp-httplib 的 httplib::Client 可以非常轻量地把请求经由 HTTP 代理发出:只需要一个 set_proxy() 调用指定代理主机与端口,HTTPS 场景下库会自动通过 CONNECT 方法建立隧道,无需任何额外开关。本文以官方文档 c16-proxy.md 为骨架,结合 httplib.h 的源码实现与 test/test.cc 中的 NoProxyTest 测试套件,完整讲解代理的基础用法、三类代理认证、代理认证与端服务器认证的并存、NO_PROXY 直连白名单的精确匹配规则,以及从环境变量读取代理配置的安全姿势。读完即可在自己项目中安全、可控地接入代理链路。
基本用法:一行代码把请求送进代理
先连接代理主机与端口,再正常发起请求即可,其余逻辑全部由库内部完成:
httplib::Client cli("https://api.example.com");
cli.set_proxy("proxy.internal", 8080);
auto res = cli.Get("/users");
set_proxy() 的签名在 httplib.h 声明为 void set_proxy(const std::string &host, int port),对应的 ClientImpl::set_proxy() 实现(httplib.h)只做三件事:记录 proxy_host_、proxy_port_,然后加锁断开当前已建立的连接。也就是说,调用 set_proxy() 会让正在复用的 keep-alive 连接失效——这保证了后续请求一定走新的、经过代理的 socket,而不是继续沿用直连通道。
连接究竟建到了哪里
从源码看,代理是否生效由 ClientImpl::is_proxy_enabled_for_host()(httplib.h)判定,create_client_socket()(httplib.h)在代理启用时把目标地址从 host_ 换成 proxy_host_:proxy_port_。因此「代理」本质上只是改变了 TCP 连接的落地位置:HTTP 请求以完整 URL 形式发给代理,代理再代为转发。
HTTPS 场景:CONNECT 隧道与 407 自动重试
文档明确指出:HTTPS 请求会通过 CONNECT 方法在代理上打一条隧道,cpp-httplib 侧无需任何特殊配置。这条结论在源码里可以得到完整印证:
SSLClient::connect_with_proxy()(httplib.h)在建立 TLS 之前,先向代理发送一条 method = "CONNECT"、path = host:port(通过 detail::make_host_and_port_string_always_port() 保证永远带端口号,见 httplib.h 注释)的请求;代理返回 2xx 后,隧道建立,再在隧道内完成 TLS 握手与真正的业务请求。整条隧道对代理透明,代理只能看到目标主机名和端口,看不到加密后的请求体。
如果代理返回 407 Proxy Authentication Required,且应用预先通过 set_proxy_digest_auth() 配置了用户名密码,库会解析 Proxy-Authenticate 挑战、关闭旧连接、重新建连并携带 Authorization 摘要重发 CONNECT(httplib.h)。这就是「HTTPS + Digest 代理认证」在 cpp-httplib 里的完整闭环。
注意一个细节:普通 HTTP 经代理时若收到 407,库会只在「确实是代理跳发来的 407」时才重试——HTTPS 隧道内由源站返回的 407 不会被误判为代理挑战(见 httplib.h 的
is_proxy判定逻辑)。
给代理加上认证
代理本身要求认证时,cpp-httplib 提供三套互不冲突的 API:
| API | 认证类型 | 说明 |
|---|---|---|
set_proxy_basic_auth(username, password) |
Basic | 最常用,明文 Base64 编码,仅适合信任的内网代理 |
set_proxy_bearer_token_auth(token) |
Bearer Token | 携带不记名令牌 |
set_proxy_digest_auth(username, password) |
Digest | 挑战-响应式摘要认证,需以 OpenSSL(或其他 TLS 后端)编译,见 httplib.h 的 #ifdef CPPHTTPLIB_SSL_ENABLED 保护 |
cli.set_proxy("proxy.internal", 8080);
cli.set_proxy_basic_auth("user", "password"); // Basic
cli.set_proxy_bearer_token_auth("token"); // Bearer(二选一即可)
// 使用 Digest 的前提:库以 OpenSSL / MbedTLS / GnuTLS 等 TLS 后端编译
cli.set_proxy_digest_auth("user", "password");
三者的内部实现(httplib.h 与 httplib.h)都只是把凭据存进对应的成员变量,真正的写入发生在发送请求时——见下文「凭据只发给该发的一跳」。
代理认证 ≠ 端服务器认证:两类凭据互不干扰
官方文档特别强调:代理认证与端服务器认证是两回事。内网代理要验证「你是谁」,远端 API 要验证「你有没有调用权限」,两者可以同时存在(组合示例参考 C05. Basic 认证 与 C06. Bearer Token 调用 API):
cli.set_proxy("proxy.internal", 8080);
cli.set_proxy_basic_auth("proxy-user", "proxy-pass"); // 代理跳:Proxy-Authorization
cli.set_bearer_token_auth("api-token"); // 端服务器:Authorization
请求发出时,代理凭据写入 Proxy-Authorization 头,端服务器凭据写入 Authorization 头。这条规则的实现非常讲究,见 httplib.h:
Proxy-Authorization只在代理能读到这条消息时才发送:条件是「该主机未被 NO_PROXY 命中」且「(非 HTTPS 请求 或 当前就是 CONNECT 请求)」。这样既不会把代理凭据漏给 NO_PROXY 直连的服务器,也不会把它漏进 TLS 隧道内层的加密请求里——隧道内的消息只有端服务器能看到;Authorization(端服务器凭据)则只在非 CONNECT 请求上发送,避免把 API 令牌交给代理。
NO_PROXY 直连白名单:精确到点的匹配规则
内网端点往往不值得绕代理。set_no_proxy() 接收一个模式列表,命中模式的目标主机将直连,跳过代理:
cli.set_proxy("proxy.internal", 8080);
cli.set_no_proxy({"internal.corp", "10.0.0.0/8", "*.dev.local"});
条目支持四种形式(官方文档原文,均已在 detail::parse_no_proxy_entry() 中实现,见 httplib.h):
*—— 所有主机全部直连(等价于全局禁用代理);- 主机名后缀,如
example.com—— 命中example.com本体及任意子域(foo.example.com);写不写前导点(.example.com)语义相同,实现中会被剥掉; - 单一 IP 字面量,如
192.168.1.1、::1(也接受[::1]带括号写法); - CIDR 网段,如
10.0.0.0/8、fe80::/10(IPv6 同样可用[fe80::]/10括号形式)。
匹配语义的三个硬性约束
- 主机名大小写不敏感 + 点边界匹配。
parse_no_proxy_entry()会把模式转小写并去掉首尾点;host_matches_no_proxy()(httplib.h)对后缀命中要求前一个字符必须是.,源码注释直接写明:这是为了防止evilexample.com这类域名撞上example.com条目。测试 test/test.cc 中的NoProxyTest.ExactHostnameBypasses、NoProxyTest.EvilExampleDoesNotMatchExample、NoProxyTest.ExampleDotEvilDoesNotMatchExample分别验证了这三种情况。 - IP 一律走
inet_pton规范化。normalize_target()(httplib.h)对目标主机做规范化,CIDR 命中由ip_in_cidr()(httplib.h)按字节比对。因此127.0.0.1的旁门左道写法127.000.000.001不会命中127.0.0.1条目——正则化后的字节序列不同。 - 命中即免代理,认证头一并撤销。目标主机命中 NO_PROXY 后,
is_proxy_enabled_for_host()返回 false,请求走直连;配合前面提到的「Proxy-Authorization仅在代理跳可读时发送」逻辑,直连请求自动不再携带代理凭据,不会把凭据泄给白名单内的服务器。
无效条目被静默丢弃
实现上 set_no_proxy()(httplib.h)会依次 trim_copy 并对每个条目调用 parse_no_proxy_entry(),解析失败的条目直接跳过(continue),绝不报错。官方文档列出两类典型的无效写法:
example.com:8080—— 带端口的主机条目不受支持。解析器检测到:即返回 false(httplib.h),这与库内其他以主机名为键的 API(如addr_map_地址映射)保持一致的设计约束;- 畸形 CIDR(如
10.0.0.0/斜杠后无前缀)——同样被拒绝,不会退化成/32或/128而误伤流量(httplib.h)。
另外注意:空字符串条目、纯 . 或 .. 之类的条目也会在 trim 与去点后被丢弃。
从环境变量读取代理:cpp-httplib 的明确立场
cpp-httplib 本体完全不读取 HTTP_PROXY / HTTPS_PROXY / NO_PROXY 环境变量。文档明确指出,这与 set_ca_cert_path() 的设计哲学一致:配置一律显式化,绝不隐式读取进程环境。想让应用跟随环境变量走,需在应用侧自行读取后传给 set_proxy() / set_no_proxy()。
官方示例给出了 NO_PROXY 的解析写法(逗号分隔、空条目过滤):
if (const char *v = std::getenv("no_proxy")) {
std::vector<std::string> patterns;
std::stringstream ss(v);
for (std::string item; std::getline(ss, item, ',');) {
if (!item.empty()) { patterns.push_back(item); }
}
cli.set_no_proxy(patterns);
}
httpoxy 安全隐患:大写的 HTTP_PROXY 别用
文档特别给出安全红线:如果自行读取 HTTP_PROXY,只应采纳小写 http_proxy。原因是 CVE-2016-5385(即著名的 "httpoxy" 漏洞):在 CGI / FastCGI 等网关环境下,客户端发来的 Proxy: 请求头会被某些服务器转写为 HTTP_PROXY 环境变量,攻击者可借此把受害进程的出站流量劫持到自己的服务器上。而 HTTPS_PROXY、NO_PROXY 因名字不以 HTTP_ 开头、不受该污染路径影响,大小写读取均安全。这是任何「从环境变量装配代理配置」的代码都必须遵守的防御原则。
重定向场景下的代理继承与凭据隔离
一个容易忽略但源码处理得很周密的点是跨主机重定向。ClientImpl::setup_redirect_client()(httplib.h)在跳转时:
- 无条件复制代理配置(
proxy_host_、proxy_port_、no_proxy_entries_以及三类代理凭据),并注释说明:NO_PROXY 的按目标主机判定会在每次发送时重新评估,因此跳到未被豁免的主机仍会走代理; - 故意不复制端服务器凭据(basic / bearer / digest),源码注释引用 RFC 9110 §15.4:跨主机重定向时不得转发原凭据,防止令牌被带到第三方域名;同时从请求头中清理
Host、Proxy-Authorization、Authorization、Cookie、Cookie2(httplib.h),避免把这些逐跳/逐域敏感头带给新目标。
用仓库自带设施快速验证代理行为
仓库为代理功能准备了可直接运行的测试基建:
- test_proxy.cc 是专门的代理集成测试程序,配合 test/proxy 目录下的 Squid 代理 Docker 环境使用;
- test/proxy/basic_squid.conf 与 test/proxy/basic_passwd 提供 Basic 认证代理的最小配置;test/proxy/digest_squid.conf 与 test/proxy/digest_passwd 对应 Digest 认证;test/proxy/docker-compose.yml 一键拉起代理与目标服务;
- test/test.cc 的
NoProxyTest测试套件内置了一个ProxyAndTargetServers夹具(用bind_to_any_port+listen_after_bind起一个可计数的 mock 代理),覆盖了「精确主机名直连」「子域直连」「邪恶域名不误伤」「前导点模式匹配裸域名」等全部 NO_PROXY 边界情形,还通过last_had_proxy_authz_断言验证了凭据随免代理路径自动撤销的行为——写自己的 NO_PROXY 逻辑前,值得通读一遍这些用例。
小结
cpp-httplib 的代理支持是一套「显式、分层、防泄漏」的完整设计:set_proxy() 一行接入,CONNECT 隧道对 HTTPS 透明;Basic / Bearer / Digest 三种代理认证按需选用;Proxy-Authorization 与 Authorization 严格按跳隔离;set_no_proxy() 提供带点边界、CIDR 规范化与无效条目容忍的精确白名单;环境变量则由应用显式接管,并需警惕 HTTP_PROXY 的 httpoxy 污染。对绝大多数内网出网需求,这份配置清单已经足够闭环。