From 3c7ab8c5128645c45b8573d21c7c064b35786863 Mon Sep 17 00:00:00 2001 From: Matheus Gabriel Werny Date: Tue, 23 Sep 2025 17:50:38 +0200 Subject: [PATCH 1/8] ETag for static file server Programmed feature https://github.com/yhirose/cpp-httplib/issues/2242. First of, the static file server now sends HTTP response header "ETag". Following HTTP requests by the client which include HTTP request header "If-None-Match" are only served if the value for HTTP response header "ETag" is not included in the value of HTTP request header "If-None-Match", otherwise an HTTP response with status code 304 is served which includes the HTTP response header "ETag" again that would have been sent with a normal status code of 200. Useful resources: - https://http.dev/caching - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/If-None-Match - https://www.rfc-editor.org/rfc/rfc9110.html#name-304-not-modified --- httplib.h | 38 ++++++++++++++++++++++++++++++++++++++ test/test.cc | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/httplib.h b/httplib.h index db55d07e25..80e316b410 100644 --- a/httplib.h +++ b/httplib.h @@ -7881,6 +7881,44 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { return false; } +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + // Value for HTTP response header ETag. + const std::string etag = + R"(")" + detail::SHA_512(mm->data()) + R"(")"; + res.set_header("ETag", etag); + + if (req.has_header("If-None-Match")) { + const std::string header_if_none_match = + req.get_header_value("If-None-Match"); + + /* + * Values of HTTP request header If-None-Match which are cached + * values of previous HTTP response header ETag. + */ + std::set etags; + detail::split(header_if_none_match.c_str(), + header_if_none_match.c_str() + + header_if_none_match.length(), + ',', [&](const char *b, const char *e) { + std::string etag(b, e); + + // Weak validation is not supported. + if (etag.length() >= 2 && etag.at(0) == 'W' && + etag.at(1) == '/') { + etag.erase(0, 2); + } + + etags.insert(std::move(etag)); + }); + + if (etags.find("*") != etags.cend() || + etags.find(etag) != etags.cend()) { + res.status = StatusCode::NotModified_304; + return true; + } + } +#endif + res.set_content_provider( mm->size(), detail::find_content_type(path, file_extension_and_mimetype_map_, diff --git a/test/test.cc b/test/test.cc index a9ac0d17f1..97df13bbd3 100644 --- a/test/test.cc +++ b/test/test.cc @@ -10828,3 +10828,43 @@ TEST(HeaderSmugglingTest, ChunkedTrailerHeadersMerged) { std::string res; ASSERT_TRUE(send_request(1, req, &res)); } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +TEST(StaticFileSever, CacheValidation) { + for ( + const std::string header_if_none_match : { + R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", + R"("d", "db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", + R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b", "g")", + R"(*)", + R"("f", *)", + R"(*, "i")", + }) { + httplib::Server svr; + svr.set_mount_point("/", "./www/"); + + std::thread t = thread([&]() { svr.listen(HOST, PORT); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + httplib::Client client(HOST, PORT); + const httplib::Result result = + client.Get("/file", Headers({{"If-None-Match", header_if_none_match}})); + + ASSERT_NE(result, nullptr); + EXPECT_EQ(result.error(), Error::Success); + EXPECT_EQ(result->status, StatusCode::NotModified_304); + + EXPECT_TRUE(result->has_header("ETag")); + EXPECT_EQ( + result->get_header_value("ETag"), + R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")"); + EXPECT_TRUE(result->body.empty()); + } +} +#endif From fd88931bc0e5dbcce8af54ab300180476bc4d523 Mon Sep 17 00:00:00 2001 From: Matheus Gabriel Werny Date: Wed, 24 Sep 2025 13:19:21 +0200 Subject: [PATCH 2/8] Minor adjustments Added more comments. Check for method to be GET or HEAD. Explicitly clear the body. --- httplib.h | 80 ++++++++++++++++++++++++++++++++-------------------- test/test.cc | 13 ++++----- 2 files changed, 55 insertions(+), 38 deletions(-) diff --git a/httplib.h b/httplib.h index 80e316b410..5cc2abf3bf 100644 --- a/httplib.h +++ b/httplib.h @@ -7882,39 +7882,59 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT - // Value for HTTP response header ETag. - const std::string etag = - R"(")" + detail::SHA_512(mm->data()) + R"(")"; - res.set_header("ETag", etag); - - if (req.has_header("If-None-Match")) { - const std::string header_if_none_match = - req.get_header_value("If-None-Match"); + /* + * The HTTP request header If-None-Match can be used with other + * methods where it has the meaning to only execute if the resource + * does not already exist but uploading does not matter here. + * HTTP response header ETag is only set where content is + * pulled and not pushed as those HTTP response bodies do not have to + * be related to the content. + */ + if (req.method == "GET" || req.method == "HEAD") { + // Value for HTTP response header ETag. + const std::string etag = + R"(")" + detail::SHA_512(mm->data()) + R"(")"; + /* + * Weak validation is not used. + * HTTP response header ETag must be set as if normal HTTP response + * was sent. + */ + res.set_header("ETag", etag); /* - * Values of HTTP request header If-None-Match which are cached - * values of previous HTTP response header ETag. + * Semantic: If value exists, the server will send status code 304. + * * always results in status code 304. */ - std::set etags; - detail::split(header_if_none_match.c_str(), - header_if_none_match.c_str() + - header_if_none_match.length(), - ',', [&](const char *b, const char *e) { - std::string etag(b, e); - - // Weak validation is not supported. - if (etag.length() >= 2 && etag.at(0) == 'W' && - etag.at(1) == '/') { - etag.erase(0, 2); - } - - etags.insert(std::move(etag)); - }); - - if (etags.find("*") != etags.cend() || - etags.find(etag) != etags.cend()) { - res.status = StatusCode::NotModified_304; - return true; + if (req.has_header("If-None-Match")) { + const std::string header_if_none_match = + req.get_header_value("If-None-Match"); + + /* + * Values of HTTP request header If-None-Match which are cached + * values of previous HTTP response header ETag. + */ + std::set etags; + detail::split(header_if_none_match.c_str(), + header_if_none_match.c_str() + + header_if_none_match.length(), + ',', [&](const char *b, const char *e) { + std::string etag(b, e); + + // Weak validation is not used. + if (etag.length() >= 2 && etag.at(0) == 'W' && + etag.at(1) == '/') { + etag.erase(0, 2); + } + + etags.insert(std::move(etag)); + }); + + if (etags.find("*") != etags.cend() || + etags.find(etag) != etags.cend()) { + res.status = StatusCode::NotModified_304; + res.body.clear(); + return true; + } } } #endif diff --git a/test/test.cc b/test/test.cc index 97df13bbd3..d6df76b863 100644 --- a/test/test.cc +++ b/test/test.cc @@ -10832,14 +10832,11 @@ TEST(HeaderSmugglingTest, ChunkedTrailerHeadersMerged) { #ifdef CPPHTTPLIB_OPENSSL_SUPPORT TEST(StaticFileSever, CacheValidation) { for ( - const std::string header_if_none_match : { - R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", - R"("d", "db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", - R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b", "g")", - R"(*)", - R"("f", *)", - R"(*, "i")", - }) { + const std::string header_if_none_match : + {"*", R"("f", *)", R"(*, "i")", + R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", + R"("d", "db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", + R"(W/"db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b", "g")"}) { httplib::Server svr; svr.set_mount_point("/", "./www/"); From d38da7b7aff3372cdc85e423e903a2139416b371 Mon Sep 17 00:00:00 2001 From: Matheus Gabriel Werny Date: Sat, 27 Sep 2025 21:42:47 +0200 Subject: [PATCH 3/8] Bug fix Fixed c-string without null termination. --- httplib.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/httplib.h b/httplib.h index 5cc2abf3bf..377b59b68d 100644 --- a/httplib.h +++ b/httplib.h @@ -7892,8 +7892,9 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { */ if (req.method == "GET" || req.method == "HEAD") { // Value for HTTP response header ETag. + const std::string file_data(mm->data(), mm->size()); const std::string etag = - R"(")" + detail::SHA_512(mm->data()) + R"(")"; + R"(")" + detail::SHA_512(file_data) + R"(")"; /* * Weak validation is not used. * HTTP response header ETag must be set as if normal HTTP response From be8af8e39590133dfd7f8113dffff3a48226a907 Mon Sep 17 00:00:00 2001 From: Matheus Gabriel Werny Date: Sat, 25 Oct 2025 17:25:24 +0200 Subject: [PATCH 4/8] Allow disabling and programme If-Match Server, new member variable: is_etag_enabled Server, new member function: get_is_etag_enabled() Server, new member function: set_is_etag_enabled() Enable ETag, If-Match and If-None-Match conditionally depending on is_etag_enabled. Programme If-Match. --- httplib.h | 130 +++++++++++++++++++++++++++++------------------ test/test.cc | 140 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 193 insertions(+), 77 deletions(-) diff --git a/httplib.h b/httplib.h index 377b59b68d..9f5c5273c3 100644 --- a/httplib.h +++ b/httplib.h @@ -1146,6 +1146,9 @@ class Server { Server &set_payload_max_length(size_t length); + bool get_is_etag_enabled() const; + Server &set_is_etag_enabled(const bool &enabled); + bool bind_to_port(const std::string &host, int port, int socket_flags = 0); int bind_to_any_port(const std::string &host, int socket_flags = 0); bool listen_after_bind(); @@ -1237,6 +1240,7 @@ class Server { std::atomic is_running_{false}; std::atomic is_decommissioned{false}; + std::atomic is_etag_enabled{false}; struct MountPointEntry { std::string mount_point; @@ -7506,6 +7510,13 @@ inline Server &Server::set_payload_max_length(size_t length) { return *this; } +inline bool Server::get_is_etag_enabled() const { return is_etag_enabled; } + +inline Server &Server::set_is_etag_enabled(const bool &enabled) { + is_etag_enabled = enabled; + return *this; +} + inline bool Server::bind_to_port(const std::string &host, int port, int socket_flags) { auto ret = bind_internal(host, port, socket_flags); @@ -7882,62 +7893,83 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT - /* - * The HTTP request header If-None-Match can be used with other - * methods where it has the meaning to only execute if the resource - * does not already exist but uploading does not matter here. - * HTTP response header ETag is only set where content is - * pulled and not pushed as those HTTP response bodies do not have to - * be related to the content. - */ - if (req.method == "GET" || req.method == "HEAD") { - // Value for HTTP response header ETag. - const std::string file_data(mm->data(), mm->size()); - const std::string etag = - R"(")" + detail::SHA_512(file_data) + R"(")"; + if (is_etag_enabled) /* - * Weak validation is not used. - * HTTP response header ETag must be set as if normal HTTP response - * was sent. + * The HTTP request header If-Match and If-None-Match can be used + * with other methods where they have the meaning to only execute if + * the resource does not already exist but uploading does not matter + * here. + * + * HTTP response header ETag is only set where content is + * pulled and not pushed as those HTTP response bodies do not have + * to be related to the content. */ - res.set_header("ETag", etag); - - /* - * Semantic: If value exists, the server will send status code 304. - * * always results in status code 304. - */ - if (req.has_header("If-None-Match")) { - const std::string header_if_none_match = - req.get_header_value("If-None-Match"); + if (req.method == "GET" || req.method == "HEAD") { + // Value for HTTP response header ETag. + const std::string file_data(mm->data(), mm->size()); + const std::string etag = + R"(")" + detail::SHA_512(file_data) + R"(")"; + /* + * Weak validation is not used in both cases. + * HTTP response header ETag must be set as if normal HTTP + * response was sent. + */ + res.set_header("ETag", etag); /* - * Values of HTTP request header If-None-Match which are cached - * values of previous HTTP response header ETag. + * If-Match + * If-value exists, the server will send status code 200. + * Else, the server will send status code 412. + * + * If-None-Match + * If value exists, the server will send status code 304. + * * always results in status code 304. */ - std::set etags; - detail::split(header_if_none_match.c_str(), - header_if_none_match.c_str() + - header_if_none_match.length(), - ',', [&](const char *b, const char *e) { - std::string etag(b, e); - - // Weak validation is not used. - if (etag.length() >= 2 && etag.at(0) == 'W' && - etag.at(1) == '/') { - etag.erase(0, 2); - } - - etags.insert(std::move(etag)); - }); - - if (etags.find("*") != etags.cend() || - etags.find(etag) != etags.cend()) { - res.status = StatusCode::NotModified_304; - res.body.clear(); - return true; + if (req.has_header("If-Match") || + req.has_header("If-None-Match")) { + std::string header_value; + + if (req.has_header("If-Match")) { + header_value = req.get_header_value("If-Match"); + } else if (req.has_header("If-None-Match")) { + header_value = req.get_header_value("If-None-Match"); + } + + std::set etags; + detail::split(header_value.c_str(), + header_value.c_str() + header_value.length(), ',', + [&](const char *b, const char *e) { + std::string etag(b, e); + + // Weak validation is not used in both cases. + // However, do not remove W/ with HTTP request + // header If-Match as such ETags are need to + // result in false when comparing. + if (req.has_header("If-None-Match") && + etag.length() >= 2 && etag.at(0) == 'W' && + etag.at(1) == '/') { + etag.erase(0, 2); + } + + etags.insert(std::move(etag)); + }); + + if (req.has_header("If-Match")) { + if (etags.find("*") == etags.cend() && + etags.find(etag) == etags.cend()) { + res.status = StatusCode::PreconditionFailed_412; + res.body.clear(); + return true; + } + } else if (req.has_header("If-None-Match") && + etags.find("*") != etags.cend() || + etags.find(etag) != etags.cend()) { + res.status = StatusCode::NotModified_304; + res.body.clear(); + return true; + } } } - } #endif res.set_content_provider( diff --git a/test/test.cc b/test/test.cc index d6df76b863..14290f2fd4 100644 --- a/test/test.cc +++ b/test/test.cc @@ -10829,39 +10829,123 @@ TEST(HeaderSmugglingTest, ChunkedTrailerHeadersMerged) { ASSERT_TRUE(send_request(1, req, &res)); } +TEST(is_etag_enabled, getter_and_setter) { + httplib::Server svr; + + EXPECT_FALSE(svr.get_is_etag_enabled()); + svr.set_is_etag_enabled(true); + EXPECT_TRUE(svr.get_is_etag_enabled()); +} + #ifdef CPPHTTPLIB_OPENSSL_SUPPORT -TEST(StaticFileSever, CacheValidation) { - for ( - const std::string header_if_none_match : - {"*", R"("f", *)", R"(*, "i")", - R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", - R"("d", "db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", - R"(W/"db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b", "g")"}) { - httplib::Server svr; - svr.set_mount_point("/", "./www/"); - - std::thread t = thread([&]() { svr.listen(HOST, PORT); }); - auto se = detail::scope_exit([&] { - svr.stop(); - t.join(); - ASSERT_FALSE(svr.is_running()); - }); +TEST(StaticFileSever, If_Match) { + /* + * 0: is_etag_enabled = false + * 1: is_etag_enabled = true + */ + for (std::uint8_t i = 0; i < 2; ++i) { + for ( + const std::string header_if_match : + {R"("wcupin")", "*", R"("r", *)", R"(*, "x")", + R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", + R"("o", "db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", + R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b", "a")"}) { + httplib::Server svr; + svr.set_mount_point("/", "./www/"); + svr.set_is_etag_enabled(i == 1); + + std::thread t = thread([&]() { svr.listen(HOST, PORT); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); - svr.wait_until_ready(); + svr.wait_until_ready(); + + httplib::Client client(HOST, PORT); + const httplib::Result result = + client.Get("/file", Headers({{"If-Match", header_if_match}})); + + ASSERT_NE(result, nullptr); + EXPECT_EQ(result.error(), Error::Success); - httplib::Client client(HOST, PORT); - const httplib::Result result = - client.Get("/file", Headers({{"If-None-Match", header_if_none_match}})); + if (i == 0) { + EXPECT_EQ(result->status, StatusCode::OK_200); + } else if (i == 1) { + if (header_if_match == R"("wcupin")") { + EXPECT_EQ(result->status, StatusCode::PreconditionFailed_412); + } else { + EXPECT_EQ(result->status, StatusCode::OK_200); + } + } - ASSERT_NE(result, nullptr); - EXPECT_EQ(result.error(), Error::Success); - EXPECT_EQ(result->status, StatusCode::NotModified_304); + if (i == 0) { + EXPECT_FALSE(result->has_header("ETag")); + } else if (i == 1) { + EXPECT_TRUE(result->has_header("ETag")); + EXPECT_EQ( + result->get_header_value("ETag"), + R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")"); - EXPECT_TRUE(result->has_header("ETag")); - EXPECT_EQ( - result->get_header_value("ETag"), - R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")"); - EXPECT_TRUE(result->body.empty()); + if (header_if_match == R"("wcupin")") { + EXPECT_TRUE(result->body.empty()); + } + } + } + } +} +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +TEST(StaticFileSever, If_None_Match) { + /* + * 0: is_etag_enabled = false + * 1: is_etag_enabled = true + */ + for (std::uint8_t i = 0; i < 2; ++i) { + for ( + const std::string header_if_none_match : + {"*", R"("f", *)", R"(*, "i")", + R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", + R"("d", "db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", + R"(W/"db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b", "g")"}) { + httplib::Server svr; + svr.set_mount_point("/", "./www/"); + svr.set_is_etag_enabled(i == 1); + + std::thread t = thread([&]() { svr.listen(HOST, PORT); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + httplib::Client client(HOST, PORT); + const httplib::Result result = client.Get( + "/file", Headers({{"If-None-Match", header_if_none_match}})); + + ASSERT_NE(result, nullptr); + EXPECT_EQ(result.error(), Error::Success); + + if (i == 0) { + EXPECT_EQ(result->status, StatusCode::OK_200); + } else if (i == 1) { + EXPECT_EQ(result->status, StatusCode::NotModified_304); + } + + if (i == 0) { + EXPECT_FALSE(result->has_header("ETag")); + } else if (i == 1) { + EXPECT_TRUE(result->has_header("ETag")); + EXPECT_EQ( + result->get_header_value("ETag"), + R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")"); + EXPECT_TRUE(result->body.empty()); + } + } } } #endif From b0af1002a444255a7ad9d2072deca5e73bdb6b5f Mon Sep 17 00:00:00 2001 From: Matheus Gabriel Werny Date: Sun, 26 Oct 2025 12:19:31 +0100 Subject: [PATCH 5/8] New ETag constitution Fix operator precedence issue. Made httplib::detail::Filestat::st_ public as it is needed. Changed ETag constitution. Adjusted tests. Resources: https://stackoverflow.com/questions/40504281/c-how-to-check-the-last-modified-time-of-a-file https://serverfault.com/questions/690341/algorithm-behind-nginx-etag-generation --- httplib.h | 11 ++++++----- test/test.cc | 45 +++++++++++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/httplib.h b/httplib.h index 9f5c5273c3..d57127267f 100644 --- a/httplib.h +++ b/httplib.h @@ -2408,7 +2408,6 @@ struct FileStat { bool is_file() const; bool is_dir() const; -private: #if defined(_WIN32) struct _stat st_; #else @@ -7893,7 +7892,7 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT - if (is_etag_enabled) + if (is_etag_enabled && stat.ret_ >= 0) /* * The HTTP request header If-Match and If-None-Match can be used * with other methods where they have the meaning to only execute if @@ -7908,7 +7907,9 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { // Value for HTTP response header ETag. const std::string file_data(mm->data(), mm->size()); const std::string etag = - R"(")" + detail::SHA_512(file_data) + R"(")"; + R"(")" + detail::from_i_to_hex(stat.st_.st_mtim.tv_sec) + + "-" + detail::from_i_to_hex(mm->size()) + R"(")"; + /* * Weak validation is not used in both cases. * HTTP response header ETag must be set as if normal HTTP @@ -7962,8 +7963,8 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { return true; } } else if (req.has_header("If-None-Match") && - etags.find("*") != etags.cend() || - etags.find(etag) != etags.cend()) { + (etags.find("*") != etags.cend() || + etags.find(etag) != etags.cend())) { res.status = StatusCode::NotModified_304; res.body.clear(); return true; diff --git a/test/test.cc b/test/test.cc index 14290f2fd4..0039e8de3f 100644 --- a/test/test.cc +++ b/test/test.cc @@ -10839,17 +10839,23 @@ TEST(is_etag_enabled, getter_and_setter) { #ifdef CPPHTTPLIB_OPENSSL_SUPPORT TEST(StaticFileSever, If_Match) { + detail::FileStat stat("./www/file"); + ASSERT_GE(stat.ret_, 0); + auto mm = std::make_shared("./www/file"); + ASSERT_TRUE(mm->is_open()); + + const std::string etag = R"(")" + + detail::from_i_to_hex(stat.st_.st_mtim.tv_sec) + + "-" + detail::from_i_to_hex(mm->size()) + R"(")"; + /* * 0: is_etag_enabled = false * 1: is_etag_enabled = true */ for (std::uint8_t i = 0; i < 2; ++i) { - for ( - const std::string header_if_match : - {R"("wcupin")", "*", R"("r", *)", R"(*, "x")", - R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", - R"("o", "db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", - R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b", "a")"}) { + for (const std::string header_if_match : std::initializer_list{ + R"("wcupin")", "*", R"("r", *)", R"(*, "x")", etag, + R"("o", )" + etag, etag + R"(, "a")"}) { httplib::Server svr; svr.set_mount_point("/", "./www/"); svr.set_is_etag_enabled(i == 1); @@ -10884,9 +10890,7 @@ TEST(StaticFileSever, If_Match) { EXPECT_FALSE(result->has_header("ETag")); } else if (i == 1) { EXPECT_TRUE(result->has_header("ETag")); - EXPECT_EQ( - result->get_header_value("ETag"), - R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")"); + EXPECT_EQ(result->get_header_value("ETag"), etag); if (header_if_match == R"("wcupin")") { EXPECT_TRUE(result->body.empty()); @@ -10899,17 +10903,24 @@ TEST(StaticFileSever, If_Match) { #ifdef CPPHTTPLIB_OPENSSL_SUPPORT TEST(StaticFileSever, If_None_Match) { + detail::FileStat stat("./www/file"); + ASSERT_GE(stat.ret_, 0); + auto mm = std::make_shared("./www/file"); + ASSERT_TRUE(mm->is_open()); + + const std::string etag = R"(")" + + detail::from_i_to_hex(stat.st_.st_mtim.tv_sec) + + "-" + detail::from_i_to_hex(mm->size()) + R"(")"; + /* * 0: is_etag_enabled = false * 1: is_etag_enabled = true */ for (std::uint8_t i = 0; i < 2; ++i) { - for ( - const std::string header_if_none_match : - {"*", R"("f", *)", R"(*, "i")", - R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", - R"("d", "db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", - R"(W/"db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b", "g")"}) { + for (const std::string header_if_none_match : + std::initializer_list{"*", R"("f", *)", R"(*, "i")", etag, + R"("d", )" + etag, + "W/" + etag + R"(, "g")"}) { httplib::Server svr; svr.set_mount_point("/", "./www/"); svr.set_is_etag_enabled(i == 1); @@ -10940,9 +10951,7 @@ TEST(StaticFileSever, If_None_Match) { EXPECT_FALSE(result->has_header("ETag")); } else if (i == 1) { EXPECT_TRUE(result->has_header("ETag")); - EXPECT_EQ( - result->get_header_value("ETag"), - R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")"); + EXPECT_EQ(result->get_header_value("ETag"), etag); EXPECT_TRUE(result->body.empty()); } } From dc30799710a8a2d55397cd43c105896029f8c84c Mon Sep 17 00:00:00 2001 From: Matheus Gabriel Werny Date: Sun, 26 Oct 2025 12:38:58 +0100 Subject: [PATCH 6/8] ETags: Erase whitespace Erase whitespace. Adjusted tests. --- httplib.h | 2 ++ test/test.cc | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/httplib.h b/httplib.h index d57127267f..d70eba4337 100644 --- a/httplib.h +++ b/httplib.h @@ -7941,6 +7941,8 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { header_value.c_str() + header_value.length(), ',', [&](const char *b, const char *e) { std::string etag(b, e); + etag.erase(0, etag.find_first_not_of(" \t")); + etag.erase(etag.find_last_not_of(" \t") + 1); // Weak validation is not used in both cases. // However, do not remove W/ with HTTP request diff --git a/test/test.cc b/test/test.cc index 0039e8de3f..814f64a6db 100644 --- a/test/test.cc +++ b/test/test.cc @@ -10854,7 +10854,7 @@ TEST(StaticFileSever, If_Match) { */ for (std::uint8_t i = 0; i < 2; ++i) { for (const std::string header_if_match : std::initializer_list{ - R"("wcupin")", "*", R"("r", *)", R"(*, "x")", etag, + R"("wcupin")", " * ", R"("r", *)", R"(*, "x")", etag, R"("o", )" + etag, etag + R"(, "a")"}) { httplib::Server svr; svr.set_mount_point("/", "./www/"); @@ -10918,8 +10918,8 @@ TEST(StaticFileSever, If_None_Match) { */ for (std::uint8_t i = 0; i < 2; ++i) { for (const std::string header_if_none_match : - std::initializer_list{"*", R"("f", *)", R"(*, "i")", etag, - R"("d", )" + etag, + std::initializer_list{" * ", R"("f", *)", R"(*, "i")", + etag, R"("d", )" + etag, "W/" + etag + R"(, "g")"}) { httplib::Server svr; svr.set_mount_point("/", "./www/"); From 60d2a37921ecc5161badb4d1ab8267b9732f9696 Mon Sep 17 00:00:00 2001 From: Matheus Gabriel Werny Date: Tue, 28 Oct 2025 17:33:22 +0100 Subject: [PATCH 7/8] Improved FileStat Added last_modified() to struct FileStat. Removed condition #ifdef CPPHTTPLIB_OPENSSL_SUPPORT around ETag generation. Adjusted tests. --- httplib.h | 22 ++++++++++++++++------ test/test.cc | 25 ++++++++++++++----------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/httplib.h b/httplib.h index d70eba4337..505e75aed4 100644 --- a/httplib.h +++ b/httplib.h @@ -2407,7 +2407,9 @@ struct FileStat { FileStat(const std::string &path); bool is_file() const; bool is_dir() const; + std::uint64_t last_modified() const; +private: #if defined(_WIN32) struct _stat st_; #else @@ -2883,6 +2885,17 @@ inline bool FileStat::is_file() const { inline bool FileStat::is_dir() const { return ret_ >= 0 && S_ISDIR(st_.st_mode); } +inline std::uint64_t FileStat::last_modified() const { + if (is_dir() || is_file()) { +#if defined(_WIN32) + return st_.st_mtime; +#else + return st_.st_mtim.tv_sec; +#endif + } else { + throw std::runtime_error("Invalid directory or file."); + } +} inline std::string encode_path(const std::string &s) { std::string result; @@ -7891,8 +7904,7 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { return false; } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - if (is_etag_enabled && stat.ret_ >= 0) + if (is_etag_enabled) /* * The HTTP request header If-Match and If-None-Match can be used * with other methods where they have the meaning to only execute if @@ -7905,10 +7917,9 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { */ if (req.method == "GET" || req.method == "HEAD") { // Value for HTTP response header ETag. - const std::string file_data(mm->data(), mm->size()); const std::string etag = - R"(")" + detail::from_i_to_hex(stat.st_.st_mtim.tv_sec) + - "-" + detail::from_i_to_hex(mm->size()) + R"(")"; + R"(")" + detail::from_i_to_hex(stat.last_modified()) + "-" + + detail::from_i_to_hex(mm->size()) + R"(")"; /* * Weak validation is not used in both cases. @@ -7973,7 +7984,6 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { } } } -#endif res.set_content_provider( mm->size(), diff --git a/test/test.cc b/test/test.cc index 814f64a6db..2c1242975e 100644 --- a/test/test.cc +++ b/test/test.cc @@ -10306,17 +10306,24 @@ TEST(UniversalClientImplTest, Ipv6LiteralAddress) { EXPECT_EQ(cli.port(), port); } -TEST(FileSystemTest, FileAndDirExistenceCheck) { +TEST(FileSystemTest, FileStatTest) { auto file_path = "./www/dir/index.html"; auto dir_path = "./www/dir"; detail::FileStat stat_file(file_path); EXPECT_TRUE(stat_file.is_file()); EXPECT_FALSE(stat_file.is_dir()); + EXPECT_GT(stat_file.last_modified(), 0); detail::FileStat stat_dir(dir_path); EXPECT_FALSE(stat_dir.is_file()); EXPECT_TRUE(stat_dir.is_dir()); + EXPECT_GT(stat_dir.last_modified(), 0); + + detail::FileStat stat_error("ranipsd"); + EXPECT_FALSE(stat_error.is_file()); + EXPECT_FALSE(stat_error.is_dir()); + EXPECT_THROW(stat_error.last_modified(), std::runtime_error); } TEST(DirtyDataRequestTest, HeadFieldValueContains_CR_LF_NUL) { @@ -10837,16 +10844,15 @@ TEST(is_etag_enabled, getter_and_setter) { EXPECT_TRUE(svr.get_is_etag_enabled()); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT TEST(StaticFileSever, If_Match) { detail::FileStat stat("./www/file"); - ASSERT_GE(stat.ret_, 0); + ASSERT_TRUE(stat.is_file()); auto mm = std::make_shared("./www/file"); ASSERT_TRUE(mm->is_open()); const std::string etag = R"(")" + - detail::from_i_to_hex(stat.st_.st_mtim.tv_sec) + - "-" + detail::from_i_to_hex(mm->size()) + R"(")"; + detail::from_i_to_hex(stat.last_modified()) + "-" + + detail::from_i_to_hex(mm->size()) + R"(")"; /* * 0: is_etag_enabled = false @@ -10899,18 +10905,16 @@ TEST(StaticFileSever, If_Match) { } } } -#endif -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT TEST(StaticFileSever, If_None_Match) { detail::FileStat stat("./www/file"); - ASSERT_GE(stat.ret_, 0); + ASSERT_TRUE(stat.is_file()); auto mm = std::make_shared("./www/file"); ASSERT_TRUE(mm->is_open()); const std::string etag = R"(")" + - detail::from_i_to_hex(stat.st_.st_mtim.tv_sec) + - "-" + detail::from_i_to_hex(mm->size()) + R"(")"; + detail::from_i_to_hex(stat.last_modified()) + "-" + + detail::from_i_to_hex(mm->size()) + R"(")"; /* * 0: is_etag_enabled = false @@ -10957,4 +10961,3 @@ TEST(StaticFileSever, If_None_Match) { } } } -#endif From 8815e843a9f9e79aed2e8edc7729c2b089d705d8 Mon Sep 17 00:00:00 2001 From: Matheus Gabriel Werny Date: Tue, 28 Oct 2025 18:46:10 +0100 Subject: [PATCH 8/8] Applied clang-tidy suggestions Applied clang-tidy suggestions. --- httplib.h | 8 +++++--- test/test.cc | 14 ++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/httplib.h b/httplib.h index a7f034592a..d63c9078bd 100644 --- a/httplib.h +++ b/httplib.h @@ -7942,12 +7942,13 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { for (const auto &entry : base_dirs_) { // Prefix match if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point)) { - std::string sub_path = "/" + req.path.substr(entry.mount_point.size()); + const std::string sub_path = + "/" + req.path.substr(entry.mount_point.size()); if (detail::is_valid_path(sub_path)) { auto path = entry.base_dir + sub_path; if (path.back() == '/') { path += "index.html"; } - detail::FileStat stat(path); + const detail::FileStat stat(path); if (stat.is_dir()) { res.set_redirect(sub_path + "/", StatusCode::MovedPermanently_301); @@ -7965,7 +7966,7 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { return false; } - if (is_etag_enabled) + if (is_etag_enabled) { /* * The HTTP request header If-Match and If-None-Match can be used * with other methods where they have the meaning to only execute if @@ -8045,6 +8046,7 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { } } } + } res.set_content_provider( mm->size(), diff --git a/test/test.cc b/test/test.cc index 89eccf2449..87d9fe8639 100644 --- a/test/test.cc +++ b/test/test.cc @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -11460,8 +11461,8 @@ TEST(is_etag_enabled, getter_and_setter) { EXPECT_TRUE(svr.get_is_etag_enabled()); } -TEST(StaticFileSever, If_Match) { - detail::FileStat stat("./www/file"); +TEST(StaticFileSever, IfMatch) { + const detail::FileStat stat("./www/file"); ASSERT_TRUE(stat.is_file()); auto mm = std::make_shared("./www/file"); ASSERT_TRUE(mm->is_open()); @@ -11475,7 +11476,8 @@ TEST(StaticFileSever, If_Match) { * 1: is_etag_enabled = true */ for (std::uint8_t i = 0; i < 2; ++i) { - for (const std::string header_if_match : std::initializer_list{ + for (const std::string &header_if_match : + std::initializer_list{ R"("wcupin")", " * ", R"("r", *)", R"(*, "x")", etag, R"("o", )" + etag, etag + R"(, "a")"}) { httplib::Server svr; @@ -11522,8 +11524,8 @@ TEST(StaticFileSever, If_Match) { } } -TEST(StaticFileSever, If_None_Match) { - detail::FileStat stat("./www/file"); +TEST(StaticFileSever, IfNoneMatch) { + const detail::FileStat stat("./www/file"); ASSERT_TRUE(stat.is_file()); auto mm = std::make_shared("./www/file"); ASSERT_TRUE(mm->is_open()); @@ -11537,7 +11539,7 @@ TEST(StaticFileSever, If_None_Match) { * 1: is_etag_enabled = true */ for (std::uint8_t i = 0; i < 2; ++i) { - for (const std::string header_if_none_match : + for (const std::string &header_if_none_match : std::initializer_list{" * ", R"("f", *)", R"(*, "i")", etag, R"("d", )" + etag, "W/" + etag + R"(, "g")"}) {