diff --git a/README.md b/README.md index 77b587b..762f080 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,20 @@ httplib::Params 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 ```c++ diff --git a/httplib.h b/httplib.h index 81ef728..c6ea343 100644 --- a/httplib.h +++ b/httplib.h @@ -67,6 +67,7 @@ typedef int socket_t; #include #include #include +#include #include #include #include @@ -138,6 +139,14 @@ struct MultipartFile { }; typedef std::multimap MultipartFiles; +struct MultipartFormData { + std::string name; + std::string content; + std::string filename; + std::string content_type; +}; +typedef std::vector MultipartFormDataItems; + struct Request { std::string version; std::string method; @@ -340,6 +349,11 @@ public: std::shared_ptr Post(const char *path, const Headers &headers, const Params ¶ms); + std::shared_ptr Post(const char *path, + const MultipartFormDataItems &items); + std::shared_ptr Post(const char *path, const Headers &headers, + const MultipartFormDataItems &items); + std::shared_ptr Put(const char *path, const std::string &body, const char *content_type); std::shared_ptr 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) { out.push_back('='); @@ -1231,16 +1243,13 @@ bool read_content(Stream &strm, T &x, uint64_t payload_max_length, int &status, template inline int write_headers(Stream &strm, const T &info) { auto write_len = 0; 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; } auto len = strm.write("\r\n"); - if (len < 0) { - return len; - } + if (len < 0) { return len; } write_len += 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()); - if (len < 0) { - return len; - } + if (len < 0) { return len; } write_len += len; } return write_len; @@ -1444,6 +1451,22 @@ inline std::string to_lower(const char *beg, const char *end) { 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 &) {} template @@ -1486,9 +1509,9 @@ inline std::pair make_range_header(uint64_t value, return std::make_pair("Range", field); } - -inline std::pair -make_basic_authentication_header(const std::string& username, const std::string& password) { +inline std::pair +make_basic_authentication_header(const std::string &username, + const std::string &password) { auto field = "Basic " + detail::base64_encode(username + ":" + password); return std::make_pair("Authorization", field); } @@ -1583,9 +1606,7 @@ inline int Stream::write_format(const char *fmt, const Args &... args) { #else auto n = snprintf(buf, bufsiz - 1, fmt, args...); #endif - if (n <= 0) { - return n; - } + if (n <= 0) { return n; } if (n >= bufsiz - 1) { std::vector glowable_buf(bufsiz); @@ -1769,7 +1790,7 @@ inline bool Server::write_response(Stream &strm, bool last_connection, // Response line 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; } @@ -1811,20 +1832,14 @@ inline bool Server::write_response(Stream &strm, bool last_connection, 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 if (req.method != "HEAD") { 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) { - 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 ¶ms) { return Post(path, headers, query, "application/x-www-form-urlencoded"); } +inline std::shared_ptr +Client::Post(const char *path, const MultipartFormDataItems &items) { + return Post(path, Headers(), items); +} + +inline std::shared_ptr +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(); + + return send(req, *res) ? res : nullptr; +} + inline std::shared_ptr Client::Put(const char *path, const std::string &body, const char *content_type) { diff --git a/test/test.cc b/test/test.cc index 9526b1e..8b46fd0 100644 --- a/test/test.cc +++ b/test/test.cc @@ -264,9 +264,8 @@ TEST(CancelTest, NoCancel) { httplib::Client cli(host, port, sec); #endif - httplib::Headers headers; 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); EXPECT_EQ(res->body, "abcdefghijklmnopqrstuvwxyzabcdef"); EXPECT_EQ(200, res->status); @@ -284,9 +283,8 @@ TEST(CancelTest, WithCancelSmallPayload) { httplib::Client cli(host, port, sec); #endif - httplib::Headers headers; 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); } @@ -964,53 +962,17 @@ TEST_F(ServerTest, EndWithPercentCharacterInQuery) { } TEST_F(ServerTest, MultipartFormData) { - Request req; - req.method = "POST"; - req.path = "/multipart"; + 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" }, + }; - std::string host_and_port; - host_and_port += HOST; - host_and_port += ":"; - host_and_port += std::to_string(PORT); + auto res = cli_.Post("/multipart", items); - 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"; - - auto res = std::make_shared(); - auto ret = cli_.send(req, *res); - - ASSERT_TRUE(ret); + ASSERT_TRUE(res != nullptr); EXPECT_EQ(200, res->status); }