From 656e936f49d526ca337d2effed5ae15911bc9ced Mon Sep 17 00:00:00 2001 From: Gopinath K <85661676+Gopinath-Kalaiyarasan@users.noreply.github.com> Date: Fri, 5 Aug 2022 06:12:13 +0530 Subject: [PATCH] add multipart formdata for PUT requests. (#1351) * httplib.h add multipart formdata for PUT in addition to POST as some REST APIs use that. Factor the boundary checking code into a helper and use it from both Post() and Put(). * test/test.cc add test cases for the above. --- httplib.h | 123 +++++++++++++++++++++------ test/test.cc | 235 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 333 insertions(+), 25 deletions(-) diff --git a/httplib.h b/httplib.h index 8363df0..41c5a11 100644 --- a/httplib.h +++ b/httplib.h @@ -949,6 +949,11 @@ public: Result Put(const std::string &path, const Params ¶ms); Result Put(const std::string &path, const Headers &headers, const Params ¶ms); + Result Put(const std::string &path, const MultipartFormDataItems &items); + Result Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items); + Result Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, const std::string &boundary); Result Patch(const std::string &path); Result Patch(const std::string &path, const char *body, size_t content_length, @@ -1304,6 +1309,11 @@ public: Result Put(const std::string &path, const Params ¶ms); Result Put(const std::string &path, const Headers &headers, const Params ¶ms); + Result Put(const std::string &path, const MultipartFormDataItems &items); + Result Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items); + Result Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, const std::string &boundary); Result Patch(const std::string &path); Result Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type); @@ -4064,6 +4074,46 @@ inline std::string make_multipart_data_boundary() { return result; } +inline bool is_multipart_boundary_chars_valid(const std::string& boundary) +{ + bool valid = true; + for (size_t i = 0; i < boundary.size(); i++) { + char c = boundary[i]; + if (!std::isalnum(c) && c != '-' && c != '_') { + valid = false; + break; + } + } + return valid; +} + + +inline std::string serialize_multipart_formdata(const MultipartFormDataItems& items, std::string& content_type, const std::string& boundary_str) +{ + const std::string& boundary = boundary_str.empty() ? make_multipart_data_boundary() : boundary_str; + + std::string body; + + for (const auto &item : items) { + body += "--" + boundary + "\r\n"; + body += "Content-Disposition: form-data; name=\"" + item.name + "\""; + if (!item.filename.empty()) { + body += "; filename=\"" + item.filename + "\""; + } + body += "\r\n"; + if (!item.content_type.empty()) { + body += "Content-Type: " + item.content_type + "\r\n"; + } + body += "\r\n"; + body += item.content + "\r\n"; + } + + body += "--" + boundary + "--\r\n"; + + content_type = "multipart/form-data; boundary=" + boundary; + return body; +} + inline std::pair get_range_offset_and_length(const Request &req, size_t content_length, size_t index) { @@ -6745,37 +6795,21 @@ inline Result ClientImpl::Post(const std::string &path, inline Result ClientImpl::Post(const std::string &path, const Headers &headers, const MultipartFormDataItems &items) { - return Post(path, headers, items, detail::make_multipart_data_boundary()); + std::string content_type; + const std::string& body = detail::serialize_multipart_formdata(items, content_type, std::string()); + return Post(path, headers, body, content_type.c_str()); } + inline Result ClientImpl::Post(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, - const std::string &boundary) { - for (size_t i = 0; i < boundary.size(); i++) { - char c = boundary[i]; - if (!std::isalnum(c) && c != '-' && c != '_') { + const std::string &boundary) +{ + if (!detail::is_multipart_boundary_chars_valid(boundary)) { return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; - } } - std::string body; - - for (const auto &item : items) { - body += "--" + boundary + "\r\n"; - body += "Content-Disposition: form-data; name=\"" + item.name + "\""; - if (!item.filename.empty()) { - body += "; filename=\"" + item.filename + "\""; - } - body += "\r\n"; - if (!item.content_type.empty()) { - body += "Content-Type: " + item.content_type + "\r\n"; - } - body += "\r\n"; - body += item.content + "\r\n"; - } - - body += "--" + boundary + "--\r\n"; - - std::string content_type = "multipart/form-data; boundary=" + boundary; + std::string content_type; + const std::string& body = detail::serialize_multipart_formdata(items, content_type, boundary); return Post(path, headers, body, content_type.c_str()); } @@ -6848,6 +6882,31 @@ inline Result ClientImpl::Put(const std::string &path, const Headers &headers, return Put(path, headers, query, "application/x-www-form-urlencoded"); } +inline Result ClientImpl::Put(const std::string &path, const MultipartFormDataItems &items) +{ + return Put(path, Headers(), items); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items) +{ + std::string content_type; + const std::string& body = detail::serialize_multipart_formdata(items, content_type, std::string()); + return Put(path, headers, body, content_type); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, + const std::string &boundary) +{ + if (!detail::is_multipart_boundary_chars_valid(boundary)) { + return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; + } + std::string content_type; + const std::string& body = detail::serialize_multipart_formdata(items, content_type, boundary); + return Put(path, headers, body, content_type); +} + inline Result ClientImpl::Patch(const std::string &path) { return Patch(path, std::string(), std::string()); } @@ -8099,6 +8158,20 @@ inline Result Client::Put(const std::string &path, const Headers &headers, const Params ¶ms) { return cli_->Put(path, headers, params); } +inline Result Client::Put(const std::string &path, const MultipartFormDataItems &items) +{ + return cli_->Put(path, items); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items) +{ + return cli_->Put(path, headers, items); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, const std::string &boundary) +{ + return cli_->Put(path, headers, items, boundary); +} inline Result Client::Patch(const std::string &path) { return cli_->Patch(path); } diff --git a/test/test.cc b/test/test.cc index fc0fa09..aa7eeca 100644 --- a/test/test.cc +++ b/test/test.cc @@ -5062,6 +5062,241 @@ TEST(MultipartFormDataTest, WithPreamble) { t.join(); } +TEST(MultipartFormDataTest, PostCustomBoundary) { + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + + svr.Post("/post_customboundary", [&](const Request &req, Response & /*res*/, + const ContentReader &content_reader) { + if (req.is_multipart_form_data()) { + MultipartFormDataItems files; + content_reader( + [&](const MultipartFormData &file) { + files.push_back(file); + return true; + }, + [&](const char *data, size_t data_length) { + files.back().content.append(data, data_length); + return true; + }); + + EXPECT_TRUE(std::string(files[0].name) == "document"); + EXPECT_EQ(size_t(1024 * 1024 * 2), files[0].content.size()); + EXPECT_TRUE(files[0].filename == "2MB_data"); + EXPECT_TRUE(files[0].content_type == "application/octet-stream"); + + EXPECT_TRUE(files[1].name == "hello"); + EXPECT_TRUE(files[1].content == "world"); + EXPECT_TRUE(files[1].filename == ""); + EXPECT_TRUE(files[1].content_type == ""); + } else { + std::string body; + content_reader([&](const char *data, size_t data_length) { + body.append(data, data_length); + return true; + }); + } + }); + + auto t = std::thread([&]() { svr.listen("localhost", 8080); }); + while (!svr.is_running()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + + { + std::string data(1024 * 1024 * 2, '.'); + std::stringstream buffer; + buffer << data; + + Client cli("https://localhost:8080"); + cli.enable_server_certificate_verification(false); + + MultipartFormDataItems items{ + {"document", buffer.str(), "2MB_data", "application/octet-stream"}, + {"hello", "world", "", ""}, + }; + + auto res = cli.Post("/post_customboundary", {}, items, "abc-abc"); + ASSERT_TRUE(res); + ASSERT_EQ(200, res->status); + } + + svr.stop(); + t.join(); +} + +TEST(MultipartFormDataTest, PostInvalidBoundaryChars) { + + std::this_thread::sleep_for(std::chrono::seconds(1)); + + std::string data(1024 * 1024 * 2, '&'); + std::stringstream buffer; + buffer << data; + + Client cli("https://localhost:8080"); + + MultipartFormDataItems items{ + {"document", buffer.str(), "2MB_data", "application/octet-stream"}, + {"hello", "world", "", ""}, + }; + + for (const char& c: " \t\r\n") { + auto res = cli.Post("/invalid_boundary", {}, items, string("abc123").append(1, c)); + ASSERT_EQ(Error::UnsupportedMultipartBoundaryChars, res.error()); + ASSERT_FALSE(res); + } + +} + +TEST(MultipartFormDataTest, PutFormData) { + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + + svr.Put("/put", [&](const Request &req, const Response & /*res*/, + const ContentReader &content_reader) { + if (req.is_multipart_form_data()) { + MultipartFormDataItems files; + content_reader( + [&](const MultipartFormData &file) { + files.push_back(file); + return true; + }, + [&](const char *data, size_t data_length) { + files.back().content.append(data, data_length); + return true; + }); + + EXPECT_TRUE(std::string(files[0].name) == "document"); + EXPECT_EQ(size_t(1024 * 1024 * 2), files[0].content.size()); + EXPECT_TRUE(files[0].filename == "2MB_data"); + EXPECT_TRUE(files[0].content_type == "application/octet-stream"); + + EXPECT_TRUE(files[1].name == "hello"); + EXPECT_TRUE(files[1].content == "world"); + EXPECT_TRUE(files[1].filename == ""); + EXPECT_TRUE(files[1].content_type == ""); + } else { + std::string body; + content_reader([&](const char *data, size_t data_length) { + body.append(data, data_length); + return true; + }); + } + }); + + auto t = std::thread([&]() { svr.listen("localhost", 8080); }); + while (!svr.is_running()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + + { + std::string data(1024 * 1024 * 2, '&'); + std::stringstream buffer; + buffer << data; + + Client cli("https://localhost:8080"); + cli.enable_server_certificate_verification(false); + + MultipartFormDataItems items{ + {"document", buffer.str(), "2MB_data", "application/octet-stream"}, + {"hello", "world", "", ""}, + }; + + auto res = cli.Put("/put", items); + ASSERT_TRUE(res); + ASSERT_EQ(200, res->status); + } + + svr.stop(); + t.join(); +} + +TEST(MultipartFormDataTest, PutFormDataCustomBoundary) { + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + + svr.Put("/put_customboundary", [&](const Request &req, const Response & /*res*/, + const ContentReader &content_reader) { + if (req.is_multipart_form_data()) { + MultipartFormDataItems files; + content_reader( + [&](const MultipartFormData &file) { + files.push_back(file); + return true; + }, + [&](const char *data, size_t data_length) { + files.back().content.append(data, data_length); + return true; + }); + + EXPECT_TRUE(std::string(files[0].name) == "document"); + EXPECT_EQ(size_t(1024 * 1024 * 2), files[0].content.size()); + EXPECT_TRUE(files[0].filename == "2MB_data"); + EXPECT_TRUE(files[0].content_type == "application/octet-stream"); + + EXPECT_TRUE(files[1].name == "hello"); + EXPECT_TRUE(files[1].content == "world"); + EXPECT_TRUE(files[1].filename == ""); + EXPECT_TRUE(files[1].content_type == ""); + } else { + std::string body; + content_reader([&](const char *data, size_t data_length) { + body.append(data, data_length); + return true; + }); + } + }); + + auto t = std::thread([&]() { svr.listen("localhost", 8080); }); + while (!svr.is_running()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + + { + std::string data(1024 * 1024 * 2, '&'); + std::stringstream buffer; + buffer << data; + + Client cli("https://localhost:8080"); + cli.enable_server_certificate_verification(false); + + MultipartFormDataItems items{ + {"document", buffer.str(), "2MB_data", "application/octet-stream"}, + {"hello", "world", "", ""}, + }; + + auto res = cli.Put("/put_customboundary", {}, items, "abc-abc_"); + ASSERT_TRUE(res); + ASSERT_EQ(200, res->status); + } + + svr.stop(); + t.join(); +} + +TEST(MultipartFormDataTest, PutInvalidBoundaryChars) { + + std::this_thread::sleep_for(std::chrono::seconds(1)); + + std::string data(1024 * 1024 * 2, '&'); + std::stringstream buffer; + buffer << data; + + Client cli("https://localhost:8080"); + cli.enable_server_certificate_verification(false); + + MultipartFormDataItems items{ + {"document", buffer.str(), "2MB_data", "application/octet-stream"}, + {"hello", "world", "", ""}, + }; + + for (const char& c: " \t\r\n") { + auto res = cli.Put("/put", {}, items, string("abc123").append(1, c)); + ASSERT_EQ(Error::UnsupportedMultipartBoundaryChars, res.error()); + ASSERT_FALSE(res); + } +} + #endif #ifndef _WIN32