mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-11-03 13:38:36 +00:00
Compare commits
3
Commits
v0.57.1
...
4f3f9ef19b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f3f9ef19b | ||
|
|
6d59d1e2df | ||
|
|
9386b25dd7 |
No files matched your search
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
||||
|
||||
[site]
|
||||
title = "cpp-httplib"
|
||||
version = "0.57.1"
|
||||
version = "0.58.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.57.1"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x003901"
|
||||
#define CPPHTTPLIB_VERSION "0.58.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x003a00"
|
||||
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||
@@ -2986,7 +2986,7 @@ private:
|
||||
bool read_response_line(Stream &strm, const Request &req, Response &res,
|
||||
bool skip_100_continue = true) const;
|
||||
bool write_request(Stream &strm, Request &req, bool close_connection,
|
||||
Error &error, bool skip_body = false);
|
||||
Error &error, bool skip_body, bool &rejected_locally);
|
||||
bool write_request_body(Stream &strm, Request &req, Error &error);
|
||||
void prepare_default_headers(Request &r, bool for_stream,
|
||||
const std::string &ct);
|
||||
@@ -15119,16 +15119,30 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
|
||||
prepare_default_headers(req, true, content_type);
|
||||
|
||||
auto &strm = *handle.stream_;
|
||||
if (detail::write_request_line(strm, req.method, req.path) < 0) {
|
||||
handle.error = Error::Write;
|
||||
handle.response.reset();
|
||||
return handle;
|
||||
}
|
||||
|
||||
if (!detail::check_and_write_headers(strm, req.headers, header_writer_,
|
||||
handle.error)) {
|
||||
handle.response.reset();
|
||||
return handle;
|
||||
// Build the request line and headers in memory first, like write_request()
|
||||
// does, so that a rejected header leaves nothing on the wire.
|
||||
{
|
||||
detail::BufferStream bstrm;
|
||||
|
||||
if (detail::write_request_line(bstrm, req.method, req.path) < 0) {
|
||||
handle.error = Error::Write;
|
||||
handle.response.reset();
|
||||
return handle;
|
||||
}
|
||||
|
||||
if (!detail::check_and_write_headers(bstrm, req.headers, header_writer_,
|
||||
handle.error)) {
|
||||
handle.response.reset();
|
||||
return handle;
|
||||
}
|
||||
|
||||
const auto &data = bstrm.get_buffer();
|
||||
if (!detail::write_data(strm, data.data(), data.size())) {
|
||||
handle.error = Error::Write;
|
||||
handle.response.reset();
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
if (!body.empty()) {
|
||||
@@ -15681,7 +15695,9 @@ inline bool ClientImpl::write_content_with_provider(Stream &strm,
|
||||
|
||||
inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
||||
bool close_connection, Error &error,
|
||||
bool skip_body) {
|
||||
bool skip_body, bool &rejected_locally) {
|
||||
rejected_locally = false;
|
||||
|
||||
// Prepare additional headers
|
||||
if (close_connection) {
|
||||
if (!req.has_header("Connection")) {
|
||||
@@ -15775,11 +15791,13 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
||||
// set_path_encode(false)) must fail the request cleanly instead of
|
||||
// emitting a request-line-less, header-injecting request.
|
||||
error = Error::Write;
|
||||
rejected_locally = true;
|
||||
output_error_log(error, &req);
|
||||
return false;
|
||||
}
|
||||
if (!detail::check_and_write_headers(bstrm, req.headers, header_writer_,
|
||||
error)) {
|
||||
rejected_locally = true;
|
||||
output_error_log(error, &req);
|
||||
return false;
|
||||
}
|
||||
@@ -16048,8 +16066,16 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
|
||||
detail::has_header_token(req.headers, "Expect", "100-continue");
|
||||
|
||||
// Send request (skip body if using Expect: 100-continue)
|
||||
auto rejected_locally = false;
|
||||
auto write_request_success =
|
||||
write_request(strm, req, close_connection, error, expect_100_continue);
|
||||
write_request(strm, req, close_connection, error, expect_100_continue,
|
||||
rejected_locally);
|
||||
|
||||
// A failed write normally still reads the response below, since the server
|
||||
// may have answered early (e.g. 413/414) and closed while the body was being
|
||||
// sent. A request rejected before any byte reached the socket gets no such
|
||||
// response, and waiting for one would block until the read timeout.
|
||||
if (rejected_locally) { return false; }
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
if (is_ssl() && !expect_100_continue) {
|
||||
|
||||
+90
-3
@@ -10196,9 +10196,6 @@ TEST(RequestLineInjectionTest, ClientRejectsNonTokenMethodEndToEnd) {
|
||||
|
||||
{
|
||||
Client cli(HOST, port);
|
||||
// Nothing is written, so shorten the read timeout the connection would
|
||||
// otherwise sit in.
|
||||
cli.set_read_timeout(1, 0);
|
||||
|
||||
const std::string evil_methods[] = {
|
||||
"GET /smuggled HTTP/1.1\r\nHost: x\r\n\r\nGET",
|
||||
@@ -17415,6 +17412,96 @@ TEST(VulnerabilityTest, CRLFInjectionInHeaders) {
|
||||
server_thread.join();
|
||||
}
|
||||
|
||||
// A request rejected before any byte reaches the socket must fail right away
|
||||
// instead of waiting for a response the server will never send.
|
||||
TEST(ClientRejectedRequestTest, DoesNotWaitForResponse) {
|
||||
// The kernel completes the TCP handshake from the listen backlog, so the
|
||||
// client connects, but nothing ever reads, responds or closes.
|
||||
auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
default_socket_options(srv);
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(static_cast<uint16_t>(PORT + 1));
|
||||
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
ASSERT_EQ(0, ::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)));
|
||||
ASSERT_EQ(0, ::listen(srv, 8));
|
||||
|
||||
auto cli = Client("127.0.0.1", PORT + 1);
|
||||
cli.set_read_timeout(10, 0);
|
||||
|
||||
auto elapsed_ms = [](std::chrono::steady_clock::time_point start) {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start)
|
||||
.count();
|
||||
};
|
||||
|
||||
{
|
||||
Request req;
|
||||
req.method = "GE T";
|
||||
req.path = "/";
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
auto res = cli.send(req);
|
||||
EXPECT_FALSE(res);
|
||||
EXPECT_EQ(Error::Write, res.error());
|
||||
EXPECT_LT(elapsed_ms(start), 1000);
|
||||
}
|
||||
|
||||
{
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
auto res = cli.Get("/", Headers{{"A", "B\r\nEvil: 1"}});
|
||||
EXPECT_FALSE(res);
|
||||
EXPECT_EQ(Error::InvalidHeaders, res.error());
|
||||
EXPECT_LT(elapsed_ms(start), 1000);
|
||||
}
|
||||
|
||||
EXPECT_FALSE(cli.is_socket_open());
|
||||
|
||||
detail::close_socket(srv);
|
||||
}
|
||||
|
||||
TEST(ClientRejectedRequestTest, OpenStreamSendsNothingOnInvalidHeader) {
|
||||
auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
default_socket_options(srv);
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(static_cast<uint16_t>(PORT + 1));
|
||||
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
ASSERT_EQ(0, ::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)));
|
||||
ASSERT_EQ(0, ::listen(srv, 1));
|
||||
|
||||
std::string received;
|
||||
auto server_thread = std::thread([&] {
|
||||
auto sock = ::accept(srv, nullptr, nullptr);
|
||||
if (sock == INVALID_SOCKET) { return; }
|
||||
detail::set_socket_opt_time(sock, SOL_SOCKET, SO_RCVTIMEO, 2, 0);
|
||||
|
||||
char buf[2048];
|
||||
ssize_t n;
|
||||
while ((n = ::recv(sock, buf, sizeof(buf), 0)) > 0) {
|
||||
received.append(buf, static_cast<size_t>(n));
|
||||
}
|
||||
detail::close_socket(sock);
|
||||
});
|
||||
|
||||
{
|
||||
auto cli = Client("127.0.0.1", PORT + 1);
|
||||
|
||||
// "Z" sorts after the default headers, so writing straight to the socket
|
||||
// would have sent the request line and those headers before the rejection.
|
||||
auto handle =
|
||||
cli.open_stream("GET", "/", Params{}, Headers{{"Z", "B\r\nEvil: 1"}});
|
||||
EXPECT_FALSE(handle.is_valid());
|
||||
EXPECT_EQ(Error::InvalidHeaders, handle.error);
|
||||
}
|
||||
|
||||
server_thread.join();
|
||||
detail::close_socket(srv);
|
||||
|
||||
EXPECT_TRUE(received.empty()) << received;
|
||||
}
|
||||
|
||||
TEST(PathParamsTest, StaticMatch) {
|
||||
const auto pattern = "/users/all";
|
||||
detail::PathParamsMatcher matcher(pattern);
|
||||
|
||||
Reference in new issue
Block a user