Compare commits

...

14 Commits

Author SHA1 Message Date
yhirose 3779800322 Release v0.18.5 2025-01-17 17:38:03 -05:00
yhirose 986a20fb7d Resolve #2017 (#2022)
* Resolve #2017

* Fix warning

* Update README
2025-01-17 17:37:07 -05:00
yhirose 8311e1105f Fix Windows build problem 2025-01-16 23:26:04 -05:00
yhirose ba6845925d Fix #2014 2025-01-16 23:10:58 -05:00
yhirose 343a0fc073 Fix #2011 2025-01-16 21:38:45 -05:00
yhirose 54f8a4d0f3 Release v0.18.4 2025-01-16 01:00:25 -05:00
yhirose 9c36aae4b7 Fix HTTP Response Splitting Vulnerability 2025-01-16 00:04:33 -05:00
yhirose b766025a83 clangformat 2025-01-16 00:03:10 -05:00
yhirose 9b5f76f833 Fix #2012 2024-12-27 17:19:23 -05:00
sinnren d647f484a4 fix:set_file_content with range request return 416. (#2010)
Co-authored-by: fenlog <bakurise@qq.com>
2024-12-24 09:38:59 -05:00
Sergey Bobrenok 8794792baa Treat out-of-range last_pos as the end of the content (#2009)
RFC-9110 '14.1.2. Byte Ranges':
A client can limit the number of bytes requested without knowing the
size of the selected representation. If the last-pos value is absent,
or if the value is greater than or equal to the current length of the
representation data, the byte range is interpreted as the remainder of
the representation (i.e., the server replaces the value of last-pos
with a value that is one less than the current length of the selected
representation).

https://www.rfc-editor.org/rfc/rfc9110.html#section-14.1.2-6
2024-12-23 13:14:36 -05:00
yhirose b85768c1f3 Fix #2005 2024-12-16 17:43:50 -05:00
yhirose e6d71bd702 Add a unit test for Issue #2004 2024-12-12 18:15:22 -05:00
yhirose 258992a160 Changed to use non-blocking socket in is_ssl_peer_could_be_closed 2024-12-03 19:26:08 -05:00
4 changed files with 286 additions and 35 deletions
+15
View File
@@ -125,6 +125,21 @@ int main(void)
res.set_content(req.body, "text/plain");
});
// If the handler takes time to finish, you can also poll the connection state
svr.Get("/task", [&](const Request& req, Response& res) {
const char * result = nullptr;
process.run(); // for example, starting an external process
while (result == nullptr) {
sleep(1);
if (req.is_connection_closed()) {
process.kill(); // kill the process
return;
}
result = process.stdout(); // != nullptr if the process finishes
}
res.set_content(result, "text/plain");
});
svr.Get("/stop", [&](const Request& req, Response& res) {
svr.stop();
});
+1 -1
View File
@@ -18,7 +18,7 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
BROTLI_DIR = $(PREFIX)/opt/brotli
BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
all: server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark issue
all: server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client
server : server.cc ../httplib.h Makefile
$(CXX) -o server $(CXXFLAGS) server.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT)
+117 -21
View File
@@ -8,7 +8,7 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.18.3"
#define CPPHTTPLIB_VERSION "0.18.5"
/*
* Configuration
@@ -628,6 +628,7 @@ struct Request {
Ranges ranges;
Match matches;
std::unordered_map<std::string, std::string> path_params;
std::function<bool()> is_connection_closed = []() { return true; };
// for client
ResponseHandler response_handler;
@@ -2012,18 +2013,34 @@ inline void duration_to_sec_and_usec(const T &duration, U callback) {
callback(static_cast<time_t>(sec), static_cast<time_t>(usec));
}
inline bool is_numeric(const std::string &str) {
return !str.empty() && std::all_of(str.begin(), str.end(), ::isdigit);
}
inline uint64_t get_header_value_u64(const Headers &headers,
const std::string &key, uint64_t def,
size_t id) {
size_t id, bool &is_invalid_value) {
is_invalid_value = false;
auto rng = headers.equal_range(key);
auto it = rng.first;
std::advance(it, static_cast<ssize_t>(id));
if (it != rng.second) {
return std::strtoull(it->second.data(), nullptr, 10);
if (is_numeric(it->second)) {
return std::strtoull(it->second.data(), nullptr, 10);
} else {
is_invalid_value = true;
}
}
return def;
}
inline uint64_t get_header_value_u64(const Headers &headers,
const std::string &key, uint64_t def,
size_t id) {
bool dummy = false;
return get_header_value_u64(headers, key, def, id, dummy);
}
} // namespace detail
inline uint64_t Request::get_header_value_u64(const std::string &key,
@@ -2041,8 +2058,6 @@ inline void default_socket_options(socket_t sock) {
#ifdef _WIN32
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
reinterpret_cast<const char *>(&opt), sizeof(opt));
setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
reinterpret_cast<const char *>(&opt), sizeof(opt));
#else
#ifdef SO_REUSEPORT
setsockopt(sock, SOL_SOCKET, SO_REUSEPORT,
@@ -2506,6 +2521,60 @@ private:
bool is_open_empty_file = false;
};
// NOTE: https://www.rfc-editor.org/rfc/rfc9110#section-5
namespace fields {
inline bool is_token_char(char c) {
return std::isalnum(c) || c == '!' || c == '#' || c == '$' || c == '%' ||
c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' ||
c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
}
inline bool is_token(const std::string &s) {
if (s.empty()) { return false; }
for (auto c : s) {
if (!is_token_char(c)) { return false; }
}
return true;
}
inline bool is_field_name(const std::string &s) { return is_token(s); }
inline bool is_vchar(char c) { return c >= 33 && c <= 126; }
inline bool is_obs_text(char c) { return 128 <= static_cast<unsigned char>(c); }
inline bool is_field_vchar(char c) { return is_vchar(c) || is_obs_text(c); }
inline bool is_field_content(const std::string &s) {
if (s.empty()) { return false; }
if (s.size() == 1) {
return is_field_vchar(s[0]);
} else if (s.size() == 2) {
return is_field_vchar(s[0]) && is_field_vchar(s[1]);
} else {
size_t i = 0;
if (!is_field_vchar(s[i])) { return false; }
i++;
while (i < s.size() - 1) {
auto c = s[i++];
if (c == ' ' || c == '\t' || is_field_vchar(c)) {
} else {
return false;
}
}
return is_field_vchar(s[i]);
}
}
inline bool is_field_value(const std::string &s) { return is_field_content(s); }
} // namespace fields
} // namespace detail
// ----------------------------------------------------------------------------
@@ -4386,8 +4455,14 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
} else if (!has_header(x.headers, "Content-Length")) {
ret = read_content_without_length(strm, out);
} else {
auto len = get_header_value_u64(x.headers, "Content-Length", 0, 0);
if (len > payload_max_length) {
auto is_invalid_value = false;
auto len = get_header_value_u64(x.headers, "Content-Length",
std::numeric_limits<uint64_t>::max(),
0, is_invalid_value);
if (is_invalid_value) {
ret = false;
} else if (len > payload_max_length) {
exceed_payload_max_length = true;
skip_content_with_length(strm, len);
ret = false;
@@ -5156,7 +5231,18 @@ inline bool range_error(Request &req, Response &res) {
last_pos = contant_len - 1;
}
if (last_pos == -1) { last_pos = contant_len - 1; }
// NOTE: RFC-9110 '14.1.2. Byte Ranges':
// A client can limit the number of bytes requested without knowing the
// size of the selected representation. If the last-pos value is absent,
// or if the value is greater than or equal to the current length of the
// representation data, the byte range is interpreted as the remainder of
// the representation (i.e., the server replaces the value of last-pos
// with a value that is one less than the current length of the selected
// representation).
// https://www.rfc-editor.org/rfc/rfc9110.html#section-14.1.2-6
if (last_pos == -1 || last_pos >= contant_len) {
last_pos = contant_len - 1;
}
// Range must be within content length
if (!(0 <= first_pos && first_pos <= last_pos &&
@@ -5688,7 +5774,8 @@ inline size_t Request::get_header_value_count(const std::string &key) const {
inline void Request::set_header(const std::string &key,
const std::string &val) {
if (!detail::has_crlf(key) && !detail::has_crlf(val)) {
if (detail::fields::is_field_name(key) &&
detail::fields::is_field_value(val)) {
headers.emplace(key, val);
}
}
@@ -5754,13 +5841,14 @@ inline size_t Response::get_header_value_count(const std::string &key) const {
inline void Response::set_header(const std::string &key,
const std::string &val) {
if (!detail::has_crlf(key) && !detail::has_crlf(val)) {
if (detail::fields::is_field_name(key) &&
detail::fields::is_field_value(val)) {
headers.emplace(key, val);
}
}
inline void Response::set_redirect(const std::string &url, int stat) {
if (!detail::has_crlf(url)) {
if (detail::fields::is_field_value(url)) {
set_header("Location", url);
if (300 <= stat && stat < 400) {
this->status = stat;
@@ -7130,6 +7218,11 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
}
}
// Setup `is_connection_closed` method
req.is_connection_closed = [&]() {
return !detail::is_socket_alive(strm.socket());
};
// Routing
auto routed = false;
#ifdef CPPHTTPLIB_NO_EXCEPTIONS
@@ -7172,14 +7265,6 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
: StatusCode::PartialContent_206;
}
if (detail::range_error(req, res)) {
res.body.clear();
res.content_length_ = 0;
res.content_provider_ = nullptr;
res.status = StatusCode::RangeNotSatisfiable_416;
return write_response(strm, close_connection, req, res);
}
// Serve file content by using a content provider
if (!res.file_content_path_.empty()) {
const auto &path = res.file_content_path_;
@@ -7206,6 +7291,14 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
});
}
if (detail::range_error(req, res)) {
res.body.clear();
res.content_length_ = 0;
res.content_provider_ = nullptr;
res.status = StatusCode::RangeNotSatisfiable_416;
return write_response(strm, close_connection, req, res);
}
return write_response_with_content(strm, close_connection, req, res);
} else {
if (res.status == -1) { res.status = StatusCode::NotFound_404; }
@@ -7418,6 +7511,10 @@ inline bool ClientImpl::send(Request &req, Response &res, Error &error) {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
inline bool ClientImpl::is_ssl_peer_could_be_closed(SSL *ssl) const {
detail::set_nonblocking(socket_.sock, true);
auto se = detail::scope_exit(
[&]() { detail::set_nonblocking(socket_.sock, false); });
char buf[1];
return !SSL_peek(ssl, buf, 1) &&
SSL_get_error(ssl, 0) == SSL_ERROR_ZERO_RETURN;
@@ -7438,9 +7535,7 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
if (is_alive && is_ssl()) {
detail::set_nonblocking(socket_.sock, true);
if (is_ssl_peer_could_be_closed(socket_.ssl)) { is_alive = false; }
detail::set_nonblocking(socket_.sock, false);
}
#endif
@@ -8906,6 +9001,7 @@ inline void ssl_delete(std::mutex &ctx_mutex, SSL *ssl, socket_t sock,
// best-efforts.
if (shutdown_gracefully) {
#ifdef _WIN32
(void)(sock);
SSL_shutdown(ssl);
#else
timeval tv;
+153 -13
View File
@@ -511,6 +511,15 @@ TEST(GetHeaderValueTest, RegularValueInt) {
EXPECT_EQ(100ull, val);
}
TEST(GetHeaderValueTest, RegularInvalidValueInt) {
Headers headers = {{"Content-Length", "x"}};
auto is_invalid_value = false;
auto val = detail::get_header_value_u64(headers, "Content-Length", 0, 0,
is_invalid_value);
EXPECT_EQ(0ull, val);
EXPECT_TRUE(is_invalid_value);
}
TEST(GetHeaderValueTest, Range) {
{
Headers headers = {make_range_header({{1, -1}})};
@@ -3036,6 +3045,16 @@ TEST_F(ServerTest, GetFileContent) {
EXPECT_EQ("test.html", res->body);
}
TEST_F(ServerTest, GetFileContentWithRange) {
auto res = cli_.Get("/file_content", {{make_range_header({{1, 3}})}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
EXPECT_EQ("text/html", res->get_header_value("Content-Type"));
EXPECT_EQ("bytes 1-3/9", res->get_header_value("Content-Range"));
EXPECT_EQ(3, std::stoi(res->get_header_value("Content-Length")));
EXPECT_EQ("est", res->body);
}
TEST_F(ServerTest, GetFileContentWithContentType) {
auto res = cli_.Get("/file_content_with_content_type");
ASSERT_TRUE(res);
@@ -3794,12 +3813,14 @@ TEST_F(ServerTest, GetStreamedWithRangeError) {
TEST_F(ServerTest, GetRangeWithMaxLongLength) {
auto res = cli_.Get(
"/with-range",
{{"Range",
"bytes=0-" + std::to_string(std::numeric_limits<long>::max())}});
EXPECT_EQ(StatusCode::RangeNotSatisfiable_416, res->status);
EXPECT_EQ("0", res->get_header_value("Content-Length"));
EXPECT_EQ(false, res->has_header("Content-Range"));
EXPECT_EQ(0U, res->body.size());
{{"Range", "bytes=0-" + std::to_string(std::numeric_limits<long>::max())},
{"Accept-Encoding", ""}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
EXPECT_EQ("7", res->get_header_value("Content-Length"));
EXPECT_EQ(true, res->has_header("Content-Range"));
EXPECT_EQ("bytes 0-6/7", res->get_header_value("Content-Range"));
EXPECT_EQ(std::string("abcdefg"), res->body);
}
TEST_F(ServerTest, GetRangeWithZeroToInfinite) {
@@ -6132,6 +6153,18 @@ TEST(SSLClientTest, WildcardHostNameMatch_Online) {
ASSERT_EQ(StatusCode::OK_200, res->status);
}
TEST(SSLClientTest, Issue2004) {
Client client("https://google.com");
client.set_follow_location(true);
auto res = client.Get("/");
ASSERT_TRUE(res);
ASSERT_EQ(StatusCode::OK_200, res->status);
auto body = res->body;
EXPECT_EQ(body.substr(0, 15), "<!doctype html>");
}
#if 0
TEST(SSLClientTest, SetInterfaceWithINET6) {
auto cli = std::make_shared<httplib::Client>("https://httpbin.org");
@@ -7472,9 +7505,9 @@ TEST(MultipartFormDataTest, CloseDelimiterWithoutCRLF) {
"text2"
"\r\n------------";
std::string resonse;
ASSERT_TRUE(send_request(1, req, &resonse));
ASSERT_EQ("200", resonse.substr(9, 3));
std::string response;
ASSERT_TRUE(send_request(1, req, &response));
ASSERT_EQ("200", response.substr(9, 3));
}
TEST(MultipartFormDataTest, ContentLength) {
@@ -7519,11 +7552,10 @@ TEST(MultipartFormDataTest, ContentLength) {
"text2"
"\r\n------------\r\n";
std::string resonse;
ASSERT_TRUE(send_request(1, req, &resonse));
ASSERT_EQ("200", resonse.substr(9, 3));
std::string response;
ASSERT_TRUE(send_request(1, req, &response));
ASSERT_EQ("200", response.substr(9, 3));
}
#endif
TEST(TaskQueueTest, IncreaseAtomicInteger) {
@@ -7901,6 +7933,114 @@ TEST(DirtyDataRequestTest, HeadFieldValueContains_CR_LF_NUL) {
cli.Get("/test", {{"Test", "_\n\r_\n\r_"}});
}
TEST(InvalidHeaderCharsTest, is_field_name) {
EXPECT_TRUE(detail::fields::is_field_name("exampleToken"));
EXPECT_TRUE(detail::fields::is_field_name("token123"));
EXPECT_TRUE(detail::fields::is_field_name("!#$%&'*+-.^_`|~"));
EXPECT_FALSE(detail::fields::is_field_name("example token"));
EXPECT_FALSE(detail::fields::is_field_name(" example_token"));
EXPECT_FALSE(detail::fields::is_field_name("example_token "));
EXPECT_FALSE(detail::fields::is_field_name("token@123"));
EXPECT_FALSE(detail::fields::is_field_name(""));
EXPECT_FALSE(detail::fields::is_field_name("example\rtoken"));
EXPECT_FALSE(detail::fields::is_field_name("example\ntoken"));
EXPECT_FALSE(detail::fields::is_field_name(std::string("\0", 1)));
EXPECT_FALSE(detail::fields::is_field_name("example\ttoken"));
}
TEST(InvalidHeaderCharsTest, is_field_value) {
EXPECT_TRUE(detail::fields::is_field_value("exampleToken"));
EXPECT_TRUE(detail::fields::is_field_value("token123"));
EXPECT_TRUE(detail::fields::is_field_value("!#$%&'*+-.^_`|~"));
EXPECT_TRUE(detail::fields::is_field_value("example token"));
EXPECT_FALSE(detail::fields::is_field_value(" example_token"));
EXPECT_FALSE(detail::fields::is_field_value("example_token "));
EXPECT_TRUE(detail::fields::is_field_value("token@123"));
EXPECT_FALSE(detail::fields::is_field_value(""));
EXPECT_FALSE(detail::fields::is_field_value("example\rtoken"));
EXPECT_FALSE(detail::fields::is_field_value("example\ntoken"));
EXPECT_FALSE(detail::fields::is_field_value(std::string("\0", 1)));
EXPECT_TRUE(detail::fields::is_field_value("example\ttoken"));
EXPECT_TRUE(detail::fields::is_field_value("0"));
}
TEST(InvalidHeaderCharsTest, OnServer) {
Server svr;
svr.Get("/test_name", [&](const Request &req, Response &res) {
std::string header = "Not Set";
if (req.has_param("header")) { header = req.get_param_value("header"); }
res.set_header(header, "value");
res.set_content("Page Content Page Content", "text/plain");
});
svr.Get("/test_value", [&](const Request &req, Response &res) {
std::string header = "Not Set";
if (req.has_param("header")) { header = req.get_param_value("header"); }
res.set_header("X-Test", header);
res.set_content("Page Content Page Content", "text/plain");
});
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, PORT);
{
auto res = cli.Get(
R"(/test_name?header=Value%00%0d%0aHEADER_KEY%3aHEADER_VALUE%0d%0a%0d%0aBODY_BODY_BODY)");
ASSERT_TRUE(res);
EXPECT_EQ("Page Content Page Content", res->body);
EXPECT_FALSE(res->has_header("HEADER_KEY"));
}
{
auto res = cli.Get(
R"(/test_value?header=Value%00%0d%0aHEADER_KEY%3aHEADER_VALUE%0d%0a%0d%0aBODY_BODY_BODY)");
ASSERT_TRUE(res);
EXPECT_EQ("Page Content Page Content", res->body);
EXPECT_FALSE(res->has_header("HEADER_KEY"));
}
}
TEST(InvalidHeaderValueTest, InvalidContentLength) {
auto handled = false;
Server svr;
svr.Post("/test", [&](const Request &, Response &) { handled = true; });
thread t = thread([&] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
ASSERT_FALSE(handled);
});
svr.wait_until_ready();
auto req = "POST /test HTTP/1.1\r\n"
"Content-Length: x\r\n"
"\r\n";
std::string response;
ASSERT_TRUE(send_request(1, req, &response));
ASSERT_EQ("HTTP/1.1 400 Bad Request",
response.substr(0, response.find("\r\n")));
}
#ifndef _WIN32
TEST(Expect100ContinueTest, ServerClosesConnection) {
static constexpr char reject[] = "Unauthorized";