Browse Source

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.
Gopinath K 3 years ago
parent
commit
656e936f49
2 changed files with 333 additions and 25 deletions
  1. 98 25
      httplib.h
  2. 235 0
      test/test.cc

+ 98 - 25
httplib.h

@@ -949,6 +949,11 @@ public:
   Result Put(const std::string &path, const Params &params);
   Result Put(const std::string &path, const Params &params);
   Result Put(const std::string &path, const Headers &headers,
   Result Put(const std::string &path, const Headers &headers,
              const Params &params);
              const Params &params);
+  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 &params);
   Result Put(const std::string &path, const Params &params);
   Result Put(const std::string &path, const Headers &headers,
   Result Put(const std::string &path, const Headers &headers,
              const Params &params);
              const Params &params);
+  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) {
-  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};
       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());
   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 &params) {
                           const Params &params) {
   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 - 0
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