yhirose 6 years ago
parent
commit
34651ef89b
3 changed files with 108 additions and 78 deletions
  1. 14 0
      README.md
  2. 83 29
      httplib.h
  3. 11 49
      test/test.cc

+ 14 - 0
README.md

@@ -150,6 +150,20 @@ httplib::Params params{
 auto res = cli.Post("/post", params);
 auto res = cli.Post("/post", params);
 ```
 ```
 
 
+### POST with Multipart Form Data
+
+```c++
+  httplib::MultipartFormDataItems items = {
+    { "text1", "text default", "", "" },
+    { "text2", "aωb", "", "" },
+    { "file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain" },
+    { "file2", "{\n  \"world\", true\n}\n", "world.json", "application/json" },
+    { "file3", "", "", "application/octet-stream" },
+  };
+
+  auto res = cli.Post("/multipart", items);
+```
+
 ### PUT
 ### PUT
 
 
 ```c++
 ```c++

+ 83 - 29
httplib.h

@@ -67,6 +67,7 @@ typedef int socket_t;
 #include <map>
 #include <map>
 #include <memory>
 #include <memory>
 #include <mutex>
 #include <mutex>
+#include <random>
 #include <regex>
 #include <regex>
 #include <string>
 #include <string>
 #include <sys/stat.h>
 #include <sys/stat.h>
@@ -138,6 +139,14 @@ struct MultipartFile {
 };
 };
 typedef std::multimap<std::string, MultipartFile> MultipartFiles;
 typedef std::multimap<std::string, MultipartFile> MultipartFiles;
 
 
+struct MultipartFormData {
+  std::string name;
+  std::string content;
+  std::string filename;
+  std::string content_type;
+};
+typedef std::vector<MultipartFormData> MultipartFormDataItems;
+
 struct Request {
 struct Request {
   std::string version;
   std::string version;
   std::string method;
   std::string method;
@@ -340,6 +349,11 @@ public:
   std::shared_ptr<Response> Post(const char *path, const Headers &headers,
   std::shared_ptr<Response> Post(const char *path, const Headers &headers,
                                  const Params &params);
                                  const Params &params);
 
 
+  std::shared_ptr<Response> Post(const char *path,
+                                 const MultipartFormDataItems &items);
+  std::shared_ptr<Response> Post(const char *path, const Headers &headers,
+                                 const MultipartFormDataItems &items);
+
   std::shared_ptr<Response> Put(const char *path, const std::string &body,
   std::shared_ptr<Response> Put(const char *path, const std::string &body,
                                 const char *content_type);
                                 const char *content_type);
   std::shared_ptr<Response> Put(const char *path, const Headers &headers,
   std::shared_ptr<Response> Put(const char *path, const Headers &headers,
@@ -551,9 +565,7 @@ inline std::string base64_encode(const std::string &in) {
     }
     }
   }
   }
 
 
-  if (valb > -6) {
-    out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]);
-  }
+  if (valb > -6) { out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]); }
 
 
   while (out.size() % 4) {
   while (out.size() % 4) {
     out.push_back('=');
     out.push_back('=');
@@ -1231,16 +1243,13 @@ bool read_content(Stream &strm, T &x, uint64_t payload_max_length, int &status,
 template <typename T> inline int write_headers(Stream &strm, const T &info) {
 template <typename T> inline int write_headers(Stream &strm, const T &info) {
   auto write_len = 0;
   auto write_len = 0;
   for (const auto &x : info.headers) {
   for (const auto &x : info.headers) {
-    auto len = strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str());
-    if (len < 0) {
-      return len;
-    }
+    auto len =
+        strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str());
+    if (len < 0) { return len; }
     write_len += len;
     write_len += len;
   }
   }
   auto len = strm.write("\r\n");
   auto len = strm.write("\r\n");
-  if (len < 0) {
-    return len;
-  }
+  if (len < 0) { return len; }
   write_len += len;
   write_len += len;
   return write_len;
   return write_len;
 }
 }
@@ -1262,9 +1271,7 @@ inline int write_content_chunked(Stream &strm, const T &x) {
     }
     }
 
 
     auto len = strm.write(chunk.c_str(), chunk.size());
     auto len = strm.write(chunk.c_str(), chunk.size());
-    if (len < 0) {
-      return len;
-    }
+    if (len < 0) { return len; }
     write_len += len;
     write_len += len;
   }
   }
   return write_len;
   return write_len;
@@ -1444,6 +1451,22 @@ inline std::string to_lower(const char *beg, const char *end) {
   return out;
   return out;
 }
 }
 
 
+inline std::string make_multipart_data_boundary() {
+  static const char data[] =
+      "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+
+  std::random_device seed_gen;
+  std::mt19937 engine(seed_gen());
+
+  std::string result = "--cpp-httplib-form-data-";
+
+  for (auto i = 0; i < 16; i++) {
+    result += data[engine() % (sizeof(data) - 1)];
+  }
+
+  return result;
+}
+
 inline void make_range_header_core(std::string &) {}
 inline void make_range_header_core(std::string &) {}
 
 
 template <typename uint64_t>
 template <typename uint64_t>
@@ -1486,9 +1509,9 @@ inline std::pair<std::string, std::string> make_range_header(uint64_t value,
   return std::make_pair("Range", field);
   return std::make_pair("Range", field);
 }
 }
 
 
-
-inline std::pair<std::string, std::string> 
-make_basic_authentication_header(const std::string& username, const std::string& password) {
+inline std::pair<std::string, std::string>
+make_basic_authentication_header(const std::string &username,
+                                 const std::string &password) {
   auto field = "Basic " + detail::base64_encode(username + ":" + password);
   auto field = "Basic " + detail::base64_encode(username + ":" + password);
   return std::make_pair("Authorization", field);
   return std::make_pair("Authorization", field);
 }
 }
@@ -1583,9 +1606,7 @@ inline int Stream::write_format(const char *fmt, const Args &... args) {
 #else
 #else
   auto n = snprintf(buf, bufsiz - 1, fmt, args...);
   auto n = snprintf(buf, bufsiz - 1, fmt, args...);
 #endif
 #endif
-  if (n <= 0) {
-    return n;
-  }
+  if (n <= 0) { return n; }
 
 
   if (n >= bufsiz - 1) {
   if (n >= bufsiz - 1) {
     std::vector<char> glowable_buf(bufsiz);
     std::vector<char> glowable_buf(bufsiz);
@@ -1769,7 +1790,7 @@ inline bool Server::write_response(Stream &strm, bool last_connection,
 
 
   // Response line
   // Response line
   if (!strm.write_format("HTTP/1.1 %d %s\r\n", res.status,
   if (!strm.write_format("HTTP/1.1 %d %s\r\n", res.status,
-                    detail::status_message(res.status))) {
+                         detail::status_message(res.status))) {
     return false;
     return false;
   }
   }
 
 
@@ -1811,20 +1832,14 @@ inline bool Server::write_response(Stream &strm, bool last_connection,
     res.set_header("Content-Length", length.c_str());
     res.set_header("Content-Length", length.c_str());
   }
   }
 
 
-  if (!detail::write_headers(strm, res)) {
-    return false;
-  }
+  if (!detail::write_headers(strm, res)) { return false; }
 
 
   // Body
   // Body
   if (req.method != "HEAD") {
   if (req.method != "HEAD") {
     if (!res.body.empty()) {
     if (!res.body.empty()) {
-      if (!strm.write(res.body.c_str(), res.body.size())) {
-        return false;
-      }
+      if (!strm.write(res.body.c_str(), res.body.size())) { return false; }
     } else if (res.content_producer) {
     } else if (res.content_producer) {
-      if (!detail::write_content_chunked(strm, res)) {
-        return false;
-      }
+      if (!detail::write_content_chunked(strm, res)) { return false; }
     }
     }
   }
   }
 
 
@@ -2325,6 +2340,45 @@ Client::Post(const char *path, const Headers &headers, const Params &params) {
   return Post(path, headers, query, "application/x-www-form-urlencoded");
   return Post(path, headers, query, "application/x-www-form-urlencoded");
 }
 }
 
 
+inline std::shared_ptr<Response>
+Client::Post(const char *path, const MultipartFormDataItems &items) {
+  return Post(path, Headers(), items);
+}
+
+inline std::shared_ptr<Response>
+Client::Post(const char *path, const Headers &headers,
+             const MultipartFormDataItems &items) {
+  Request req;
+  req.method = "POST";
+  req.headers = headers;
+  req.path = path;
+
+  auto boundary = detail::make_multipart_data_boundary();
+
+  req.headers.emplace("Content-Type",
+                      "multipart/form-data; boundary=" + boundary);
+
+  for (const auto &item : items) {
+    req.body += "--" + boundary + "\r\n";
+    req.body += "Content-Disposition: form-data; name=\"" + item.name + "\"";
+    if (!item.filename.empty()) {
+      req.body += "; filename=\"" + item.filename + "\"";
+    }
+    req.body += "\r\n";
+    if (!item.content_type.empty()) {
+      req.body += "Content-Type: " + item.content_type + "\r\n";
+    }
+    req.body += "\r\n";
+    req.body += item.content + "\r\n";
+  }
+
+  req.body += "--" + boundary + "--\r\n";
+
+  auto res = std::make_shared<Response>();
+
+  return send(req, *res) ? res : nullptr;
+}
+
 inline std::shared_ptr<Response> Client::Put(const char *path,
 inline std::shared_ptr<Response> Client::Put(const char *path,
                                              const std::string &body,
                                              const std::string &body,
                                              const char *content_type) {
                                              const char *content_type) {

+ 11 - 49
test/test.cc

@@ -264,9 +264,8 @@ TEST(CancelTest, NoCancel) {
   httplib::Client cli(host, port, sec);
   httplib::Client cli(host, port, sec);
 #endif
 #endif
 
 
-  httplib::Headers headers;
   auto res =
   auto res =
-      cli.Get("/range/32", headers, [](uint64_t, uint64_t) { return true; });
+      cli.Get("/range/32", [](uint64_t, uint64_t) { return true; });
   ASSERT_TRUE(res != nullptr);
   ASSERT_TRUE(res != nullptr);
   EXPECT_EQ(res->body, "abcdefghijklmnopqrstuvwxyzabcdef");
   EXPECT_EQ(res->body, "abcdefghijklmnopqrstuvwxyzabcdef");
   EXPECT_EQ(200, res->status);
   EXPECT_EQ(200, res->status);
@@ -284,9 +283,8 @@ TEST(CancelTest, WithCancelSmallPayload) {
   httplib::Client cli(host, port, sec);
   httplib::Client cli(host, port, sec);
 #endif
 #endif
 
 
-  httplib::Headers headers;
   auto res =
   auto res =
-      cli.Get("/range/32", headers, [](uint64_t, uint64_t) { return false; });
+      cli.Get("/range/32", [](uint64_t, uint64_t) { return false; });
   ASSERT_TRUE(res == nullptr);
   ASSERT_TRUE(res == nullptr);
 }
 }
 
 
@@ -964,53 +962,17 @@ TEST_F(ServerTest, EndWithPercentCharacterInQuery) {
 }
 }
 
 
 TEST_F(ServerTest, MultipartFormData) {
 TEST_F(ServerTest, MultipartFormData) {
-  Request req;
-  req.method = "POST";
-  req.path = "/multipart";
-
-  std::string host_and_port;
-  host_and_port += HOST;
-  host_and_port += ":";
-  host_and_port += std::to_string(PORT);
-
-  req.headers.emplace("Host", host_and_port.c_str());
-  req.headers.emplace("Accept", "*/*");
-  req.headers.emplace("User-Agent", "cpp-httplib/0.1");
-
-  req.headers.emplace(
-      "Content-Type",
-      "multipart/form-data; boundary=----WebKitFormBoundarysBREP3G013oUrLB4");
-
-  req.body =
-      "------WebKitFormBoundarysBREP3G013oUrLB4\r\n"
-      "Content-Disposition: form-data; name=\"text1\"\r\n"
-      "\r\n"
-      "text default\r\n"
-      "------WebKitFormBoundarysBREP3G013oUrLB4\r\n"
-      "Content-Disposition: form-data; name=\"text2\"\r\n"
-      "\r\n"
-      "aωb\r\n"
-      "------WebKitFormBoundarysBREP3G013oUrLB4\r\n"
-      "Content-Disposition: form-data; name=\"file1\"; filename=\"hello.txt\"\r\n"
-      "Content-Type: text/plain\r\n"
-      "\r\n"
-      "h\ne\n\nl\nl\no\n\r\n"
-      "------WebKitFormBoundarysBREP3G013oUrLB4\r\n"
-      "Content-Disposition: form-data; name=\"file2\"; filename=\"world.json\"\r\n"
-      "Content-Type: application/json\r\n"
-      "\r\n"
-      "{\n  \"world\", true\n}\n\r\n"
-      "------WebKitFormBoundarysBREP3G013oUrLB4\r\n"
-      "content-disposition: form-data; name=\"file3\"; filename=\"\"\r\n"
-      "content-type: application/octet-stream\r\n"
-      "\r\n"
-      "\r\n"
-      "------WebKitFormBoundarysBREP3G013oUrLB4--\r\n";
+  MultipartFormDataItems items = {
+    { "text1", "text default", "", "" },
+    { "text2", "aωb", "", "" },
+    { "file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain" },
+    { "file2", "{\n  \"world\", true\n}\n", "world.json", "application/json" },
+    { "file3", "", "", "application/octet-stream" },
+  };
 
 
-  auto res = std::make_shared<Response>();
-  auto ret = cli_.send(req, *res);
+  auto res = cli_.Post("/multipart", items);
 
 
-  ASSERT_TRUE(ret);
+  ASSERT_TRUE(res != nullptr);
   EXPECT_EQ(200, res->status);
   EXPECT_EQ(200, res->status);
 }
 }