LCOV - code coverage report
Current view: top level - http/server - multipart.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 80.6 % 144 116
Test Date: 2026-11-10 10:38:07 Functions: 88.9 % 27 24

            Line data    Source code
       1              : #include "multipart.hpp"
       2              : #include "request.hpp"
       3              : #include "body_decoders.hpp"
       4              : #include "../../util/ascii.hpp"
       5              : #include "../../util/logger.hpp"
       6              : #include <algorithm>
       7              : #include <cerrno>
       8              : #include <cstring>
       9              : #include <cstdlib>
      10              : #include <fcntl.h>
      11              : #include <unistd.h>
      12              : 
      13              : namespace thinger::http {
      14              : 
      15              :     // --- multipart_reader ---
      16              : 
      17          123 :     multipart_reader::multipart_reader(request& req, multipart_limits limits, size_t buffer_size)
      18          123 :         : req_(req), buffer_size_(std::max<size_t>(buffer_size, 1)) {
      19          123 :         auto http_request = req.get_http_request();
      20          123 :         if (!http_request) {
      21            0 :             fail(multipart_error::not_multipart);
      22            0 :             return;
      23              :         }
      24              : 
      25              :         // A single Content-Type of a multipart type
      26          123 :         const std::string* content_type = nullptr;
      27          627 :         for (const auto& [key, value] : http_request->get_headers()) {
      28          507 :             if (!headers::is_header(key, header::content_type)) continue;
      29          126 :             if (content_type && value != *content_type) {
      30            3 :                 fail(multipart_error::not_multipart);
      31            3 :                 return;
      32              :             }
      33          123 :             content_type = &value;
      34              :         }
      35          120 :         auto type = content_type ? media_type::parse(*content_type) : std::nullopt;
      36          120 :         if (!type || !type->type.starts_with("multipart/")) {
      37            6 :             fail(multipart_error::not_multipart);
      38            6 :             return;
      39              :         }
      40          114 :         media_type_ = type->type;
      41              : 
      42              :         // A compressed body would have to be decoded first (the server decodes the bodies it
      43              :         // reads, removing their Content-Encoding)
      44          120 :         if (http_request->has_header("Content-Encoding") &&
      45          120 :             !::thinger::util::ascii::iequals(http_request->get_header("Content-Encoding"), "identity")) {
      46            3 :             fail(multipart_error::not_multipart);
      47            3 :             return;
      48              :         }
      49              : 
      50              :         // Exactly one boundary: a repeated one is ambiguous
      51          111 :         std::string_view boundary;
      52          111 :         size_t boundaries = 0;
      53          222 :         for (const auto& [name, value] : type->parameters) {
      54          111 :             if (name != "boundary") continue;
      55          111 :             boundary = value;
      56          111 :             ++boundaries;
      57              :         }
      58          111 :         if (boundaries != 1) {
      59            6 :             fail(multipart_error::invalid_boundary);
      60            6 :             return;
      61              :         }
      62          105 :         parser_.emplace(boundary, limits);
      63          105 :         if (parser_->failed()) {
      64            9 :             fail(parser_->error());
      65            9 :             return;
      66              :         }
      67              : 
      68              :         // Body already read by the server (routes that are not deferred), or still to read
      69           96 :         if (req.get_body_error() != body_error::none) {
      70            0 :             fail(req.get_body_error() == body_error::too_large ? multipart_error::body_too_large
      71              :                                                                : multipart_error::invalid_body);
      72            0 :             return;
      73              :         }
      74           96 :         in_memory_ = !req.has_pending_body();
      75           96 :         if (!in_memory_) buffer_ = std::make_unique<uint8_t[]>(buffer_size_);
      76          147 :     }
      77              : 
      78           66 :     void multipart_reader::fail(multipart_error error) {
      79           66 :         if (error_ == multipart_error::none) error_ = error;
      80           66 :         in_part_ = false;
      81           66 :     }
      82              : 
      83        24734 :     thinger::awaitable<bool> multipart_reader::fill() {
      84              :         if (in_memory_) {
      85              :             input_ = req_.get_http_request()->get_body();
      86              :             finished_ = true;
      87              :             co_return true;
      88              :         }
      89              :         size_t bytes = co_await req_.read_some(buffer_.get(), buffer_size_);
      90              :         if (bytes > 0) {
      91              :             input_ = std::string_view(reinterpret_cast<const char*>(buffer_.get()), bytes);
      92              :             co_return true;
      93              :         }
      94              :         auto error = req_.get_body_error();
      95              :         if (error == body_error::too_large) {
      96              :             fail(multipart_error::body_too_large);
      97              :         } else if (error != body_error::none) {
      98              :             fail(multipart_error::invalid_body);
      99              :         } else if (req_.has_pending_body()) {
     100              :             co_return false;    // cancelled: the body is still there
     101              :         } else {
     102              :             finished_ = true;
     103              :         }
     104              :         co_return true;
     105        49468 :     }
     106              : 
     107        25172 :     thinger::awaitable<multipart_parser::event> multipart_reader::advance() {
     108              :         using event = multipart_parser::event;
     109              :         for (;;) {
     110              :             if (error_ != multipart_error::none) co_return event::error;
     111              :             auto result = parser_->parse(input_);
     112              :             if (result == event::error) {
     113              :                 fail(parser_->error());
     114              :                 co_return result;
     115              :             }
     116              :             // The end once the epilogue is read too, so the whole body is read (and checked)
     117              :             if (result != event::need_more && (result != event::end || finished_)) co_return result;
     118              :             if (finished_) {
     119              :                 result = parser_->finish();
     120              :                 if (result == event::error) fail(parser_->error());
     121              :                 co_return result;
     122              :             }
     123              :             if (!co_await fill()) co_return event::need_more;
     124              :         }
     125        50344 :     }
     126              : 
     127          279 :     thinger::awaitable<const multipart_part*> multipart_reader::next() {
     128              :         using event = multipart_parser::event;
     129              :         if (!parser_ || error_ != multipart_error::none) co_return nullptr;
     130              :         pending_ = {};
     131              :         for (;;) {
     132              :             switch (co_await advance()) {
     133              :                 case event::part:
     134              :                     in_part_ = true;
     135              :                     co_return &parser_->part();
     136              :                 case event::data:
     137              :                     continue;   // the rest of the current part, skipped
     138              :                 case event::part_end:
     139              :                     in_part_ = false;
     140              :                     continue;
     141              :                 case event::end:
     142              :                     in_part_ = false;
     143              :                     done_ = true;
     144              :                     co_return nullptr;
     145              :                 default:
     146              :                     co_return nullptr;
     147              :             }
     148              :         }
     149          558 :     }
     150              : 
     151        24962 :     thinger::awaitable<std::string_view> multipart_reader::read(size_t max_size) {
     152              :         using event = multipart_parser::event;
     153              :         if (max_size == 0 || !in_part_ || error_ != multipart_error::none) co_return std::string_view{};
     154              :         if (pending_.empty()) {
     155              :             auto result = co_await advance();
     156              :             if (result != event::data) {
     157              :                 if (result == event::part_end) in_part_ = false;
     158              :                 co_return std::string_view{};
     159              :             }
     160              :             pending_ = parser_->data();
     161              :         }
     162              :         auto data = pending_.substr(0, max_size);
     163              :         pending_.remove_prefix(data.size());
     164              :         co_return data;
     165        49924 :     }
     166              : 
     167           93 :     thinger::awaitable<bool> multipart_reader::read_all(std::string& value) {
     168              :         while (in_part_) {
     169              :             auto data = co_await read();
     170              :             if (data.empty()) break;
     171              :             value.append(data);
     172              :         }
     173              :         co_return !in_part_ && error_ == multipart_error::none;
     174          186 :     }
     175              : 
     176              :     // --- multipart_file ---
     177              : 
     178           30 :     multipart_file::multipart_file(multipart_part part, std::filesystem::path path, std::uint64_t size)
     179           30 :         : part_(std::move(part)), path_(std::move(path)), size_(size) {}
     180              : 
     181           39 :     multipart_file::~multipart_file() {
     182           39 :         remove();
     183           39 :     }
     184              : 
     185            9 :     multipart_file::multipart_file(multipart_file&& other) noexcept
     186            9 :         : part_(std::move(other.part_)), path_(std::move(other.path_)), size_(other.size_), owned_(other.owned_) {
     187            9 :         other.owned_ = false;
     188            9 :     }
     189              : 
     190            0 :     multipart_file& multipart_file::operator=(multipart_file&& other) noexcept {
     191            0 :         if (this != &other) {
     192            0 :             remove();
     193            0 :             part_ = std::move(other.part_);
     194            0 :             path_ = std::move(other.path_);
     195            0 :             size_ = other.size_;
     196            0 :             owned_ = other.owned_;
     197            0 :             other.owned_ = false;
     198              :         }
     199            0 :         return *this;
     200              :     }
     201              : 
     202           39 :     void multipart_file::remove() {
     203           39 :         if (!owned_ || path_.empty()) return;
     204           24 :         owned_ = false;
     205           24 :         std::error_code ignored;
     206           24 :         std::filesystem::remove(path_, ignored);
     207              :     }
     208              : 
     209            3 :     bool multipart_file::move_to(const std::filesystem::path& destination, std::error_code& error) {
     210            3 :         error.clear();
     211            3 :         std::filesystem::rename(path_, destination, error);
     212            3 :         if (error == std::errc::cross_device_link) {
     213            0 :             error.clear();
     214            0 :             std::filesystem::copy_file(path_, destination, std::filesystem::copy_options::overwrite_existing, error);
     215            0 :             if (error) return false;
     216            0 :             std::error_code ignored;
     217            0 :             std::filesystem::remove(path_, ignored);
     218            3 :         } else if (error) {
     219            0 :             return false;
     220              :         }
     221            3 :         path_ = destination;
     222            3 :         owned_ = false;
     223            3 :         return true;
     224              :     }
     225              : 
     226            3 :     bool multipart_file::move_to(const std::filesystem::path& destination) {
     227            3 :         std::error_code error;
     228            6 :         return move_to(destination, error);
     229              :     }
     230              : 
     231            3 :     std::filesystem::path multipart_file::release() {
     232            3 :         owned_ = false;
     233            3 :         return path_;
     234              :     }
     235              : 
     236              :     // --- multipart_form ---
     237              : 
     238            0 :     const std::string* multipart_form::field(std::string_view name) const {
     239            0 :         for (const auto& [key, value] : fields_) {
     240            0 :             if (key == name) return &value;
     241              :         }
     242            0 :         return nullptr;
     243              :     }
     244              : 
     245            0 :     std::string multipart_form::field(std::string_view name, std::string_view default_value) const {
     246            0 :         auto value = field(name);
     247            0 :         return value ? *value : std::string(default_value);
     248              :     }
     249              : 
     250            9 :     multipart_file* multipart_form::file(std::string_view name) {
     251           18 :         for (auto& file : files_) {
     252           15 :             if (file.name() == name) return &file;
     253              :         }
     254            3 :         return nullptr;
     255              :     }
     256              : 
     257              :     namespace {
     258              : 
     259              :         // A temporary file being written, deleted unless released
     260              :         class temporary_file {
     261              :         public:
     262           36 :             explicit temporary_file(const std::filesystem::path& directory) {
     263           36 :                 std::string pattern = (directory / "thinger-upload-XXXXXX").string();
     264              :                 // (readable by its owner only, created exclusively)
     265           36 :                 fd_ = ::mkstemp(pattern.data());
     266           36 :                 if (fd_ >= 0) path_ = std::move(pattern);
     267           36 :             }
     268              : 
     269           36 :             ~temporary_file() {
     270           36 :                 close();
     271           36 :                 if (!path_.empty()) ::unlink(path_.c_str());
     272           36 :             }
     273              : 
     274              :             temporary_file(const temporary_file&) = delete;
     275              :             temporary_file& operator=(const temporary_file&) = delete;
     276              : 
     277           36 :             bool is_open() const { return fd_ >= 0; }
     278              : 
     279         9851 :             bool write(std::string_view data) {
     280        19702 :                 while (!data.empty()) {
     281         9851 :                     auto written = ::write(fd_, data.data(), data.size());
     282         9851 :                     if (written < 0) {
     283            0 :                         if (errno == EINTR) continue;
     284            0 :                         return false;
     285              :                     }
     286         9851 :                     data.remove_prefix(static_cast<size_t>(written));
     287              :                 }
     288         9851 :                 return true;
     289              :             }
     290              : 
     291           72 :             bool close() {
     292           72 :                 if (fd_ < 0) return true;
     293           36 :                 bool closed = ::close(fd_) == 0;
     294           36 :                 fd_ = -1;
     295           36 :                 return closed;
     296              :             }
     297              : 
     298              :             // The file, no longer deleted
     299           30 :             std::string release() {
     300           30 :                 return std::exchange(path_, {});
     301              :             }
     302              : 
     303              :         private:
     304              :             int fd_ = -1;
     305              :             std::string path_;
     306              :         };
     307              : 
     308              :         // Where uploads are written: the directory of the options, or the system's
     309           99 :         std::filesystem::path upload_directory(const multipart_options& options) {
     310           99 :             if (!options.temp_directory.empty()) return options.temp_directory;
     311            9 :             std::error_code error;
     312            9 :             auto directory = std::filesystem::temp_directory_path(error);
     313            9 :             return error ? std::filesystem::path("/tmp") : directory;
     314            9 :         }
     315              : 
     316              :         // A part stored in a temporary file: file_failed if it could not be written, and no
     317              :         // path if the body failed first (the file is deleted then)
     318              :         struct stored_part {
     319              :             multipart_error error = multipart_error::none;
     320              :             std::string path;
     321              :             std::uint64_t size = 0;
     322              :         };
     323              : 
     324              :         // (blocking writes, as Go's ParseMultipartForm: there is no portable asynchronous
     325              :         // file I/O, and the parts arrive slower than a disk takes them)
     326           36 :         thinger::awaitable<stored_part> store_part(multipart_reader& reader, const std::filesystem::path& directory) {
     327              :             temporary_file file(directory);
     328              :             if (!file.is_open()) {
     329              :                 LOG_ERROR("Cannot create a temporary file for an upload in {}: {}", directory.string(), std::strerror(errno));
     330              :                 co_return stored_part{multipart_error::file_failed};
     331              :             }
     332              :             std::uint64_t size = 0;
     333              :             bool written = true;
     334              :             for (auto data = co_await reader.read(); !data.empty(); data = co_await reader.read()) {
     335              :                 if (!file.write(data)) {
     336              :                     written = false;
     337              :                     break;
     338              :                 }
     339              :                 size += data.size();
     340              :             }
     341              :             if (!written || !file.close()) {
     342              :                 LOG_ERROR("Cannot write the temporary file of an upload: {}", std::strerror(errno));
     343              :                 co_return stored_part{multipart_error::file_failed};
     344              :             }
     345              :             if (reader.failed()) co_return stored_part{};
     346              :             co_return stored_part{multipart_error::none, file.release(), size};
     347           72 :         }
     348              : 
     349              :     }
     350              : 
     351          102 :     thinger::awaitable<multipart_form> read_multipart(request& req, multipart_options options) {
     352              :         multipart_form form;
     353              :         multipart_reader reader(req, options.limits);
     354              :         if (!reader.failed() && reader.media_type() != "multipart/form-data") {
     355              :             form.error_ = multipart_error::not_multipart;
     356              :             co_return form;
     357              :         }
     358              : 
     359              :         auto directory = upload_directory(options);
     360              :         multipart_error failure = multipart_error::none;
     361              :         while (const multipart_part* part = co_await reader.next()) {
     362              :             // Parts that are not form fields are skipped, as Go and busboy do
     363              :             if (part->disposition != "form-data" || part->name.empty()) continue;
     364              : 
     365              :             if (!part->is_file()) {
     366              :                 std::string value;
     367              :                 if (!co_await reader.read_all(value)) break;
     368              :                 form.fields_.emplace_back(part->name, std::move(value));
     369              :                 continue;
     370              :             }
     371              : 
     372              :             if (options.on_file) {
     373              :                 co_await options.on_file(*part, reader);
     374              :                 continue;
     375              :             }
     376              : 
     377              :             auto stored = co_await store_part(reader, directory);
     378              :             if (stored.error != multipart_error::none) {
     379              :                 failure = stored.error;
     380              :                 break;
     381              :             }
     382              :             // The body failed while the part was being written
     383              :             if (stored.path.empty()) break;
     384              :             form.files_.emplace_back(*part, std::move(stored.path), stored.size);
     385              :         }
     386              : 
     387              :         if (failure == multipart_error::none && !reader.done()) {
     388              :             // An error, or a read cancelled: the body was not read whole
     389              :             failure = reader.failed() ? reader.error() : multipart_error::truncated;
     390              :         }
     391              :         if (failure != multipart_error::none) {
     392              :             form.error_ = failure;
     393              :             form.fields_.clear();
     394              :             form.files_.clear();
     395              :         }
     396              :         co_return form;
     397          204 :     }
     398              : 
     399              : }
        

Generated by: LCOV version 2.0-1