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

+70
View File
@@ -291,6 +291,76 @@ TEST_F(UnixSocketTest, HostHeaderAutoSet) {
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
TEST(SocketStream, wait_writable_UNIX) {
int fds[2];