From 290c4c0bbee4fc22e8250873c7fd6c08dafce9d7 Mon Sep 17 00:00:00 2001 From: "Kim, Hyuk" Date: Wed, 28 Oct 2026 10:09:25 +0900 Subject: [PATCH] Add set_tunnel_hostname_addr_map() and deprecate set_hostname_addr_map() (#2647) Through a CONNECT proxy it is the proxy that connects to the origin, and the CONNECT target was always the original hostname, so there was no way to send the tunnel to another address. set_tunnel_hostname_addr_map() sets that target on SSLClient, Client and WebSocketClient. The CONNECT request's Host follows it, including the Digest retry, while the Host header, the SNI and the certificate check inside the tunnel keep the original hostname. The map is also copied to a redirect client. set_hostname_addr_map() is deprecated in favor of set_direct_hostname_addr_map(), which behaves the same and names the connection it applies to. The two maps are independent and correspond to curl's --resolve and --connect-to; neither applies to the proxy's own address. --- README-websocket.md | 3 +- README.md | 40 ++- docs-src/pages/en/tour/09-whats-next.md | 2 +- docs-src/pages/ja/tour/09-whats-next.md | 2 +- httplib.h | 80 +++++- test/test.cc | 309 ++++++++++++++++++++++-- 6 files changed, 399 insertions(+), 37 deletions(-) diff --git a/README-websocket.md b/README-websocket.md index 49175c6c..b787118f 100644 --- a/README-websocket.md +++ b/README-websocket.md @@ -233,7 +233,8 @@ void set_address_family(int family); void set_ipv6_v6only(bool on); void set_socket_options(SocketOptions socket_options); void set_interface(const std::string &intf); -void set_hostname_addr_map(std::map addr_map); +void set_direct_hostname_addr_map(std::map addr_map); +void set_tunnel_hostname_addr_map(std::map addr_map); // SSL configuration (wss:// only, SSL builds only) void set_ca_cert_path(const std::string &ca_cert_file_path, diff --git a/README.md b/README.md index 3c1304cb..73cd39ca 100644 --- a/README.md +++ b/README.md @@ -1477,17 +1477,17 @@ The same method is available on `httplib::ws::WebSocketClient`. ### Override the connection target for a hostname -`set_hostname_addr_map` redirects where the socket connects, without changing -the identity of the request. The hostname the client was constructed with keeps -supplying the `Host` header, the SNI, and the name that the server certificate -is verified against, so this is a connection-level override only, not a way to -talk to a different origin. +`set_direct_hostname_addr_map` redirects where the socket connects, without +changing the identity of the request. The hostname the client was constructed +with keeps supplying the `Host` header, the SNI, and the name that the server +certificate is verified against, so this is a connection-level override only, +not a way to talk to a different origin. ```cpp httplib::Client cli("https://example.com"); // Connect to this IP address instead of resolving "example.com" -cli.set_hostname_addr_map({{"example.com", "192.168.1.10"}}); +cli.set_direct_hostname_addr_map({{"example.com", "192.168.1.10"}}); ``` A mapped value may be an IP literal or another hostname. An IP literal is used @@ -1495,13 +1495,37 @@ as-is; anything else is resolved as a name, so a host that is only reachable under a different name works too: ```cpp -cli.set_hostname_addr_map({{"example.com", "internal.example.lan"}}); +cli.set_direct_hostname_addr_map({{"example.com", "internal.example.lan"}}); ``` An empty value is ignored, leaving the original hostname as the connection target. -The same method is available on `httplib::ws::WebSocketClient`. +The direct map applies only when the client connects to the origin itself. +Through a proxy, use `set_tunnel_hostname_addr_map`: its value becomes the +`CONNECT` target (and that request's `Host`) that the proxy is asked to connect +to (HTTPS and WebSocket). The `Host` header, the SNI and the certificate check +inside the tunnel keep the original hostname: + +```cpp +httplib::Client cli("https://sni.example"); + +cli.set_tunnel_hostname_addr_map({{"sni.example", "server.example"}}); +cli.set_proxy("proxy.example", 3128); +// CONNECT server.example:443, SNI sni.example +``` + +The two maps are independent; to use the same target on both paths, set both. +They correspond to curl's `--resolve` and `--connect-to`. + +Neither map applies to the proxy's own address ("direct" means not through a +proxy, as in a PAC file's `DIRECT`). To connect to the proxy at a fixed +address, pass an IP literal to `set_proxy()`. + +`set_hostname_addr_map` is deprecated and works the same as +`set_direct_hostname_addr_map`. + +These methods are also available on `httplib::ws::WebSocketClient`. ### Automatic Path Encoding diff --git a/docs-src/pages/en/tour/09-whats-next.md b/docs-src/pages/en/tour/09-whats-next.md index 0ec66d23..8b2e5bb4 100644 --- a/docs-src/pages/en/tour/09-whats-next.md +++ b/docs-src/pages/en/tour/09-whats-next.md @@ -214,7 +214,7 @@ svr.listen("/tmp/httplib.sock", 80); // The port is unused, but must not be 0 // Client httplib::Client cli("http://localhost"); cli.set_address_family(AF_UNIX); -cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}}); +cli.set_direct_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}}); auto res = cli.Get("/"); ``` diff --git a/docs-src/pages/ja/tour/09-whats-next.md b/docs-src/pages/ja/tour/09-whats-next.md index 0ff8625a..62ff091c 100644 --- a/docs-src/pages/ja/tour/09-whats-next.md +++ b/docs-src/pages/ja/tour/09-whats-next.md @@ -214,7 +214,7 @@ svr.listen("/tmp/httplib.sock", 80); // ポート番号は使われない(0 // クライアント httplib::Client cli("http://localhost"); cli.set_address_family(AF_UNIX); -cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}}); +cli.set_direct_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}}); auto res = cli.Get("/"); ``` diff --git a/httplib.h b/httplib.h index 1fea12dc..d223fd29 100644 --- a/httplib.h +++ b/httplib.h @@ -2927,7 +2927,12 @@ public: size_t is_socket_open() const; socket_t socket() const; + [[deprecated("Use set_direct_hostname_addr_map() instead.")]] void set_hostname_addr_map(std::map addr_map); + void + set_direct_hostname_addr_map(std::map addr_map); + void + set_tunnel_hostname_addr_map(std::map addr_map); void set_default_headers(Headers headers); @@ -3059,6 +3064,9 @@ protected: // hostname; only the connection target changes, never the identity. std::map addr_map_; + // Hostname to CONNECT target map, used only through a proxy. + std::map tunnel_addr_map_; + // Default headers Headers default_headers_; @@ -3348,7 +3356,12 @@ public: size_t is_socket_open() const; socket_t socket() const; + [[deprecated("Use set_direct_hostname_addr_map() instead.")]] void set_hostname_addr_map(std::map addr_map); + void + set_direct_hostname_addr_map(std::map addr_map); + void + set_tunnel_hostname_addr_map(std::map addr_map); void set_default_headers(Headers headers); @@ -4748,7 +4761,12 @@ public: set_connection_timeout(const std::chrono::duration &duration); void set_interface(const std::string &intf); + [[deprecated("Use set_direct_hostname_addr_map() instead.")]] void set_hostname_addr_map(std::map addr_map); + void + set_direct_hostname_addr_map(std::map addr_map); + void + set_tunnel_hostname_addr_map(std::map addr_map); void set_proxy(const std::string &host, int port); void set_proxy_basic_auth(const std::string &username, @@ -4820,6 +4838,9 @@ private: // hostname; only the connection target changes, never the identity. std::map addr_map_; + // Hostname to CONNECT target map, used only through a proxy. + std::map tunnel_addr_map_; + std::string proxy_host_; int proxy_port_ = -1; std::string proxy_basic_auth_username_; @@ -13046,6 +13067,17 @@ make_host_and_port_string_always_port(const std::string &host, int port) { return prepare_host_string(host) + ":" + std::to_string(port); } +// Request target of a CONNECT for `host`, with `addr_map` applied. +inline std::string +make_connect_request_target(const std::map &addr_map, + const std::string &host, int port) { + std::string connect_host; + std::string ip; + apply_addr_map(addr_map, host, connect_host, ip); + return make_host_and_port_string_always_port(ip.empty() ? connect_host : ip, + port); +} + // Value for the Host header a client sends when the caller supplied none. // Only the value: callers decide where in their header list it goes. inline std::string make_default_host_header_value(const std::string &host, @@ -16574,6 +16606,7 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) { // Copy network and socket settings client.addr_map_ = addr_map_; + client.tunnel_addr_map_ = tunnel_addr_map_; client.set_address_family(address_family_); client.set_tcp_nodelay(tcp_nodelay_); client.set_ipv6_v6only(ipv6_v6only_); @@ -18086,9 +18119,19 @@ inline void ClientImpl::set_path_encode(bool on) { path_encode_ = on; } inline void ClientImpl::set_hostname_addr_map(std::map addr_map) { + set_direct_hostname_addr_map(std::move(addr_map)); +} + +inline void ClientImpl::set_direct_hostname_addr_map( + std::map addr_map) { addr_map_ = std::move(addr_map); } +inline void ClientImpl::set_tunnel_hostname_addr_map( + std::map addr_map) { + tunnel_addr_map_ = std::move(addr_map); +} + inline void ClientImpl::set_default_headers(Headers headers) { default_headers_ = std::move(headers); } @@ -18797,7 +18840,17 @@ inline socket_t Client::socket() const { return cli_->socket(); } inline void Client::set_hostname_addr_map(std::map addr_map) { - cli_->set_hostname_addr_map(std::move(addr_map)); + cli_->set_direct_hostname_addr_map(std::move(addr_map)); +} + +inline void Client::set_direct_hostname_addr_map( + std::map addr_map) { + cli_->set_direct_hostname_addr_map(std::move(addr_map)); +} + +inline void Client::set_tunnel_hostname_addr_map( + std::map addr_map) { + cli_->set_tunnel_hostname_addr_map(std::move(addr_map)); } inline void Client::set_default_headers(Headers headers) { @@ -19165,6 +19218,10 @@ inline bool SSLClient::connect_with_proxy( std::chrono::time_point start_time, Response &res, bool &success, Error &error) { success = true; + auto connect_target = + detail::make_connect_request_target(tunnel_addr_map_, host_, port_); + auto retargeted = connect_target != + detail::make_host_and_port_string_always_port(host_, port_); Response proxy_res; if (!detail::process_client_socket( socket.sock, read_timeout_sec_, read_timeout_usec_, @@ -19172,8 +19229,8 @@ inline bool SSLClient::connect_with_proxy( start_time, [&](Stream &strm) { Request req2; req2.method = "CONNECT"; - req2.path = - detail::make_host_and_port_string_always_port(host_, port_); + req2.path = connect_target; + if (retargeted) { req2.set_header("Host", connect_target); } if (max_timeout_msec_ > 0) { req2.start_time_ = std::chrono::steady_clock::now(); } @@ -19212,8 +19269,8 @@ inline bool SSLClient::connect_with_proxy( start_time, [&](Stream &strm) { Request req3; req3.method = "CONNECT"; - req3.path = detail::make_host_and_port_string_always_port( - host_, port_); + req3.path = connect_target; + if (retargeted) { req3.set_header("Host", connect_target); } req3.headers.insert(detail::make_digest_authentication_header( req3, make_request_target(req3), auth, 1, detail::random_string(10), proxy_digest_auth_username_, @@ -24058,9 +24115,19 @@ inline void WebSocketClient::set_interface(const std::string &intf) { inline void WebSocketClient::set_hostname_addr_map( std::map addr_map) { + set_direct_hostname_addr_map(std::move(addr_map)); +} + +inline void WebSocketClient::set_direct_hostname_addr_map( + std::map addr_map) { addr_map_ = std::move(addr_map); } +inline void WebSocketClient::set_tunnel_hostname_addr_map( + std::map addr_map) { + tunnel_addr_map_ = std::move(addr_map); +} + inline bool WebSocketClient::is_proxy_enabled() const { if (proxy_host_.empty() || proxy_port_ == -1) { return false; } if (no_proxy_entries_.empty()) { return true; } @@ -24110,7 +24177,8 @@ inline bool WebSocketClient::connect_with_proxy(Response &proxy_res, Error &error) { Request req; req.method = "CONNECT"; - req.path = detail::make_host_and_port_string_always_port(host_, port_); + req.path = + detail::make_connect_request_target(tunnel_addr_map_, host_, port_); req.headers.emplace("Host", req.path); detail::add_default_user_agent_header(req); detail::add_authorization_header(req, proxy_basic_auth_username_, diff --git a/test/test.cc b/test/test.cc index d5a9bd72..a1a7e262 100644 --- a/test/test.cc +++ b/test/test.cc @@ -3748,7 +3748,7 @@ TEST(SpecifyServerIPAddressTest, AnotherHostname_Online) { Client cli(host); #endif - cli.set_hostname_addr_map({{another_host, wrong_ip}}); + cli.set_direct_hostname_addr_map({{another_host, wrong_ip}}); auto res = cli.Get("/"); ASSERT_TRUE(res) << "Error: " << to_string(res.error()); ASSERT_EQ(StatusCode::MovedPermanently_301, res->status); @@ -3764,7 +3764,7 @@ TEST(SpecifyServerIPAddressTest, RealHostname_Online) { Client cli(host); #endif - cli.set_hostname_addr_map({{host, wrong_ip}}); + cli.set_direct_hostname_addr_map({{host, wrong_ip}}); auto res = cli.Get("/"); ASSERT_TRUE(!res); EXPECT_EQ(Error::Connection, res.error()); @@ -3795,7 +3795,7 @@ TEST(SpecifyServerIPAddressTest, HostnameAsAddrMapValue) { svr.wait_until_ready(); Client cli(host, port); - cli.set_hostname_addr_map({{host, HOST}}); + cli.set_direct_hostname_addr_map({{host, HOST}}); auto res = cli.Get("/hi"); ASSERT_TRUE(res) << "Error: " << to_string(res.error()); @@ -3825,7 +3825,7 @@ TEST(SpecifyServerIPAddressTest, IPAddressAsAddrMapValue) { svr.wait_until_ready(); Client cli(host, port); - cli.set_hostname_addr_map({{host, "127.0.0.1"}}); + cli.set_direct_hostname_addr_map({{host, "127.0.0.1"}}); auto res = cli.Get("/hi"); ASSERT_TRUE(res) << "Error: " << to_string(res.error()); @@ -3858,13 +3858,73 @@ TEST(SpecifyServerIPAddressTest, EmptyAddrMapValueIsIgnored) { svr.wait_until_ready(); Client cli(blackhole, port); - cli.set_hostname_addr_map({{blackhole, ""}}); + cli.set_direct_hostname_addr_map({{blackhole, ""}}); cli.set_connection_timeout(1); auto res = cli.Get("/hi"); EXPECT_FALSE(res) << "empty mapping must not redirect to loopback"; } +TEST(SpecifyServerIPAddressTest, DeprecatedSetHostnameAddrMapStillWorks) { + auto host = "target.invalid"; + + Server svr; + std::string received_host; + svr.Get("/hi", [&](const Request &req, Response &res) { + received_host = req.get_header_value("Host"); + res.set_content("Hello World!", "text/plain"); + }); + + auto port = svr.bind_to_any_port("127.0.0.1"); + auto thread = std::thread([&]() { svr.listen_after_bind(); }); + + auto se = detail::scope_exit([&] { + svr.stop(); + thread.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + Client cli(host, port); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + cli.set_hostname_addr_map({{host, "127.0.0.1"}}); +#pragma GCC diagnostic pop + + auto res = cli.Get("/hi"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ(StatusCode::OK_200, res->status); + EXPECT_EQ(std::string(host) + ":" + std::to_string(port), received_host); +} + +TEST(TunnelHostnameAddrMapTest, IgnoredOnDirectConnection) { + // 192.0.2.1 is TEST-NET-1: applying the map would fail to connect. + Server svr; + svr.Get("/hi", [](const Request & /*req*/, Response &res) { + res.set_content("Hello World!", "text/plain"); + }); + + auto port = svr.bind_to_any_port("127.0.0.1"); + auto thread = std::thread([&]() { svr.listen_after_bind(); }); + + auto se = detail::scope_exit([&] { + svr.stop(); + thread.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + Client cli("127.0.0.1", port); + cli.set_tunnel_hostname_addr_map({{"127.0.0.1", "192.0.2.1"}}); + cli.set_connection_timeout(1); + + auto res = cli.Get("/hi"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ(StatusCode::OK_200, res->status); +} + TEST(AbsoluteRedirectTest, Redirect_Online) { auto host = "httpbingo.org"; auto path = std::string{"/absolute-redirect/3"}; @@ -13347,7 +13407,7 @@ TEST(SNI_AutoDetectionTest, FailedHandshakeLeavesNoSNI) { { // Sends SNI, then rejects the server certificate, which the CA did not sign SSLClient cli("localhost", port); - cli.set_hostname_addr_map({{"localhost", "127.0.0.1"}}); + cli.set_direct_hostname_addr_map({{"localhost", "127.0.0.1"}}); cli.set_ca_cert_path(CLIENT_CA_CERT_FILE); auto res = cli.Get("/sni"); ASSERT_FALSE(res); @@ -16436,7 +16496,7 @@ TEST(SSLClientServerTest, ClientCertNotSentToUnverifiedServer) { svr.wait_until_ready(); SSLClient cli(c.host, port, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE); - cli.set_hostname_addr_map({{c.host, "127.0.0.1"}}); + cli.set_direct_hostname_addr_map({{c.host, "127.0.0.1"}}); cli.set_ca_cert_path(c.trusted_ca); if (with_verifier) { cli.set_server_certificate_verifier( @@ -21530,6 +21590,36 @@ TEST(MakeHostAndPortStringTest, VariousPatterns) { detail::make_host_and_port_string("example.com", 65536, false)); } +namespace httplib { +namespace detail { +std::string +make_connect_request_target(const std::map &addr_map, + const std::string &host, int port); +} // namespace detail +} // namespace httplib + +TEST(MakeConnectRequestTargetTest, FollowsHostnameAddrMap) { + std::map addr_map = { + {"ipv4.example", "192.0.2.10"}, + {"ipv6.example", "2001:db8::1"}, + {"name.example", "internal.example.lan"}, + {"empty.example", ""}}; + + EXPECT_EQ("192.0.2.10:443", + detail::make_connect_request_target(addr_map, "ipv4.example", 443)); + EXPECT_EQ("[2001:db8::1]:443", + detail::make_connect_request_target(addr_map, "ipv6.example", 443)); + EXPECT_EQ("internal.example.lan:8443", detail::make_connect_request_target( + addr_map, "name.example", 8443)); + + // An empty or absent mapping keeps the hostname as the target + EXPECT_EQ("empty.example:443", detail::make_connect_request_target( + addr_map, "empty.example", 443)); + EXPECT_EQ("other.example:443", detail::make_connect_request_target( + addr_map, "other.example", 443)); + EXPECT_EQ("[::1]:443", detail::make_connect_request_target({}, "::1", 443)); +} + #ifdef CPPHTTPLIB_SSL_ENABLED TEST(SSLClientHostHeaderTest, Issue2301_Online) { httplib::SSLClient cli("roblox.com", 443); @@ -27908,7 +27998,7 @@ TEST(WebSocketTest, SpecifyServerIPAddress_AnotherHostname) { auto wrong_ip = "192.0.2.1"; ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws"); - client.set_hostname_addr_map({{another_host, wrong_ip}}); + client.set_direct_hostname_addr_map({{another_host, wrong_ip}}); ASSERT_TRUE(client.connect()); EXPECT_TRUE(client.is_open()); @@ -27932,7 +28022,7 @@ TEST(WebSocketTest, SpecifyServerIPAddress_RealHostname) { auto wrong_ip = "192.0.2.1"; ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws"); - client.set_hostname_addr_map({{"localhost", wrong_ip}}); + client.set_direct_hostname_addr_map({{"localhost", wrong_ip}}); client.set_connection_timeout(1); client.set_read_timeout(1); client.set_write_timeout(1); @@ -27972,7 +28062,7 @@ TEST(WebSocketTest, SpecifyServerIPAddress_HostnameAsAddrMapValue) { ws::WebSocketClient client("ws://" + std::string(host) + ":" + std::to_string(port) + "/ws"); - client.set_hostname_addr_map({{host, HOST}}); + client.set_direct_hostname_addr_map({{host, HOST}}); ASSERT_TRUE(client.connect()); EXPECT_TRUE(client.is_open()); @@ -30961,7 +31051,7 @@ private: inline std::unique_ptr make_client(const std::string &host, ProxyAndTargetServers &s) { auto cli = detail::make_unique(host, s.target_port()); - cli->set_hostname_addr_map({{host, "127.0.0.1"}}); + cli->set_direct_hostname_addr_map({{host, "127.0.0.1"}}); cli->set_proxy("127.0.0.1", s.proxy_port()); return cli; } @@ -31170,7 +31260,7 @@ TEST(NoProxyTest, IPv4LiteralExactMatch) { TEST(NoProxyTest, IPv6LiteralExactMatchAcrossEquivalentForms) { ProxyAndTargetServers s; Client cli("0:0:0:0:0:0:0:1", s.target_port()); - cli.set_hostname_addr_map({{"0:0:0:0:0:0:0:1", "127.0.0.1"}}); + cli.set_direct_hostname_addr_map({{"0:0:0:0:0:0:0:1", "127.0.0.1"}}); cli.set_proxy("127.0.0.1", s.proxy_port()); cli.set_no_proxy({"::1"}); @@ -31187,7 +31277,7 @@ TEST(NoProxyTest, BareIPv6LiteralMatchesIPv6Cidr) { // host as a hostname and miss the match. ProxyAndTargetServers s; Client cli("fe80::1", s.target_port()); - cli.set_hostname_addr_map({{"fe80::1", "127.0.0.1"}}); + cli.set_direct_hostname_addr_map({{"fe80::1", "127.0.0.1"}}); cli.set_proxy("127.0.0.1", s.proxy_port()); cli.set_no_proxy({"fe80::/10"}); @@ -31200,7 +31290,7 @@ TEST(NoProxyTest, BareIPv6LiteralMatchesIPv6Cidr) { TEST(NoProxyTest, BracketedIPv6EntryAccepted) { ProxyAndTargetServers s; Client cli("::1", s.target_port()); - cli.set_hostname_addr_map({{"::1", "127.0.0.1"}}); + cli.set_direct_hostname_addr_map({{"::1", "127.0.0.1"}}); cli.set_proxy("127.0.0.1", s.proxy_port()); cli.set_no_proxy({"[::1]"}); @@ -31213,7 +31303,7 @@ TEST(NoProxyTest, BracketedIPv6EntryAccepted) { TEST(NoProxyTest, BracketedIPv6CidrEntryAccepted) { ProxyAndTargetServers s; Client cli("fe80::1", s.target_port()); - cli.set_hostname_addr_map({{"fe80::1", "127.0.0.1"}}); + cli.set_direct_hostname_addr_map({{"fe80::1", "127.0.0.1"}}); cli.set_proxy("127.0.0.1", s.proxy_port()); cli.set_no_proxy({"[fe80::]/10"}); @@ -31229,7 +31319,7 @@ TEST(NoProxyTest, IPv4MappedIPv6IsNotCrossMatchedAgainstIPv4Entry) { // tricks via address-family conversion. ProxyAndTargetServers s; Client cli("::ffff:127.0.0.1", s.target_port()); - cli.set_hostname_addr_map({{"::ffff:127.0.0.1", "127.0.0.1"}}); + cli.set_direct_hostname_addr_map({{"::ffff:127.0.0.1", "127.0.0.1"}}); cli.set_proxy("127.0.0.1", s.proxy_port()); cli.set_no_proxy({"127.0.0.1"}); @@ -31445,7 +31535,7 @@ TEST(NoProxyTest, RedirectToBypassedHostStripsProxyAndProxyAuth) { target.listen(); Client cli("public.example", target.port()); - cli.set_hostname_addr_map({{"public.example", "127.0.0.1"}}); + cli.set_direct_hostname_addr_map({{"public.example", "127.0.0.1"}}); cli.set_proxy("127.0.0.1", proxy_mock.port()); cli.set_proxy_basic_auth("u", "p"); cli.set_no_proxy({"127.0.0.1"}); @@ -31481,7 +31571,7 @@ TEST(NoProxyTest, BypassedTargetReturning407DoesNotLeakProxyDigestCredentials) { // if it doesn't, the test still routes "via proxy" to the same server and // the Proxy-Authorization assertion below catches the leak. Client cli("evil.example", target.port()); - cli.set_hostname_addr_map({{"evil.example", "127.0.0.1"}}); + cli.set_direct_hostname_addr_map({{"evil.example", "127.0.0.1"}}); cli.set_proxy("127.0.0.1", target.port()); cli.set_proxy_digest_auth("proxy-user", "proxy-pass"); cli.set_no_proxy({"evil.example"}); @@ -31532,7 +31622,7 @@ TEST(NoProxyTest, MultiHopRedirectThroughBypassedHostKeepsProxy) { bypass_server.listen(); Client cli("public.example", public_port); - cli.set_hostname_addr_map({{"public.example", "127.0.0.1"}}); + cli.set_direct_hostname_addr_map({{"public.example", "127.0.0.1"}}); cli.set_proxy("127.0.0.1", proxy_mock.port()); cli.set_no_proxy({"127.0.0.1"}); cli.set_follow_location(true); @@ -31566,7 +31656,7 @@ TEST(NoProxyTest, KeepAliveSocketInvalidatedOnSetNoProxy) { target.listen(); Client cli("public.example", target.port()); - cli.set_hostname_addr_map({{"public.example", "127.0.0.1"}}); + cli.set_direct_hostname_addr_map({{"public.example", "127.0.0.1"}}); cli.set_proxy("127.0.0.1", proxy_mock.port()); cli.set_keep_alive(true); @@ -31898,6 +31988,141 @@ TEST(ProxyTunnelTest, OriginReturning407InsideTunnelDoesNotLeakProxyDigest) { EXPECT_FALSE(origin_saw_proxy_authz.load()); EXPECT_EQ(1, proxy.connect_hits()); } + +TEST(ProxyTunnelTest, ConnectTargetFollowsTunnelHostnameAddrMap) { + std::string origin_sni; + std::string origin_host; + + proxy_tunnel_test::ScopedSSLServer origin; + origin.svr().Get(".*", [&](const Request &req, Response &res) { + origin_sni = req.sni(); + origin_host = req.get_header_value("Host"); + res.set_content("ok", "text/plain"); + }); + origin.listen(); + + proxy_tunnel_test::ScopedConnectProxy proxy(origin.port()); + ASSERT_NE(0, proxy.port()); + + SSLClient cli("origin.example", origin.port()); + cli.enable_server_certificate_verification(false); + cli.set_tunnel_hostname_addr_map({{"origin.example", "127.0.0.1"}}); + cli.set_proxy("127.0.0.1", proxy.port()); + + auto res = cli.Get("/x"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ(StatusCode::OK_200, res->status); + + auto port = std::to_string(origin.port()); + auto target = "127.0.0.1:" + port; + auto connect_req = proxy.connect_request(); + EXPECT_EQ(1, proxy.connect_hits()); + EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n")); + EXPECT_NE(std::string::npos, + connect_req.find("\r\nHost: " + target + "\r\n")); + EXPECT_EQ("origin.example", origin_sni); + EXPECT_EQ("origin.example:" + port, origin_host); +} + +TEST(ProxyTunnelTest, DigestRetryConnectTargetFollowsTunnelHostnameAddrMap) { + proxy_tunnel_test::ScopedSSLServer origin; + origin.svr().Get(".*", [&](const Request &, Response &res) { + res.set_content("ok", "text/plain"); + }); + origin.listen(); + + proxy_tunnel_test::ScopedConnectProxy proxy( + origin.port(), [](const std::string &head) -> std::string { + if (head.find( + "\r\nProxy-Authorization: Digest username=\"proxy-user\"") != + std::string::npos) { + return proxy_tunnel_test::kTunnelEstablished; + } + return "HTTP/1.1 407 Proxy Authentication Required\r\n" + "Proxy-Authenticate: Digest realm=\"proxy\", qop=\"auth\", " + "nonce=\"abc\", algorithm=MD5\r\n" + "Content-Length: 0\r\n\r\n"; + }); + ASSERT_NE(0, proxy.port()); + + SSLClient cli("origin.example", origin.port()); + cli.enable_server_certificate_verification(false); + cli.set_tunnel_hostname_addr_map({{"origin.example", "127.0.0.1"}}); + cli.set_proxy("127.0.0.1", proxy.port()); + cli.set_proxy_digest_auth("proxy-user", "proxy-pass"); + + auto res = cli.Get("/x"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ(StatusCode::OK_200, res->status); + + // The retried CONNECT is the one the proxy saw last. + auto target = "127.0.0.1:" + std::to_string(origin.port()); + auto connect_req = proxy.connect_request(); + EXPECT_EQ(2, proxy.connect_hits()); + EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n")); + EXPECT_NE(std::string::npos, + connect_req.find("\r\nHost: " + target + "\r\n")); +} + +TEST(ProxyTunnelTest, DirectHostnameAddrMapLeavesConnectTarget) { + proxy_tunnel_test::ScopedSSLServer origin; + origin.svr().Get(".*", [&](const Request &, Response &res) { + res.set_content("ok", "text/plain"); + }); + origin.listen(); + + proxy_tunnel_test::ScopedConnectProxy proxy(origin.port()); + ASSERT_NE(0, proxy.port()); + + SSLClient cli("origin.example", origin.port()); + cli.enable_server_certificate_verification(false); + cli.set_direct_hostname_addr_map({{"origin.example", "127.0.0.1"}}); + cli.set_proxy("127.0.0.1", proxy.port()); + + auto res = cli.Get("/x"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ(StatusCode::OK_200, res->status); + + auto target = "origin.example:" + std::to_string(origin.port()); + auto connect_req = proxy.connect_request(); + EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n")); + EXPECT_NE(std::string::npos, + connect_req.find("\r\nHost: " + target + "\r\n")); +} + +TEST(ProxyTunnelTest, RedirectKeepsTunnelHostnameAddrMap) { + proxy_tunnel_test::ScopedSSLServer origin; + auto port = std::to_string(origin.port()); + origin.svr().Get("/x", [&](const Request &, Response &res) { + res.set_redirect("https://next.example:" + port + "/y"); + }); + origin.svr().Get("/y", [](const Request &, Response &res) { + res.set_content("ok", "text/plain"); + }); + origin.listen(); + + proxy_tunnel_test::ScopedConnectProxy proxy(origin.port()); + ASSERT_NE(0, proxy.port()); + + SSLClient cli("origin.example", origin.port()); + cli.enable_server_certificate_verification(false); + cli.set_tunnel_hostname_addr_map( + {{"origin.example", "127.0.0.1"}, {"next.example", "127.0.0.1"}}); + cli.set_proxy("127.0.0.1", proxy.port()); + cli.set_follow_location(true); + + auto res = cli.Get("/x"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ("ok", res->body); + + // The last CONNECT is the redirect client's. + ASSERT_EQ(2, proxy.connect_hits()); + auto target = "127.0.0.1:" + port; + auto connect_req = proxy.connect_request(); + EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n")); + EXPECT_NE(std::string::npos, + connect_req.find("\r\nHost: " + target + "\r\n")); +} #endif namespace proxy_tunnel_test { @@ -32089,6 +32314,50 @@ TEST(WebSocketProxyTest, ProxySettingAppliesOnReconnect) { EXPECT_EQ(2, proxy.connect_hits()); } +TEST(WebSocketProxyTest, ConnectTargetFollowsTunnelHostnameAddrMap) { + proxy_tunnel_test::ScopedWebSocketOrigin origin; + proxy_tunnel_test::ScopedConnectProxy proxy(origin.port()); + ASSERT_NE(0, proxy.port()); + + auto port = std::to_string(origin.port()); + ws::WebSocketClient client("ws://origin.example:" + port + "/ws"); + client.set_tunnel_hostname_addr_map({{"origin.example", "127.0.0.1"}}); + client.set_proxy("127.0.0.1", proxy.port()); + auto res = client.connect(); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + + std::string msg; + ASSERT_TRUE(client.read(msg)); + client.close(); + + EXPECT_EQ(1, proxy.connect_hits()); + auto target = "127.0.0.1:" + port; + auto connect_req = proxy.connect_request(); + EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n")); + EXPECT_NE(std::string::npos, + connect_req.find("\r\nHost: " + target + "\r\n")); +} + +TEST(WebSocketProxyTest, DirectHostnameAddrMapLeavesConnectTarget) { + proxy_tunnel_test::ScopedWebSocketOrigin origin; + proxy_tunnel_test::ScopedConnectProxy proxy(origin.port()); + ASSERT_NE(0, proxy.port()); + + auto port = std::to_string(origin.port()); + ws::WebSocketClient client("ws://origin.example:" + port + "/ws"); + client.set_direct_hostname_addr_map({{"origin.example", "127.0.0.1"}}); + client.set_proxy("127.0.0.1", proxy.port()); + auto res = client.connect(); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + client.close(); + + auto target = "origin.example:" + port; + auto connect_req = proxy.connect_request(); + EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n")); + EXPECT_NE(std::string::npos, + connect_req.find("\r\nHost: " + target + "\r\n")); +} + #ifdef CPPHTTPLIB_SSL_ENABLED TEST(WebSocketProxyTest, DigestRetriesConnectOnFreshSocket) { proxy_tunnel_test::ScopedWebSocketOrigin origin;