diff --git a/example/simplesvr.cc b/example/simplesvr.cc
index 1c9d5a2..bcbe14d 100644
--- a/example/simplesvr.cc
+++ b/example/simplesvr.cc
@@ -28,6 +28,38 @@ string dump_headers(const MultiMap& headers)
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 s;
@@ -49,6 +81,7 @@ string log(const Request& req, const Response& res)
s += buf;
s += dump_headers(req.headers);
+ s += dump_multipart_files(req.files);
s += "--------------------------------\n";
@@ -72,7 +105,15 @@ int main(int argc, const char** argv)
Server svr;
#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 = "
Error Status: %d
";
char buf[BUFSIZ];
snprintf(buf, sizeof(buf), fmt, res.status);
@@ -83,7 +124,7 @@ int main(int argc, const char** argv)
cout << log(req, res);
});
- auto port = 80;
+ auto port = 8080;
if (argc > 1) {
port = atoi(argv[1]);
}
diff --git a/httplib.h b/httplib.h
index 4513f1e..ab22db1 100644
--- a/httplib.h
+++ b/httplib.h
@@ -74,20 +74,32 @@ typedef std::multimap MultiMap;
typedef std::smatch Match;
typedef std::function Progress;
+struct MultipartFile {
+ std::string filename;
+ std::string content_type;
+ size_t offset = 0;
+ size_t length = 0;
+};
+typedef std::multimap MultipartFiles;
+
struct Request {
- std::string method;
- std::string path;
- MultiMap headers;
- std::string body;
- Map params;
- Match matches;
- Progress progress;
+ std::string method;
+ std::string path;
+ MultiMap headers;
+ std::string body;
+ Map params;
+ MultipartFiles files;
+ Match matches;
+ Progress progress;
bool has_header(const char* key) const;
std::string get_header_value(const char* key) const;
void set_header(const char* key, const char* val);
bool has_param(const char* key) const;
+
+ bool has_file(const char* key) const;
+ MultipartFile get_file_value(const char* key) const;
};
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
class WSInit {
public:
@@ -899,6 +1006,20 @@ inline bool Request::has_param(const char* key) const
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
inline bool Response::has_header(const char* key) const
{
@@ -1148,9 +1269,18 @@ inline void Server::process_request(Stream& strm)
return;
}
- static std::string type = "application/x-www-form-urlencoded";
- if (!req.get_header_value("Content-Type").compare(0, type.size(), type)) {
+ const auto& content_type = req.get_header_value("Content-Type");
+
+ if (!content_type.find("application/x-www-form-urlencoded")) {
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;
+ }
}
}
diff --git a/test/Makefile b/test/Makefile
index 9a78dda..d802397 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -1,6 +1,6 @@
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
all : test
diff --git a/test/test.cc b/test/test.cc
index b3a2d47..b38d08d 100644
--- a/test/test.cc
+++ b/test/test.cc
@@ -173,6 +173,36 @@ protected:
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*/) {
svr_.stop();
});
@@ -458,6 +488,31 @@ TEST_F(ServerTest, InvalidPercentEncodingUnicode)
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();
+ auto ret = cli_.send(req, *res);
+
+ ASSERT_TRUE(ret);
+ EXPECT_EQ(200, res->status);
+}
+
class ServerTestWithAI_PASSIVE : public ::testing::Test {
protected:
ServerTestWithAI_PASSIVE()