0.00% Lines (0/85) 0.00% Functions (0/3)
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/send_file.hpp> 10   #include <boost/http/server/send_file.hpp>
11   #include <boost/http/server/etag.hpp> 11   #include <boost/http/server/etag.hpp>
12   #include <boost/http/server/fresh.hpp> 12   #include <boost/http/server/fresh.hpp>
13   #include <boost/http/server/mime_types.hpp> 13   #include <boost/http/server/mime_types.hpp>
14   #include <boost/http/server/range_parser.hpp> 14   #include <boost/http/server/range_parser.hpp>
15   #include <boost/http/field.hpp> 15   #include <boost/http/field.hpp>
16   #include <boost/http/status.hpp> 16   #include <boost/http/status.hpp>
17   #include <ctime> 17   #include <ctime>
18   #include <filesystem> 18   #include <filesystem>
19   19  
20   namespace boost { 20   namespace boost {
21   namespace http { 21   namespace http {
22   22  
23   namespace { 23   namespace {
24   24  
25   // Get file stats 25   // Get file stats
26   bool 26   bool
MISUBC 27   get_file_stats( 27   get_file_stats(
28   core::string_view path, 28   core::string_view path,
29   std::uint64_t& size, 29   std::uint64_t& size,
30   std::uint64_t& mtime) 30   std::uint64_t& mtime)
31   { 31   {
MISUBC 32   system::error_code ec; 32   system::error_code ec;
MISUBC 33   std::filesystem::path p(path.begin(), path.end()); 33   std::filesystem::path p(path.begin(), path.end());
34   34  
MISUBC 35   auto status = std::filesystem::status(p, ec); 35   auto status = std::filesystem::status(p, ec);
MISUBC 36   if(ec.failed() || ! std::filesystem::is_regular_file(status)) 36   if(ec.failed() || ! std::filesystem::is_regular_file(status))
MISUBC 37   return false; 37   return false;
38   38  
MISUBC 39   size = static_cast<std::uint64_t>( 39   size = static_cast<std::uint64_t>(
MISUBC 40   std::filesystem::file_size(p, ec)); 40   std::filesystem::file_size(p, ec));
MISUBC 41   if(ec.failed()) 41   if(ec.failed())
MISUBC 42   return false; 42   return false;
43   43  
MISUBC 44   auto ftime = std::filesystem::last_write_time(p, ec); 44   auto ftime = std::filesystem::last_write_time(p, ec);
MISUBC 45   if(ec.failed()) 45   if(ec.failed())
MISUBC 46   return false; 46   return false;
47   47  
48   // Convert to Unix timestamp 48   // Convert to Unix timestamp
49   auto const sctp = std::chrono::time_point_cast< 49   auto const sctp = std::chrono::time_point_cast<
MISUBC 50   std::chrono::system_clock::duration>( 50   std::chrono::system_clock::duration>(
MISUBC 51   ftime - std::filesystem::file_time_type::clock::now() + 51   ftime - std::filesystem::file_time_type::clock::now() +
MISUBC 52   std::chrono::system_clock::now()); 52   std::chrono::system_clock::now());
MISUBC 53   mtime = static_cast<std::uint64_t>( 53   mtime = static_cast<std::uint64_t>(
MISUBC 54   std::chrono::system_clock::to_time_t(sctp)); 54   std::chrono::system_clock::to_time_t(sctp));
55   55  
MISUBC 56   return true; 56   return true;
MISUBC 57   } 57   }
58   58  
59   } // (anon) 59   } // (anon)
60   60  
61   std::string 61   std::string
MISUBC 62   format_http_date(std::uint64_t mtime) 62   format_http_date(std::uint64_t mtime)
63   { 63   {
MISUBC 64   std::time_t t = static_cast<std::time_t>(mtime); 64   std::time_t t = static_cast<std::time_t>(mtime);
65   std::tm tm; 65   std::tm tm;
66   #ifdef _WIN32 66   #ifdef _WIN32
67   gmtime_s(&tm, &t); 67   gmtime_s(&tm, &t);
68   #else 68   #else
MISUBC 69   gmtime_r(&t, &tm); 69   gmtime_r(&t, &tm);
70   #endif 70   #endif
71   71  
72   char buf[64]; 72   char buf[64];
MISUBC 73   std::strftime(buf, sizeof(buf), 73   std::strftime(buf, sizeof(buf),
74   "%a, %d %b %Y %H:%M:%S GMT", &tm); 74   "%a, %d %b %Y %H:%M:%S GMT", &tm);
MISUBC 75   return std::string(buf); 75   return std::string(buf);
76   } 76   }
77   77  
78   void 78   void
MISUBC 79   send_file_init( 79   send_file_init(
80   send_file_info& info, 80   send_file_info& info,
81   route_params& rp, 81   route_params& rp,
82   core::string_view path, 82   core::string_view path,
83   send_file_options const& opts) 83   send_file_options const& opts)
84   { 84   {
MISUBC 85   info = send_file_info{}; 85   info = send_file_info{};
86   86  
87   // Get file stats 87   // Get file stats
MISUBC 88   if(! get_file_stats(path, info.size, info.mtime)) 88   if(! get_file_stats(path, info.size, info.mtime))
89   { 89   {
MISUBC 90   info.result = send_file_result::not_found; 90   info.result = send_file_result::not_found;
MISUBC 91   return; 91   return;
92   } 92   }
93   93  
94   // Determine content type 94   // Determine content type
MISUBC 95   if(! opts.content_type.empty()) 95   if(! opts.content_type.empty())
96   { 96   {
MISUBC 97   info.content_type = opts.content_type; 97   info.content_type = opts.content_type;
98   } 98   }
99   else 99   else
100   { 100   {
MISUBC 101   auto ct = mime_types::content_type(path); 101   auto ct = mime_types::content_type(path);
MISUBC 102   if(ct.empty()) 102   if(ct.empty())
MISUBC 103   ct = "application/octet-stream"; 103   ct = "application/octet-stream";
MISUBC 104   info.content_type = std::move(ct); 104   info.content_type = std::move(ct);
MISUBC 105   } 105   }
106   106  
107   // Generate ETag if enabled 107   // Generate ETag if enabled
MISUBC 108   if(opts.etag) 108   if(opts.etag)
109   { 109   {
MISUBC 110   info.etag = etag(info.size, info.mtime); 110   info.etag = etag(info.size, info.mtime);
MISUBC 111   rp.res.set(field::etag, info.etag); 111   rp.res.set(field::etag, info.etag);
112   } 112   }
113   113  
114   // Set Last-Modified if enabled 114   // Set Last-Modified if enabled
MISUBC 115   if(opts.last_modified) 115   if(opts.last_modified)
116   { 116   {
MISUBC 117   info.last_modified = format_http_date(info.mtime); 117   info.last_modified = format_http_date(info.mtime);
MISUBC 118   rp.res.set(field::last_modified, info.last_modified); 118   rp.res.set(field::last_modified, info.last_modified);
119   } 119   }
120   120  
121   // Set Cache-Control 121   // Set Cache-Control
MISUBC 122   if(opts.max_age > 0) 122   if(opts.max_age > 0)
123   { 123   {
124   std::string cc = "public, max-age=" + 124   std::string cc = "public, max-age=" +
MISUBC 125   std::to_string(opts.max_age); 125   std::to_string(opts.max_age);
MISUBC 126   rp.res.set(field::cache_control, cc); 126   rp.res.set(field::cache_control, cc);
MISUBC 127   } 127   }
128   128  
129   // Check freshness (conditional GET) 129   // Check freshness (conditional GET)
MISUBC 130   if(is_fresh(rp.req, rp.res)) 130   if(is_fresh(rp.req, rp.res))
131   { 131   {
MISUBC 132   info.result = send_file_result::not_modified; 132   info.result = send_file_result::not_modified;
MISUBC 133   return; 133   return;
134   } 134   }
135   135  
136   // Set Content-Type 136   // Set Content-Type
MISUBC 137   rp.res.set(field::content_type, info.content_type); 137   rp.res.set(field::content_type, info.content_type);
138   138  
139   // Handle Range header 139   // Handle Range header
MISUBC 140   auto range_header = rp.req.value_or(field::range, ""); 140   auto range_header = rp.req.value_or(field::range, "");
MISUBC 141   if(! range_header.empty()) 141   if(! range_header.empty())
142   { 142   {
143   auto range_result = parse_range( 143   auto range_result = parse_range(
MISUBC 144   static_cast<std::int64_t>(info.size), 144   static_cast<std::int64_t>(info.size),
MISUBC 145   range_header); 145   range_header);
146   146  
MISUBC 147   if(range_result.type == range_result_type::ok && 147   if(range_result.type == range_result_type::ok &&
MISUBC 148   ! range_result.ranges.empty()) 148   ! range_result.ranges.empty())
149   { 149   {
150   // Use first range only (simplification) 150   // Use first range only (simplification)
MISUBC 151   auto const& range = range_result.ranges[0]; 151   auto const& range = range_result.ranges[0];
MISUBC 152   info.is_range = true; 152   info.is_range = true;
MISUBC 153   info.range_start = range.start; 153   info.range_start = range.start;
MISUBC 154   info.range_end = range.end; 154   info.range_end = range.end;
155   155  
156   // Set 206 Partial Content 156   // Set 206 Partial Content
MISUBC 157   rp.res.set_status(status::partial_content); 157   rp.res.set_status(status::partial_content);
158   158  
MISUBC 159   auto const content_length = 159   auto const content_length =
MISUBC 160   range.end - range.start + 1; 160   range.end - range.start + 1;
MISUBC 161   rp.res.set_payload_size( 161   rp.res.set_payload_size(
162   static_cast<std::uint64_t>(content_length)); 162   static_cast<std::uint64_t>(content_length));
163   163  
164   // Content-Range header 164   // Content-Range header
MISUBC 165   std::string cr = "bytes " + 165   std::string cr = "bytes " +
MISUBC 166   std::to_string(range.start) + "-" + 166   std::to_string(range.start) + "-" +
MISUBC 167   std::to_string(range.end) + "/" + 167   std::to_string(range.end) + "/" +
MISUBC 168   std::to_string(info.size); 168   std::to_string(info.size);
MISUBC 169   rp.res.set(field::content_range, cr); 169   rp.res.set(field::content_range, cr);
170   170  
MISUBC 171   info.result = send_file_result::ok; 171   info.result = send_file_result::ok;
MISUBC 172   return; 172   return;
MISUBC 173   } 173   }
174   174  
MISUBC 175   if(range_result.type == range_result_type::unsatisfiable) 175   if(range_result.type == range_result_type::unsatisfiable)
176   { 176   {
MISUBC 177   rp.res.set_status( 177   rp.res.set_status(
178   status::range_not_satisfiable); 178   status::range_not_satisfiable);
MISUBC 179   rp.res.set(field::content_range, 179   rp.res.set(field::content_range,
MISUBC 180   "bytes */" + std::to_string(info.size)); 180   "bytes */" + std::to_string(info.size));
MISUBC 181   info.result = send_file_result::error; 181   info.result = send_file_result::error;
MISUBC 182   return; 182   return;
183   } 183   }
184   // If malformed, ignore and serve full content 184   // If malformed, ignore and serve full content
MISUBC 185   } 185   }
186   186  
187   // Full content response 187   // Full content response
MISUBC 188   rp.res.set_status(status::ok); 188   rp.res.set_status(status::ok);
MISUBC 189   rp.res.set_payload_size(info.size); 189   rp.res.set_payload_size(info.size);
MISUBC 190   info.range_start = 0; 190   info.range_start = 0;
MISUBC 191   info.range_end = static_cast<std::int64_t>(info.size) - 1; 191   info.range_end = static_cast<std::int64_t>(info.size) - 1;
MISUBC 192   info.result = send_file_result::ok; 192   info.result = send_file_result::ok;
193   } 193   }
194   194  
195   } // http 195   } // http
196   } // boost 196   } // boost