yhirose 8 years ago
parent
commit
bb8a1df7a3
4 changed files with 238 additions and 12 deletions
  1. 43 2
      example/simplesvr.cc
  2. 139 9
      httplib.h
  3. 1 1
      test/Makefile
  4. 55 0
      test/test.cc

+ 43 - 2
example/simplesvr.cc

@@ -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]);
     }
     }

+ 139 - 9
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 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;
     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";
-        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);
             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 - 1
test/Makefile

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