mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-23 11:54:42 +00:00
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.
This commit is contained in:
1 parent
49b921b52d
commit
ae8356d86e
3 files changed
+101
-27
No files matched your search
@@ -1289,6 +1289,34 @@ res->status; // 200
|
|||||||
cli.set_interface("eth0"); // Interface name, IP address or host name
|
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
|
### Automatic Path Encoding
|
||||||
|
|
||||||
The client automatically encodes special characters in URL paths by default:
|
The client automatically encodes special characters in URL paths by default:
|
||||||
|
|||||||
@@ -9105,6 +9105,32 @@ inline bool is_ip_address(const std::string &host) {
|
|||||||
inet_pton(AF_INET6, host.c_str(), &addr6) == 1;
|
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
|
} // namespace detail
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -12993,20 +13019,10 @@ inline socket_t ClientImpl::create_client_socket(Error &error) const {
|
|||||||
write_timeout_sec_, write_timeout_usec_, interface_, error);
|
write_timeout_sec_, write_timeout_usec_, interface_, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check is custom IP or hostname specified for host_.
|
// Check is custom IP or hostname specified for host_
|
||||||
// An IP literal goes to the ip argument, which keeps create_socket's
|
std::string connect_host;
|
||||||
// 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_;
|
|
||||||
std::string ip;
|
std::string ip;
|
||||||
auto it = addr_map_.find(host_);
|
detail::apply_addr_map(addr_map_, host_, connect_host, ip);
|
||||||
if (it != addr_map_.end() && !it->second.empty()) {
|
|
||||||
if (detail::is_ip_address(it->second)) {
|
|
||||||
ip = it->second;
|
|
||||||
} else {
|
|
||||||
connect_host = it->second;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return detail::create_client_socket(
|
return detail::create_client_socket(
|
||||||
connect_host, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
|
connect_host, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
|
||||||
@@ -20871,21 +20887,10 @@ inline bool WebSocketClient::connect() {
|
|||||||
if (!is_valid_) { return false; }
|
if (!is_valid_) { return false; }
|
||||||
shutdown_and_close();
|
shutdown_and_close();
|
||||||
|
|
||||||
// Check is custom IP or hostname specified for host_.
|
// Check is custom IP or hostname specified for host_
|
||||||
// host_ stays the identity used for the Host header and for SNI, while the
|
std::string connect_host;
|
||||||
// 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_;
|
|
||||||
std::string ip;
|
std::string ip;
|
||||||
auto it = addr_map_.find(host_);
|
detail::apply_addr_map(addr_map_, host_, connect_host, ip);
|
||||||
if (it != addr_map_.end() && !it->second.empty()) {
|
|
||||||
if (detail::is_ip_address(it->second)) {
|
|
||||||
ip = it->second;
|
|
||||||
} else {
|
|
||||||
connect_host = it->second;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Error error;
|
Error error;
|
||||||
sock_ = detail::create_client_socket(
|
sock_ = detail::create_client_socket(
|
||||||
|
|||||||
@@ -19395,6 +19395,47 @@ TEST(WebSocketTest, SpecifyServerIPAddress_RealHostname) {
|
|||||||
t.join();
|
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 {
|
class WebSocketIntegrationTest : public ::testing::Test {
|
||||||
protected:
|
protected:
|
||||||
void SetUp() override {
|
void SetUp() override {
|
||||||
|
|||||||
Reference in new issue
Block a user