Przeglądaj źródła

Resume the multipart header search where it stopped

A part header arriving a few bytes at a time was searched for its CRLF
from the start on every read, which made parsing it quadratic. Remember
how far the search got.
yhirose 3 dni temu
rodzic
commit
d5851d7e89
2 zmienionych plików z 55 dodań i 4 usunięć
  1. 12 4
      httplib.h
  2. 43 0
      test/test.cc

+ 12 - 4
httplib.h

@@ -9465,11 +9465,14 @@ public:
       }
       case 1: { // New entry
         clear_file_info();
+        header_searched_ = 0;
         state_ = 2;
         break;
       }
       case 2: { // Headers
-        auto pos = buf_find(crlf_);
+        // Resume where the last call stopped looking, so that a line arriving
+        // a few bytes at a time is not searched from its start each time.
+        auto pos = buf_find(crlf_, header_searched_);
         if (pos > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
         while (pos < buf_size()) {
           // Empty line
@@ -9550,7 +9553,11 @@ public:
           buf_erase(pos + crlf_.size());
           pos = buf_find(crlf_);
         }
-        if (state_ != 3) { return true; }
+        if (state_ != 3) {
+          // The CR of the CRLF may be the last byte so far.
+          header_searched_ = buf_size() > 0 ? buf_size() - 1 : 0;
+          return true;
+        }
         break;
       }
       case 3: { // Body
@@ -9668,6 +9675,7 @@ private:
   bool is_valid_ = false;
   FormData file_;
   size_t header_count_ = 0;
+  size_t header_searched_ = 0;
 
   // Buffer
   bool start_with(const std::string &a, size_t spos, size_t epos,
@@ -9689,10 +9697,10 @@ private:
     return start_with(buf_, buf_spos_, buf_epos_, s);
   }
 
-  size_t buf_find(const std::string &s) const {
+  size_t buf_find(const std::string &s, size_t from = 0) const {
     auto c = s.front();
 
-    size_t off = buf_spos_;
+    size_t off = buf_spos_ + from;
     while (off < buf_epos_) {
       auto pos = off;
       while (true) {

+ 43 - 0
test/test.cc

@@ -1872,6 +1872,49 @@ TEST(ParseDispositionParamsTest, QuotedValues) {
   }
 }
 
+TEST(MultipartFormDataTest, PartArrivingAByteAtATime) {
+  Server svr;
+  std::string filename;
+  std::string content;
+  svr.Post("/upload", [&](const Request &req, Response &) {
+    const auto &file = req.form.get_file("file");
+    filename = file.filename;
+    content = file.content;
+  });
+
+  auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    thread.join();
+    ASSERT_FALSE(svr.is_running());
+  });
+  svr.wait_until_ready();
+
+  std::string body = "--B\r\n"
+                     "Content-Disposition: form-data; name=\"file\"; "
+                     "filename=\"a.txt\"\r\n"
+                     "Content-Type: text/plain\r\n"
+                     "\r\n"
+                     "hello\r\n"
+                     "--B--\r\n";
+  Client cli(HOST, PORT);
+  auto res = cli.Post(
+      "/upload",
+      [&](size_t offset, DataSink &sink) {
+        if (offset < body.size()) {
+          sink.write(&body[offset], 1);
+        } else {
+          sink.done();
+        }
+        return true;
+      },
+      "multipart/form-data; boundary=B");
+  ASSERT_TRUE(res) << "Error: " << to_string(res.error());
+  EXPECT_EQ(StatusCode::OK_200, res->status);
+  EXPECT_EQ("a.txt", filename);
+  EXPECT_EQ("hello", content);
+}
+
 TEST(ParseDispositionParamsTest, QuotedValuesSurviveTheRoundTrip) {
   // escape_multipart_field() only escapes '"', CR and LF, so a name or
   // filename holding '=' or ';' reaches the server's parameter parser as is.