network-experiment/bmrshared-web/lib/directory_request_handler.cpp

160 lines
5.9 KiB
C++

// GNU Lesser General Public License v3.0
// Copyright (c) 2025 Bart Beumer <bart@4beumer.nl>
//
// 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 <bmrshared/directory_request_handler.hpp>
#include <algorithm>
#include <array>
#include <bmrshared/magic_file_info.hpp>
#include <filesystem>
#include <future>
#include <iomanip>
#include <iostream>
#include <optional>
#include <string_view>
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(request_response rs)
{
const auto& req = rs.get_request();
if ((req.method() != http::verb::get) && (req.method() != http::verb::head))
{
// Other methods are not supported for directory acces
//
auto& bad_request = rs.create_response<http::response<http::string_body>>(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());
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<std::filesystem::path> 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)
{
auto& not_found = rs.create_response<http::response<http::string_body>>(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());
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)
{
auto& internal = rs.create_response<http::response<http::string_body>>(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());
return;
}
const auto file_size = body.size();
const auto file_last_modified_utc = std::chrono::time_point_cast<std::chrono::seconds>(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))
{
auto& head = rs.create_response<http::response<http::string_body>>(http::status::not_modified, req.version());
head.set(http::field::server, BOOST_BEAST_VERSION_STRING);
head.keep_alive(req.keep_alive());
return;
}
// Okay, we have a file so let us send the correct response.
if (req.method() == http::verb::head)
{
auto& head = rs.create_response<http::response<http::string_body>>(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());
return;
}
else if (req.method() == http::verb::get)
{
http::response<http::file_body> 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());
rs.create_response<http::response<http::file_body>>(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.
}