From 8415bf082306c9e89e14b5eeca171011cfa182d9 Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 6 Sep 2024 23:51:39 -0400 Subject: [PATCH] Resolve #1906 --- README.md | 12 +++++++++++ httplib.h | 42 +++++++++++++++++++++++++++++++++++++ test/test.cc | 59 +++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 103 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 640aa2c..a25f4b3 100644 --- a/README.md +++ b/README.md @@ -384,6 +384,18 @@ svr.Get("/chunked", [&](const Request& req, Response& res) { }); ``` +### Send file content + +```cpp +svr.Get("/content", [&](const Request &req, Response &res) { + res.set_file_content("./path/to/conent.html"); +}); + +svr.Get("/content", [&](const Request &req, Response &res) { + res.set_file_content("./path/to/conent", "text/html"); +}); +``` + ### 'Expect: 100-continue' handler By default, the server sends a `100 Continue` response for an `Expect: 100-continue` header. diff --git a/httplib.h b/httplib.h index d0d0603..c9d8552 100644 --- a/httplib.h +++ b/httplib.h @@ -675,6 +675,10 @@ struct Response { const std::string &content_type, ContentProviderWithoutLength provider, ContentProviderResourceReleaser resource_releaser = nullptr); + void set_file_content(const std::string &path, + const std::string &content_type); + void set_file_content(const std::string &path); + Response() = default; Response(const Response &) = default; Response &operator=(const Response &) = default; @@ -692,6 +696,8 @@ struct Response { ContentProviderResourceReleaser content_provider_resource_releaser_; bool is_chunked_content_provider_ = false; bool content_provider_success_ = false; + std::string file_content_path_; + std::string file_content_content_type_; }; class Stream { @@ -5703,6 +5709,16 @@ inline void Response::set_chunked_content_provider( is_chunked_content_provider_ = true; } +inline void Response::set_file_content(const std::string &path, + const std::string &content_type) { + file_content_path_ = path; + file_content_content_type_ = content_type; +} + +inline void Response::set_file_content(const std::string &path) { + file_content_path_ = path; +} + // Result implementation inline bool Result::has_request_header(const std::string &key) const { return request_headers_.find(key) != request_headers_.end(); @@ -7043,6 +7059,32 @@ Server::process_request(Stream &strm, bool close_connection, 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_; + auto mm = std::make_shared(path.c_str()); + if (!mm->is_open()) { + res.body.clear(); + res.content_length_ = 0; + res.content_provider_ = nullptr; + res.status = StatusCode::NotFound_404; + return write_response(strm, close_connection, req, res); + } + + auto content_type = res.file_content_content_type_; + if (content_type.empty()) { + content_type = detail::find_content_type( + path, file_extension_and_mimetype_map_, default_file_mimetype_); + } + + res.set_content_provider( + mm->size(), content_type, + [mm](size_t offset, size_t length, DataSink &sink) -> bool { + sink.write(mm->data() + offset, length); + return true; + }); + } + return write_response_with_content(strm, close_connection, req, res); } else { if (res.status == -1) { res.status = StatusCode::NotFound_404; } diff --git a/test/test.cc b/test/test.cc index 5c6ceef..e3e202a 100644 --- a/test/test.cc +++ b/test/test.cc @@ -2300,6 +2300,18 @@ protected: [&](const Request & /*req*/, Response &res) { res.set_content("Hello World!", "text/plain"); }) + .Get("/file_content", + [&](const Request & /*req*/, Response &res) { + res.set_file_content("./www/dir/test.html"); + }) + .Get("/file_content_with_content_type", + [&](const Request & /*req*/, Response &res) { + res.set_file_content("./www/file", "text/plain"); + }) + .Get("/invalid_file_content", + [&](const Request & /*req*/, Response &res) { + res.set_file_content("./www/dir/invalid_file_path"); + }) .Get("/http_response_splitting", [&](const Request & /*req*/, Response &res) { res.set_header("a", "1\r\nSet-Cookie: a=1"); @@ -2904,6 +2916,30 @@ TEST_F(ServerTest, GetMethod200) { EXPECT_EQ("Hello World!", res->body); } +TEST_F(ServerTest, GetFileContent) { + auto res = cli_.Get("/file_content"); + ASSERT_TRUE(res); + EXPECT_EQ(StatusCode::OK_200, res->status); + EXPECT_EQ("text/html", res->get_header_value("Content-Type")); + EXPECT_EQ(9, std::stoi(res->get_header_value("Content-Length"))); + EXPECT_EQ("test.html", res->body); +} + +TEST_F(ServerTest, GetFileContentWithContentType) { + auto res = cli_.Get("/file_content_with_content_type"); + ASSERT_TRUE(res); + EXPECT_EQ(StatusCode::OK_200, res->status); + EXPECT_EQ("text/plain", res->get_header_value("Content-Type")); + EXPECT_EQ(5, std::stoi(res->get_header_value("Content-Length"))); + EXPECT_EQ("file\n", res->body); +} + +TEST_F(ServerTest, GetInvalidFileContent) { + auto res = cli_.Get("/invalid_file_content"); + ASSERT_TRUE(res); + EXPECT_EQ(StatusCode::NotFound_404, res->status); +} + TEST_F(ServerTest, GetMethod200withPercentEncoding) { auto res = cli_.Get("/%68%69"); // auto res = cli_.Get("/hi"); ASSERT_TRUE(res); @@ -4722,9 +4758,10 @@ static void test_raw_request(const std::string &req, svr.Put("/put_hi", [&](const Request & /*req*/, Response &res) { res.set_content("ok", "text/plain"); }); - svr.Get("/header_field_value_check", [&](const Request &/*req*/, Response &res) { - res.set_content("ok", "text/plain"); - }); + svr.Get("/header_field_value_check", + [&](const Request & /*req*/, Response &res) { + res.set_content("ok", "text/plain"); + }); // Server read timeout must be longer than the client read timeout for the // bug to reproduce, probably to force the server to process a request @@ -7640,7 +7677,7 @@ TEST(FileSystemTest, FileAndDirExistenceCheck) { TEST(DirtyDataRequestTest, HeadFieldValueContains_CR_LF_NUL) { Server svr; - svr.Get("/test", [&](const Request &/*req*/, Response &res) { + svr.Get("/test", [&](const Request & /*req*/, Response &res) { EXPECT_EQ(res.status, 400); }); @@ -7666,11 +7703,12 @@ TEST(Expect100ContinueTest, ServerClosesConnection) { Server svr; - svr.set_expect_100_continue_handler([](const Request &/*req*/, Response &res) { - res.status = StatusCode::Unauthorized_401; - res.set_content(reject, "text/plain"); - return res.status; - }); + svr.set_expect_100_continue_handler( + [](const Request & /*req*/, Response &res) { + res.status = StatusCode::Unauthorized_401; + res.set_content(reject, "text/plain"); + return res.status; + }); svr.Post("/", [&](const Request & /*req*/, Response &res) { res.set_content(accept, "text/plain"); }); @@ -7745,7 +7783,8 @@ TEST(Expect100ContinueTest, ServerClosesConnection) { { auto dl = curl_off_t{}; - const auto res = curl_easy_getinfo(curl.get(), CURLINFO_SIZE_DOWNLOAD_T, &dl); + const auto res = + curl_easy_getinfo(curl.get(), CURLINFO_SIZE_DOWNLOAD_T, &dl); ASSERT_EQ(res, CURLE_OK); ASSERT_EQ(dl, (curl_off_t)sizeof reject - 1); }