Keep a redirect on the unix socket in ClientImpl::redirect (#2658)

A client using set_address_family(AF_UNIX) took the host of an absolute Location as a socket path, so a redirect to http://localhost/next failed and one to http://other.sock/next went to ./other.sock. Every hop now stays on the client's socket, as with curl's --unix-socket.

The same host is still decided from the Location, as curl does: only localhost (the Host this client sends) counts as this client, so a redirect to any other host goes through the cross-host client and drops Authorization and Cookie. That client reaches the original socket through addr_map_, and a host the caller mapped with set_direct_hostname_addr_map keeps its mapping. An https Location runs TLS over the socket with the Location's host.

The mapped-target lookup shared with make_connect_request_target is factored into detail::addr_map_target.
This commit is contained in:
metsw24-max authored and GitHub committed 2026-11-07 10:22:00 -05:00
1 parent 7e5d0683ac
commit 6d8dec3c3e
3 files changed
+98 -5

No files matched your search

+2
View File
@@ -1716,6 +1716,8 @@ cli.set_address_family(AF_UNIX);
This library automatically sets the Host header to "localhost" for Unix socket connections, similar to curl's behavior. This library automatically sets the Host header to "localhost" for Unix socket connections, similar to curl's behavior.
With `set_follow_location(true)`, a redirect is followed over the same socket, whatever host the `Location` names, as curl's `--unix-socket` does. A host mapped with `set_direct_hostname_addr_map` goes to its mapped socket instead. Only a `Location` naming `localhost` counts as the same host; any other host is treated as cross-host, so `Authorization` and `Cookie` are not sent to it.
URI Encoding/Decoding Utilities URI Encoding/Decoding Utilities
------------------------------- -------------------------------
+26 -5
View File
@@ -13093,14 +13093,22 @@ 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);
} }
// Where a client connects for `host`, as one string: the mapped address or
// name, or `host` itself.
inline std::string
addr_map_target(const std::map<std::string, std::string> &addr_map,
const std::string &host) {
std::string connect_host;
std::string ip;
apply_addr_map(addr_map, host, connect_host, ip);
return ip.empty() ? connect_host : ip;
}
// Request target of a CONNECT for `host`, with `addr_map` applied. // Request target of a CONNECT for `host`, with `addr_map` applied.
inline std::string inline std::string
make_connect_request_target(const std::map<std::string, std::string> &addr_map, make_connect_request_target(const std::map<std::string, std::string> &addr_map,
const std::string &host, int port) { const std::string &host, int port) {
std::string connect_host; return make_host_and_port_string_always_port(addr_map_target(addr_map, 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); port);
} }
@@ -16488,8 +16496,13 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
auto path = std::move(next_path) + next_query; auto path = std::move(next_path) + next_query;
// A Unix socket client sends "Host: localhost", so a Location naming
// localhost names this client too.
auto is_same_host = next_host == host_ ||
(address_family_ == AF_UNIX && next_host == "localhost");
// Same host redirect - use current client // Same host redirect - use current client
if (next_scheme == scheme && next_host == host_ && next_port == port_) { if (next_scheme == scheme && is_same_host && next_port == port_) {
return detail::redirect(*this, req, res, path, location, error); return detail::redirect(*this, req, res, path, location, error);
} }
@@ -16640,6 +16653,14 @@ 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.tunnel_addr_map_ = tunnel_addr_map_;
// A Unix socket client reaches every host through its own socket, as curl's
// --unix-socket does, unless the caller mapped that host elsewhere. The
// Location's host is never taken as a socket path.
if (address_family_ == AF_UNIX) {
auto &target = client.addr_map_[client.host()];
if (target.empty()) { target = detail::addr_map_target(addr_map_, host_); }
}
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_);
+70
View File
@@ -291,6 +291,76 @@ TEST_F(UnixSocketTest, HostHeaderAutoSet) {
EXPECT_EQ(received_host_header, "localhost"); EXPECT_EQ(received_host_header, "localhost");
} }
TEST_F(UnixSocketTest, RedirectStaysOnSocket) {
// Another socket in the working directory, which a Location can name as
// its host
const auto other_path = "other-" + pathname_.substr(2);
httplib::Server svr;
svr.Get("/redirect", [&](const httplib::Request &, httplib::Response &res) {
// An absolute Location naming the Host this client sends
res.set_redirect("http://localhost" + pattern_);
});
svr.Get("/cross", [&](const httplib::Request &, httplib::Response &res) {
res.set_redirect("http://" + other_path + pattern_);
});
svr.Get("/mapped", [&](const httplib::Request &, httplib::Response &res) {
res.set_redirect("http://mapped.test" + pattern_);
});
svr.Get(pattern_, [&](const httplib::Request &req, httplib::Response &res) {
res.set_header("X-Auth", req.get_header_value("Authorization"));
res.set_content(content_, "text/plain");
});
httplib::Server other;
other.Get(pattern_, [&](const httplib::Request &, httplib::Response &res) {
res.set_content("other", "text/plain");
});
std::thread t{[&] {
ASSERT_TRUE(svr.set_address_family(AF_UNIX).listen(pathname_, 80));
}};
std::thread t2{[&] {
ASSERT_TRUE(other.set_address_family(AF_UNIX).listen(other_path, 80));
}};
auto se = detail::scope_exit([&] {
svr.stop();
other.stop();
t.join();
t2.join();
std::remove(other_path.c_str());
});
svr.wait_until_ready();
other.wait_until_ready();
httplib::Client cli{pathname_};
cli.set_address_family(AF_UNIX);
cli.set_follow_location(true);
cli.set_bearer_token_auth("token");
// Another host: the same socket, without the credentials
auto res = cli.Get("/cross");
ASSERT_TRUE(res) << "error: " << res.error();
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(content_, res->body);
EXPECT_EQ("", res->get_header_value("X-Auth"));
// localhost is this client's own host
res = cli.Get("/redirect");
ASSERT_TRUE(res) << "error: " << res.error();
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(content_, res->body);
EXPECT_EQ("Bearer token", res->get_header_value("X-Auth"));
// A host the caller mapped to a socket goes there
cli.set_direct_hostname_addr_map({{"mapped.test", other_path}});
res = cli.Get("/mapped");
ASSERT_TRUE(res) << "error: " << res.error();
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ("other", res->body);
}
#ifndef _WIN32 #ifndef _WIN32
TEST(SocketStream, wait_writable_UNIX) { TEST(SocketStream, wait_writable_UNIX) {
int fds[2]; int fds[2];