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