// GNU Lesser General Public License v3.0 // Copyright (c) 2025 Bart Beumer // // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License v3.0 as published by // the Free Software Foundation. // #include #include #include #include #include #include #include #include #include #include #include namespace beast = boost::beast; namespace http = beast::http; namespace { struct ExtensionMimeMapping { std::string_view extension; std::string_view mime; }; bmrshared::magic_file_info mfi("magic.mgc"); } // namespace using bmrshared::web::directory_request_handler; directory_request_handler::directory_request_handler(const std::filesystem::path& document_root) : m_document_root(std::filesystem::absolute(document_root)) { } directory_request_handler::~directory_request_handler() = default; void directory_request_handler::handle_request_http(const address_type& address, const request_type& req, bmrshared::web::response_promise promise) { if ((req.method() != http::verb::get) && (req.method() != http::verb::head)) { // Other methods are not supported for directory acces // http::response bad_request{http::status::bad_request, req.version()}; bad_request.set(http::field::server, BOOST_BEAST_VERSION_STRING); bad_request.set(http::field::content_type, "text/plain"); bad_request.prepare_payload(); bad_request.body() = "Bad request type.\nOnly GET and HEAD are expected for this URL."; bad_request.keep_alive(req.keep_alive()); promise.SendResponse(std::move(bad_request)); return; } std::string_view target = req.target(); { target.remove_prefix(std::min(target.find_first_not_of(R"(/\)"), target.size())); } std::filesystem::path requested_file = m_document_root / std::filesystem::path(target); std::optional found_file; // See if requested path is a directory, use index.html if that is the case. if (std::filesystem::is_directory(requested_file)) { auto indexPath = requested_file / "index.html"; if (std::filesystem::is_regular_file(indexPath)) { found_file = indexPath; } } // See if requested path is a file. if (std::filesystem::is_regular_file(requested_file)) { found_file = requested_file; } if (!found_file) { http::response not_found{http::status::not_found, req.version()}; not_found.set(http::field::server, BOOST_BEAST_VERSION_STRING); not_found.set(http::field::content_type, "text/plain"); not_found.prepare_payload(); not_found.body() = "File not found."; not_found.keep_alive(req.keep_alive()); promise.SendResponse(std::move(not_found)); return; } beast::error_code ec; http::file_body::value_type body; body.open(found_file->native().c_str(), beast::file_mode::scan, ec); if (ec) { http::response internal{http::status::internal_server_error, req.version()}; internal.set(http::field::server, BOOST_BEAST_VERSION_STRING); internal.set(http::field::content_type, "text/plain"); internal.prepare_payload(); internal.body() = "Internal server error."; internal.keep_alive(req.keep_alive()); promise.SendResponse(std::move(internal)); return; } const auto file_size = body.size(); const auto file_last_modified_utc = std::chrono::time_point_cast(std::filesystem::last_write_time(*found_file)); const std::string last_modified = std::format("{0:%a}, {0:0>2%d} {0:0>2%e} {0:0>4%Y} {0:%H}:{0:%M}:{0:%S} GMT", file_last_modified_utc); const std::string mime_type = mfi.get_mime(*found_file); if (req.count(http::field::if_modified_since) != 0 && (req[http::field::if_modified_since] == last_modified)) { http::response head{http::status::not_modified, req.version()}; head.set(http::field::server, BOOST_BEAST_VERSION_STRING); head.keep_alive(req.keep_alive()); promise.SendResponse(std::move(head)); return; } // Okay, we have a file so let us send the correct response. if (req.method() == http::verb::head) { http::response head{http::status::ok, req.version()}; head.set(http::field::server, BOOST_BEAST_VERSION_STRING); head.set(http::field::content_type, mime_type); head.set(http::field::last_modified, last_modified); head.set(http::field::cache_control, "public, max-age=360"); head.set(http::field::content_length, std::to_string(file_size)); head.keep_alive(req.keep_alive()); promise.SendResponse(std::move(head)); return; } else if (req.method() == http::verb::get) { http::response head{std::piecewise_construct, std::make_tuple(std::move(body)), std::make_tuple(http::status::ok, req.version())}; head.set(http::field::server, BOOST_BEAST_VERSION_STRING); head.set(http::field::content_type, mime_type); head.set(http::field::last_modified, last_modified); head.set(http::field::cache_control, "public, max-age=360"); head.set(http::field::content_length, std::to_string(file_size)); head.keep_alive(req.keep_alive()); //promise.SendResponse(std::move(head)); return; } } void directory_request_handler::handle_request_websocket_upgrade(const address_type&, const request_type&, boost::asio::ip::tcp::socket) { // We ignore websocket upgrades. throw away the socket to close connection. }