mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-01 13:12:40 +07:00
Allow addr_map_ values to be hostnames, not just IP literals (#2515)
addr_map_ only accepted IP literals as mapped values; a non-IP value was passed as the `ip` argument and rejected by getaddrinfo's AI_NUMERICHOST path. The lookup in ClientImpl::create_client_socket and WebSocketClient::connect now checks the mapped value with detail::is_ip_address: IP literals keep the existing AI_NUMERICHOST path, while hostnames are passed as the connect host so they get resolved. host_ is never touched, so it keeps supplying the Host header and SNI in both cases. is_ip_address() moved into the non-SSL detail block so that both client code paths can use it without CPPHTTPLIB_SSL_ENABLED. This also fixes the documented Unix domain socket client example, where the mapped value is a socket path: it previously took the AI_NUMERICHOST path and never reached the AF_UNIX branch in create_socket.
This commit is contained in:
@@ -15,7 +15,9 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <clocale>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <future>
|
||||
@@ -2573,6 +2575,101 @@ TEST(SpecifyServerIPAddressTest, RealHostname_Online) {
|
||||
EXPECT_EQ(Error::Connection, res.error());
|
||||
}
|
||||
|
||||
TEST(SpecifyServerIPAddressTest, HostnameAsAddrMapValue) {
|
||||
// A mapped value that is not an IP literal must be resolved. "localhost"
|
||||
// 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.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(HOST);
|
||||
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);
|
||||
cli.set_hostname_addr_map({{host, HOST}});
|
||||
|
||||
auto res = cli.Get("/hi");
|
||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
// The mapping only redirects the connection; the identity stays host_.
|
||||
EXPECT_EQ(std::string(host) + ":" + std::to_string(port), received_host);
|
||||
}
|
||||
|
||||
TEST(SpecifyServerIPAddressTest, IPAddressAsAddrMapValue) {
|
||||
// A mapped value that is an IP literal keeps the AI_NUMERICHOST path.
|
||||
auto host = "target.invalid";
|
||||
|
||||
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(host, port);
|
||||
cli.set_hostname_addr_map({{host, "127.0.0.1"}});
|
||||
|
||||
auto res = cli.Get("/hi");
|
||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
TEST(SpecifyServerIPAddressTest, EmptyAddrMapValueIsIgnored) {
|
||||
// An empty mapped value must leave host_ as the connection target. Without
|
||||
// that guard the empty value would become the host argument, getaddrinfo
|
||||
// would be called with a null node, and (no AI_PASSIVE) it would resolve to
|
||||
// loopback - silently connecting somewhere the caller never asked for.
|
||||
// The server listens on loopback, so such a fallback would succeed and is
|
||||
// therefore observable as a failure of this test.
|
||||
auto blackhole = "192.0.2.1"; // TEST-NET-1, never routable
|
||||
|
||||
Server svr;
|
||||
svr.Get("/hi", [](const Request & /*req*/, Response &res) {
|
||||
res.set_content("Hello World!", "text/plain");
|
||||
});
|
||||
|
||||
auto port = svr.bind_to_any_port(HOST);
|
||||
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(blackhole, port);
|
||||
cli.set_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(AbsoluteRedirectTest, Redirect_Online) {
|
||||
auto host = "httpbingo.org";
|
||||
auto path = std::string{"/absolute-redirect/3"};
|
||||
|
||||
Reference in New Issue
Block a user