LCOV - code coverage report
Current view: top level - src/server - serve_static.cpp (source / functions) Coverage Total Missed
Test: coverage_remapped.info Lines: 0.0 % 40 40
Test Date: 2026-06-13 19:44:58 Functions: 0.0 % 8 8

           TLA  Line data    Source code
       1                 : //
       2                 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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/file.hpp>
      14                 : #include <boost/http/status.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 MIS           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                 :         auto [ec] = co_await rp.send();
     127                 :         if(ec)
     128                 :             co_return route_error(ec);
     129                 :         co_return route_done;
     130                 :     }
     131                 : 
     132                 :     // Get the request path
     133                 :     auto req_path = rp.url.path();
     134                 : 
     135                 :     // Check for dotfiles
     136                 :     if(is_dotfile(req_path))
     137                 :     {
     138                 :         switch(impl_->opts.dotfiles)
     139                 :         {
     140                 :         case dotfiles_policy::deny:
     141                 :         {
     142                 :             rp.res.set_status(status::forbidden);
     143                 :             auto [ec] = co_await rp.send("Forbidden");
     144                 :             if(ec)
     145                 :                 co_return route_error(ec);
     146                 :             co_return route_done;
     147                 :         }
     148                 : 
     149                 :         case dotfiles_policy::ignore:
     150                 :         {
     151                 :             if(impl_->opts.fallthrough)
     152                 :                 co_return route_next;
     153                 :             rp.res.set_status(status::not_found);
     154                 :             auto [ec] = co_await rp.send("Not Found");
     155                 :             if(ec)
     156                 :                 co_return route_error(ec);
     157                 :             co_return route_done;
     158                 :         }
     159                 : 
     160                 :         case dotfiles_policy::allow:
     161                 :             break;
     162                 :         }
     163                 :     }
     164                 : 
     165                 :     // Build the file path
     166                 :     std::string path;
     167                 :     path_cat(path, impl_->root, req_path);
     168                 : 
     169                 :     // Check if it's a directory
     170                 :     system::error_code fec;
     171                 :     bool is_dir = std::filesystem::is_directory(path, fec);
     172                 :     if(is_dir && ! fec.failed())
     173                 :     {
     174                 :         // Check for trailing slash
     175                 :         if(req_path.empty() || req_path.back() != '/')
     176                 :         {
     177                 :             if(impl_->opts.redirect)
     178                 :             {
     179                 :                 // Redirect to add trailing slash
     180                 :                 std::string location(req_path);
     181                 :                 location += '/';
     182                 :                 rp.res.set_status(status::moved_permanently);
     183                 :                 rp.res.set(field::location, location);
     184                 :                 auto [ec] = co_await rp.send("");
     185                 :                 if(ec)
     186                 :                     co_return route_error(ec);
     187                 :                 co_return route_done;
     188                 :             }
     189                 :         }
     190                 : 
     191                 :         // Try index file
     192                 :         if(impl_->opts.index)
     193                 :         {
     194                 : #ifdef BOOST_MSVC
     195                 :             path += "\\index.html";
     196                 : #else
     197                 :             path += "/index.html";
     198                 : #endif
     199                 :         }
     200                 :     }
     201                 : 
     202                 :     // Prepare file response using send_file utilities
     203                 :     send_file_options opts;
     204                 :     opts.etag = impl_->opts.etag;
     205                 :     opts.last_modified = impl_->opts.last_modified;
     206                 :     opts.max_age = impl_->opts.max_age;
     207                 : 
     208                 :     send_file_info info;
     209                 :     send_file_init(info, rp, path, opts);
     210                 : 
     211                 :     // Handle result
     212                 :     switch(info.result)
     213                 :     {
     214                 :     case send_file_result::not_found:
     215                 :     {
     216                 :         if(impl_->opts.fallthrough)
     217                 :             co_return route_next;
     218                 :         rp.res.set_status(status::not_found);
     219                 :         auto [ec] = co_await rp.send("Not Found");
     220                 :         if(ec)
     221                 :             co_return route_error(ec);
     222                 :         co_return route_done;
     223                 :     }
     224                 : 
     225                 :     case send_file_result::not_modified:
     226                 :     {
     227                 :         rp.res.set_status(status::not_modified);
     228                 :         auto [ec] = co_await rp.send("");
     229                 :         if(ec)
     230                 :             co_return route_error(ec);
     231                 :         co_return route_done;
     232                 :     }
     233                 : 
     234                 :     case send_file_result::error:
     235                 :     {
     236                 :         // Range error - headers already set by send_file_init
     237                 :         auto [ec] = co_await rp.send("");
     238                 :         if(ec)
     239                 :             co_return route_error(ec);
     240                 :         co_return route_done;
     241                 :     }
     242                 : 
     243                 :     case send_file_result::ok:
     244                 :         break;
     245                 :     }
     246                 : 
     247                 :     // Set Accept-Ranges if enabled
     248                 :     if(impl_->opts.accept_ranges)
     249                 :         rp.res.set(field::accept_ranges, "bytes");
     250                 : 
     251                 :     // Set Cache-Control with immutable if configured
     252                 :     if(impl_->opts.immutable && opts.max_age > 0)
     253                 :     {
     254                 :         std::string cc = "public, max-age=" +
     255                 :             std::to_string(opts.max_age) + ", immutable";
     256                 :         rp.res.set(field::cache_control, cc);
     257                 :     }
     258                 : 
     259                 :     // For HEAD requests, don't send body
     260                 :     if(rp.req.method() == method::head)
     261                 :     {
     262                 :         auto [ec] = co_await rp.send("");
     263                 :         if(ec)
     264                 :             co_return route_error(ec);
     265                 :         co_return route_done;
     266                 :     }
     267                 : 
     268                 :     // Open and stream the file
     269                 :     file f;
     270                 :     system::error_code ec;
     271                 :     f.open(path.c_str(), file_mode::scan, ec);
     272                 :     if(ec)
     273                 :     {
     274                 :         if(impl_->opts.fallthrough)
     275                 :             co_return route_next;
     276                 :         rp.res.set_status(status::internal_server_error);
     277                 :         auto [ec2] = co_await rp.send("Internal Server Error");
     278                 :         if(ec2)
     279                 :             co_return route_error(ec2);
     280                 :         co_return route_done;
     281                 :     }
     282                 : 
     283                 :     // Seek to range start if needed
     284                 :     if(info.is_range && info.range_start > 0)
     285                 :     {
     286                 :         f.seek(static_cast<std::uint64_t>(info.range_start), ec);
     287                 :         if(ec.failed())
     288                 :         {
     289                 :             rp.res.set_status(status::internal_server_error);
     290                 :             auto [ec2] = co_await rp.send("Internal Server Error");
     291                 :             if(ec2)
     292                 :                 co_return route_error(ec2);
     293                 :             co_return route_done;
     294                 :         }
     295                 :     }
     296                 : 
     297                 :     // Calculate how much to send
     298                 :     std::int64_t remaining = info.range_end - info.range_start + 1;
     299                 : 
     300                 :     // Stream file content using serializer's internal buffer
     301                 :     while(remaining > 0)
     302                 :     {
     303                 :         capy::mutable_buffer arr[1];
     304                 :         auto bufs = rp.res_body.prepare(arr);
     305                 :         if(bufs.empty())
     306                 :         {
     307                 :             auto [ec2] = co_await rp.res_body.commit(0);
     308                 :             if(ec2)
     309                 :                 co_return route_error(ec2);
     310                 :             continue;
     311                 :         }
     312                 : 
     313                 :         auto const to_read = static_cast<std::size_t>(
     314                 :             (std::min)(remaining,
     315                 :                 static_cast<std::int64_t>(bufs[0].size())));
     316                 : 
     317                 :         auto const n1 = f.read(bufs[0].data(), to_read, ec);
     318                 :         if(ec.failed())
     319                 :             co_return route_error(ec);
     320                 :         if(n1 == 0)
     321                 :             break;
     322                 : 
     323                 :         auto [ec2] = co_await rp.res_body.commit(n1);
     324                 :         if(ec2)
     325                 :             co_return route_error(ec2);
     326                 :         remaining -= static_cast<std::int64_t>(n1);
     327                 :     }
     328                 : 
     329                 :     auto [ec3] = co_await rp.res_body.commit_eof(0);
     330                 :     if(ec3)
     331                 :         co_return route_error(ec3);
     332                 :     co_return route_done;
     333               0 : }
     334                 : 
     335                 : } // http
     336                 : } // boost
        

Generated by: LCOV version 2.3