Fixed #21
This commit is contained in:
parent
ea9c8ee46b
commit
bb8a1df7a3
@ -28,6 +28,38 @@ string dump_headers(const MultiMap& headers)
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string dump_multipart_files(const MultipartFiles& files)
|
||||||
|
{
|
||||||
|
string s;
|
||||||
|
char buf[BUFSIZ];
|
||||||
|
|
||||||
|
s += "--------------------------------\n";
|
||||||
|
|
||||||
|
for (const auto& x: files) {
|
||||||
|
const auto& name = x.first;
|
||||||
|
const auto& file = x.second;
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "name: %s\n", name.c_str());
|
||||||
|
s += buf;
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "filename: %s\n", file.filename.c_str());
|
||||||
|
s += buf;
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "content type: %s\n", file.content_type.c_str());
|
||||||
|
s += buf;
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "text offset: %lu\n", file.offset);
|
||||||
|
s += buf;
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "text length: %lu\n", file.length);
|
||||||
|
s += buf;
|
||||||
|
|
||||||
|
s += "----------------\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
string log(const Request& req, const Response& res)
|
string log(const Request& req, const Response& res)
|
||||||
{
|
{
|
||||||
string s;
|
string s;
|
||||||
@ -49,6 +81,7 @@ string log(const Request& req, const Response& res)
|
|||||||
s += buf;
|
s += buf;
|
||||||
|
|
||||||
s += dump_headers(req.headers);
|
s += dump_headers(req.headers);
|
||||||
|
s += dump_multipart_files(req.files);
|
||||||
|
|
||||||
s += "--------------------------------\n";
|
s += "--------------------------------\n";
|
||||||
|
|
||||||
@ -72,7 +105,15 @@ int main(int argc, const char** argv)
|
|||||||
Server svr;
|
Server svr;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
svr.set_error_handler([](const auto& req, auto& res) {
|
svr.post("/multipart", [](const auto& req, auto& res) {
|
||||||
|
auto body =
|
||||||
|
dump_headers(req.headers) +
|
||||||
|
dump_multipart_files(req.files);
|
||||||
|
|
||||||
|
res.set_content(body, "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.set_error_handler([](const auto& /*req*/, auto& res) {
|
||||||
const char* fmt = "<p>Error Status: <span style='color:red;'>%d</span></p>";
|
const char* fmt = "<p>Error Status: <span style='color:red;'>%d</span></p>";
|
||||||
char buf[BUFSIZ];
|
char buf[BUFSIZ];
|
||||||
snprintf(buf, sizeof(buf), fmt, res.status);
|
snprintf(buf, sizeof(buf), fmt, res.status);
|
||||||
@ -83,7 +124,7 @@ int main(int argc, const char** argv)
|
|||||||
cout << log(req, res);
|
cout << log(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
auto port = 80;
|
auto port = 8080;
|
||||||
if (argc > 1) {
|
if (argc > 1) {
|
||||||
port = atoi(argv[1]);
|
port = atoi(argv[1]);
|
||||||
}
|
}
|
||||||
|
148
httplib.h
148
httplib.h
@ -74,20 +74,32 @@ typedef std::multimap<std::string, std::string> MultiMap;
|
|||||||
typedef std::smatch Match;
|
typedef std::smatch Match;
|
||||||
typedef std::function<void (int64_t current, int64_t total)> Progress;
|
typedef std::function<void (int64_t current, int64_t total)> Progress;
|
||||||
|
|
||||||
|
struct MultipartFile {
|
||||||
|
std::string filename;
|
||||||
|
std::string content_type;
|
||||||
|
size_t offset = 0;
|
||||||
|
size_t length = 0;
|
||||||
|
};
|
||||||
|
typedef std::multimap<std::string, MultipartFile> MultipartFiles;
|
||||||
|
|
||||||
struct Request {
|
struct Request {
|
||||||
std::string method;
|
std::string method;
|
||||||
std::string path;
|
std::string path;
|
||||||
MultiMap headers;
|
MultiMap headers;
|
||||||
std::string body;
|
std::string body;
|
||||||
Map params;
|
Map params;
|
||||||
Match matches;
|
MultipartFiles files;
|
||||||
Progress progress;
|
Match matches;
|
||||||
|
Progress progress;
|
||||||
|
|
||||||
bool has_header(const char* key) const;
|
bool has_header(const char* key) const;
|
||||||
std::string get_header_value(const char* key) const;
|
std::string get_header_value(const char* key) const;
|
||||||
void set_header(const char* key, const char* val);
|
void set_header(const char* key, const char* val);
|
||||||
|
|
||||||
bool has_param(const char* key) const;
|
bool has_param(const char* key) const;
|
||||||
|
|
||||||
|
bool has_file(const char* key) const;
|
||||||
|
MultipartFile get_file_value(const char* key) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Response {
|
struct Response {
|
||||||
@ -860,6 +872,101 @@ inline void parse_query_text(const std::string& s, Map& params)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool parse_multipart_boundary(const std::string& content_type, std::string& boundary)
|
||||||
|
{
|
||||||
|
auto pos = content_type.find("boundary=");
|
||||||
|
if (pos == std::string::npos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boundary = content_type.substr(pos + 9);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool parse_multipart_formdata(
|
||||||
|
const std::string& boundary, const std::string& body, MultipartFiles& files)
|
||||||
|
{
|
||||||
|
static std::string dash = "--";
|
||||||
|
static std::string crlf = "\r\n";
|
||||||
|
|
||||||
|
static std::regex re_content_type(
|
||||||
|
"Content-Type: (.*?)");
|
||||||
|
|
||||||
|
static std::regex re_content_disposition(
|
||||||
|
"Content-Disposition: form-data; name=\"(.*?)\"(?:; filename=\"(.*?)\")?");
|
||||||
|
|
||||||
|
auto dash_boundary = dash + boundary;
|
||||||
|
|
||||||
|
auto pos = body.find(dash_boundary);
|
||||||
|
if (pos != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos += dash_boundary.size();
|
||||||
|
|
||||||
|
auto next_pos = body.find(crlf, pos);
|
||||||
|
if (next_pos == std::string::npos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = next_pos + crlf.size();
|
||||||
|
|
||||||
|
while (pos < body.size()) {
|
||||||
|
next_pos = body.find(crlf, pos);
|
||||||
|
if (next_pos == std::string::npos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
MultipartFile file;
|
||||||
|
|
||||||
|
auto header = body.substr(pos, (next_pos - pos));
|
||||||
|
|
||||||
|
while (pos != next_pos) {
|
||||||
|
std::smatch m;
|
||||||
|
if (std::regex_match(header, m, re_content_type)) {
|
||||||
|
file.content_type = m[1];
|
||||||
|
} else if (std::regex_match(header, m, re_content_disposition)) {
|
||||||
|
name = m[1];
|
||||||
|
file.filename = m[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = next_pos + crlf.size();
|
||||||
|
|
||||||
|
next_pos = body.find(crlf, pos);
|
||||||
|
if (next_pos == std::string::npos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
header = body.substr(pos, (next_pos - pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = next_pos + crlf.size();
|
||||||
|
|
||||||
|
next_pos = body.find(crlf + dash_boundary, pos);
|
||||||
|
|
||||||
|
if (next_pos == std::string::npos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.offset = pos;
|
||||||
|
file.length = next_pos - pos;
|
||||||
|
|
||||||
|
pos = next_pos + crlf.size() + dash_boundary.size();
|
||||||
|
|
||||||
|
next_pos = body.find(crlf, pos);
|
||||||
|
if (next_pos == std::string::npos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
files.insert(std::make_pair(name, file));
|
||||||
|
|
||||||
|
pos = next_pos + crlf.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef _MSC_VER
|
#ifdef _MSC_VER
|
||||||
class WSInit {
|
class WSInit {
|
||||||
public:
|
public:
|
||||||
@ -899,6 +1006,20 @@ inline bool Request::has_param(const char* key) const
|
|||||||
return params.find(key) != params.end();
|
return params.find(key) != params.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool Request::has_file(const char* key) const
|
||||||
|
{
|
||||||
|
return files.find(key) != files.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline MultipartFile Request::get_file_value(const char* key) const
|
||||||
|
{
|
||||||
|
auto it = files.find(key);
|
||||||
|
if (it != files.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
return MultipartFile();
|
||||||
|
}
|
||||||
|
|
||||||
// Response implementation
|
// Response implementation
|
||||||
inline bool Response::has_header(const char* key) const
|
inline bool Response::has_header(const char* key) const
|
||||||
{
|
{
|
||||||
@ -1148,9 +1269,18 @@ inline void Server::process_request(Stream& strm)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::string type = "application/x-www-form-urlencoded";
|
const auto& content_type = req.get_header_value("Content-Type");
|
||||||
if (!req.get_header_value("Content-Type").compare(0, type.size(), type)) {
|
|
||||||
|
if (!content_type.find("application/x-www-form-urlencoded")) {
|
||||||
detail::parse_query_text(req.body, req.params);
|
detail::parse_query_text(req.body, req.params);
|
||||||
|
} else if(!content_type.find("multipart/form-data")) {
|
||||||
|
std::string boundary;
|
||||||
|
if (!detail::parse_multipart_boundary(content_type, boundary) ||
|
||||||
|
!detail::parse_multipart_formdata(boundary, req.body, req.files)) {
|
||||||
|
res.status = 400;
|
||||||
|
write_response(strm, req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
CC = clang++
|
CC = clang++
|
||||||
CFLAGS = -std=c++14 -DGTEST_USE_OWN_TR1_TUPLE -I.. -I. -Wall -Wextra
|
CFLAGS = -std=c++11 -DGTEST_USE_OWN_TR1_TUPLE -I.. -I. -Wall -Wextra
|
||||||
#OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib -lssl -lcrypto
|
#OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib -lssl -lcrypto
|
||||||
|
|
||||||
all : test
|
all : test
|
||||||
|
55
test/test.cc
55
test/test.cc
@ -173,6 +173,36 @@ protected:
|
|||||||
res.status = 404;
|
res.status = 404;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.post("/multipart", [&](const Request& req, Response& /*res*/) {
|
||||||
|
EXPECT_EQ(5u, req.files.size());
|
||||||
|
ASSERT_TRUE(!req.has_file("???"));
|
||||||
|
|
||||||
|
{
|
||||||
|
const auto& file = req.get_file_value("text1");
|
||||||
|
EXPECT_EQ("", file.filename);
|
||||||
|
EXPECT_EQ("text default", req.body.substr(file.offset, file.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const auto& file = req.get_file_value("text2");
|
||||||
|
EXPECT_EQ("", file.filename);
|
||||||
|
EXPECT_EQ("aωb", req.body.substr(file.offset, file.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const auto& file = req.get_file_value("file1");
|
||||||
|
EXPECT_EQ("hello.txt", file.filename);
|
||||||
|
EXPECT_EQ("text/plain", file.content_type);
|
||||||
|
EXPECT_EQ("h\ne\n\nl\nl\no\n", req.body.substr(file.offset, file.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const auto& file = req.get_file_value("file3");
|
||||||
|
EXPECT_EQ("", file.filename);
|
||||||
|
EXPECT_EQ("application/octet-stream", file.content_type);
|
||||||
|
EXPECT_EQ(0u, file.length);
|
||||||
|
}
|
||||||
|
})
|
||||||
.get("/stop", [&](const Request& /*req*/, Response& /*res*/) {
|
.get("/stop", [&](const Request& /*req*/, Response& /*res*/) {
|
||||||
svr_.stop();
|
svr_.stop();
|
||||||
});
|
});
|
||||||
@ -458,6 +488,31 @@ TEST_F(ServerTest, InvalidPercentEncodingUnicode)
|
|||||||
EXPECT_EQ(404, res->status);
|
EXPECT_EQ(404, res->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.set_header("Host", host_and_port.c_str());
|
||||||
|
req.set_header("Accept", "*/*");
|
||||||
|
req.set_header("User-Agent", "cpp-httplib/0.1");
|
||||||
|
req.set_header("Content-Type", "multipart/form-data; boundary=----WebKitFormBoundarysBREP3G013oUrLB4");
|
||||||
|
|
||||||
|
req.body = "------WebKitFormBoundarysBREP3G013oUrLB4\r\nContent-Disposition: form-data; name=\"text1\"\r\n\r\ntext default\r\n------WebKitFormBoundarysBREP3G013oUrLB4\r\nContent-Disposition: form-data; name=\"text2\"\r\n\r\naωb\r\n------WebKitFormBoundarysBREP3G013oUrLB4\r\nContent-Disposition: form-data; name=\"file1\"; filename=\"hello.txt\"\r\nContent-Type: text/plain\r\n\r\nh\ne\n\nl\nl\no\n\r\n------WebKitFormBoundarysBREP3G013oUrLB4\r\nContent-Disposition: form-data; name=\"file2\"; filename=\"world.json\"\r\nContent-Type: application/json\r\n\r\n{\n \"world\", true\n}\n\r\n------WebKitFormBoundarysBREP3G013oUrLB4\r\nContent-Disposition: form-data; name=\"file3\"; filename=\"\"\r\nContent-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);
|
||||||
|
}
|
||||||
|
|
||||||
class ServerTestWithAI_PASSIVE : public ::testing::Test {
|
class ServerTestWithAI_PASSIVE : public ::testing::Test {
|
||||||
protected:
|
protected:
|
||||||
ServerTestWithAI_PASSIVE()
|
ServerTestWithAI_PASSIVE()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user