libs/http/src/server/serve_static.cpp

0.0% Lines (0/40) 0.0% Functions (0/8) 0.0% Branches (0/26)
libs/http/src/server/serve_static.cpp
Line Hits Source Code
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/file.hpp>
14 #include <boost/http/status.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 auto [ec] = co_await rp.send();
127 if(ec)
128 co_return route_error(ec);
129 co_return route_done;
130 }
131
132 // Get the request path
133 auto req_path = rp.url.path();
134
135 // Check for dotfiles
136 if(is_dotfile(req_path))
137 {
138 switch(impl_->opts.dotfiles)
139 {
140 case dotfiles_policy::deny:
141 {
142 rp.res.set_status(status::forbidden);
143 auto [ec] = co_await rp.send("Forbidden");
144 if(ec)
145 co_return route_error(ec);
146 co_return route_done;
147 }
148
149 case dotfiles_policy::ignore:
150 {
151 if(impl_->opts.fallthrough)
152 co_return route_next;
153 rp.res.set_status(status::not_found);
154 auto [ec] = co_await rp.send("Not Found");
155 if(ec)
156 co_return route_error(ec);
157 co_return route_done;
158 }
159
160 case dotfiles_policy::allow:
161 break;
162 }
163 }
164
165 // Build the file path
166 std::string path;
167 path_cat(path, impl_->root, req_path);
168
169 // Check if it's a directory
170 system::error_code fec;
171 bool is_dir = std::filesystem::is_directory(path, fec);
172 if(is_dir && ! fec.failed())
173 {
174 // Check for trailing slash
175 if(req_path.empty() || req_path.back() != '/')
176 {
177 if(impl_->opts.redirect)
178 {
179 // Redirect to add trailing slash
180 std::string location(req_path);
181 location += '/';
182 rp.res.set_status(status::moved_permanently);
183 rp.res.set(field::location, location);
184 auto [ec] = co_await rp.send("");
185 if(ec)
186 co_return route_error(ec);
187 co_return route_done;
188 }
189 }
190
191 // Try index file
192 if(impl_->opts.index)
193 {
194 #ifdef BOOST_MSVC
195 path += "\\index.html";
196 #else
197 path += "/index.html";
198 #endif
199 }
200 }
201
202 // Prepare file response using send_file utilities
203 send_file_options opts;
204 opts.etag = impl_->opts.etag;
205 opts.last_modified = impl_->opts.last_modified;
206 opts.max_age = impl_->opts.max_age;
207
208 send_file_info info;
209 send_file_init(info, rp, path, opts);
210
211 // Handle result
212 switch(info.result)
213 {
214 case send_file_result::not_found:
215 {
216 if(impl_->opts.fallthrough)
217 co_return route_next;
218 rp.res.set_status(status::not_found);
219 auto [ec] = co_await rp.send("Not Found");
220 if(ec)
221 co_return route_error(ec);
222 co_return route_done;
223 }
224
225 case send_file_result::not_modified:
226 {
227 rp.res.set_status(status::not_modified);
228 auto [ec] = co_await rp.send("");
229 if(ec)
230 co_return route_error(ec);
231 co_return route_done;
232 }
233
234 case send_file_result::error:
235 {
236 // Range error - headers already set by send_file_init
237 auto [ec] = co_await rp.send("");
238 if(ec)
239 co_return route_error(ec);
240 co_return route_done;
241 }
242
243 case send_file_result::ok:
244 break;
245 }
246
247 // Set Accept-Ranges if enabled
248 if(impl_->opts.accept_ranges)
249 rp.res.set(field::accept_ranges, "bytes");
250
251 // Set Cache-Control with immutable if configured
252 if(impl_->opts.immutable && opts.max_age > 0)
253 {
254 std::string cc = "public, max-age=" +
255 std::to_string(opts.max_age) + ", immutable";
256 rp.res.set(field::cache_control, cc);
257 }
258
259 // For HEAD requests, don't send body
260 if(rp.req.method() == method::head)
261 {
262 auto [ec] = co_await rp.send("");
263 if(ec)
264 co_return route_error(ec);
265 co_return route_done;
266 }
267
268 // Open and stream the file
269 file f;
270 system::error_code ec;
271 f.open(path.c_str(), file_mode::scan, ec);
272 if(ec)
273 {
274 if(impl_->opts.fallthrough)
275 co_return route_next;
276 rp.res.set_status(status::internal_server_error);
277 auto [ec2] = co_await rp.send("Internal Server Error");
278 if(ec2)
279 co_return route_error(ec2);
280 co_return route_done;
281 }
282
283 // Seek to range start if needed
284 if(info.is_range && info.range_start > 0)
285 {
286 f.seek(static_cast<std::uint64_t>(info.range_start), ec);
287 if(ec.failed())
288 {
289 rp.res.set_status(status::internal_server_error);
290 auto [ec2] = co_await rp.send("Internal Server Error");
291 if(ec2)
292 co_return route_error(ec2);
293 co_return route_done;
294 }
295 }
296
297 // Calculate how much to send
298 std::int64_t remaining = info.range_end - info.range_start + 1;
299
300 // Stream file content
301 constexpr std::size_t buf_size = 16384;
302 char buffer[buf_size];
303
304 while(remaining > 0)
305 {
306 auto const to_read = static_cast<std::size_t>(
307 (std::min)(remaining, static_cast<std::int64_t>(buf_size)));
308
309 auto const n1 = f.read(buffer, to_read, ec);
310 if(ec.failed() || n1 == 0)
311 break;
312
313 auto [ec2, n2] = co_await rp.res_body.write(
314 capy::const_buffer(buffer, n1));
315 (void)n2;
316 if(ec2)
317 co_return route_error(ec2);
318 remaining -= static_cast<std::int64_t>(n1);
319 }
320
321 auto [ec3] = co_await rp.res_body.write_eof();
322 if(ec3)
323 co_return route_error(ec3);
324 co_return route_done;
325 }
326
327 } // http
328 } // boost
329