mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2025-05-12 06:01:40 +00:00
Fix #121
This commit is contained in:
parent
4f237af813
commit
34651ef89b
14
README.md
14
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++
|
||||||
|
110
httplib.h
110
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 ¶ms);
|
const Params ¶ms);
|
||||||
|
|
||||||
|
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) {
|
if (valb > -6) { out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]); }
|
||||||
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());
|
auto len =
|
||||||
if (len < 0) {
|
strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str());
|
||||||
return len;
|
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) {
|
if (len < 0) { return len; }
|
||||||
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) {
|
if (len < 0) { return len; }
|
||||||
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>
|
inline std::pair<std::string, std::string>
|
||||||
make_basic_authentication_header(const std::string& username, const std::string& password) {
|
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) {
|
if (n <= 0) { return n; }
|
||||||
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)) {
|
if (!detail::write_headers(strm, res)) { return false; }
|
||||||
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())) {
|
if (!strm.write(res.body.c_str(), res.body.size())) { return false; }
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (res.content_producer) {
|
} else if (res.content_producer) {
|
||||||
if (!detail::write_content_chunked(strm, res)) {
|
if (!detail::write_content_chunked(strm, res)) { return false; }
|
||||||
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");
|
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) {
|
||||||
|
60
test/test.cc
60
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;
|
MultipartFormDataItems items = {
|
||||||
req.method = "POST";
|
{ "text1", "text default", "", "" },
|
||||||
req.path = "/multipart";
|
{ "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;
|
auto res = cli_.Post("/multipart", items);
|
||||||
host_and_port += HOST;
|
|
||||||
host_and_port += ":";
|
|
||||||
host_and_port += std::to_string(PORT);
|
|
||||||
|
|
||||||
req.headers.emplace("Host", host_and_port.c_str());
|
ASSERT_TRUE(res != nullptr);
|
||||||
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<Response>();
|
|
||||||
auto ret = cli_.send(req, *res);
|
|
||||||
|
|
||||||
ASSERT_TRUE(ret);
|
|
||||||
EXPECT_EQ(200, res->status);
|
EXPECT_EQ(200, res->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user