Compare commits

..
9 Commits
Author SHA1 Message Date
yhirose cdcd2ac6e6 Release v0.66.0 2026-11-10 17:19:39 -05:00
yhirose bd23a7b179 Keep the job queued when a dynamic worker thread cannot be created
ThreadPool::enqueue constructed the dynamic thread without handling
std::system_error. When thread creation failed (e.g. EAGAIN under a
pids limit), the exception escaped the accept loop and destroying the
pool's joinable threads called std::terminate. Treat the failure as
"no spare capacity" and leave the job for an existing worker.
2026-11-10 16:54:53 -05:00
metsw24-max 6d8dec3c3e 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.
2026-11-07 10:22:00 -05:00
yhirose 7e5d0683ac Release v0.65.0 2026-11-05 07:39:47 -05:00
yhirose 6df971434e Do not carry Request::params over to a redirect's Location
A request sent with send(Request) and params got those params appended
to the Location of a redirect that had no query, on the same origin or
another one. The Location alone names the next target, as it already
did for Get/Post(path, params).
2026-11-05 07:04:31 -05:00
Eminy 39c98d4671 Ignore a Range header with a unit other than bytes (#2657)
RFC 9110 14.2 says an origin server MUST ignore a Range header field
with a range unit it does not understand. parse_range_header() accepts
only bytes=, so a request such as "Range: items=1-2" got a 416 instead
of the full response. The unit is now checked first, and only a bytes
range is parsed and answered with a 416 when it is invalid.
2026-11-04 22:44:22 -05:00
Wei-Lun Liandyhirose a1ba73a256 Cap a send() or wolfSSL_read() length at INT_MAX (#2655)
* Cap a send() or wolfSSL_read() length at INT_MAX

On 64-bit Windows SocketStream::write() handed size to send(), whose
length is an int, so a single write of 4 GiB or more went out as its low
32 bits: a 4 GiB + 1 byte write sent one byte, and write_data() then
retried the remaining 4 GiB as zero-byte sends forever. The INT_MAX cap
that read() has was limited to 32-bit Windows in df20c27 (#1294), when
the body was still written with one unchecked write(); write_data() now
sends the rest, so the cap applies to every Windows build again.

The wolfSSL tls::read() had the same cast, so a read into a buffer over
INT_MAX failed. Cap it as the OpenSSL backend has done since f80864c.

* Drop the 4 GiB write test and shorten comments

The Windows-only test allocated 4 GiB and took about 5 s, which is too
heavy for CI.

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-11-04 18:06:30 -05:00
metsw24-maxandyhirose 453b1b8613 keep a relative Location on its host in resolve_relative_location (#2654)
* keep a relative Location on its host in resolve_relative_location

* Shorten the comment on on_this_host in resolve_relative_location

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-11-02 13:12:31 -05:00
Yoshiki Matsuda a18895f42e Let ClientImpl::stop() interrupt a request waiting on Windows (#2653)
stop() shuts the socket down to end a request in flight, but on Windows
that does not wake a read waiting in WSAPoll (#1396). The request then
waited until the server answered, the read timeout passed, or the OS
dropped the connection.

stop() now also sets a flag, and send_() gives the request's stream an
interrupt check that reads it, as SSEClient::stop() does since 801bf2e.
The wait for data then ends within 100 ms on every platform. A request
started after stop() clears the flag. A redirect or a digest retry sent
inside the request keeps it.

select_read_interruptible() now rounds its last wait up to a whole
millisecond. poll() takes milliseconds, and the wait ended up to 1 ms
before the deadline, which MaxTimeoutTest caught once every client
request waited through this function.
2026-10-30 13:40:50 -04:00
4 changed files with 384 additions and 18 deletions

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
------------------------------- -------------------------------
+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site] [site]
title = "cpp-httplib" title = "cpp-httplib"
version = "0.64.0" version = "0.66.0"
hostname = "https://yhirose.github.io" hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib" base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved." footer_message = "© 2026 Yuji Hirose. All rights reserved."
+79 -17
View File
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H #ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.64.0" #define CPPHTTPLIB_VERSION "0.66.0"
#define CPPHTTPLIB_VERSION_NUM "0x004000" #define CPPHTTPLIB_VERSION_NUM "0x004200"
#ifdef _WIN32 #ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00 #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -1029,18 +1029,27 @@ inline std::string resolve_relative_location(const std::string &location,
// A ':' in the first segment means the value has a scheme. // A ':' in the first segment means the value has a scheme.
if (location.find(':') < location.find_first_of("/?#")) { return location; } if (location.find(':') < location.find_first_of("/?#")) { return location; }
if (location[0] == '#') { return base.substr(0, base.find('#')) + location; } // A path starting with "//" would be re-parsed as an authority. Prefix "/."
// (as the URL Standard does); remove_dot_segments strips it later.
auto on_this_host = [](std::string path) -> std::string {
if (path.compare(0, 2, "//") == 0) { path.insert(0, "/."); }
return path;
};
if (location[0] == '#') {
return on_this_host(base.substr(0, base.find('#')) + location);
}
auto base_path = base.substr(0, base.find_first_of("?#")); auto base_path = base.substr(0, base.find_first_of("?#"));
if (location[0] == '?') { return base_path + location; } if (location[0] == '?') { return on_this_host(base_path + location); }
if (base_path.empty() || base_path[0] != '/') { base_path = "/"; } if (base_path.empty() || base_path[0] != '/') { base_path = "/"; }
auto merged = base_path.substr(0, base_path.rfind('/') + 1) + location; auto merged = base_path.substr(0, base_path.rfind('/') + 1) + location;
// Remove "." and ".." segments from the merged path. // Remove "." and ".." segments from the merged path.
auto path_end = (std::min)(merged.find_first_of("?#"), merged.size()); auto path_end = (std::min)(merged.find_first_of("?#"), merged.size());
return remove_dot_segments(merged.substr(0, path_end)) + return on_this_host(remove_dot_segments(merged.substr(0, path_end)) +
merged.substr(path_end); merged.substr(path_end));
} }
} // namespace detail } // namespace detail
@@ -3058,6 +3067,10 @@ protected:
bool socket_should_be_closed_when_request_is_done_ = false; bool socket_should_be_closed_when_request_is_done_ = false;
size_t socket_tls_settings_version_ = 0; size_t socket_tls_settings_version_ = 0;
// Set by stop() while a request is in flight. The request's reads poll it,
// because shutting the socket down does not wake a read on Windows.
std::atomic<bool> stop_requested_{false};
std::atomic<size_t> tls_settings_version_{0}; std::atomic<size_t> tls_settings_version_{0};
// Hostname to connection target map. The value is an IP literal or another // Hostname to connection target map. The value is an IP literal or another
@@ -6840,10 +6853,14 @@ select_read_interruptible(socket_t sock, time_t sec, time_t usec,
while (!interrupted()) { while (!interrupted()) {
auto left = std::chrono::duration_cast<std::chrono::microseconds>( auto left = std::chrono::duration_cast<std::chrono::microseconds>(
deadline - std::chrono::steady_clock::now()); deadline - std::chrono::steady_clock::now());
auto wait = auto last = left < slice;
(std::max)((std::min)(left, slice), std::chrono::microseconds(0)); // poll() takes whole milliseconds, so round the last wait up to keep it
auto ret = select_impl(sock, POLLIN, 0, static_cast<time_t>(wait.count())); // from ending before the deadline.
if (ret != 0 || wait < slice) { return ret; } auto wait_usec = last ? (std::max)((left.count() + 999) / 1000 * 1000,
static_cast<decltype(left.count())>(0))
: slice.count();
auto ret = select_impl(sock, POLLIN, 0, static_cast<time_t>(wait_usec));
if (ret != 0 || last) { return ret; }
} }
return 0; return 0;
} }
@@ -9335,6 +9352,7 @@ inline bool redirect(T &cli, Request &req, Response &res,
Error &error) { Error &error) {
Request new_req = req; Request new_req = req;
new_req.path = path; new_req.path = path;
new_req.params.clear();
new_req.redirect_count_ -= 1; new_req.redirect_count_ -= 1;
if (res.status == StatusCode::SeeOther_303 && if (res.status == StatusCode::SeeOther_303 &&
@@ -12620,7 +12638,16 @@ inline bool ThreadPool::enqueue(std::function<void()> fn) {
if (jobs_.size() > idle_thread_count_ && if (jobs_.size() > idle_thread_count_ &&
threads_.size() + dynamic_threads_.size() < max_thread_count_) { threads_.size() + dynamic_threads_.size() < max_thread_count_) {
cleanup_finished_threads(); cleanup_finished_threads();
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
try {
#endif
dynamic_threads_.emplace_back(std::thread([this]() { worker(true); })); dynamic_threads_.emplace_back(std::thread([this]() { worker(true); }));
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
} catch (const std::system_error &) {
// No thread budget left: keep the job queued for an existing worker
// instead of letting the exception escape the accept loop.
}
#endif
} }
} }
@@ -12859,7 +12886,8 @@ inline ssize_t SocketStream::read(char *ptr, size_t size) {
inline ssize_t SocketStream::write(const char *ptr, size_t size) { inline ssize_t SocketStream::write(const char *ptr, size_t size) {
if (!wait_writable()) { return -1; } if (!wait_writable()) { return -1; }
#if defined(_WIN32) && !defined(_WIN64) // Winsock's send() takes an int length, even on 64-bit Windows
#ifdef _WIN32
size = size =
(std::min)(size, static_cast<size_t>((std::numeric_limits<int>::max)())); (std::min)(size, static_cast<size_t>((std::numeric_limits<int>::max)()));
#endif #endif
@@ -13074,14 +13102,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);
} }
@@ -15274,7 +15310,10 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
if ((req.method == "GET" || req.method == "HEAD") && if ((req.method == "GET" || req.method == "HEAD") &&
req.has_header("Range")) { req.has_header("Range")) {
const auto &range_header_value = req.get_header_value("Range"); const auto &range_header_value = req.get_header_value("Range");
if (!detail::parse_range_header(range_header_value, req.ranges)) { // RFC 9110 14.2: a range unit other than bytes must be ignored
auto unit = range_header_value.substr(0, range_header_value.find('='));
if (detail::case_ignore::equal(unit, "bytes") &&
!detail::parse_range_header(range_header_value, req.ranges)) {
connection_closed = true; connection_closed = true;
res.status = StatusCode::RangeNotSatisfiable_416; res.status = StatusCode::RangeNotSatisfiable_416;
output_error_log(Error::InvalidRangeHeader, &req); output_error_log(Error::InvalidRangeHeader, &req);
@@ -15803,6 +15842,9 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
// socket. // socket.
socket_should_be_closed_when_request_is_done_ = false; socket_should_be_closed_when_request_is_done_ = false;
// A redirect or a retry sent inside a request keeps its stop()
if (socket_requests_in_flight_ == 0) { stop_requested_ = false; }
if (!socket_reusable()) { if (!socket_reusable()) {
if (!ensure_socket_connection(socket_, error)) { return false; } if (!ensure_socket_connection(socket_, error)) { return false; }
@@ -15850,6 +15892,7 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
}); });
ret = process_socket(socket_, req.start_time_, [&](Stream &strm) { ret = process_socket(socket_, req.start_time_, [&](Stream &strm) {
strm.set_read_interrupt([this] { return stop_requested_.load(); });
return handle_request(strm, req, res, close_connection, error); return handle_request(strm, req, res, close_connection, error);
}); });
@@ -16462,8 +16505,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);
} }
@@ -16614,6 +16662,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_);
@@ -18068,6 +18124,9 @@ inline void ClientImpl::stop() {
// are not thread-safe. // are not thread-safe.
if (socket_requests_in_flight_ > 0) { if (socket_requests_in_flight_ > 0) {
shutdown_socket(socket_); shutdown_socket(socket_);
// shutdown() does not wake a read waiting in WSAPoll on Windows, so the
// request's reads also give up once they see this flag.
stop_requested_ = true;
// Aside from that, we set a flag for the socket to be closed when we're // Aside from that, we set a flag for the socket to be closed when we're
// done. // done.
@@ -23030,6 +23089,9 @@ inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
} }
auto wsession = static_cast<impl::WolfSSLSession *>(session); auto wsession = static_cast<impl::WolfSSLSession *>(session);
constexpr auto max_len =
static_cast<size_t>((std::numeric_limits<int>::max)());
if (len > max_len) { len = max_len; }
int ret = wolfSSL_read(wsession->ssl, buf, static_cast<int>(len)); int ret = wolfSSL_read(wsession->ssl, buf, static_cast<int>(len));
if (ret > 0) { if (ret > 0) {
+302
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];
@@ -2818,6 +2888,35 @@ TEST(RangeTest, IgnoredForPost) {
} }
} }
TEST(RangeTest, IgnoredForUnknownUnit) {
Server svr;
svr.Get("/", [](const Request &, Response &res) {
res.set_content("abcdefg", "text/plain");
});
auto port = svr.bind_to_any_port(HOST);
ASSERT_GT(port, 0);
thread t = thread([&] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, port);
for (auto range : {"items=1-2", "none=x"}) {
auto res = cli.Get("/", Headers{{"Range", range}});
ASSERT_TRUE(res) << range << ": " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status) << range;
EXPECT_EQ("abcdefg", res->body) << range;
}
auto res = cli.Get("/", Headers{{"Range", "bytes=x"}});
ASSERT_TRUE(res) << to_string(res.error());
EXPECT_EQ(StatusCode::RangeNotSatisfiable_416, res->status);
}
TEST(GetAddrInfoDanglingRefTest, LongTimeout) { TEST(GetAddrInfoDanglingRefTest, LongTimeout) {
auto host = "unresolvableaddress.local"; auto host = "unresolvableaddress.local";
auto path = std::string{"/"}; auto path = std::string{"/"};
@@ -3445,6 +3544,55 @@ TEST(CancelTest, WithCancelLargePayloadDelete) {
EXPECT_EQ(Error::Canceled, res.error()); EXPECT_EQ(Error::Canceled, res.error());
} }
// One stop() from another thread ends a request that waits for the response
TEST(ClientStopTest, StopEndsWaitForResponse) {
Server svr;
std::atomic<bool> received{false};
std::atomic<bool> done{false};
svr.Get("/wait", [&](const Request & /*req*/, Response &res) {
received.store(true);
while (!done.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
res.set_content("Hello World!", "text/plain");
});
auto port = svr.bind_to_any_port(HOST);
auto thread = std::thread([&]() { svr.listen_after_bind(); });
Client cli(HOST, port);
cli.set_read_timeout(std::chrono::seconds(30));
std::future<Result> request;
auto se = detail::scope_exit([&] {
done.store(true);
svr.stop();
thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
request = std::async(std::launch::async, [&]() { return cli.Get("/wait"); });
for (int i = 0; i < 500 && !received.load(); ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
ASSERT_TRUE(received.load());
// Let the client block in the wait for the response
std::this_thread::sleep_for(std::chrono::milliseconds(100));
auto start = std::chrono::steady_clock::now();
cli.stop();
ASSERT_EQ(std::future_status::ready,
request.wait_for(std::chrono::seconds(5)));
EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds(2));
auto res = request.get();
ASSERT_TRUE(!res);
EXPECT_EQ(Error::Read, res.error());
}
static std::string remove_whitespace(const std::string &input) { static std::string remove_whitespace(const std::string &input) {
std::string output; std::string output;
output.reserve(input.size()); output.reserve(input.size());
@@ -4248,6 +4396,62 @@ TEST(RedirectToSamePort, ForwardCookie) {
EXPECT_EQ("session_id=SECRET", captured_cookie); EXPECT_EQ("session_id=SECRET", captured_cookie);
} }
TEST(RedirectTest, DoNotCarryRequestParamsToLocation) {
// The Location alone names the next target, on any origin.
Server target_svr;
std::string cross_target;
target_svr.Get("/landing", [&](const Request &req, Response &res) {
cross_target = req.target;
res.set_content("OK", "text/plain");
});
auto target_port = target_svr.bind_to_any_port(HOST);
auto target_thread = std::thread([&]() { target_svr.listen_after_bind(); });
Server origin_svr;
std::string same_target;
origin_svr.Get("/cross", [&](const Request &, Response &res) {
res.set_redirect(
"http://localhost:" + std::to_string(target_port) + "/landing", 302);
});
origin_svr.Get("/same", [&](const Request &, Response &res) {
res.set_redirect("/landing", 302);
});
origin_svr.Get("/landing", [&](const Request &req, Response &res) {
same_target = req.target;
res.set_content("OK", "text/plain");
});
auto origin_port = origin_svr.bind_to_any_port(HOST);
auto origin_thread = std::thread([&]() { origin_svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
origin_svr.stop();
origin_thread.join();
target_svr.stop();
target_thread.join();
});
target_svr.wait_until_ready();
origin_svr.wait_until_ready();
Client cli("localhost", origin_port);
cli.set_follow_location(true);
Request req;
req.method = "GET";
req.params = {{"api_key", "SECRET"}};
req.path = "/cross";
auto res = cli.send(req);
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ("/landing", cross_target);
req.path = "/same";
res = cli.send(req);
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ("/landing", same_target);
}
TEST(RedirectToDifferentPort, ForwardDefaultHeadersButNotCredentials) { TEST(RedirectToDifferentPort, ForwardDefaultHeadersButNotCredentials) {
std::string authorization; std::string authorization;
std::string trace_id; std::string trace_id;
@@ -20676,6 +20880,13 @@ TEST(RedirectTest, ResolveRelativeLocation) {
detail::resolve_relative_location(c.first, "/b/c/d;p?q")) detail::resolve_relative_location(c.first, "/b/c/d;p?q"))
<< c.first; << c.first;
} }
// A result starting with "//" is still a path, not a network-path reference
EXPECT_EQ("/.//g",
detail::resolve_relative_location("../../..//g", "/b/c/d;p?q"));
EXPECT_EQ("/.//g/x", detail::resolve_relative_location("x", "//g/h"));
EXPECT_EQ("/.//g/h?y", detail::resolve_relative_location("?y", "//g/h"));
EXPECT_EQ("/.//g/h#s", detail::resolve_relative_location("#s", "//g/h"));
} }
TEST(RedirectTest, RemoveDotSegments) { TEST(RedirectTest, RemoveDotSegments) {
@@ -20763,6 +20974,54 @@ TEST(RedirectTest, RelativeLocationWithoutLeadingSlash) {
} }
} }
// "..//host:port/x" resolves to the path "//host:port/x" on the same host
// (RFC 3986 5.2), as curl and the URL Standard have it, not to another host.
TEST(RedirectTest, RelativeLocationStaysOnHost) {
Server other;
std::atomic<int> other_hits{0};
other.Get(".*", [&](const Request & /*req*/, Response &res) {
other_hits++;
res.set_content("other", "text/plain");
});
auto other_port = other.bind_to_any_port("127.0.0.1");
auto other_thread = std::thread([&]() { other.listen_after_bind(); });
Server svr;
svr.Get("/dir/page", [](const Request &req, Response &res) {
res.set_redirect(req.get_param_value("to"));
});
svr.Get(".*", [](const Request &req, Response &res) {
res.set_content(req.path, "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();
other.stop();
other_thread.join();
ASSERT_FALSE(svr.is_running());
ASSERT_FALSE(other.is_running());
});
svr.wait_until_ready();
other.wait_until_ready();
auto authority = "127.0.0.1:" + std::to_string(other_port);
for (const auto &to :
{"..//" + authority + "/x", "../..//" + authority + "/x"}) {
Client cli(HOST, port);
cli.set_follow_location(true);
auto res = cli.Get("/dir/page?to=" + encode_query_component(to));
ASSERT_TRUE(res) << to << ": " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status) << to;
EXPECT_EQ("//" + authority + "/x", res->body) << to;
}
EXPECT_EQ(0, other_hits.load());
}
#ifdef CPPHTTPLIB_SSL_ENABLED #ifdef CPPHTTPLIB_SSL_ENABLED
TEST(RedirectTest, Issue2185_Online) { TEST(RedirectTest, Issue2185_Online) {
SSLClient client("github.com"); SSLClient client("github.com");
@@ -24952,6 +25211,49 @@ TEST(SSLTest, ResponseBodyTerminatedByConnectionClose) {
EXPECT_EQ(StatusCode::OK_200, res->status); EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(expected_body, res->body); EXPECT_EQ(expected_body, res->body);
} }
// Without Content-Length, the full buffer size reaches tls::read().
TEST(SSLTest, ReadIntoBufferOverIntMax) {
if (sizeof(size_t) <= sizeof(int)) {
GTEST_SKIP() << "size_t cannot exceed INT_MAX here";
}
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(svr.is_valid());
svr.Get("/no-content-length", [&](const Request & /*req*/, Response &res) {
res.set_content_provider("text/plain",
[&](size_t offset, DataSink &sink) -> bool {
if (offset == 0) { sink.write("Hello", 5); }
sink.done();
return true;
});
});
auto port = svr.bind_to_any_port(HOST);
auto listen_thread = std::thread([&svr] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
listen_thread.join();
});
svr.wait_until_ready();
SSLClient cli(HOST, port);
cli.enable_server_certificate_verification(false);
auto handle = cli.open_stream("GET", "/no-content-length");
ASSERT_TRUE(handle.is_valid()) << to_string(handle.error);
const auto len = static_cast<size_t>((std::numeric_limits<int>::max)()) + 1;
std::unique_ptr<char[]> buf(new char[len]);
std::string body;
ssize_t n;
while ((n = handle.read(buf.get(), len)) > 0) {
body.append(buf.get(), static_cast<size_t>(n));
}
EXPECT_EQ(0, n);
EXPECT_EQ("Hello", body);
}
#endif // CPPHTTPLIB_SSL_ENABLED #endif // CPPHTTPLIB_SSL_ENABLED
//============================================================================== //==============================================================================