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.
This commit is contained in:
parent
d92c314466
commit
656e936f49
123
httplib.h
123
httplib.h
@ -949,6 +949,11 @@ public:
|
|||||||
Result Put(const std::string &path, const Params ¶ms);
|
Result Put(const std::string &path, const Params ¶ms);
|
||||||
Result Put(const std::string &path, const Headers &headers,
|
Result Put(const std::string &path, const Headers &headers,
|
||||||
const Params ¶ms);
|
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);
|
||||||
Result Patch(const std::string &path, const char *body, size_t content_length,
|
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 Params ¶ms);
|
||||||
Result Put(const std::string &path, const Headers &headers,
|
Result Put(const std::string &path, const Headers &headers,
|
||||||
const Params ¶ms);
|
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);
|
||||||
Result Patch(const std::string &path, const char *body, size_t content_length,
|
Result Patch(const std::string &path, const char *body, size_t content_length,
|
||||||
const std::string &content_type);
|
const std::string &content_type);
|
||||||
@ -4064,6 +4074,46 @@ inline std::string make_multipart_data_boundary() {
|
|||||||
return result;
|
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<size_t, size_t>
|
inline std::pair<size_t, size_t>
|
||||||
get_range_offset_and_length(const Request &req, size_t content_length,
|
get_range_offset_and_length(const Request &req, size_t content_length,
|
||||||
size_t index) {
|
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,
|
inline Result ClientImpl::Post(const std::string &path, const Headers &headers,
|
||||||
const MultipartFormDataItems &items) {
|
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,
|
inline Result ClientImpl::Post(const std::string &path, const Headers &headers,
|
||||||
const MultipartFormDataItems &items,
|
const MultipartFormDataItems &items,
|
||||||
const std::string &boundary) {
|
const std::string &boundary)
|
||||||
for (size_t i = 0; i < boundary.size(); i++) {
|
{
|
||||||
char c = boundary[i];
|
if (!detail::is_multipart_boundary_chars_valid(boundary)) {
|
||||||
if (!std::isalnum(c) && c != '-' && c != '_') {
|
|
||||||
return Result{nullptr, Error::UnsupportedMultipartBoundaryChars};
|
return Result{nullptr, Error::UnsupportedMultipartBoundaryChars};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::string body;
|
std::string content_type;
|
||||||
|
const std::string& body = detail::serialize_multipart_formdata(items, content_type, boundary);
|
||||||
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;
|
|
||||||
return Post(path, headers, body, content_type.c_str());
|
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");
|
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) {
|
inline Result ClientImpl::Patch(const std::string &path) {
|
||||||
return Patch(path, std::string(), std::string());
|
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) {
|
const Params ¶ms) {
|
||||||
return cli_->Put(path, headers, params);
|
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) {
|
inline Result Client::Patch(const std::string &path) {
|
||||||
return cli_->Patch(path);
|
return cli_->Patch(path);
|
||||||
}
|
}
|
||||||
|
235
test/test.cc
235
test/test.cc
@ -5062,6 +5062,241 @@ TEST(MultipartFormDataTest, WithPreamble) {
|
|||||||
t.join();
|
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
|
#endif
|
||||||
|
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
|
Loading…
x
Reference in New Issue
Block a user