166 lines
6.2 KiB
C++
166 lines
6.2 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 <bmrshared/response_promise.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(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<http::string_body> 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<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)
|
|
{
|
|
http::response<http::string_body> 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<http::string_body> 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::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))
|
|
{
|
|
http::response<http::string_body> 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<http::string_body> 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<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());
|
|
//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.
|
|
}
|