Line data 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 0 : path_cat(
26 : std::string& result,
27 : core::string_view prefix,
28 : core::string_view suffix)
29 : {
30 0 : result = prefix;
31 :
32 : #ifdef BOOST_MSVC
33 : char constexpr path_separator = '\\';
34 : #else
35 0 : char constexpr path_separator = '/';
36 : #endif
37 0 : if(! result.empty() && result.back() == path_separator)
38 0 : 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 0 : for(auto const& c : suffix)
46 : {
47 0 : if(c == '/')
48 0 : result.push_back(path_separator);
49 : else
50 0 : result.push_back(c);
51 : }
52 0 : }
53 :
54 : // Check if path segment is a dotfile
55 : bool
56 0 : is_dotfile(core::string_view path) noexcept
57 : {
58 0 : auto pos = path.rfind('/');
59 0 : if(pos == core::string_view::npos)
60 0 : pos = 0;
61 : else
62 0 : ++pos;
63 :
64 0 : if(pos < path.size() && path[pos] == '.')
65 0 : return true;
66 :
67 0 : return false;
68 : }
69 :
70 : } // (anon)
71 :
72 : struct serve_static::impl
73 : {
74 : std::string root;
75 : serve_static_options opts;
76 :
77 0 : impl(
78 : core::string_view root_,
79 : serve_static_options const& opts_)
80 0 : : root(root_)
81 0 : , opts(opts_)
82 : {
83 0 : }
84 : };
85 :
86 0 : serve_static::
87 : ~serve_static()
88 : {
89 0 : delete impl_;
90 0 : }
91 :
92 0 : serve_static::
93 0 : serve_static(core::string_view root)
94 0 : : serve_static(root, serve_static_options{})
95 : {
96 0 : }
97 :
98 0 : serve_static::
99 : serve_static(
100 : core::string_view root,
101 0 : serve_static_options const& opts)
102 0 : : impl_(new impl(root, opts))
103 : {
104 0 : }
105 :
106 0 : serve_static::
107 0 : serve_static(serve_static&& other) noexcept
108 0 : : impl_(other.impl_)
109 : {
110 0 : other.impl_ = nullptr;
111 0 : }
112 :
113 : route_task
114 0 : 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 0 : }
326 :
327 : } // http
328 : } // boost
|