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/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 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 : 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 0 : }
277 :
278 : } // http
279 : } // boost
|