diff --git a/README.md b/README.md index 73cd39ca..3316e6a3 100644 --- a/README.md +++ b/README.md @@ -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. +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 ------------------------------- diff --git a/httplib.h b/httplib.h index 483a46c7..8ba0917e 100644 --- a/httplib.h +++ b/httplib.h @@ -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); } +// 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 &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. inline std::string make_connect_request_target(const std::map &addr_map, const std::string &host, int port) { - std::string connect_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, + return make_host_and_port_string_always_port(addr_map_target(addr_map, host), port); } @@ -16488,8 +16496,13 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) { 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 - 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); } @@ -16640,6 +16653,14 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) { // Copy network and socket settings client.addr_map_ = 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_tcp_nodelay(tcp_nodelay_); client.set_ipv6_v6only(ipv6_v6only_); diff --git a/test/test.cc b/test/test.cc index 123bf6ef..042e6209 100644 --- a/test/test.cc +++ b/test/test.cc @@ -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];