LCOV - code coverage report
Current view: top level - libs/http/src/server - serve_static.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 0.0 % 40 0
Test Date: 2026-01-20 00:11:34 Functions: 0.0 % 8 0

            Line data    Source code
       1              : //
       2              : // Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
       3              : //
       4              : // Distributed under the Boost Software License, Version 1.0. (See accompanying
       5              : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
       6              : //
       7              : // Official repository: https://github.com/cppalliance/http
       8              : //
       9              : 
      10              : #include <boost/http/server/serve_static.hpp>
      11              : #include <boost/http/server/send_file.hpp>
      12              : #include <boost/http/field.hpp>
      13              : #include <boost/http/status.hpp>
      14              : #include <boost/capy/file.hpp>
      15              : #include <filesystem>
      16              : #include <string>
      17              : 
      18              : namespace boost {
      19              : namespace http {
      20              : 
      21              : namespace {
      22              : 
      23              : // Append an HTTP rel-path to a local filesystem path.
      24              : void
      25            0 : path_cat(
      26              :     std::string& result,
      27              :     core::string_view prefix,
      28              :     core::string_view suffix)
      29              : {
      30            0 :     result = prefix;
      31              : 
      32              : #ifdef BOOST_MSVC
      33              :     char constexpr path_separator = '\\';
      34              : #else
      35            0 :     char constexpr path_separator = '/';
      36              : #endif
      37            0 :     if(! result.empty() && result.back() == path_separator)
      38            0 :         result.resize(result.size() - 1);
      39              : 
      40              : #ifdef BOOST_MSVC
      41              :     for(auto& c : result)
      42              :         if(c == '/')
      43              :             c = path_separator;
      44              : #endif
      45            0 :     for(auto const& c : suffix)
      46              :     {
      47            0 :         if(c == '/')
      48            0 :             result.push_back(path_separator);
      49              :         else
      50            0 :             result.push_back(c);
      51              :     }
      52            0 : }
      53              : 
      54              : // Check if path segment is a dotfile
      55              : bool
      56            0 : is_dotfile(core::string_view path) noexcept
      57              : {
      58            0 :     auto pos = path.rfind('/');
      59            0 :     if(pos == core::string_view::npos)
      60            0 :         pos = 0;
      61              :     else
      62            0 :         ++pos;
      63              : 
      64            0 :     if(pos < path.size() && path[pos] == '.')
      65            0 :         return true;
      66              : 
      67            0 :     return false;
      68              : }
      69              : 
      70              : } // (anon)
      71              : 
      72              : struct serve_static::impl
      73              : {
      74              :     std::string root;
      75              :     serve_static_options opts;
      76              : 
      77            0 :     impl(
      78              :         core::string_view root_,
      79              :         serve_static_options const& opts_)
      80            0 :         : root(root_)
      81            0 :         , opts(opts_)
      82              :     {
      83            0 :     }
      84              : };
      85              : 
      86            0 : serve_static::
      87              : ~serve_static()
      88              : {
      89            0 :     delete impl_;
      90            0 : }
      91              : 
      92            0 : serve_static::
      93            0 : serve_static(core::string_view root)
      94            0 :     : serve_static(root, serve_static_options{})
      95              : {
      96            0 : }
      97              : 
      98            0 : serve_static::
      99              : serve_static(
     100              :     core::string_view root,
     101            0 :     serve_static_options const& opts)
     102            0 :     : impl_(new impl(root, opts))
     103              : {
     104            0 : }
     105              : 
     106            0 : serve_static::
     107            0 : serve_static(serve_static&& other) noexcept
     108            0 :     : impl_(other.impl_)
     109              : {
     110            0 :     other.impl_ = nullptr;
     111            0 : }
     112              : 
     113              : route_task
     114            0 : serve_static::
     115              : operator()(route_params& rp) const
     116              : {
     117              :     // Only handle GET and HEAD
     118              :     if(rp.req.method() != method::get &&
     119              :         rp.req.method() != method::head)
     120              :     {
     121              :         if(impl_->opts.fallthrough)
     122              :             co_return route::next;
     123              : 
     124              :         rp.res.set_status(status::method_not_allowed);
     125              :         rp.res.set(field::allow, "GET, HEAD");
     126              :         co_return co_await rp.send("");
     127              :     }
     128              : 
     129              :     // Get the request path
     130              :     auto req_path = rp.url.path();
     131              : 
     132              :     // Check for dotfiles
     133              :     if(is_dotfile(req_path))
     134              :     {
     135              :         switch(impl_->opts.dotfiles)
     136              :         {
     137              :         case dotfiles_policy::deny:
     138              :             rp.res.set_status(status::forbidden);
     139              :             co_return co_await rp.send("Forbidden");
     140              : 
     141              :         case dotfiles_policy::ignore:
     142              :             if(impl_->opts.fallthrough)
     143              :                 co_return route::next;
     144              :             rp.res.set_status(status::not_found);
     145              :             co_return co_await rp.send("Not Found");
     146              : 
     147              :         case dotfiles_policy::allow:
     148              :             break;
     149              :         }
     150              :     }
     151              : 
     152              :     // Build the file path
     153              :     std::string path;
     154              :     path_cat(path, impl_->root, req_path);
     155              : 
     156              :     // Check if it's a directory
     157              :     std::error_code fec;
     158              :     bool is_dir = std::filesystem::is_directory(path, fec);
     159              :     if(is_dir && ! fec)
     160              :     {
     161              :         // Check for trailing slash
     162              :         if(req_path.empty() || req_path.back() != '/')
     163              :         {
     164              :             if(impl_->opts.redirect)
     165              :             {
     166              :                 // Redirect to add trailing slash
     167              :                 std::string location(req_path);
     168              :                 location += '/';
     169              :                 rp.res.set_status(status::moved_permanently);
     170              :                 rp.res.set(field::location, location);
     171              :                 co_return co_await rp.send("");
     172              :             }
     173              :         }
     174              : 
     175              :         // Try index file
     176              :         if(impl_->opts.index)
     177              :         {
     178              : #ifdef BOOST_MSVC
     179              :             path += "\\index.html";
     180              : #else
     181              :             path += "/index.html";
     182              : #endif
     183              :         }
     184              :     }
     185              : 
     186              :     // Prepare file response using send_file utilities
     187              :     send_file_options opts;
     188              :     opts.etag = impl_->opts.etag;
     189              :     opts.last_modified = impl_->opts.last_modified;
     190              :     opts.max_age = impl_->opts.max_age;
     191              : 
     192              :     send_file_info info;
     193              :     send_file_init(info, rp, path, opts);
     194              : 
     195              :     // Handle result
     196              :     switch(info.result)
     197              :     {
     198              :     case send_file_result::not_found:
     199              :         if(impl_->opts.fallthrough)
     200              :             co_return route::next;
     201              :         rp.res.set_status(status::not_found);
     202              :         co_return co_await rp.send("Not Found");
     203              : 
     204              :     case send_file_result::not_modified:
     205              :         rp.res.set_status(status::not_modified);
     206              :         co_return co_await rp.send("");
     207              : 
     208              :     case send_file_result::error:
     209              :         // Range error - headers already set by send_file_init
     210              :         co_return co_await rp.send("");
     211              : 
     212              :     case send_file_result::ok:
     213              :         break;
     214              :     }
     215              : 
     216              :     // Set Accept-Ranges if enabled
     217              :     if(impl_->opts.accept_ranges)
     218              :         rp.res.set(field::accept_ranges, "bytes");
     219              : 
     220              :     // Set Cache-Control with immutable if configured
     221              :     if(impl_->opts.immutable && opts.max_age > 0)
     222              :     {
     223              :         std::string cc = "public, max-age=" +
     224              :             std::to_string(opts.max_age) + ", immutable";
     225              :         rp.res.set(field::cache_control, cc);
     226              :     }
     227              : 
     228              :     // For HEAD requests, don't send body
     229              :     if(rp.req.method() == method::head)
     230              :         co_return co_await rp.send("");
     231              : 
     232              :     // Open and stream the file
     233              :     capy::file f;
     234              :     system::error_code ec;
     235              :     f.open(path.c_str(), capy::file_mode::scan, ec);
     236              :     if(ec)
     237              :     {
     238              :         if(impl_->opts.fallthrough)
     239              :             co_return route::next;
     240              :         rp.res.set_status(status::internal_server_error);
     241              :         co_return co_await rp.send("Internal Server Error");
     242              :     }
     243              : 
     244              :     // Seek to range start if needed
     245              :     if(info.is_range && info.range_start > 0)
     246              :     {
     247              :         f.seek(static_cast<std::uint64_t>(info.range_start), ec);
     248              :         if(ec)
     249              :         {
     250              :             rp.res.set_status(status::internal_server_error);
     251              :             co_return co_await rp.send("Internal Server Error");
     252              :         }
     253              :     }
     254              : 
     255              :     // Calculate how much to send
     256              :     std::int64_t remaining = info.range_end - info.range_start + 1;
     257              : 
     258              :     // Stream file content
     259              :     constexpr std::size_t buf_size = 16384;
     260              :     char buffer[buf_size];
     261              : 
     262              :     while(remaining > 0)
     263              :     {
     264              :         auto const to_read = static_cast<std::size_t>(
     265              :             (std::min)(remaining, static_cast<std::int64_t>(buf_size)));
     266              : 
     267              :         auto const n = f.read(buffer, to_read, ec);
     268              :         if(ec || n == 0)
     269              :             break;
     270              : 
     271              :         co_await rp.write(capy::const_buffer(buffer, n));
     272              :         remaining -= static_cast<std::int64_t>(n);
     273              :     }
     274              : 
     275              :     co_return co_await rp.end();
     276            0 : }
     277              : 
     278              : } // http
     279              : } // boost
        

Generated by: LCOV version 2.3