mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-11-11 16:40:21 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdcd2ac6e6 | ||
|
|
bd23a7b179 | ||
|
|
6d8dec3c3e | ||
|
|
7e5d0683ac | ||
|
|
6df971434e | ||
|
|
39c98d4671 | ||
|
|
a1ba73a256 | ||
|
|
453b1b8613 | ||
|
|
a18895f42e |
No files matched your search
@@ -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
|
||||
-------------------------------
|
||||
|
||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
||||
|
||||
[site]
|
||||
title = "cpp-httplib"
|
||||
version = "0.64.0"
|
||||
version = "0.66.0"
|
||||
hostname = "https://yhirose.github.io"
|
||||
base_path = "/cpp-httplib"
|
||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||
#define CPPHTTPLIB_HTTPLIB_H
|
||||
|
||||
#define CPPHTTPLIB_VERSION "0.64.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x004000"
|
||||
#define CPPHTTPLIB_VERSION "0.66.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x004200"
|
||||
|
||||
#ifdef _WIN32
|
||||
#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.
|
||||
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("?#"));
|
||||
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 = "/"; }
|
||||
auto merged = base_path.substr(0, base_path.rfind('/') + 1) + location;
|
||||
|
||||
// Remove "." and ".." segments from the merged path.
|
||||
auto path_end = (std::min)(merged.find_first_of("?#"), merged.size());
|
||||
return remove_dot_segments(merged.substr(0, path_end)) +
|
||||
merged.substr(path_end);
|
||||
return on_this_host(remove_dot_segments(merged.substr(0, path_end)) +
|
||||
merged.substr(path_end));
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
@@ -3058,6 +3067,10 @@ protected:
|
||||
bool socket_should_be_closed_when_request_is_done_ = false;
|
||||
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};
|
||||
|
||||
// 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()) {
|
||||
auto left = std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
deadline - std::chrono::steady_clock::now());
|
||||
auto wait =
|
||||
(std::max)((std::min)(left, slice), std::chrono::microseconds(0));
|
||||
auto ret = select_impl(sock, POLLIN, 0, static_cast<time_t>(wait.count()));
|
||||
if (ret != 0 || wait < slice) { return ret; }
|
||||
auto last = left < slice;
|
||||
// poll() takes whole milliseconds, so round the last wait up to keep it
|
||||
// from ending before the deadline.
|
||||
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;
|
||||
}
|
||||
@@ -9335,6 +9352,7 @@ inline bool redirect(T &cli, Request &req, Response &res,
|
||||
Error &error) {
|
||||
Request new_req = req;
|
||||
new_req.path = path;
|
||||
new_req.params.clear();
|
||||
new_req.redirect_count_ -= 1;
|
||||
|
||||
if (res.status == StatusCode::SeeOther_303 &&
|
||||
@@ -12620,7 +12638,16 @@ inline bool ThreadPool::enqueue(std::function<void()> fn) {
|
||||
if (jobs_.size() > idle_thread_count_ &&
|
||||
threads_.size() + dynamic_threads_.size() < max_thread_count_) {
|
||||
cleanup_finished_threads();
|
||||
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
|
||||
try {
|
||||
#endif
|
||||
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) {
|
||||
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 =
|
||||
(std::min)(size, static_cast<size_t>((std::numeric_limits<int>::max)()));
|
||||
#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);
|
||||
}
|
||||
|
||||
// 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.
|
||||
inline std::string
|
||||
make_connect_request_target(const std::map<std::string, std::string> &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);
|
||||
}
|
||||
|
||||
@@ -15274,7 +15310,10 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
if ((req.method == "GET" || req.method == "HEAD") &&
|
||||
req.has_header("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;
|
||||
res.status = StatusCode::RangeNotSatisfiable_416;
|
||||
output_error_log(Error::InvalidRangeHeader, &req);
|
||||
@@ -15803,6 +15842,9 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
|
||||
// socket.
|
||||
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 (!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) {
|
||||
strm.set_read_interrupt([this] { return stop_requested_.load(); });
|
||||
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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -16614,6 +16662,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_);
|
||||
@@ -18068,6 +18124,9 @@ inline void ClientImpl::stop() {
|
||||
// are not thread-safe.
|
||||
if (socket_requests_in_flight_ > 0) {
|
||||
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
|
||||
// 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);
|
||||
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));
|
||||
|
||||
if (ret > 0) {
|
||||
|
||||
+302
@@ -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];
|
||||
@@ -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) {
|
||||
auto host = "unresolvableaddress.local";
|
||||
auto path = std::string{"/"};
|
||||
@@ -3445,6 +3544,55 @@ TEST(CancelTest, WithCancelLargePayloadDelete) {
|
||||
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) {
|
||||
std::string output;
|
||||
output.reserve(input.size());
|
||||
@@ -4248,6 +4396,62 @@ TEST(RedirectToSamePort, ForwardCookie) {
|
||||
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) {
|
||||
std::string authorization;
|
||||
std::string trace_id;
|
||||
@@ -20676,6 +20880,13 @@ TEST(RedirectTest, ResolveRelativeLocation) {
|
||||
detail::resolve_relative_location(c.first, "/b/c/d;p?q"))
|
||||
<< 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) {
|
||||
@@ -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
|
||||
TEST(RedirectTest, Issue2185_Online) {
|
||||
SSLClient client("github.com");
|
||||
@@ -24952,6 +25211,49 @@ TEST(SSLTest, ResponseBodyTerminatedByConnectionClose) {
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
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
|
||||
|
||||
//==============================================================================
|
||||
|
||||
Reference in new issue
Block a user