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;