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