Bläddra i källkod

Clean up addr_map hostname support

Follow-up to 49b921b.

Move the duplicated addr_map lookup into detail::apply_addr_map, shared by
ClientImpl::create_client_socket and WebSocketClient::connect.

Add a WebSocketClient test for a hostname mapped value, so that path has
the same coverage as the Client one. Guard its teardown with scope_exit:
a failing ASSERT_TRUE returns from the test body, and destroying a still
joinable std::thread calls std::terminate, taking the whole binary down.

Document set_hostname_addr_map in README. It had no entry at all.
yhirose 2 månader sedan
förälder
incheckning
ae8356d86e
3 ändrade filer med 101 tillägg och 27 borttagningar
  1. 28 0
      README.md
  2. 32 27
      httplib.h
  3. 41 0
      test/test.cc

+ 28 - 0
README.md

@@ -1289,6 +1289,34 @@ res->status; // 200
 cli.set_interface("eth0"); // Interface name, IP address or host name
 ```
 
+### 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.
+
+```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"}});
+```
+
+A mapped value may be an IP literal or another hostname. An IP literal is used
+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"}});
+```
+
+An empty value is ignored, leaving the original hostname as the connection
+target.
+
+The same method is available on `httplib::ws::WebSocketClient`.
+
 ### Automatic Path Encoding
 
 The client automatically encodes special characters in URL paths by default:

+ 32 - 27
httplib.h

@@ -9105,6 +9105,32 @@ inline bool is_ip_address(const std::string &host) {
          inet_pton(AF_INET6, host.c_str(), &addr6) == 1;
 }
 
+// Resolve where a client should connect for `host`, honoring a user-supplied
+// hostname-to-address map. `host` itself is never rewritten, so it keeps
+// supplying the Host header and SNI; only the connection target changes.
+//
+// A mapped IP literal goes to `ip`, which keeps create_socket's AI_NUMERICHOST
+// path. Anything else goes to `connect_host`, which create_socket resolves as
+// a name, or uses as the socket path when the address family is AF_UNIX. An
+// absent or empty mapping leaves `host` as the connection target; without the
+// empty check the value would reach getaddrinfo as a null node and silently
+// resolve to loopback.
+inline void apply_addr_map(const std::map<std::string, std::string> &addr_map,
+                           const std::string &host, std::string &connect_host,
+                           std::string &ip) {
+  connect_host = host;
+  ip.clear();
+
+  auto it = addr_map.find(host);
+  if (it == addr_map.end() || it->second.empty()) { return; }
+
+  if (is_ip_address(it->second)) {
+    ip = it->second;
+  } else {
+    connect_host = it->second;
+  }
+}
+
 } // namespace detail
 
 /*
@@ -12993,20 +13019,10 @@ inline socket_t ClientImpl::create_client_socket(Error &error) const {
         write_timeout_sec_, write_timeout_usec_, interface_, error);
   }
 
-  // Check is custom IP or hostname specified for host_.
-  // An IP literal goes to the ip argument, which keeps create_socket's
-  // AI_NUMERICHOST path; a hostname goes to the host argument so that it is
-  // resolved. Either way host_ still supplies the Host header and SNI.
-  auto connect_host = host_;
+  // Check is custom IP or hostname specified for host_
+  std::string connect_host;
   std::string ip;
-  auto it = addr_map_.find(host_);
-  if (it != addr_map_.end() && !it->second.empty()) {
-    if (detail::is_ip_address(it->second)) {
-      ip = it->second;
-    } else {
-      connect_host = it->second;
-    }
-  }
+  detail::apply_addr_map(addr_map_, host_, connect_host, ip);
 
   return detail::create_client_socket(
       connect_host, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
@@ -20871,21 +20887,10 @@ inline bool WebSocketClient::connect() {
   if (!is_valid_) { return false; }
   shutdown_and_close();
 
-  // Check is custom IP or hostname specified for host_.
-  // host_ stays the identity used for the Host header and for SNI, while the
-  // mapped value only redirects where the socket connects. An IP literal goes
-  // to the ip argument, which keeps create_socket's AI_NUMERICHOST path; a
-  // hostname goes to the host argument so that it is resolved.
-  auto connect_host = host_;
+  // Check is custom IP or hostname specified for host_
+  std::string connect_host;
   std::string ip;
-  auto it = addr_map_.find(host_);
-  if (it != addr_map_.end() && !it->second.empty()) {
-    if (detail::is_ip_address(it->second)) {
-      ip = it->second;
-    } else {
-      connect_host = it->second;
-    }
-  }
+  detail::apply_addr_map(addr_map_, host_, connect_host, ip);
 
   Error error;
   sock_ = detail::create_client_socket(

+ 41 - 0
test/test.cc

@@ -19395,6 +19395,47 @@ TEST(WebSocketTest, SpecifyServerIPAddress_RealHostname) {
   t.join();
 }
 
+TEST(WebSocketTest, SpecifyServerIPAddress_HostnameAsAddrMapValue) {
+  // A mapped value that is not an IP literal must be resolved. HOST resolves
+  // from the hosts file, so this test needs no external DNS. "target.invalid"
+  // (RFC 6761) is only a map key and the Host header value.
+  auto host = "target.invalid";
+
+  Server svr;
+  std::string received_host;
+  svr.WebSocket("/ws", [&](const Request &req, ws::WebSocket &ws) {
+    received_host = req.get_header_value("Host");
+    std::string msg;
+    while (ws.read(msg)) {}
+  });
+
+  auto port = svr.bind_to_any_port(HOST);
+  std::thread t([&]() { svr.listen_after_bind(); });
+
+  // ASSERT_* below returns from the test body, which would leave t joinable
+  // and make ~thread call std::terminate.
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    if (t.joinable()) { t.join(); }
+  });
+
+  svr.wait_until_ready();
+
+  ws::WebSocketClient client("ws://" + std::string(host) + ":" +
+                             std::to_string(port) + "/ws");
+  client.set_hostname_addr_map({{host, HOST}});
+
+  ASSERT_TRUE(client.connect());
+  EXPECT_TRUE(client.is_open());
+  client.close();
+
+  svr.stop();
+  t.join();
+
+  // The mapping only redirects the connection; the identity stays host_.
+  EXPECT_EQ(std::string(host) + ":" + std::to_string(port), received_host);
+}
+
 class WebSocketIntegrationTest : public ::testing::Test {
 protected:
   void SetUp() override {