diff --git a/httplib.h b/httplib.h index db55d07e25..377b59b68d 100644 --- a/httplib.h +++ b/httplib.h @@ -7881,6 +7881,65 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { return false; } +#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"(")"; + /* + * Weak validation is not used. + * HTTP response header ETag must be set as if normal HTTP response + * was sent. + */ + 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"); + + /* + * 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 + 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..d6df76b863 100644 --- a/test/test.cc +++ b/test/test.cc @@ -10828,3 +10828,40 @@ 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"("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()); + }); + + 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