LCOV - code coverage report
Current view: top level - src/server - serve_index.cpp (source / functions) Coverage Total Hit
Test: coverage_remapped.info Lines: 0.0 % 137 0
Test Date: 2026-02-09 01:37:05 Functions: 0.0 % 14 0

            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_index.hpp>
      11              : #include <boost/http/server/accepts.hpp>
      12              : #include <boost/http/server/escape_html.hpp>
      13              : #include <boost/http/server/encode_url.hpp>
      14              : #include <boost/http/field.hpp>
      15              : #include <boost/http/status.hpp>
      16              : #include <chrono>
      17              : #include <filesystem>
      18              : #include <string>
      19              : #include <vector>
      20              : 
      21              : namespace boost {
      22              : namespace http {
      23              : 
      24              : namespace {
      25              : 
      26              : // Append an HTTP rel-path to a local filesystem path.
      27              : void
      28            0 : path_cat(
      29              :     std::string& result,
      30              :     core::string_view prefix,
      31              :     core::string_view suffix)
      32              : {
      33            0 :     result = prefix;
      34              : 
      35              : #ifdef _WIN32
      36              :     char constexpr path_separator = '\\';
      37              : #else
      38            0 :     char constexpr path_separator = '/';
      39              : #endif
      40            0 :     if(! result.empty() && result.back() == path_separator)
      41            0 :         result.resize(result.size() - 1);
      42              : 
      43              : #ifdef _WIN32
      44              :     for(auto& c : result)
      45              :         if(c == '/')
      46              :             c = path_separator;
      47              : #endif
      48            0 :     for(auto const& c : suffix)
      49              :     {
      50            0 :         if(c == '/')
      51            0 :             result.push_back(path_separator);
      52              :         else
      53            0 :             result.push_back(c);
      54              :     }
      55            0 : }
      56              : 
      57              : struct dir_entry
      58              : {
      59              :     std::string name;
      60              :     bool is_dir = false;
      61              :     std::uint64_t size = 0;
      62              :     std::uint64_t mtime = 0;
      63              : };
      64              : 
      65              : // Directories first, then case-insensitive alphabetical
      66              : bool
      67            0 : entry_less(
      68              :     dir_entry const& a,
      69              :     dir_entry const& b) noexcept
      70              : {
      71            0 :     if(a.is_dir != b.is_dir)
      72            0 :         return a.is_dir;
      73              : 
      74              :     // Case-insensitive compare
      75            0 :     auto const& an = a.name;
      76            0 :     auto const& bn = b.name;
      77            0 :     auto const n = (std::min)(an.size(), bn.size());
      78            0 :     for(std::size_t i = 0; i < n; ++i)
      79              :     {
      80            0 :         auto ac = static_cast<unsigned char>(an[i]);
      81            0 :         auto bc = static_cast<unsigned char>(bn[i]);
      82            0 :         if(ac >= 'A' && ac <= 'Z') ac += 32;
      83            0 :         if(bc >= 'A' && bc <= 'Z') bc += 32;
      84            0 :         if(ac != bc)
      85            0 :             return ac < bc;
      86              :     }
      87            0 :     return an.size() < bn.size();
      88              : }
      89              : 
      90              : std::uint64_t
      91            0 : to_epoch(std::filesystem::file_time_type tp)
      92              : {
      93              :     auto const sctp = std::chrono::clock_cast<
      94            0 :         std::chrono::system_clock>(tp);
      95            0 :     auto const dur = sctp.time_since_epoch();
      96              :     return static_cast<std::uint64_t>(
      97              :         std::chrono::duration_cast<
      98            0 :             std::chrono::seconds>(dur).count());
      99              : }
     100              : 
     101              : std::string
     102            0 : format_size(std::uint64_t bytes)
     103              : {
     104            0 :     if(bytes < 1024)
     105            0 :         return std::to_string(bytes) + " B";
     106            0 :     if(bytes < 1024 * 1024)
     107            0 :         return std::to_string(bytes / 1024) + " KB";
     108            0 :     if(bytes < 1024 * 1024 * 1024)
     109            0 :         return std::to_string(bytes / (1024 * 1024)) + " MB";
     110            0 :     return std::to_string(
     111            0 :         bytes / (1024ULL * 1024 * 1024)) + " GB";
     112              : }
     113              : 
     114              : std::string
     115            0 : format_time(std::uint64_t epoch)
     116              : {
     117            0 :     if(epoch == 0)
     118            0 :         return "-";
     119              : 
     120            0 :     auto const t = static_cast<std::time_t>(epoch);
     121              :     std::tm tm;
     122              : #ifdef _WIN32
     123              :     gmtime_s(&tm, &t);
     124              : #else
     125            0 :     gmtime_r(&t, &tm);
     126              : #endif
     127              :     char buf[64];
     128            0 :     std::strftime(buf, sizeof(buf),
     129              :         "%Y-%m-%d %H:%M:%S", &tm);
     130            0 :     return buf;
     131              : }
     132              : 
     133              : 
     134              : std::string
     135            0 : render_html(
     136              :     core::string_view dir,
     137              :     std::vector<dir_entry> const& entries,
     138              :     bool show_parent)
     139              : {
     140            0 :     std::string body;
     141            0 :     body.reserve(4096);
     142              : 
     143            0 :     body.append(
     144              :         "<!DOCTYPE html>\n"
     145              :         "<html>\n<head>\n"
     146              :         "<meta charset=\"utf-8\">\n"
     147              :         "<meta name=\"viewport\" "
     148              :             "content=\"width=device-width\">\n"
     149              :         "<title>Index of ");
     150            0 :     body.append(escape_html(dir));
     151            0 :     body.append(
     152              :         "</title>\n"
     153              :         "<style>\n"
     154              :         "body { font-family: -apple-system, "
     155              :             "BlinkMacSystemFont, sans-serif; "
     156              :             "margin: 2em; }\n"
     157              :         "h1 { font-size: 1.4em; }\n"
     158              :         "table { border-collapse: collapse; "
     159              :             "width: 100%; max-width: 900px; }\n"
     160              :         "th, td { text-align: left; "
     161              :             "padding: 0.4em 1em; }\n"
     162              :         "th { border-bottom: 2px solid #ddd; }\n"
     163              :         "td { border-bottom: 1px solid #eee; }\n"
     164              :         "a { text-decoration: none; "
     165              :             "color: #0366d6; }\n"
     166              :         "a:hover { text-decoration: underline; }\n"
     167              :         ".size, .date { color: #586069; }\n"
     168              :         "</style>\n"
     169              :         "</head>\n<body>\n"
     170              :         "<h1>Index of ");
     171            0 :     body.append(escape_html(dir));
     172            0 :     body.append("</h1>\n");
     173              : 
     174            0 :     body.append(
     175              :         "<table>\n"
     176              :         "<tr><th>Name</th>"
     177              :         "<th>Size</th>"
     178              :         "<th>Modified</th></tr>\n");
     179              : 
     180            0 :     if(show_parent)
     181              :     {
     182            0 :         body.append(
     183              :             "<tr><td><a href=\"../\">"
     184              :             "..</a></td>"
     185              :             "<td class=\"size\">-</td>"
     186              :             "<td class=\"date\">-</td></tr>\n");
     187              :     }
     188              : 
     189            0 :     for(auto const& e : entries)
     190              :     {
     191            0 :         auto display_name = escape_html(e.name);
     192            0 :         auto href = encode_url(e.name);
     193            0 :         if(e.is_dir)
     194            0 :             href += '/';
     195              : 
     196            0 :         body.append("<tr><td><a href=\"");
     197            0 :         body.append(href);
     198            0 :         body.append("\">");
     199            0 :         body.append(display_name);
     200            0 :         if(e.is_dir)
     201            0 :             body.append("/");
     202            0 :         body.append("</a></td>");
     203            0 :         body.append("<td class=\"size\">");
     204            0 :         body.append(e.is_dir ? "-" : format_size(e.size));
     205            0 :         body.append("</td>");
     206            0 :         body.append("<td class=\"date\">");
     207            0 :         body.append(format_time(e.mtime));
     208            0 :         body.append("</td></tr>\n");
     209            0 :     }
     210              : 
     211            0 :     body.append("</table>\n</body>\n</html>\n");
     212            0 :     return body;
     213            0 : }
     214              : 
     215              : std::string
     216            0 : render_json(
     217              :     std::vector<dir_entry> const& entries)
     218              : {
     219            0 :     std::string body;
     220            0 :     body.reserve(1024);
     221            0 :     body.push_back('[');
     222              : 
     223            0 :     bool first = true;
     224            0 :     for(auto const& e : entries)
     225              :     {
     226            0 :         if(! first)
     227            0 :             body.push_back(',');
     228            0 :         first = false;
     229              : 
     230            0 :         body.append("{\"name\":\"");
     231              : 
     232              :         // Escape JSON string
     233            0 :         for(auto c : e.name)
     234              :         {
     235            0 :             switch(c)
     236              :             {
     237            0 :             case '"':  body.append("\\\""); break;
     238            0 :             case '\\': body.append("\\\\"); break;
     239            0 :             case '\n': body.append("\\n");  break;
     240            0 :             case '\r': body.append("\\r");  break;
     241            0 :             case '\t': body.append("\\t");  break;
     242            0 :             default:   body.push_back(c);   break;
     243              :             }
     244              :         }
     245              : 
     246            0 :         body.append("\",\"type\":\"");
     247            0 :         body.append(e.is_dir ? "directory" : "file");
     248            0 :         body.append("\",\"size\":");
     249            0 :         body.append(std::to_string(e.size));
     250            0 :         body.append(",\"mtime\":");
     251            0 :         body.append(std::to_string(e.mtime));
     252            0 :         body.push_back('}');
     253              :     }
     254              : 
     255            0 :     body.push_back(']');
     256            0 :     return body;
     257            0 : }
     258              : 
     259              : std::string
     260            0 : render_plain(
     261              :     std::vector<dir_entry> const& entries)
     262              : {
     263            0 :     std::string body;
     264            0 :     body.reserve(1024);
     265            0 :     for(auto const& e : entries)
     266              :     {
     267            0 :         body.append(e.name);
     268            0 :         if(e.is_dir)
     269            0 :             body.push_back('/');
     270            0 :         body.push_back('\n');
     271              :     }
     272            0 :     return body;
     273            0 : }
     274              : 
     275              : } // (anon)
     276              : 
     277              : //------------------------------------------------
     278              : 
     279              : struct serve_index::impl
     280              : {
     281              :     std::string root;
     282              :     serve_index::options opts;
     283              : 
     284            0 :     impl(
     285              :         core::string_view root_,
     286              :         serve_index::options const& opts_)
     287            0 :         : root(root_)
     288            0 :         , opts(opts_)
     289              :     {
     290            0 :     }
     291              : };
     292              : 
     293            0 : serve_index::
     294              : ~serve_index()
     295              : {
     296            0 :     delete impl_;
     297            0 : }
     298              : 
     299            0 : serve_index::
     300            0 : serve_index(core::string_view root)
     301            0 :     : serve_index(root, options{})
     302              : {
     303            0 : }
     304              : 
     305            0 : serve_index::
     306              : serve_index(
     307              :     core::string_view root,
     308            0 :     options const& opts)
     309            0 :     : impl_(new impl(root, opts))
     310              : {
     311            0 : }
     312              : 
     313            0 : serve_index::
     314            0 : serve_index(serve_index&& other) noexcept
     315            0 :     : impl_(other.impl_)
     316              : {
     317            0 :     other.impl_ = nullptr;
     318            0 : }
     319              : 
     320              : route_task
     321            0 : serve_index::
     322              : operator()(route_params& rp) const
     323              : {
     324              :     // Only handle GET and HEAD
     325              :     if(rp.req.method() != method::get &&
     326              :         rp.req.method() != method::head)
     327              :     {
     328              :         if(impl_->opts.fallthrough)
     329              :             co_return route_next;
     330              : 
     331              :         rp.res.set_status(status::method_not_allowed);
     332              :         rp.res.set(field::allow, "GET, HEAD, OPTIONS");
     333              :         auto [ec] = co_await rp.send();
     334              :         if(ec)
     335              :             co_return route_error(ec);
     336              :         co_return route_done;
     337              :     }
     338              : 
     339              :     auto req_path = rp.url.path();
     340              : 
     341              :     // Build filesystem path
     342              :     std::string path;
     343              :     path_cat(path, impl_->root, req_path);
     344              : 
     345              :     // Must be a directory
     346              :     std::error_code fec;
     347              :     auto fs_status = std::filesystem::status(path, fec);
     348              :     if(fec || fs_status.type() !=
     349              :         std::filesystem::file_type::directory)
     350              :         co_return route_next;
     351              : 
     352              :     // Redirect if missing trailing slash
     353              :     if(req_path.empty() || req_path.back() != '/')
     354              :     {
     355              :         std::string location(req_path);
     356              :         location += '/';
     357              :         rp.res.set_status(status::moved_permanently);
     358              :         rp.res.set(field::location, location);
     359              :         auto [ec] = co_await rp.send("");
     360              :         if(ec)
     361              :             co_return route_error(ec);
     362              :         co_return route_done;
     363              :     }
     364              : 
     365              :     // Read directory entries
     366              :     std::vector<dir_entry> entries;
     367              :     {
     368              :         std::filesystem::directory_iterator it(path, fec);
     369              :         if(fec)
     370              :             co_return route_next;
     371              : 
     372              :         for(auto const& de :
     373              :             std::filesystem::directory_iterator(path, fec))
     374              :         {
     375              :             auto name = de.path().filename().string();
     376              : 
     377              :             // Skip hidden files unless configured
     378              :             if(! impl_->opts.hidden &&
     379              :                 ! name.empty() && name[0] == '.')
     380              :                 continue;
     381              : 
     382              :             dir_entry e;
     383              :             e.name = std::move(name);
     384              : 
     385              :             std::error_code sec;
     386              :             e.is_dir = de.is_directory(sec);
     387              :             if(! e.is_dir)
     388              :                 e.size = de.file_size(sec);
     389              :             auto lwt = de.last_write_time(sec);
     390              :             if(! sec)
     391              :                 e.mtime = to_epoch(lwt);
     392              : 
     393              :             entries.push_back(std::move(e));
     394              :         }
     395              :     }
     396              : 
     397              :     std::sort(entries.begin(), entries.end(), entry_less);
     398              : 
     399              :     // Determine ".." display
     400              :     std::filesystem::path root_canonical(impl_->root);
     401              :     std::filesystem::path dir_canonical(path);
     402              :     {
     403              :         std::error_code ec2;
     404              :         root_canonical =
     405              :             std::filesystem::canonical(root_canonical, ec2);
     406              :         dir_canonical =
     407              :             std::filesystem::canonical(dir_canonical, ec2);
     408              :     }
     409              :     bool show_up = impl_->opts.show_parent &&
     410              :         dir_canonical != root_canonical;
     411              : 
     412              :     // Content negotiation
     413              :     accepts ac( rp.req );
     414              :     auto type = ac.type({ "html", "json", "text" });
     415              : 
     416              :     std::string body;
     417              :     std::string_view content_type;
     418              :     if( type == "json" )
     419              :     {
     420              :         body = render_json(entries);
     421              :         content_type = "application/json; charset=utf-8";
     422              :     }
     423              :     else if( type == "text" )
     424              :     {
     425              :         body = render_plain(entries);
     426              :         content_type = "text/plain; charset=utf-8";
     427              :     }
     428              :     else
     429              :     {
     430              :         body = render_html(req_path, entries, show_up);
     431              :         content_type = "text/html; charset=utf-8";
     432              :     }
     433              : 
     434              :     rp.res.set(field::content_type, content_type);
     435              :     rp.res.set("X-Content-Type-Options", "nosniff");
     436              : 
     437              :     auto [ec] = co_await rp.send(body);
     438              :     if(ec)
     439              :         co_return route_error(ec);
     440              :     co_return route_done;
     441            0 : }
     442              : 
     443              : } // http
     444              : } // boost
        

Generated by: LCOV version 2.3