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

           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_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 MIS           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