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.
This commit is contained in:
Kim, Hyuk authored and GitHub committed 2026-10-27 21:09:25 -04:00
1 parent 25ba40f6fe
commit 290c4c0bbe
6 files changed
+399 -37

No files matched your search

+2 -1
View File
@@ -233,7 +233,8 @@ void set_address_family(int family);
void set_ipv6_v6only(bool on); void set_ipv6_v6only(bool on);
void set_socket_options(SocketOptions socket_options); void set_socket_options(SocketOptions socket_options);
void set_interface(const std::string &intf); void set_interface(const std::string &intf);
void set_hostname_addr_map(std::map<std::string, std::string> addr_map); void set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
// SSL configuration (wss:// only, SSL builds only) // SSL configuration (wss:// only, SSL builds only)
void set_ca_cert_path(const std::string &ca_cert_file_path, void set_ca_cert_path(const std::string &ca_cert_file_path,
+32 -8
View File
@@ -1477,17 +1477,17 @@ The same method is available on `httplib::ws::WebSocketClient`.
### Override the connection target for a hostname ### Override the connection target for a hostname
`set_hostname_addr_map` redirects where the socket connects, without changing `set_direct_hostname_addr_map` redirects where the socket connects, without
the identity of the request. The hostname the client was constructed with keeps changing the identity of the request. The hostname the client was constructed
supplying the `Host` header, the SNI, and the name that the server certificate with keeps supplying the `Host` header, the SNI, and the name that the server
is verified against, so this is a connection-level override only, not a way to certificate is verified against, so this is a connection-level override only,
talk to a different origin. not a way to talk to a different origin.
```cpp ```cpp
httplib::Client cli("https://example.com"); httplib::Client cli("https://example.com");
// Connect to this IP address instead of resolving "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 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: under a different name works too:
```cpp ```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 An empty value is ignored, leaving the original hostname as the connection
target. 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 ### Automatic Path Encoding
+1 -1
View File
@@ -214,7 +214,7 @@ svr.listen("/tmp/httplib.sock", 80); // The port is unused, but must not be 0
// Client // Client
httplib::Client cli("http://localhost"); httplib::Client cli("http://localhost");
cli.set_address_family(AF_UNIX); 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("/"); auto res = cli.Get("/");
``` ```
+1 -1
View File
@@ -214,7 +214,7 @@ svr.listen("/tmp/httplib.sock", 80); // ポート番号は使われない(0
// クライアント // クライアント
httplib::Client cli("http://localhost"); httplib::Client cli("http://localhost");
cli.set_address_family(AF_UNIX); 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("/"); auto res = cli.Get("/");
``` ```
+74 -6
View File
@@ -2927,7 +2927,12 @@ public:
size_t is_socket_open() const; size_t is_socket_open() const;
socket_t socket() const; socket_t socket() const;
[[deprecated("Use set_direct_hostname_addr_map() instead.")]]
void set_hostname_addr_map(std::map<std::string, std::string> addr_map); void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_default_headers(Headers headers); void set_default_headers(Headers headers);
@@ -3059,6 +3064,9 @@ protected:
// hostname; only the connection target changes, never the identity. // hostname; only the connection target changes, never the identity.
std::map<std::string, std::string> addr_map_; std::map<std::string, std::string> addr_map_;
// Hostname to CONNECT target map, used only through a proxy.
std::map<std::string, std::string> tunnel_addr_map_;
// Default headers // Default headers
Headers default_headers_; Headers default_headers_;
@@ -3348,7 +3356,12 @@ public:
size_t is_socket_open() const; size_t is_socket_open() const;
socket_t socket() const; socket_t socket() const;
[[deprecated("Use set_direct_hostname_addr_map() instead.")]]
void set_hostname_addr_map(std::map<std::string, std::string> addr_map); void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_default_headers(Headers headers); void set_default_headers(Headers headers);
@@ -4748,7 +4761,12 @@ public:
set_connection_timeout(const std::chrono::duration<Rep, Period> &duration); set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);
void set_interface(const std::string &intf); void set_interface(const std::string &intf);
[[deprecated("Use set_direct_hostname_addr_map() instead.")]]
void set_hostname_addr_map(std::map<std::string, std::string> addr_map); void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_proxy(const std::string &host, int port); void set_proxy(const std::string &host, int port);
void set_proxy_basic_auth(const std::string &username, void set_proxy_basic_auth(const std::string &username,
@@ -4820,6 +4838,9 @@ private:
// hostname; only the connection target changes, never the identity. // hostname; only the connection target changes, never the identity.
std::map<std::string, std::string> addr_map_; std::map<std::string, std::string> addr_map_;
// Hostname to CONNECT target map, used only through a proxy.
std::map<std::string, std::string> tunnel_addr_map_;
std::string proxy_host_; std::string proxy_host_;
int proxy_port_ = -1; int proxy_port_ = -1;
std::string proxy_basic_auth_username_; 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); 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<std::string, std::string> &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. // 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. // Only the value: callers decide where in their header list it goes.
inline std::string make_default_host_header_value(const std::string &host, 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 // Copy network and socket settings
client.addr_map_ = addr_map_; client.addr_map_ = addr_map_;
client.tunnel_addr_map_ = tunnel_addr_map_;
client.set_address_family(address_family_); client.set_address_family(address_family_);
client.set_tcp_nodelay(tcp_nodelay_); client.set_tcp_nodelay(tcp_nodelay_);
client.set_ipv6_v6only(ipv6_v6only_); client.set_ipv6_v6only(ipv6_v6only_);
@@ -18086,9 +18119,19 @@ inline void ClientImpl::set_path_encode(bool on) { path_encode_ = on; }
inline void inline void
ClientImpl::set_hostname_addr_map(std::map<std::string, std::string> addr_map) { ClientImpl::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {
set_direct_hostname_addr_map(std::move(addr_map));
}
inline void ClientImpl::set_direct_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
addr_map_ = std::move(addr_map); addr_map_ = std::move(addr_map);
} }
inline void ClientImpl::set_tunnel_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
tunnel_addr_map_ = std::move(addr_map);
}
inline void ClientImpl::set_default_headers(Headers headers) { inline void ClientImpl::set_default_headers(Headers headers) {
default_headers_ = std::move(headers); default_headers_ = std::move(headers);
} }
@@ -18797,7 +18840,17 @@ inline socket_t Client::socket() const { return cli_->socket(); }
inline void inline void
Client::set_hostname_addr_map(std::map<std::string, std::string> addr_map) { Client::set_hostname_addr_map(std::map<std::string, std::string> 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<std::string, std::string> addr_map) {
cli_->set_direct_hostname_addr_map(std::move(addr_map));
}
inline void Client::set_tunnel_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
cli_->set_tunnel_hostname_addr_map(std::move(addr_map));
} }
inline void Client::set_default_headers(Headers headers) { inline void Client::set_default_headers(Headers headers) {
@@ -19165,6 +19218,10 @@ inline bool SSLClient::connect_with_proxy(
std::chrono::time_point<std::chrono::steady_clock> start_time, std::chrono::time_point<std::chrono::steady_clock> start_time,
Response &res, bool &success, Error &error) { Response &res, bool &success, Error &error) {
success = true; 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; Response proxy_res;
if (!detail::process_client_socket( if (!detail::process_client_socket(
socket.sock, read_timeout_sec_, read_timeout_usec_, socket.sock, read_timeout_sec_, read_timeout_usec_,
@@ -19172,8 +19229,8 @@ inline bool SSLClient::connect_with_proxy(
start_time, [&](Stream &strm) { start_time, [&](Stream &strm) {
Request req2; Request req2;
req2.method = "CONNECT"; req2.method = "CONNECT";
req2.path = req2.path = connect_target;
detail::make_host_and_port_string_always_port(host_, port_); if (retargeted) { req2.set_header("Host", connect_target); }
if (max_timeout_msec_ > 0) { if (max_timeout_msec_ > 0) {
req2.start_time_ = std::chrono::steady_clock::now(); req2.start_time_ = std::chrono::steady_clock::now();
} }
@@ -19212,8 +19269,8 @@ inline bool SSLClient::connect_with_proxy(
start_time, [&](Stream &strm) { start_time, [&](Stream &strm) {
Request req3; Request req3;
req3.method = "CONNECT"; req3.method = "CONNECT";
req3.path = detail::make_host_and_port_string_always_port( req3.path = connect_target;
host_, port_); if (retargeted) { req3.set_header("Host", connect_target); }
req3.headers.insert(detail::make_digest_authentication_header( req3.headers.insert(detail::make_digest_authentication_header(
req3, make_request_target(req3), auth, 1, req3, make_request_target(req3), auth, 1,
detail::random_string(10), proxy_digest_auth_username_, 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( inline void WebSocketClient::set_hostname_addr_map(
std::map<std::string, std::string> addr_map) { std::map<std::string, std::string> addr_map) {
set_direct_hostname_addr_map(std::move(addr_map));
}
inline void WebSocketClient::set_direct_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
addr_map_ = std::move(addr_map); addr_map_ = std::move(addr_map);
} }
inline void WebSocketClient::set_tunnel_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
tunnel_addr_map_ = std::move(addr_map);
}
inline bool WebSocketClient::is_proxy_enabled() const { inline bool WebSocketClient::is_proxy_enabled() const {
if (proxy_host_.empty() || proxy_port_ == -1) { return false; } if (proxy_host_.empty() || proxy_port_ == -1) { return false; }
if (no_proxy_entries_.empty()) { return true; } if (no_proxy_entries_.empty()) { return true; }
@@ -24110,7 +24177,8 @@ inline bool WebSocketClient::connect_with_proxy(Response &proxy_res,
Error &error) { Error &error) {
Request req; Request req;
req.method = "CONNECT"; 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); req.headers.emplace("Host", req.path);
detail::add_default_user_agent_header(req); detail::add_default_user_agent_header(req);
detail::add_authorization_header(req, proxy_basic_auth_username_, detail::add_authorization_header(req, proxy_basic_auth_username_,
+289 -20
View File
@@ -3748,7 +3748,7 @@ TEST(SpecifyServerIPAddressTest, AnotherHostname_Online) {
Client cli(host); Client cli(host);
#endif #endif
cli.set_hostname_addr_map({{another_host, wrong_ip}}); cli.set_direct_hostname_addr_map({{another_host, wrong_ip}});
auto res = cli.Get("/"); auto res = cli.Get("/");
ASSERT_TRUE(res) << "Error: " << to_string(res.error()); ASSERT_TRUE(res) << "Error: " << to_string(res.error());
ASSERT_EQ(StatusCode::MovedPermanently_301, res->status); ASSERT_EQ(StatusCode::MovedPermanently_301, res->status);
@@ -3764,7 +3764,7 @@ TEST(SpecifyServerIPAddressTest, RealHostname_Online) {
Client cli(host); Client cli(host);
#endif #endif
cli.set_hostname_addr_map({{host, wrong_ip}}); cli.set_direct_hostname_addr_map({{host, wrong_ip}});
auto res = cli.Get("/"); auto res = cli.Get("/");
ASSERT_TRUE(!res); ASSERT_TRUE(!res);
EXPECT_EQ(Error::Connection, res.error()); EXPECT_EQ(Error::Connection, res.error());
@@ -3795,7 +3795,7 @@ TEST(SpecifyServerIPAddressTest, HostnameAsAddrMapValue) {
svr.wait_until_ready(); svr.wait_until_ready();
Client cli(host, port); Client cli(host, port);
cli.set_hostname_addr_map({{host, HOST}}); cli.set_direct_hostname_addr_map({{host, HOST}});
auto res = cli.Get("/hi"); auto res = cli.Get("/hi");
ASSERT_TRUE(res) << "Error: " << to_string(res.error()); ASSERT_TRUE(res) << "Error: " << to_string(res.error());
@@ -3825,7 +3825,7 @@ TEST(SpecifyServerIPAddressTest, IPAddressAsAddrMapValue) {
svr.wait_until_ready(); svr.wait_until_ready();
Client cli(host, port); 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"); auto res = cli.Get("/hi");
ASSERT_TRUE(res) << "Error: " << to_string(res.error()); ASSERT_TRUE(res) << "Error: " << to_string(res.error());
@@ -3858,13 +3858,73 @@ TEST(SpecifyServerIPAddressTest, EmptyAddrMapValueIsIgnored) {
svr.wait_until_ready(); svr.wait_until_ready();
Client cli(blackhole, port); Client cli(blackhole, port);
cli.set_hostname_addr_map({{blackhole, ""}}); cli.set_direct_hostname_addr_map({{blackhole, ""}});
cli.set_connection_timeout(1); cli.set_connection_timeout(1);
auto res = cli.Get("/hi"); auto res = cli.Get("/hi");
EXPECT_FALSE(res) << "empty mapping must not redirect to loopback"; 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) { TEST(AbsoluteRedirectTest, Redirect_Online) {
auto host = "httpbingo.org"; auto host = "httpbingo.org";
auto path = std::string{"/absolute-redirect/3"}; 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 // Sends SNI, then rejects the server certificate, which the CA did not sign
SSLClient cli("localhost", port); 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); cli.set_ca_cert_path(CLIENT_CA_CERT_FILE);
auto res = cli.Get("/sni"); auto res = cli.Get("/sni");
ASSERT_FALSE(res); ASSERT_FALSE(res);
@@ -16436,7 +16496,7 @@ TEST(SSLClientServerTest, ClientCertNotSentToUnverifiedServer) {
svr.wait_until_ready(); svr.wait_until_ready();
SSLClient cli(c.host, port, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE); 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); cli.set_ca_cert_path(c.trusted_ca);
if (with_verifier) { if (with_verifier) {
cli.set_server_certificate_verifier( cli.set_server_certificate_verifier(
@@ -21530,6 +21590,36 @@ TEST(MakeHostAndPortStringTest, VariousPatterns) {
detail::make_host_and_port_string("example.com", 65536, false)); detail::make_host_and_port_string("example.com", 65536, false));
} }
namespace httplib {
namespace detail {
std::string
make_connect_request_target(const std::map<std::string, std::string> &addr_map,
const std::string &host, int port);
} // namespace detail
} // namespace httplib
TEST(MakeConnectRequestTargetTest, FollowsHostnameAddrMap) {
std::map<std::string, std::string> 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 #ifdef CPPHTTPLIB_SSL_ENABLED
TEST(SSLClientHostHeaderTest, Issue2301_Online) { TEST(SSLClientHostHeaderTest, Issue2301_Online) {
httplib::SSLClient cli("roblox.com", 443); httplib::SSLClient cli("roblox.com", 443);
@@ -27908,7 +27998,7 @@ TEST(WebSocketTest, SpecifyServerIPAddress_AnotherHostname) {
auto wrong_ip = "192.0.2.1"; auto wrong_ip = "192.0.2.1";
ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws"); 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()); ASSERT_TRUE(client.connect());
EXPECT_TRUE(client.is_open()); EXPECT_TRUE(client.is_open());
@@ -27932,7 +28022,7 @@ TEST(WebSocketTest, SpecifyServerIPAddress_RealHostname) {
auto wrong_ip = "192.0.2.1"; auto wrong_ip = "192.0.2.1";
ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws"); 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_connection_timeout(1);
client.set_read_timeout(1); client.set_read_timeout(1);
client.set_write_timeout(1); client.set_write_timeout(1);
@@ -27972,7 +28062,7 @@ TEST(WebSocketTest, SpecifyServerIPAddress_HostnameAsAddrMapValue) {
ws::WebSocketClient client("ws://" + std::string(host) + ":" + ws::WebSocketClient client("ws://" + std::string(host) + ":" +
std::to_string(port) + "/ws"); std::to_string(port) + "/ws");
client.set_hostname_addr_map({{host, HOST}}); client.set_direct_hostname_addr_map({{host, HOST}});
ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.connect());
EXPECT_TRUE(client.is_open()); EXPECT_TRUE(client.is_open());
@@ -30961,7 +31051,7 @@ private:
inline std::unique_ptr<Client> make_client(const std::string &host, inline std::unique_ptr<Client> make_client(const std::string &host,
ProxyAndTargetServers &s) { ProxyAndTargetServers &s) {
auto cli = detail::make_unique<Client>(host, s.target_port()); auto cli = detail::make_unique<Client>(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()); cli->set_proxy("127.0.0.1", s.proxy_port());
return cli; return cli;
} }
@@ -31170,7 +31260,7 @@ TEST(NoProxyTest, IPv4LiteralExactMatch) {
TEST(NoProxyTest, IPv6LiteralExactMatchAcrossEquivalentForms) { TEST(NoProxyTest, IPv6LiteralExactMatchAcrossEquivalentForms) {
ProxyAndTargetServers s; ProxyAndTargetServers s;
Client cli("0:0:0:0:0:0:0:1", s.target_port()); 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_proxy("127.0.0.1", s.proxy_port());
cli.set_no_proxy({"::1"}); cli.set_no_proxy({"::1"});
@@ -31187,7 +31277,7 @@ TEST(NoProxyTest, BareIPv6LiteralMatchesIPv6Cidr) {
// host as a hostname and miss the match. // host as a hostname and miss the match.
ProxyAndTargetServers s; ProxyAndTargetServers s;
Client cli("fe80::1", s.target_port()); 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_proxy("127.0.0.1", s.proxy_port());
cli.set_no_proxy({"fe80::/10"}); cli.set_no_proxy({"fe80::/10"});
@@ -31200,7 +31290,7 @@ TEST(NoProxyTest, BareIPv6LiteralMatchesIPv6Cidr) {
TEST(NoProxyTest, BracketedIPv6EntryAccepted) { TEST(NoProxyTest, BracketedIPv6EntryAccepted) {
ProxyAndTargetServers s; ProxyAndTargetServers s;
Client cli("::1", s.target_port()); 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_proxy("127.0.0.1", s.proxy_port());
cli.set_no_proxy({"[::1]"}); cli.set_no_proxy({"[::1]"});
@@ -31213,7 +31303,7 @@ TEST(NoProxyTest, BracketedIPv6EntryAccepted) {
TEST(NoProxyTest, BracketedIPv6CidrEntryAccepted) { TEST(NoProxyTest, BracketedIPv6CidrEntryAccepted) {
ProxyAndTargetServers s; ProxyAndTargetServers s;
Client cli("fe80::1", s.target_port()); 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_proxy("127.0.0.1", s.proxy_port());
cli.set_no_proxy({"[fe80::]/10"}); cli.set_no_proxy({"[fe80::]/10"});
@@ -31229,7 +31319,7 @@ TEST(NoProxyTest, IPv4MappedIPv6IsNotCrossMatchedAgainstIPv4Entry) {
// tricks via address-family conversion. // tricks via address-family conversion.
ProxyAndTargetServers s; ProxyAndTargetServers s;
Client cli("::ffff:127.0.0.1", s.target_port()); 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_proxy("127.0.0.1", s.proxy_port());
cli.set_no_proxy({"127.0.0.1"}); cli.set_no_proxy({"127.0.0.1"});
@@ -31445,7 +31535,7 @@ TEST(NoProxyTest, RedirectToBypassedHostStripsProxyAndProxyAuth) {
target.listen(); target.listen();
Client cli("public.example", target.port()); 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("127.0.0.1", proxy_mock.port());
cli.set_proxy_basic_auth("u", "p"); cli.set_proxy_basic_auth("u", "p");
cli.set_no_proxy({"127.0.0.1"}); 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 // if it doesn't, the test still routes "via proxy" to the same server and
// the Proxy-Authorization assertion below catches the leak. // the Proxy-Authorization assertion below catches the leak.
Client cli("evil.example", target.port()); 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("127.0.0.1", target.port());
cli.set_proxy_digest_auth("proxy-user", "proxy-pass"); cli.set_proxy_digest_auth("proxy-user", "proxy-pass");
cli.set_no_proxy({"evil.example"}); cli.set_no_proxy({"evil.example"});
@@ -31532,7 +31622,7 @@ TEST(NoProxyTest, MultiHopRedirectThroughBypassedHostKeepsProxy) {
bypass_server.listen(); bypass_server.listen();
Client cli("public.example", public_port); 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_proxy("127.0.0.1", proxy_mock.port());
cli.set_no_proxy({"127.0.0.1"}); cli.set_no_proxy({"127.0.0.1"});
cli.set_follow_location(true); cli.set_follow_location(true);
@@ -31566,7 +31656,7 @@ TEST(NoProxyTest, KeepAliveSocketInvalidatedOnSetNoProxy) {
target.listen(); target.listen();
Client cli("public.example", target.port()); 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("127.0.0.1", proxy_mock.port());
cli.set_keep_alive(true); cli.set_keep_alive(true);
@@ -31898,6 +31988,141 @@ TEST(ProxyTunnelTest, OriginReturning407InsideTunnelDoesNotLeakProxyDigest) {
EXPECT_FALSE(origin_saw_proxy_authz.load()); EXPECT_FALSE(origin_saw_proxy_authz.load());
EXPECT_EQ(1, proxy.connect_hits()); 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 #endif
namespace proxy_tunnel_test { namespace proxy_tunnel_test {
@@ -32089,6 +32314,50 @@ TEST(WebSocketProxyTest, ProxySettingAppliesOnReconnect) {
EXPECT_EQ(2, proxy.connect_hits()); 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 #ifdef CPPHTTPLIB_SSL_ENABLED
TEST(WebSocketProxyTest, DigestRetriesConnectOnFreshSocket) { TEST(WebSocketProxyTest, DigestRetriesConnectOnFreshSocket) {
proxy_tunnel_test::ScopedWebSocketOrigin origin; proxy_tunnel_test::ScopedWebSocketOrigin origin;