roar
Loading...
Searching...
No Matches
directory_server.hpp
Go to the documentation of this file.
1#pragma once
2
6#include <roar/mime_type.hpp>
10
11#include <boost/beast/http/file_body.hpp>
12
13#include <ctime>
14#include <utility>
15#include <filesystem>
16#include <sstream>
17#include <chrono>
18
19namespace Roar::Detail
20{
21 namespace
22 {
23 inline static std::string to_string(std::filesystem::file_time_type const& ftime)
24 {
25#ifdef _MSC_VER
26 auto cftime =
27 std::chrono::system_clock::to_time_t(std::chrono::time_point_cast<std::chrono::system_clock::duration>(
28 std::chrono::utc_clock::to_sys(std::chrono::file_clock::to_utc(ftime))));
29#else
30 auto cftime =
31 std::chrono::system_clock::to_time_t(std::chrono::time_point_cast<std::chrono::system_clock::duration>(
32 std::chrono::file_clock::to_sys(ftime)));
33#endif
34 std::string result(1024, '\0');
35#ifdef _MSC_VER
36# pragma clang diagnostic push
37# pragma clang diagnostic ignored "-Wdeprecated-declarations"
38 auto size = std::strftime(result.data(), result.size(), "%a %b %e %H:%M:%S %Y", std::localtime(&cftime));
39# pragma clang diagnostic pop
40#else
41 auto size = std::strftime(result.data(), result.size(), "%a %b %e %H:%M:%S %Y", std::localtime(&cftime));
42#endif
43 result.resize(size);
44 return result;
45 }
46 }
47
48 template <typename RequestListenerT>
50 {
53 std::shared_ptr<RequestListenerT> listener_;
54 };
55
61 template <typename RequestListenerT>
62 class DirectoryServer : private DirectoryServerConstructionArgs<RequestListenerT>
63 {
64 public:
66 : DirectoryServerConstructionArgs<RequestListenerT>{std::move(args)}
68 , basePath_{this->serveInfo_.path}
69 , onError_{unwrapFlexibleProvider<RequestListenerT, std::function<void(std::string const&)>>(
70 *this->listener_,
71 this->serveInfo_.serveOptions.onError)}
72 , onFileServeComplete_{unwrapFlexibleProvider<RequestListenerT, std::function<void(bool)>>(
73 *this->listener_,
74 this->serveInfo_.serveOptions.onFileServeComplete)}
75 {
76 if (!onError_)
77 onError_ = [](std::string const&) {};
79 onFileServeComplete_ = [](bool) {};
80 }
81
82 void operator()(Session& session, EmptyBodyRequest const& req)
83 {
84 namespace http = boost::beast::http;
85
86 // Do now allow unsecure connections if not explicitly allowed
87 if (this->serverIsSecure_ && !session.isSecure() && !this->serveInfo_.routeOptions.allowUnsecure)
89
90 const auto fileAndStatus = getFileAndStatus(req.path());
91
92 // File is found, now ask the library user for permissions:
93 switch (std::invoke(
94 this->serveInfo_.handler, *this->listener_, session, req, fileAndStatus, this->serveInfo_.serveOptions))
95 {
97 break;
99 return session.sendStandardResponse(boost::beast::http::status::forbidden);
101 return;
102 };
103
104 // Filter allowed methods
105 switch (req.method())
106 {
107 case (http::verb::head):
108 {
109 if (!unwrapFlexibleProvider<RequestListenerT, bool>(
110 *this->listener_, this->serveInfo_.serveOptions.allowDownload))
111 return session.sendStandardResponse(http::status::method_not_allowed);
112 return sendHeadResponse(session, req, fileAndStatus);
113 }
114 case (http::verb::options):
115 {
116 return sendOptionsResponse(session, req, fileAndStatus);
117 }
118 case (http::verb::get):
119 {
120 if (!unwrapFlexibleProvider<RequestListenerT, bool>(
121 *this->listener_, this->serveInfo_.serveOptions.allowDownload))
122 return session.sendStandardResponse(http::status::method_not_allowed);
123 break;
124 }
125 case (http::verb::delete_):
126 {
127 if (!unwrapFlexibleProvider<RequestListenerT, bool>(
128 *this->listener_, this->serveInfo_.serveOptions.allowDelete))
129 return session.sendStandardResponse(http::status::method_not_allowed);
130 break;
131 }
132 case (http::verb::put):
133 {
134 if (!unwrapFlexibleProvider<RequestListenerT, bool>(
135 *this->listener_, this->serveInfo_.serveOptions.allowUpload))
136 return session.sendStandardResponse(http::status::method_not_allowed);
137 break;
138 }
139 default:
140 {
141 return session.sendStandardResponse(http::status::method_not_allowed);
142 }
143 }
144
145 // Handle not found files and directories:
146 if (fileAndStatus.status.type() == std::filesystem::file_type::none && req.method() != http::verb::put)
147 return session.sendStandardResponse(http::status::not_found);
148
149 handleFileServe(session, req, fileAndStatus);
150 }
151
152 private:
153 std::filesystem::path resolvePath() const
154 {
155 const auto rawPath = unwrapFlexibleProvider<RequestListenerT, std::filesystem::path>(
156 *this->listener_, this->serveInfo_.serveOptions.pathProvider);
157 return Roar::resolvePath(rawPath);
158 }
159
160 std::string listingStyle() const
161 {
162 auto style = unwrapFlexibleProvider<RequestListenerT, std::string>(
163 *this->listener_, this->serveInfo_.serveOptions.customListingStyle);
164 if (!style)
166 return *style;
167 }
168
169 FileAndStatus getFileAndStatus(boost::beast::string_view target) const
170 {
171 auto analyzeFile = [this](boost::beast::string_view target) -> FileAndStatus {
172 const auto absolute = jail_.pathAsIsInJail(std::filesystem::path{std::string{target}});
173 if (absolute)
174 {
175 const auto relative = jail_.relativeToRoot(std::filesystem::path{std::string{target}});
176 if (std::filesystem::exists(*absolute))
177 return {.file = *absolute, .relative = *relative, .status = std::filesystem::status(*absolute)};
178 else
179 return {.file = *absolute, .relative = *relative, .status = std::filesystem::file_status{}};
180 }
181 return {.file = {}, .status = std::filesystem::file_status{}};
182 };
183
184 std::pair<std::filesystem::file_type, std::filesystem::path> fileAndStatus;
185 if (target.size() == basePath_.size())
186 return analyzeFile("");
187 target.remove_prefix(basePath_.size() + (basePath_.size() == 1 ? 0 : 1));
188 return analyzeFile(target);
189 }
190
191 void handleFileServe(Session& session, EmptyBodyRequest const& req, FileAndStatus const& fileAndStatus) const
192 {
193 namespace http = boost::beast::http;
194 switch (req.method())
195 {
196 case (http::verb::get):
197 {
198 if (fileAndStatus.status.type() == std::filesystem::file_type::directory)
199 {
200 if (unwrapFlexibleProvider<RequestListenerT, bool>(
201 *this->listener_, this->serveInfo_.serveOptions.allowListing))
202 return makeListing(session, req, fileAndStatus);
203 else
204 {
205 return download(
206 session,
207 req,
208 getFileAndStatus((basePath_ / fileAndStatus.relative / "index.html").string()));
209 }
210 }
211 else
212 return download(session, req, fileAndStatus);
213 }
214 case (http::verb::delete_):
215 {
216 std::error_code ec;
217 auto type = fileAndStatus.status.type();
218 if (type == std::filesystem::file_type::regular || type == std::filesystem::file_type::symlink)
219 {
220 std::filesystem::remove(fileAndStatus.file, ec);
221 }
222 else if (fileAndStatus.status.type() == std::filesystem::file_type::directory)
223 {
224 if (unwrapFlexibleProvider<RequestListenerT, bool>(
225 *this->listener_, this->serveInfo_.serveOptions.allowDeleteOfNonEmptyDirectories))
226 std::filesystem::remove_all(fileAndStatus.file, ec);
227 else
228 {
229 if (std::filesystem::is_empty(fileAndStatus.file))
230 std::filesystem::remove(fileAndStatus.file, ec);
231 else
232 return session.sendStandardResponse(http::status::forbidden);
233 }
234 }
235 else
236 {
237 ec = std::make_error_code(std::errc::operation_not_supported);
238 }
239
240 if (ec)
241 return session.sendStandardResponse(http::status::bad_request, ec.message());
242 else
243 return (void)session.send<http::empty_body>(req)->status(http::status::no_content).commit();
244 }
245 case (http::verb::put):
246 {
247 return upload(session, req, fileAndStatus);
248 }
249 // Cannot happen:
250 default:
251 return;
252 }
253 }
254
255 void sendHeadResponse(Session& session, EmptyBodyRequest const& req, FileAndStatus const& fileAndStatus) const
256 {
257 namespace http = boost::beast::http;
258 const auto contentType = extensionToMime(fileAndStatus.file.extension().string());
259
260 if (fileAndStatus.status.type() != std::filesystem::file_type::regular &&
261 fileAndStatus.status.type() != std::filesystem::file_type::symlink)
262 return session.sendStandardResponse(http::status::not_found);
263
264 session.send<http::empty_body>(req)
265 ->status(http::status::ok)
266 .setHeader(http::field::accept_ranges, "bytes")
267 .setHeader(http::field::content_length, std::to_string(std::filesystem::file_size(fileAndStatus.file)))
268 .contentType(contentType ? *contentType : "application/octet-stream")
269 .commit()
270 .fail([onError = onError_](auto&& err) {
271 onError(err.toString());
272 });
273 }
274
275 void sendOptionsResponse(Session& session, EmptyBodyRequest const& req, FileAndStatus const&) const
276 {
277 namespace http = boost::beast::http;
278 std::string allow = "OPTIONS";
279 if (unwrapFlexibleProvider<RequestListenerT, bool>(
280 *this->listener_, this->serveInfo_.serveOptions.allowDownload))
281 allow += ", GET, HEAD";
282 if (unwrapFlexibleProvider<RequestListenerT, bool>(
283 *this->listener_, this->serveInfo_.serveOptions.allowUpload))
284 allow += ", PUT";
285 if (unwrapFlexibleProvider<RequestListenerT, bool>(
286 *this->listener_, this->serveInfo_.serveOptions.allowDelete))
287 allow += ", DELETE";
288
289 session.send<http::empty_body>(req)
290 ->status(http::status::no_content)
291 .setHeader(http::field::allow, allow)
292 .setHeader(http::field::accept_ranges, "bytes")
293 .commit();
294 }
295
296 void makeListing(Session& session, EmptyBodyRequest const& req, FileAndStatus const& fileAndStatus) const
297 {
298 namespace http = boost::beast::http;
299 std::stringstream document;
300 document << "<!DOCTYPE html>\n";
301 document << "<html>\n";
302 document << "<head>\n";
303 document << "<meta charset='utf-8'>\n";
304 document << "<meta http-equiv='X-UA-Compatible' content='IE=edge'>\n";
305 document << "<style>\n";
306 document << listingStyle() << "</style>\n";
307 document << "<title>Roar Listing of " << fileAndStatus.relative.string() << "</title>\n";
308 document << "<meta name='viewport' content='width=device-width, initial-scale=1'>\n";
309 document << "</head>\n";
310 document << "<body>\n";
311 document << "<h1>Index of " << fileAndStatus.relative.string() << "</h1>\n";
312 document << "<table class=\"styled-table\">\n";
313 document << "<thead>\n";
314 document << "<tr>\n";
315 document << "<th>"
316 << "Name"
317 << "</th>\n";
318 document << "<th>"
319 << "Last Modified"
320 << "</th>\n";
321 document << "<th>"
322 << "Size"
323 << "</th>\n";
324 document << "</tr>\n";
325 document << "</thead>\n";
326 document << "<tbody>\n";
327
328 try
329 {
330 for (auto const& entry : std::filesystem::directory_iterator{fileAndStatus.file})
331 {
332 std::string link;
333 std::string fn = entry.path().filename().string();
334 auto relative = fileAndStatus.relative.string();
335 link.reserve(relative.size() + basePath_.size() + fn.size() + 10);
336 if (basePath_.empty() || basePath_.front() != '/')
337 link.push_back('/');
338 link += basePath_;
339 if (!basePath_.empty() && basePath_.back() != '/')
340 link.push_back('/');
341
342 if (!relative.empty() && relative != ".")
343 {
344 link += relative;
345 link.push_back('/');
346 }
347 link += fn;
348
349 document << "<tr>\n";
350 document << "<td>\n";
351 document << "<a href='" << link << "'>" << fn << "</a>\n";
352 if (entry.is_regular_file())
353 {
354 document << "<td>" << to_string(entry.last_write_time()) << "</td>\n";
355 document << "<td>" << bytesToHumanReadable<1024>(entry.file_size()) << "</td>\n";
356 }
357 else
358 {
359 document << "<td>"
360 << "</td>\n";
361 document << "<td>"
362 << "</td>\n";
363 }
364 document << "</td>\n";
365 document << "</tr>\n";
366 }
367 }
368 catch (std::exception const& exc)
369 {
370 return session.sendStandardResponse(boost::beast::http::status::internal_server_error, exc.what());
371 }
372
373 document << "</table>\n";
374 document << "</tbody>\n";
375 document << "</body>\n";
376 document << "</html>\n";
377
378 session.send<http::string_body>(req)
379 ->status(http::status::ok)
380 .contentType("text/html")
381 .body(document.str())
382 .commit();
383 }
384
385 void upload(Session& session, EmptyBodyRequest const& req, FileAndStatus const& fileAndStatus) const
386 {
387 namespace http = boost::beast::http;
388 if (!req.expectsContinue())
389 {
390 return (void)session.send<http::string_body>(req)
391 ->status(http::status::expectation_failed)
392 .setHeader(http::field::connection, "close")
393 .body("Set Expect: 100-continue")
394 .commit();
395 }
396
397 switch (fileAndStatus.status.type())
398 {
399 case (std::filesystem::file_type::none):
400 break;
401 case (std::filesystem::file_type::not_found):
402 break;
403 case (std::filesystem::file_type::regular):
404 {
405 if (unwrapFlexibleProvider<RequestListenerT, bool>(
406 *this->listener_, this->serveInfo_.serveOptions.allowOverwrite))
407 break;
408 }
409 default:
410 {
411 return session.sendStandardResponse(http::status::forbidden);
412 }
413 }
414
415 auto contentLength = req.contentLength();
416 if (!contentLength)
417 return session.sendStandardResponse(http::status::bad_request, "Require Content-Length.");
418
419 auto body = std::make_shared<http::file_body::value_type>();
420 boost::beast::error_code ec;
421 body->open(fileAndStatus.file.string().c_str(), boost::beast::file_mode::write, ec);
422 if (ec)
423 return session.sendStandardResponse(
424 http::status::internal_server_error, "Cannot open file for writing.");
425
426 session.send<http::empty_body>(req)
427 ->status(http::status::continue_)
428 .commit()
429 .then([session = session.shared_from_this(),
430 req,
431 body = std::move(body),
432 contentLength,
433 onError = onError_](bool closed) {
434 if (closed)
435 return;
436
437 session->read<http::file_body>(req, std::move(*body))
438 ->bodyLimit(*contentLength)
439 .commit()
440 .then([](auto& session, auto const&) {
441 session.sendStandardResponse(http::status::ok);
442 })
443 .fail([session, onError](Error const& e) {
444 onError(e.toString());
445 session->sendStandardResponse(http::status::internal_server_error, e.toString());
446 });
447 });
448 }
449
450 void download(Session& session, EmptyBodyRequest const& req, FileAndStatus const& fileAndStatus) const
451 {
452 namespace http = boost::beast::http;
453 if (fileAndStatus.status.type() != std::filesystem::file_type::regular &&
454 fileAndStatus.status.type() != std::filesystem::file_type::symlink)
455 return session.sendStandardResponse(http::status::not_found);
456
457 const auto ranges = req.ranges();
458 if (!ranges)
459 {
460 http::file_body::value_type body;
461 boost::beast::error_code ec;
462 body.open(fileAndStatus.file.string().c_str(), boost::beast::file_mode::read, ec);
463 if (ec)
464 return session.sendStandardResponse(
465 http::status::internal_server_error, "Cannot open file for reading.");
466
467 auto contentType = extensionToMime(fileAndStatus.file.extension().string());
468 auto intermediate = session.send<http::file_body>(req, std::move(body));
469 intermediate->preparePayload();
470 intermediate->enableCors(req, this->serveInfo_.routeOptions.cors);
471 intermediate->contentType(contentType ? contentType.value() : "application/octet-stream");
472 intermediate->commit()
473 .then([session = session.shared_from_this(), req, onFileServeComplete = onFileServeComplete_](
474 bool wasClosed) {
475 onFileServeComplete(wasClosed);
476 })
477 .fail([onError = onError_](auto&& err) {
478 onError(err.toString());
479 });
480 }
481 else
482 {
484 boost::beast::error_code ec;
485 body.open(fileAndStatus.file.string().c_str(), std::ios_base::in, ec);
486 if (ec)
487 return session.sendStandardResponse(
488 http::status::internal_server_error, "Cannot open file for reading.");
489 try
490 {
491 body.setReadRanges(*ranges, "plain/text");
492
493 session.send<RangeFileBody>(req, std::move(body))
494 ->useFixedTimeout(std::chrono::seconds{10})
495 .commit()
496 .then([session = session.shared_from_this(), req, onFileServeComplete = onFileServeComplete_](
497 bool wasClosed) {
498 onFileServeComplete(wasClosed);
499 })
500 .fail([onError = onError_](auto&& err) {
501 onError(err.toString());
502 });
503 }
504 catch (...)
505 {
506 return session.sendStandardResponse(http::status::bad_request, "Invalid ranges.");
507 }
508 }
509 }
510
511 private:
513 std::string basePath_;
514 std::function<void(std::string const&)> onError_;
515 std::function<void(bool)> onFileServeComplete_;
516 };
517}
Internal helper class to serve directories.
Definition directory_server.hpp:63
Jail jail_
Definition directory_server.hpp:512
void makeListing(Session &session, EmptyBodyRequest const &req, FileAndStatus const &fileAndStatus) const
Definition directory_server.hpp:296
void operator()(Session &session, EmptyBodyRequest const &req)
Definition directory_server.hpp:82
std::string listingStyle() const
Definition directory_server.hpp:160
DirectoryServer(DirectoryServerConstructionArgs< RequestListenerT > &&args)
Definition directory_server.hpp:65
void handleFileServe(Session &session, EmptyBodyRequest const &req, FileAndStatus const &fileAndStatus) const
Definition directory_server.hpp:191
std::function< void(std::string const &)> onError_
Definition directory_server.hpp:514
FileAndStatus getFileAndStatus(boost::beast::string_view target) const
Definition directory_server.hpp:169
std::function< void(bool)> onFileServeComplete_
Definition directory_server.hpp:515
void sendOptionsResponse(Session &session, EmptyBodyRequest const &req, FileAndStatus const &) const
Definition directory_server.hpp:275
void upload(Session &session, EmptyBodyRequest const &req, FileAndStatus const &fileAndStatus) const
Definition directory_server.hpp:385
std::string basePath_
Definition directory_server.hpp:513
void sendHeadResponse(Session &session, EmptyBodyRequest const &req, FileAndStatus const &fileAndStatus) const
Definition directory_server.hpp:255
void download(Session &session, EmptyBodyRequest const &req, FileAndStatus const &fileAndStatus) const
Definition directory_server.hpp:450
std::filesystem::path resolvePath() const
Definition directory_server.hpp:153
Support range requests for get requests including multipart/byterange.
Definition range_file_body.hpp:23
void open(std::filesystem::path const &filename, std::ios_base::openmode mode, std::error_code &ec)
Opens the file.
Definition range_file_body.hpp:138
void setReadRanges(Ranges const &ranges, std::string_view contentType)
Set the Read Range.
Definition range_file_body.hpp:160
Definition jail.hpp:12
std::optional< std::filesystem::path > relativeToRoot(std::filesystem::path const &other, bool fakeJailAsRoot=false) const
Definition jail.cpp:27
std::optional< std::filesystem::path > pathAsIsInJail(std::filesystem::path const &other) const
First tests if the path is in the jail and then returns a full path of the resource including the jai...
Definition jail.cpp:44
This body is a file body, but with range request support.
Definition range_file_body.hpp:315
This class extends the boost::beast::http::request<BodyT> with additional convenience.
Definition request.hpp:52
bool expectsContinue() const
Returns true if the request expects a continue response.
Definition request.hpp:278
std::optional< Ranges > ranges() const
Extracts the Range header.
Definition request.hpp:329
std::optional< std::size_t > contentLength() const
Returns the content length header.
Definition request.hpp:305
std::string path() const
Returns only the path of the url.
Definition request.hpp:91
Definition session.hpp:41
std::shared_ptr< SendIntermediate< BodyT > > send(Request< OriginalBodyT > const &req, Forwards &&... forwards)
Definition session.hpp:398
void sendStrictTransportSecurityResponse()
Sends a 403 with Strict-Transport-Security. Used only for unencrypted request on enforced HTTPS.
Definition session.cpp:336
void sendStandardResponse(boost::beast::http::status status, std::string_view additionalInfo="")
Sends a standard response like "404 not found".
Definition session.cpp:328
std::shared_ptr< ReadIntermediate< BodyT > > read(Request< OriginalBodyT > req, Forwards &&... forwardArgs)
Read data from the client.
Definition session.hpp:629
bool isSecure() const
Returns whether or not this is an encrypted session.
Definition session.cpp:230
Definition range_file_body.hpp:18
const auto ranges
Definition ranges.cpp:19
constexpr char const * defaultListingStyle
Definition default_listing_style.hpp:5
std::string to_string(AuthorizationScheme scheme)
Definition authorization.cpp:28
std::filesystem::path resolvePath(std::filesystem::path const &path)
Will replace prefixes like "~" and "%appdata%" with actual directories on linux and windows.
Definition special_paths.cpp:73
std::optional< T > unwrapFlexibleProvider(HolderClassT &holder, FlexibleProvider< HolderClassT, T, true > const &flexibleProvider)
Definition flexible_provider.hpp:116
@ Deny
Will respond with a standard forbidden response.
@ Handled
You handled the response, so just end the connection if necessary.
@ Continue
Continue to serve/delete/upload the file.
std::optional< std::string > extensionToMime(std::string const &extension)
Definition mime_type.cpp:699
Definition directory_server.hpp:50
bool serverIsSecure_
Definition directory_server.hpp:51
std::shared_ptr< RequestListenerT > listener_
Definition directory_server.hpp:53
ServeInfo< RequestListenerT > serveInfo_
Definition directory_server.hpp:52
Holds errors that are produced asynchronously anywhere.
Definition error.hpp:20
std::string toString() const
Definition error.hpp:40
Definition request_listener.hpp:92
std::filesystem::path file
Definition request_listener.hpp:93
std::filesystem::path relative
Definition request_listener.hpp:94
std::filesystem::file_status status
Definition request_listener.hpp:95
Definition request_listener.hpp:135