0.00% Lines (0/40) 0.00% Functions (0/8)
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_static.hpp> 10   #include <boost/http/server/serve_static.hpp>
11   #include <boost/http/server/send_file.hpp> 11   #include <boost/http/server/send_file.hpp>
12   #include <boost/http/field.hpp> 12   #include <boost/http/field.hpp>
13   #include <boost/http/file.hpp> 13   #include <boost/http/file.hpp>
14   #include <boost/http/status.hpp> 14   #include <boost/http/status.hpp>
15   #include <filesystem> 15   #include <filesystem>
16   #include <string> 16   #include <string>
17   17  
18   namespace boost { 18   namespace boost {
19   namespace http { 19   namespace http {
20   20  
21   namespace { 21   namespace {
22   22  
23   // Append an HTTP rel-path to a local filesystem path. 23   // Append an HTTP rel-path to a local filesystem path.
24   void 24   void
MISUBC 25   path_cat( 25   path_cat(
26   std::string& result, 26   std::string& result,
27   core::string_view prefix, 27   core::string_view prefix,
28   core::string_view suffix) 28   core::string_view suffix)
29   { 29   {
MISUBC 30   result = prefix; 30   result = prefix;
31   31  
32   #ifdef BOOST_MSVC 32   #ifdef BOOST_MSVC
33   char constexpr path_separator = '\\'; 33   char constexpr path_separator = '\\';
34   #else 34   #else
MISUBC 35   char constexpr path_separator = '/'; 35   char constexpr path_separator = '/';
36   #endif 36   #endif
MISUBC 37   if(! result.empty() && result.back() == path_separator) 37   if(! result.empty() && result.back() == path_separator)
MISUBC 38   result.resize(result.size() - 1); 38   result.resize(result.size() - 1);
39   39  
40   #ifdef BOOST_MSVC 40   #ifdef BOOST_MSVC
41   for(auto& c : result) 41   for(auto& c : result)
42   if(c == '/') 42   if(c == '/')
43   c = path_separator; 43   c = path_separator;
44   #endif 44   #endif
MISUBC 45   for(auto const& c : suffix) 45   for(auto const& c : suffix)
46   { 46   {
MISUBC 47   if(c == '/') 47   if(c == '/')
MISUBC 48   result.push_back(path_separator); 48   result.push_back(path_separator);
49   else 49   else
MISUBC 50   result.push_back(c); 50   result.push_back(c);
51   } 51   }
MISUBC 52   } 52   }
53   53  
54   // Check if path segment is a dotfile 54   // Check if path segment is a dotfile
55   bool 55   bool
MISUBC 56   is_dotfile(core::string_view path) noexcept 56   is_dotfile(core::string_view path) noexcept
57   { 57   {
MISUBC 58   auto pos = path.rfind('/'); 58   auto pos = path.rfind('/');
MISUBC 59   if(pos == core::string_view::npos) 59   if(pos == core::string_view::npos)
MISUBC 60   pos = 0; 60   pos = 0;
61   else 61   else
MISUBC 62   ++pos; 62   ++pos;
63   63  
MISUBC 64   if(pos < path.size() && path[pos] == '.') 64   if(pos < path.size() && path[pos] == '.')
MISUBC 65   return true; 65   return true;
66   66  
MISUBC 67   return false; 67   return false;
68   } 68   }
69   69  
70   } // (anon) 70   } // (anon)
71   71  
72   struct serve_static::impl 72   struct serve_static::impl
73   { 73   {
74   std::string root; 74   std::string root;
75   serve_static_options opts; 75   serve_static_options opts;
76   76  
MISUBC 77   impl( 77   impl(
78   core::string_view root_, 78   core::string_view root_,
79   serve_static_options const& opts_) 79   serve_static_options const& opts_)
MISUBC 80   : root(root_) 80   : root(root_)
MISUBC 81   , opts(opts_) 81   , opts(opts_)
82   { 82   {
MISUBC 83   } 83   }
84   }; 84   };
85   85  
MISUBC 86   serve_static:: 86   serve_static::
87   ~serve_static() 87   ~serve_static()
88   { 88   {
MISUBC 89   delete impl_; 89   delete impl_;
MISUBC 90   } 90   }
91   91  
MISUBC 92   serve_static:: 92   serve_static::
MISUBC 93   serve_static(core::string_view root) 93   serve_static(core::string_view root)
MISUBC 94   : serve_static(root, serve_static_options{}) 94   : serve_static(root, serve_static_options{})
95   { 95   {
MISUBC 96   } 96   }
97   97  
MISUBC 98   serve_static:: 98   serve_static::
99   serve_static( 99   serve_static(
100   core::string_view root, 100   core::string_view root,
MISUBC 101   serve_static_options const& opts) 101   serve_static_options const& opts)
MISUBC 102   : impl_(new impl(root, opts)) 102   : impl_(new impl(root, opts))
103   { 103   {
MISUBC 104   } 104   }
105   105  
MISUBC 106   serve_static:: 106   serve_static::
MISUBC 107   serve_static(serve_static&& other) noexcept 107   serve_static(serve_static&& other) noexcept
MISUBC 108   : impl_(other.impl_) 108   : impl_(other.impl_)
109   { 109   {
MISUBC 110   other.impl_ = nullptr; 110   other.impl_ = nullptr;
MISUBC 111   } 111   }
112   112  
113   route_task 113   route_task
MISUBC 114   serve_static:: 114   serve_static::
115   operator()(route_params& rp) const 115   operator()(route_params& rp) const
116   { 116   {
117   // Only handle GET and HEAD 117   // Only handle GET and HEAD
118   if(rp.req.method() != method::get && 118   if(rp.req.method() != method::get &&
119   rp.req.method() != method::head) 119   rp.req.method() != method::head)
120   { 120   {
121   if(impl_->opts.fallthrough) 121   if(impl_->opts.fallthrough)
122   co_return route_next; 122   co_return route_next;
123   123  
124   rp.res.set_status(status::method_not_allowed); 124   rp.res.set_status(status::method_not_allowed);
125   rp.res.set(field::allow, "GET, HEAD"); 125   rp.res.set(field::allow, "GET, HEAD");
126   auto [ec] = co_await rp.send(); 126   auto [ec] = co_await rp.send();
127   if(ec) 127   if(ec)
128   co_return route_error(ec); 128   co_return route_error(ec);
129   co_return route_done; 129   co_return route_done;
130   } 130   }
131   131  
132   // Get the request path 132   // Get the request path
133   auto req_path = rp.url.path(); 133   auto req_path = rp.url.path();
134   134  
135   // Check for dotfiles 135   // Check for dotfiles
136   if(is_dotfile(req_path)) 136   if(is_dotfile(req_path))
137   { 137   {
138   switch(impl_->opts.dotfiles) 138   switch(impl_->opts.dotfiles)
139   { 139   {
140   case dotfiles_policy::deny: 140   case dotfiles_policy::deny:
141   { 141   {
142   rp.res.set_status(status::forbidden); 142   rp.res.set_status(status::forbidden);
143   auto [ec] = co_await rp.send("Forbidden"); 143   auto [ec] = co_await rp.send("Forbidden");
144   if(ec) 144   if(ec)
145   co_return route_error(ec); 145   co_return route_error(ec);
146   co_return route_done; 146   co_return route_done;
147   } 147   }
148   148  
149   case dotfiles_policy::ignore: 149   case dotfiles_policy::ignore:
150   { 150   {
151   if(impl_->opts.fallthrough) 151   if(impl_->opts.fallthrough)
152   co_return route_next; 152   co_return route_next;
153   rp.res.set_status(status::not_found); 153   rp.res.set_status(status::not_found);
154   auto [ec] = co_await rp.send("Not Found"); 154   auto [ec] = co_await rp.send("Not Found");
155   if(ec) 155   if(ec)
156   co_return route_error(ec); 156   co_return route_error(ec);
157   co_return route_done; 157   co_return route_done;
158   } 158   }
159   159  
160   case dotfiles_policy::allow: 160   case dotfiles_policy::allow:
161   break; 161   break;
162   } 162   }
163   } 163   }
164   164  
165   // Build the file path 165   // Build the file path
166   std::string path; 166   std::string path;
167   path_cat(path, impl_->root, req_path); 167   path_cat(path, impl_->root, req_path);
168   168  
169   // Check if it's a directory 169   // Check if it's a directory
170   system::error_code fec; 170   system::error_code fec;
171   bool is_dir = std::filesystem::is_directory(path, fec); 171   bool is_dir = std::filesystem::is_directory(path, fec);
172   if(is_dir && ! fec.failed()) 172   if(is_dir && ! fec.failed())
173   { 173   {
174   // Check for trailing slash 174   // Check for trailing slash
175   if(req_path.empty() || req_path.back() != '/') 175   if(req_path.empty() || req_path.back() != '/')
176   { 176   {
177   if(impl_->opts.redirect) 177   if(impl_->opts.redirect)
178   { 178   {
179   // Redirect to add trailing slash 179   // Redirect to add trailing slash
180   std::string location(req_path); 180   std::string location(req_path);
181   location += '/'; 181   location += '/';
182   rp.res.set_status(status::moved_permanently); 182   rp.res.set_status(status::moved_permanently);
183   rp.res.set(field::location, location); 183   rp.res.set(field::location, location);
184   auto [ec] = co_await rp.send(""); 184   auto [ec] = co_await rp.send("");
185   if(ec) 185   if(ec)
186   co_return route_error(ec); 186   co_return route_error(ec);
187   co_return route_done; 187   co_return route_done;
188   } 188   }
189   } 189   }
190   190  
191   // Try index file 191   // Try index file
192   if(impl_->opts.index) 192   if(impl_->opts.index)
193   { 193   {
194   #ifdef BOOST_MSVC 194   #ifdef BOOST_MSVC
195   path += "\\index.html"; 195   path += "\\index.html";
196   #else 196   #else
197   path += "/index.html"; 197   path += "/index.html";
198   #endif 198   #endif
199   } 199   }
200   } 200   }
201   201  
202   // Prepare file response using send_file utilities 202   // Prepare file response using send_file utilities
203   send_file_options opts; 203   send_file_options opts;
204   opts.etag = impl_->opts.etag; 204   opts.etag = impl_->opts.etag;
205   opts.last_modified = impl_->opts.last_modified; 205   opts.last_modified = impl_->opts.last_modified;
206   opts.max_age = impl_->opts.max_age; 206   opts.max_age = impl_->opts.max_age;
207   207  
208   send_file_info info; 208   send_file_info info;
209   send_file_init(info, rp, path, opts); 209   send_file_init(info, rp, path, opts);
210   210  
211   // Handle result 211   // Handle result
212   switch(info.result) 212   switch(info.result)
213   { 213   {
214   case send_file_result::not_found: 214   case send_file_result::not_found:
215   { 215   {
216   if(impl_->opts.fallthrough) 216   if(impl_->opts.fallthrough)
217   co_return route_next; 217   co_return route_next;
218   rp.res.set_status(status::not_found); 218   rp.res.set_status(status::not_found);
219   auto [ec] = co_await rp.send("Not Found"); 219   auto [ec] = co_await rp.send("Not Found");
220   if(ec) 220   if(ec)
221   co_return route_error(ec); 221   co_return route_error(ec);
222   co_return route_done; 222   co_return route_done;
223   } 223   }
224   224  
225   case send_file_result::not_modified: 225   case send_file_result::not_modified:
226   { 226   {
227   rp.res.set_status(status::not_modified); 227   rp.res.set_status(status::not_modified);
228   auto [ec] = co_await rp.send(""); 228   auto [ec] = co_await rp.send("");
229   if(ec) 229   if(ec)
230   co_return route_error(ec); 230   co_return route_error(ec);
231   co_return route_done; 231   co_return route_done;
232   } 232   }
233   233  
234   case send_file_result::error: 234   case send_file_result::error:
235   { 235   {
236   // Range error - headers already set by send_file_init 236   // Range error - headers already set by send_file_init
237   auto [ec] = co_await rp.send(""); 237   auto [ec] = co_await rp.send("");
238   if(ec) 238   if(ec)
239   co_return route_error(ec); 239   co_return route_error(ec);
240   co_return route_done; 240   co_return route_done;
241   } 241   }
242   242  
243   case send_file_result::ok: 243   case send_file_result::ok:
244   break; 244   break;
245   } 245   }
246   246  
247   // Set Accept-Ranges if enabled 247   // Set Accept-Ranges if enabled
248   if(impl_->opts.accept_ranges) 248   if(impl_->opts.accept_ranges)
249   rp.res.set(field::accept_ranges, "bytes"); 249   rp.res.set(field::accept_ranges, "bytes");
250   250  
251   // Set Cache-Control with immutable if configured 251   // Set Cache-Control with immutable if configured
252   if(impl_->opts.immutable && opts.max_age > 0) 252   if(impl_->opts.immutable && opts.max_age > 0)
253   { 253   {
254   std::string cc = "public, max-age=" + 254   std::string cc = "public, max-age=" +
255   std::to_string(opts.max_age) + ", immutable"; 255   std::to_string(opts.max_age) + ", immutable";
256   rp.res.set(field::cache_control, cc); 256   rp.res.set(field::cache_control, cc);
257   } 257   }
258   258  
259   // For HEAD requests, don't send body 259   // For HEAD requests, don't send body
260   if(rp.req.method() == method::head) 260   if(rp.req.method() == method::head)
261   { 261   {
262   auto [ec] = co_await rp.send(""); 262   auto [ec] = co_await rp.send("");
263   if(ec) 263   if(ec)
264   co_return route_error(ec); 264   co_return route_error(ec);
265   co_return route_done; 265   co_return route_done;
266   } 266   }
267   267  
268   // Open and stream the file 268   // Open and stream the file
269   file f; 269   file f;
270   system::error_code ec; 270   system::error_code ec;
271   f.open(path.c_str(), file_mode::scan, ec); 271   f.open(path.c_str(), file_mode::scan, ec);
272   if(ec) 272   if(ec)
273   { 273   {
274   if(impl_->opts.fallthrough) 274   if(impl_->opts.fallthrough)
275   co_return route_next; 275   co_return route_next;
276   rp.res.set_status(status::internal_server_error); 276   rp.res.set_status(status::internal_server_error);
277   auto [ec2] = co_await rp.send("Internal Server Error"); 277   auto [ec2] = co_await rp.send("Internal Server Error");
278   if(ec2) 278   if(ec2)
279   co_return route_error(ec2); 279   co_return route_error(ec2);
280   co_return route_done; 280   co_return route_done;
281   } 281   }
282   282  
283   // Seek to range start if needed 283   // Seek to range start if needed
284   if(info.is_range && info.range_start > 0) 284   if(info.is_range && info.range_start > 0)
285   { 285   {
286   f.seek(static_cast<std::uint64_t>(info.range_start), ec); 286   f.seek(static_cast<std::uint64_t>(info.range_start), ec);
287   if(ec.failed()) 287   if(ec.failed())
288   { 288   {
289   rp.res.set_status(status::internal_server_error); 289   rp.res.set_status(status::internal_server_error);
290   auto [ec2] = co_await rp.send("Internal Server Error"); 290   auto [ec2] = co_await rp.send("Internal Server Error");
291   if(ec2) 291   if(ec2)
292   co_return route_error(ec2); 292   co_return route_error(ec2);
293   co_return route_done; 293   co_return route_done;
294   } 294   }
295   } 295   }
296   296  
297   // Calculate how much to send 297   // Calculate how much to send
298   std::int64_t remaining = info.range_end - info.range_start + 1; 298   std::int64_t remaining = info.range_end - info.range_start + 1;
299   299  
300   // Stream file content using serializer's internal buffer 300   // Stream file content using serializer's internal buffer
301   while(remaining > 0) 301   while(remaining > 0)
302   { 302   {
303   capy::mutable_buffer arr[1]; 303   capy::mutable_buffer arr[1];
304   auto bufs = rp.res_body.prepare(arr); 304   auto bufs = rp.res_body.prepare(arr);
305   if(bufs.empty()) 305   if(bufs.empty())
306   { 306   {
307   auto [ec2] = co_await rp.res_body.commit(0); 307   auto [ec2] = co_await rp.res_body.commit(0);
308   if(ec2) 308   if(ec2)
309   co_return route_error(ec2); 309   co_return route_error(ec2);
310   continue; 310   continue;
311   } 311   }
312   312  
313   auto const to_read = static_cast<std::size_t>( 313   auto const to_read = static_cast<std::size_t>(
314   (std::min)(remaining, 314   (std::min)(remaining,
315   static_cast<std::int64_t>(bufs[0].size()))); 315   static_cast<std::int64_t>(bufs[0].size())));
316   316  
317   auto const n1 = f.read(bufs[0].data(), to_read, ec); 317   auto const n1 = f.read(bufs[0].data(), to_read, ec);
318   if(ec.failed()) 318   if(ec.failed())
319   co_return route_error(ec); 319   co_return route_error(ec);
320   if(n1 == 0) 320   if(n1 == 0)
321   break; 321   break;
322   322  
323   auto [ec2] = co_await rp.res_body.commit(n1); 323   auto [ec2] = co_await rp.res_body.commit(n1);
324   if(ec2) 324   if(ec2)
325   co_return route_error(ec2); 325   co_return route_error(ec2);
326   remaining -= static_cast<std::int64_t>(n1); 326   remaining -= static_cast<std::int64_t>(n1);
327   } 327   }
328   328  
329   auto [ec3] = co_await rp.res_body.commit_eof(0); 329   auto [ec3] = co_await rp.res_body.commit_eof(0);
330   if(ec3) 330   if(ec3)
331   co_return route_error(ec3); 331   co_return route_error(ec3);
332   co_return route_done; 332   co_return route_done;
MISUBC 333   } 333   }
334   334  
335   } // http 335   } // http
336   } // boost 336   } // boost