GCC Code Coverage Report


Directory: ./
File: libs/http/src/server/serve_static.cpp
Date: 2026-01-20 00:11:35
Exec Total Coverage
Lines: 0 40 0.0%
Functions: 0 8 0.0%
Branches: 0 26 0.0%

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