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()