mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-03 22:13:11 +07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28f8264d13 | ||
|
|
91271c062d | ||
|
|
d755c43d58 | ||
|
|
5c9285776e |
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
|||||||
|
|
||||||
[site]
|
[site]
|
||||||
title = "cpp-httplib"
|
title = "cpp-httplib"
|
||||||
version = "0.44.0"
|
version = "0.45.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."
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||||
#define CPPHTTPLIB_HTTPLIB_H
|
#define CPPHTTPLIB_HTTPLIB_H
|
||||||
|
|
||||||
#define CPPHTTPLIB_VERSION "0.44.0"
|
#define CPPHTTPLIB_VERSION "0.45.0"
|
||||||
#define CPPHTTPLIB_VERSION_NUM "0x002c00"
|
#define CPPHTTPLIB_VERSION_NUM "0x002d00"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||||
@@ -8578,17 +8578,24 @@ write_multipart_ranges_data(Stream &strm, const Request &req, Response &res,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool has_framed_body(const Request &req) {
|
||||||
|
return is_chunked_transfer_encoding(req.headers) ||
|
||||||
|
req.get_header_value_u64("Content-Length") > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool is_connection_persistent(const Request &req) {
|
||||||
|
auto conn = req.get_header_value("Connection");
|
||||||
|
if (conn == "close") { return false; }
|
||||||
|
if (req.version == "HTTP/1.0" && conn != "Keep-Alive") { return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
inline bool expect_content(const Request &req) {
|
inline bool expect_content(const Request &req) {
|
||||||
if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" ||
|
if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" ||
|
||||||
req.method == "DELETE") {
|
req.method == "DELETE") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (req.has_header("Content-Length") &&
|
return has_framed_body(req);
|
||||||
req.get_header_value_u64("Content-Length") > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (is_chunked_transfer_encoding(req.headers)) { return true; }
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
@@ -11304,29 +11311,18 @@ inline bool Server::read_content_core(
|
|||||||
size_t /*len*/) { return receiver(buf, n); };
|
size_t /*len*/) { return receiver(buf, n); };
|
||||||
}
|
}
|
||||||
|
|
||||||
// RFC 7230 Section 3.3.3: If this is a request message and none of the above
|
// RFC 9112 §6: no Transfer-Encoding and no Content-Length means no body.
|
||||||
// are true (no Transfer-Encoding and no Content-Length), then the message
|
// For non-SSL builds we still scan non-persistent connections for stray
|
||||||
// body length is zero (no message body is present).
|
// body bytes so the payload limit is enforced (413). On keep-alive,
|
||||||
//
|
// pending bytes may be the next request (issue #2450), so skip.
|
||||||
// For non-SSL builds, detect clients that send a body without a
|
|
||||||
// Content-Length header (raw HTTP over TCP). Check both the stream's
|
|
||||||
// internal read buffer (data already read from the socket during header
|
|
||||||
// parsing) and the socket itself for pending data. If data is found and
|
|
||||||
// exceeds the configured payload limit, reject with 413.
|
|
||||||
// For SSL builds we cannot reliably peek the decrypted application bytes,
|
|
||||||
// so keep the original behaviour.
|
|
||||||
#if !defined(CPPHTTPLIB_SSL_ENABLED)
|
#if !defined(CPPHTTPLIB_SSL_ENABLED)
|
||||||
if (!req.has_header("Content-Length") &&
|
if (!req.has_header("Content-Length") &&
|
||||||
!detail::is_chunked_transfer_encoding(req.headers)) {
|
!detail::is_chunked_transfer_encoding(req.headers)) {
|
||||||
// Only check if payload_max_length is set to a finite value
|
if (!detail::is_connection_persistent(req) && payload_max_length_ > 0 &&
|
||||||
if (payload_max_length_ > 0 &&
|
|
||||||
payload_max_length_ < (std::numeric_limits<size_t>::max)()) {
|
payload_max_length_ < (std::numeric_limits<size_t>::max)()) {
|
||||||
// Check if there is data already buffered in the stream (read during
|
auto has_data = strm.is_readable();
|
||||||
// header parsing) or pending on the socket. Use a non-blocking socket
|
|
||||||
// check to avoid deadlock when the client sends no body.
|
|
||||||
bool has_data = strm.is_readable();
|
|
||||||
if (!has_data) {
|
if (!has_data) {
|
||||||
socket_t s = strm.socket();
|
auto s = strm.socket();
|
||||||
if (s != INVALID_SOCKET) {
|
if (s != INVALID_SOCKET) {
|
||||||
has_data = detail::select_read(s, 0, 0) > 0;
|
has_data = detail::select_read(s, 0, 0) > 0;
|
||||||
}
|
}
|
||||||
@@ -11888,6 +11884,11 @@ get_client_ip(const std::string &x_forwarded_for,
|
|||||||
ip_list.emplace_back(std::string(b + r.first, b + r.second));
|
ip_list.emplace_back(std::string(b + r.first, b + r.second));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A malformed X-Forwarded-For (empty, comma-only, whitespace-only) yields
|
||||||
|
// no segments. Signal "no client IP derived" with an empty string so the
|
||||||
|
// caller can fall back to the connection-level remote address.
|
||||||
|
if (ip_list.empty()) { return std::string(); }
|
||||||
|
|
||||||
for (size_t i = 0; i < ip_list.size(); ++i) {
|
for (size_t i = 0; i < ip_list.size(); ++i) {
|
||||||
auto ip = ip_list[i];
|
auto ip = ip_list[i];
|
||||||
|
|
||||||
@@ -11978,7 +11979,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|||||||
|
|
||||||
if (!trusted_proxies_.empty() && req.has_header("X-Forwarded-For")) {
|
if (!trusted_proxies_.empty() && req.has_header("X-Forwarded-For")) {
|
||||||
auto x_forwarded_for = req.get_header_value("X-Forwarded-For");
|
auto x_forwarded_for = req.get_header_value("X-Forwarded-For");
|
||||||
req.remote_addr = get_client_ip(x_forwarded_for, trusted_proxies_);
|
auto derived = get_client_ip(x_forwarded_for, trusted_proxies_);
|
||||||
|
req.remote_addr = derived.empty() ? remote_addr : derived;
|
||||||
} else {
|
} else {
|
||||||
req.remote_addr = remote_addr;
|
req.remote_addr = remote_addr;
|
||||||
}
|
}
|
||||||
@@ -12180,15 +12182,14 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|||||||
ret = write_response(strm, close_connection, req, res);
|
ret = write_response(strm, close_connection, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain any unconsumed request body to prevent request smuggling on
|
// Drain any unconsumed framed body to prevent request smuggling on
|
||||||
// keep-alive connections.
|
// keep-alive. Without framing there is no body to drain — reading would
|
||||||
if (!req.body_consumed_ && detail::expect_content(req)) {
|
// consume the next request (issue #2450).
|
||||||
int drain_status = 200; // required by read_content signature
|
if (!req.body_consumed_ && detail::has_framed_body(req)) {
|
||||||
|
int dummy_status;
|
||||||
if (!detail::read_content(
|
if (!detail::read_content(
|
||||||
strm, req, payload_max_length_, drain_status, nullptr,
|
strm, req, payload_max_length_, dummy_status, nullptr,
|
||||||
[](const char *, size_t, size_t, size_t) { return true; }, false)) {
|
[](const char *, size_t, size_t, size_t) { return true; }, false)) {
|
||||||
// Body exceeds payload limit or read error — close the connection
|
|
||||||
// to prevent leftover bytes from being misinterpreted.
|
|
||||||
connection_closed = true;
|
connection_closed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+107
@@ -14251,6 +14251,53 @@ TEST(ForwardedHeadersTest, HandlesWhitespaceAroundIPs) {
|
|||||||
EXPECT_EQ(observed_remote_addr, "203.0.113.66");
|
EXPECT_EQ(observed_remote_addr, "203.0.113.66");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An X-Forwarded-For header whose value parses to zero IP segments must not
|
||||||
|
// crash the server (it used to call front() on an empty vector inside
|
||||||
|
// get_client_ip). The connection-level remote address must be retained instead.
|
||||||
|
static void run_malformed_xff_test(const std::string &xff_value) {
|
||||||
|
Server svr;
|
||||||
|
svr.set_trusted_proxies({"192.0.2.45"});
|
||||||
|
|
||||||
|
std::string observed_remote_addr;
|
||||||
|
svr.Get("/ip", [&](const Request &req, Response &res) {
|
||||||
|
observed_remote_addr = req.remote_addr;
|
||||||
|
res.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
int port = 0;
|
||||||
|
thread t = thread([&]() {
|
||||||
|
port = svr.bind_to_any_port(HOST);
|
||||||
|
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);
|
||||||
|
auto res = cli.Get("/ip", {{"X-Forwarded-For", xff_value}});
|
||||||
|
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
|
EXPECT_TRUE(observed_remote_addr == "::1" ||
|
||||||
|
observed_remote_addr == "127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ForwardedHeadersTest, EmptyXForwardedFor_DoesNotCrash) {
|
||||||
|
run_malformed_xff_test("");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ForwardedHeadersTest, CommaOnlyXForwardedFor_DoesNotCrash) {
|
||||||
|
run_malformed_xff_test(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ForwardedHeadersTest, MultipleCommasXForwardedFor_DoesNotCrash) {
|
||||||
|
run_malformed_xff_test(", , ,");
|
||||||
|
}
|
||||||
|
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
TEST(ServerRequestParsingTest, RequestWithoutContentLengthOrTransferEncoding) {
|
TEST(ServerRequestParsingTest, RequestWithoutContentLengthOrTransferEncoding) {
|
||||||
Server svr;
|
Server svr;
|
||||||
@@ -18157,3 +18204,63 @@ TEST(RequestSmugglingTest, ContentLengthAndTransferEncodingRejected) {
|
|||||||
response.substr(0, response.find("\r\n")));
|
response.substr(0, response.find("\r\n")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression for issue #2450: a DELETE without Content-Length on a
|
||||||
|
// keep-alive connection must not let the post-response drain consume the
|
||||||
|
// next request's bytes.
|
||||||
|
TEST(KeepAliveTest, DeleteWithoutContentLengthDoesNotEatNextRequest) {
|
||||||
|
Server svr;
|
||||||
|
|
||||||
|
std::atomic<int> delete_count(0);
|
||||||
|
svr.Delete("/items/:id", [&](const Request &, Response &res) {
|
||||||
|
delete_count++;
|
||||||
|
res.status = StatusCode::NoContent_204;
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port(HOST);
|
||||||
|
thread t = thread([&] { svr.listen_after_bind(); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
t.join();
|
||||||
|
});
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
auto error = Error::Success;
|
||||||
|
auto sock = detail::create_client_socket(
|
||||||
|
HOST, "", port, AF_UNSPEC, false, false, nullptr,
|
||||||
|
/*connection_timeout_sec=*/2, 0,
|
||||||
|
/*read_timeout_sec=*/2, 0,
|
||||||
|
/*write_timeout_sec=*/2, 0, std::string(), error);
|
||||||
|
ASSERT_NE(INVALID_SOCKET, sock);
|
||||||
|
auto sock_se = detail::scope_exit([&] { detail::close_socket(sock); });
|
||||||
|
|
||||||
|
auto send_request_and_read_response = [&](const std::string &req,
|
||||||
|
std::string &out) -> bool {
|
||||||
|
auto sent = send(sock, req.data(), req.size(), 0);
|
||||||
|
if (sent != static_cast<ssize_t>(req.size())) { return false; }
|
||||||
|
char buf[4096];
|
||||||
|
for (;;) {
|
||||||
|
auto n = recv(sock, buf, sizeof(buf), 0);
|
||||||
|
if (n <= 0) { return !out.empty(); }
|
||||||
|
out.append(buf, static_cast<size_t>(n));
|
||||||
|
if (out.find("\r\n\r\n") != std::string::npos) { return true; }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string req1 = "DELETE /items/1 HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"\r\n";
|
||||||
|
std::string resp1;
|
||||||
|
ASSERT_TRUE(send_request_and_read_response(req1, resp1));
|
||||||
|
EXPECT_NE(std::string::npos, resp1.find("HTTP/1.1 204"));
|
||||||
|
|
||||||
|
std::string req2 = "DELETE /items/2 HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Connection: close\r\n"
|
||||||
|
"\r\n";
|
||||||
|
std::string resp2;
|
||||||
|
ASSERT_TRUE(send_request_and_read_response(req2, resp2));
|
||||||
|
EXPECT_NE(std::string::npos, resp2.find("HTTP/1.1 204"));
|
||||||
|
|
||||||
|
EXPECT_EQ(2, delete_count.load());
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user