Working, mandelbrot
This commit is contained in:
parent
23d74e35a1
commit
541b1e4d59
|
|
@ -7,3 +7,4 @@ add_subdirectory(applications)
|
||||||
add_subdirectory(bmrshared)
|
add_subdirectory(bmrshared)
|
||||||
add_subdirectory(bmrshared-freetype)
|
add_subdirectory(bmrshared-freetype)
|
||||||
add_subdirectory(bmrshared-magic)
|
add_subdirectory(bmrshared-magic)
|
||||||
|
add_subdirectory(bmrshared-web)
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
|
add_subdirectory(http-mandelbrot)
|
||||||
add_subdirectory(text2image)
|
add_subdirectory(text2image)
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
find_package(Boost 1.84.0 REQUIRED COMPONENTS headers CONFIG)
|
||||||
|
find_package(JPEG REQUIRED)
|
||||||
|
|
||||||
|
project(http-mandelbrot)
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
${PROJECT_NAME}
|
||||||
|
./src/main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set_property(
|
||||||
|
TARGET ${PROJECT_NAME}
|
||||||
|
PROPERTY CXX_STANDARD 20
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
${PROJECT_NAME}
|
||||||
|
PRIVATE
|
||||||
|
bmrshared-web
|
||||||
|
Boost::headers
|
||||||
|
JPEG::JPEG
|
||||||
|
)
|
||||||
|
|
||||||
|
install(
|
||||||
|
TARGETS ${PROJECT_NAME}
|
||||||
|
RUNTIME DESTINATION bin
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
// 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/server.hpp>
|
||||||
|
#include <bmrshared/request_handler_interface.hpp>
|
||||||
|
#include <bmrshared/directory_request_handler.hpp>
|
||||||
|
#include <bmrshared/response_promise.hpp>
|
||||||
|
|
||||||
|
#include <boost/asio/signal_set.hpp>
|
||||||
|
#include <boost/gil/image.hpp>
|
||||||
|
#include <boost/gil/image_view.hpp>
|
||||||
|
#include <boost/gil/extension/io/jpeg.hpp>
|
||||||
|
#include <memory>
|
||||||
|
#include <sstream>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
#include <complex>
|
||||||
|
#include <regex>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const std::regex regex_renderpath(R"REGEX(\/render_(\d+)_(\d+)_(\d+))REGEX");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class window
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
window(T x_min, T x_max, T y_min, T y_max)
|
||||||
|
: m_x_min(x_min), m_x_max(x_max), m_y_min(y_min), m_y_max(y_max)
|
||||||
|
{}
|
||||||
|
|
||||||
|
T size() const {
|
||||||
|
return width() * height();
|
||||||
|
}
|
||||||
|
|
||||||
|
T width() const {
|
||||||
|
return m_x_max - m_x_min;
|
||||||
|
}
|
||||||
|
|
||||||
|
T height() const {
|
||||||
|
return m_y_max - m_y_min;
|
||||||
|
}
|
||||||
|
|
||||||
|
T x_min() const {return m_x_min;}
|
||||||
|
T x_max() const {return m_x_max;}
|
||||||
|
T y_min() const {return m_y_min;}
|
||||||
|
T y_max() const {return m_y_max;}
|
||||||
|
|
||||||
|
private:
|
||||||
|
T m_x_min;
|
||||||
|
T m_x_max;
|
||||||
|
T m_y_min;
|
||||||
|
T m_y_max;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert a pixel coordinate to the complex domain
|
||||||
|
std::complex<double> scale(const window<int>& scr, const window<double>& fr, const std::complex<double>& c)
|
||||||
|
{
|
||||||
|
return std::complex<double>(c.real() / (double)scr.width() * fr.width() + fr.x_min(),
|
||||||
|
c.imag() / (double)scr.height() * fr.height() + fr.y_min());
|
||||||
|
}
|
||||||
|
|
||||||
|
int escape(const std::complex<double>& c,
|
||||||
|
int iter_max,
|
||||||
|
const std::function<std::complex<double>(std::complex<double>, std::complex<double>)>& func)
|
||||||
|
{
|
||||||
|
std::complex<double> z(0);
|
||||||
|
int iter = 0;
|
||||||
|
|
||||||
|
while (abs(z) < 2.0 && iter < iter_max) {
|
||||||
|
z = func(z, c);
|
||||||
|
iter++;
|
||||||
|
}
|
||||||
|
return iter;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fractal(
|
||||||
|
const window<int>& scr,
|
||||||
|
const window<double>&fract,
|
||||||
|
int max_iterations,
|
||||||
|
const std::function<std::complex<double>(std::complex<double>, std::complex<double>)>& fractal_fn,
|
||||||
|
const std::function<void(int x, int y, int num_iter)>& painter)
|
||||||
|
{
|
||||||
|
for(int y = scr.y_min(); y < scr.y_max(); ++y)
|
||||||
|
{
|
||||||
|
for(int x = scr.x_min(); x < scr.x_max(); ++x)
|
||||||
|
{
|
||||||
|
auto num_iter = escape(
|
||||||
|
scale(scr,
|
||||||
|
fract,
|
||||||
|
std::complex<double>{double(y), double(x)}
|
||||||
|
),
|
||||||
|
max_iterations,
|
||||||
|
fractal_fn);
|
||||||
|
|
||||||
|
painter(x,y,num_iter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
constexpr unsigned int config_max_simultanious_requests = 50;
|
||||||
|
constexpr uint64_t config_request_body_limit = (10 * 1024);
|
||||||
|
constexpr std::chrono::seconds config_request_timeout(30);
|
||||||
|
|
||||||
|
const boost::asio::ip::tcp::endpoint listen_endpoint(boost::asio::ip::tcp::v4(), 9800);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class split_req : public bmrshared::web::request_handler_interface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
split_req(const std::filesystem::path& root_dir, boost::asio::io_context& ioc) :
|
||||||
|
m_dir(root_dir),
|
||||||
|
m_ioc(ioc)
|
||||||
|
{}
|
||||||
|
|
||||||
|
~split_req() override = default;
|
||||||
|
|
||||||
|
void handle_request_http(const address_type& addr, const request_type& req, bmrshared::web::response_promise promise) override
|
||||||
|
{
|
||||||
|
std::string_view target = req.target();
|
||||||
|
std::cout << "HTTP GET: " << target << std::endl;
|
||||||
|
if (req.method() != boost::beast::http::verb::get)
|
||||||
|
{
|
||||||
|
// Other methods are not supported for directory acces
|
||||||
|
//
|
||||||
|
boost::beast::http::response<boost::beast::http::string_body> bad_request{boost::beast::http::status::bad_request, req.version()};
|
||||||
|
bad_request.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||||
|
bad_request.set(boost::beast::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::smatch base_match;
|
||||||
|
std::string tmp(target);
|
||||||
|
if (std::regex_match(tmp, base_match, regex_renderpath) && base_match.size() == 4)
|
||||||
|
{
|
||||||
|
double offset_y = std::stoi(base_match[1]);
|
||||||
|
double offset_x = std::stoi(base_match[2]);
|
||||||
|
double z = std::stoi(base_match[3]);
|
||||||
|
|
||||||
|
std::cout << "GET render offset_x: " << offset_x << ", offset_y: " << offset_y << ", z: " << z << std::endl;
|
||||||
|
|
||||||
|
auto renderfn = [offset_y, offset_x, z, req, promise]() mutable
|
||||||
|
{
|
||||||
|
constexpr int pixel_width = 256;
|
||||||
|
constexpr int pixel_height = 256;
|
||||||
|
constexpr int max_iterations = 100;
|
||||||
|
|
||||||
|
|
||||||
|
boost::gil::gray8_image_t image(pixel_width,pixel_height);
|
||||||
|
auto view = boost::gil::view(image);
|
||||||
|
boost::gil::fill_pixels(view, boost::gil::gray8_pixel_t(0));
|
||||||
|
|
||||||
|
auto painter = [&](int x, int y, int num_iter) -> void
|
||||||
|
{
|
||||||
|
if(x >= 0 && y >= 0 && x < pixel_width && y < pixel_height)
|
||||||
|
{
|
||||||
|
const auto iter = view.at({x, y});
|
||||||
|
boost::gil::color_convert(boost::gil::gray8_pixel_t(num_iter%256), *iter);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto fract_fn = [](std::complex <double> z, std::complex<double> c) -> std::complex<double>
|
||||||
|
{
|
||||||
|
return z * z + c;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto zoomfactor = 1.0/std::pow(2, z);
|
||||||
|
|
||||||
|
fractal(window<int>{0,pixel_width, 0,pixel_height},
|
||||||
|
window<double>{offset_x * zoomfactor, (offset_x + 1)*zoomfactor, offset_y * zoomfactor ,(offset_y + 1)*zoomfactor},
|
||||||
|
max_iterations,
|
||||||
|
fract_fn,
|
||||||
|
painter);
|
||||||
|
|
||||||
|
std::stringstream out_buffer( std::ios_base::out | std::ios_base::binary );
|
||||||
|
boost::gil::write_view(out_buffer,
|
||||||
|
boost::gil::view(image),
|
||||||
|
boost::gil::image_write_info<boost::gil::jpeg_tag>(95));
|
||||||
|
|
||||||
|
boost::beast::http::response<boost::beast::http::string_body> ok{boost::beast::http::status::ok, req.version()};
|
||||||
|
ok.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||||
|
ok.set(boost::beast::http::field::content_type, "image/jpeg");
|
||||||
|
ok.body() = out_buffer.str();
|
||||||
|
ok.keep_alive(req.keep_alive());
|
||||||
|
ok.prepare_payload();
|
||||||
|
promise.SendResponse(std::move(ok));
|
||||||
|
};
|
||||||
|
|
||||||
|
m_ioc.post(renderfn);
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_dir.handle_request_http(addr, req, promise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_request_websocket_upgrade(const address_type& addr, const request_type& req, boost::asio::ip::tcp::socket socket) override
|
||||||
|
{
|
||||||
|
m_dir.handle_request_websocket_upgrade(addr, req, std::move(socket));
|
||||||
|
; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bmrshared::web::directory_request_handler m_dir;
|
||||||
|
boost::asio::io_context& m_ioc;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
std::filesystem::path root_dir = "/workspaces/network-experiment/applications/http-mandelbrot/html";
|
||||||
|
boost::asio::io_context ioc;
|
||||||
|
split_req request_handler(root_dir, ioc);
|
||||||
|
|
||||||
|
|
||||||
|
boost::asio::signal_set signals(ioc, SIGINT, SIGTERM);
|
||||||
|
|
||||||
|
std::shared_ptr<bmrshared::web::server> httpserver;
|
||||||
|
|
||||||
|
signals.async_wait([&ioc](const boost::system::error_code& error, int signal_number)
|
||||||
|
{
|
||||||
|
ioc.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
ioc.post([&ioc, &httpserver, &request_handler]{
|
||||||
|
httpserver = std::make_shared<bmrshared::web::server>(
|
||||||
|
ioc,
|
||||||
|
listen_endpoint,
|
||||||
|
config_request_timeout,
|
||||||
|
config_request_body_limit,
|
||||||
|
config_max_simultanious_requests,
|
||||||
|
request_handler);
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<std::jthread> threads;
|
||||||
|
while(threads.size() < 20)
|
||||||
|
{
|
||||||
|
threads.emplace_back([&ioc]{ioc.run();});
|
||||||
|
}
|
||||||
|
|
||||||
|
ioc.run();
|
||||||
|
|
||||||
|
threads.clear();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
add_subdirectory(lib)
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
#pragma once
|
||||||
|
#include "request_handler_interface.hpp"
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
namespace bmrshared::web
|
||||||
|
{
|
||||||
|
class directory_request_handler : public request_handler_interface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit directory_request_handler(const std::filesystem::path& document_root);
|
||||||
|
~directory_request_handler() override;
|
||||||
|
|
||||||
|
void handle_request_http(const address_type& address, const request_type& req, response_promise promise) override;
|
||||||
|
|
||||||
|
void handle_request_websocket_upgrade(const address_type& address,
|
||||||
|
const request_type& req,
|
||||||
|
boost::asio::ip::tcp::socket socket) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path m_document_root;
|
||||||
|
};
|
||||||
|
} // namespace bmrshared::web
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
#pragma once
|
||||||
|
#include "server.hpp"
|
||||||
|
|
||||||
|
namespace bmrshared::web
|
||||||
|
{
|
||||||
|
class response_promise;
|
||||||
|
|
||||||
|
class request_handler_interface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using address_type = server::address_type;
|
||||||
|
using request_type = server::request_type;
|
||||||
|
|
||||||
|
virtual ~request_handler_interface() = default;
|
||||||
|
|
||||||
|
virtual void handle_request_http(const address_type&, const request_type&, response_promise) = 0;
|
||||||
|
virtual void handle_request_websocket_upgrade(const address_type&, const request_type&, boost::asio::ip::tcp::socket) = 0;
|
||||||
|
};
|
||||||
|
} // namespace bmrshared::web
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
#pragma once
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/ip/tcp.hpp>
|
||||||
|
#include <boost/beast.hpp>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
namespace bmrshared::web
|
||||||
|
{
|
||||||
|
|
||||||
|
class response_promise final
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using response_sender = std::function<void(boost::beast::tcp_stream& stream)>;
|
||||||
|
using callback_on_response = std::function<void(const response_sender& fnSendResponse)>;
|
||||||
|
|
||||||
|
response_promise() = delete;
|
||||||
|
explicit response_promise(callback_on_response cbOnResponse);
|
||||||
|
~response_promise();
|
||||||
|
|
||||||
|
template<typename Body, typename Fields>
|
||||||
|
void SendResponse(boost::beast::http::response<Body, Fields> response)
|
||||||
|
{
|
||||||
|
response_sender responder = [&response](boost::beast::tcp_stream& stream)
|
||||||
|
{
|
||||||
|
boost::beast::http::write(stream, response);
|
||||||
|
};
|
||||||
|
m_call_on_response(responder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
callback_on_response m_call_on_response;
|
||||||
|
};
|
||||||
|
} // namespace bmrshared::web
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
#pragma once
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/ip/address.hpp>
|
||||||
|
#include <boost/asio/ip/tcp.hpp>
|
||||||
|
#include <boost/beast/http/message.hpp>
|
||||||
|
#include <boost/beast/http/string_body.hpp>
|
||||||
|
#include <chrono>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace bmrshared::web
|
||||||
|
{
|
||||||
|
namespace detail
|
||||||
|
{
|
||||||
|
class internal_server;
|
||||||
|
}
|
||||||
|
|
||||||
|
class response_promise;
|
||||||
|
class request_handler_interface;
|
||||||
|
|
||||||
|
class server final
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using address_type = boost::asio::ip::address;
|
||||||
|
using request_type = boost::beast::http::request<boost::beast::http::string_body>;
|
||||||
|
|
||||||
|
server(boost::asio::io_context& io_context,
|
||||||
|
const boost::asio::ip::tcp::endpoint& listen_endpoint,
|
||||||
|
std::chrono::seconds incoming_request_timeout,
|
||||||
|
uint64_t request_body_limit,
|
||||||
|
unsigned int max_simultaneous_requests,
|
||||||
|
request_handler_interface& handler);
|
||||||
|
|
||||||
|
~server();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<detail::internal_server> m_internal;
|
||||||
|
};
|
||||||
|
} // namespace bmrshared::web
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
find_package(Boost 1.84.0 REQUIRED COMPONENTS headers CONFIG)
|
||||||
|
project(bmrshared-web)
|
||||||
|
|
||||||
|
add_library(
|
||||||
|
${PROJECT_NAME}
|
||||||
|
directory_request_handler.cpp
|
||||||
|
internal_server.cpp
|
||||||
|
response_promise.cpp
|
||||||
|
server.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set_property(
|
||||||
|
TARGET ${PROJECT_NAME}
|
||||||
|
PROPERTY CXX_STANDARD 20
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(
|
||||||
|
${PROJECT_NAME}
|
||||||
|
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
${PROJECT_NAME}
|
||||||
|
PUBLIC
|
||||||
|
bmrshared
|
||||||
|
bmrshared-magic
|
||||||
|
Boost::headers
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
// 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.
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
// 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 "internal_server.h"
|
||||||
|
#include <bmrshared/request_handler_interface.hpp>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
using bmrshared::web::detail::internal_server;
|
||||||
|
|
||||||
|
|
||||||
|
internal_server::internal_server(boost::asio::io_context& io_context,
|
||||||
|
const boost::asio::ip::tcp::endpoint& listen_endpoint,
|
||||||
|
std::chrono::seconds incoming_request_timeout,
|
||||||
|
uint64_t request_body_limit,
|
||||||
|
unsigned int max_simultaneous_requests,
|
||||||
|
request_handler_interface& handler)
|
||||||
|
: m_io_context(io_context)
|
||||||
|
, m_endPoint(listen_endpoint)
|
||||||
|
, m_incoming_request_timeout(incoming_request_timeout)
|
||||||
|
, m_max_simultaneous_requests(max_simultaneous_requests)
|
||||||
|
, m_request_body_limit(request_body_limit)
|
||||||
|
, m_acceptor(io_context)
|
||||||
|
, m_handler(handler)
|
||||||
|
, m_sessions()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal_server::~internal_server() = default;
|
||||||
|
|
||||||
|
void internal_server::run()
|
||||||
|
{
|
||||||
|
m_acceptor.open(m_endPoint.protocol());
|
||||||
|
m_acceptor.set_option(boost::asio::socket_base::reuse_address(true));
|
||||||
|
m_acceptor.bind(m_endPoint);
|
||||||
|
m_acceptor.listen(boost::asio::socket_base::max_listen_connections);
|
||||||
|
|
||||||
|
async_accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void internal_server::async_accept()
|
||||||
|
{
|
||||||
|
// Start accepting incoming connections asynchronously.
|
||||||
|
m_acceptor.async_accept(
|
||||||
|
[weak_server = weak_from_this()](boost::system::error_code ec, boost::asio::ip::tcp::socket socket)
|
||||||
|
{
|
||||||
|
auto shared_server = weak_server.lock();
|
||||||
|
if (shared_server)
|
||||||
|
{
|
||||||
|
shared_server->do_accept(ec, std::move(socket));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void internal_server::do_accept(boost::system::error_code ec, boost::asio::ip::tcp::socket socket)
|
||||||
|
{
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
// Error while accepting incoming connection. Ignore.
|
||||||
|
// TODO: something needs to be done here.
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_sessions.push_back(std::make_shared<http_session>(std::move(socket)));
|
||||||
|
session_process_queued();
|
||||||
|
async_accept();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void internal_server::session_process_queued()
|
||||||
|
{
|
||||||
|
std::size_t count_responding = std::count_if(m_sessions.begin(),
|
||||||
|
m_sessions.end(),
|
||||||
|
[](const std::shared_ptr<http_session>& session)
|
||||||
|
{
|
||||||
|
return session && (session->state == session_state::responding);
|
||||||
|
});
|
||||||
|
|
||||||
|
// First empty out all sessions queued for getting a response as much as we are allowed.
|
||||||
|
auto iter = m_sessions.begin();
|
||||||
|
while ((count_responding < m_max_simultaneous_requests) && (iter != m_sessions.end()))
|
||||||
|
{
|
||||||
|
iter = std::find_if(
|
||||||
|
iter,
|
||||||
|
m_sessions.end(),
|
||||||
|
[](std::shared_ptr<http_session> session)
|
||||||
|
{
|
||||||
|
return session && (session->state == session_state::queue_responding);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (iter != m_sessions.end())
|
||||||
|
{
|
||||||
|
auto& session = *iter;
|
||||||
|
session->state = session_state::responding;
|
||||||
|
++count_responding;
|
||||||
|
|
||||||
|
// Regular http request. Pass to request handler.
|
||||||
|
auto finalize_response = [session, weak_server = weak_from_this()](const response_promise::response_sender& rs)
|
||||||
|
{
|
||||||
|
auto shared_server = weak_server.lock();
|
||||||
|
if (shared_server)
|
||||||
|
{
|
||||||
|
rs(session->stream);
|
||||||
|
// When we receive the response and have sent it, we proceed with reading
|
||||||
|
shared_server->session_finalize_response(session);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto& address = session->stream.socket().remote_endpoint().address();
|
||||||
|
m_handler.handle_request_http(address, session->parser->get(), response_promise(finalize_response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count_responding < m_max_simultaneous_requests)
|
||||||
|
{
|
||||||
|
// Now we can dispatch reading.
|
||||||
|
for (const auto& session : m_sessions)
|
||||||
|
{
|
||||||
|
if (session && (session->state == session_state::queue_reading))
|
||||||
|
{
|
||||||
|
http_session& s = *session;
|
||||||
|
s.state = session_state::reading;
|
||||||
|
s.parser.emplace(); // Create empty parser object.
|
||||||
|
s.parser->body_limit(m_request_body_limit);
|
||||||
|
s.stream.expires_after(m_incoming_request_timeout);
|
||||||
|
|
||||||
|
auto handler = [weak_server = weak_from_this(), session](boost::beast::error_code ec, std::size_t size)
|
||||||
|
{
|
||||||
|
auto shared_server = weak_server.lock();
|
||||||
|
if (shared_server)
|
||||||
|
{
|
||||||
|
shared_server->session_on_read(session, ec, size);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
boost::beast::http::async_read(s.stream, s.buffer, *s.parser, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void internal_server::session_on_read(std::shared_ptr<http_session> session,
|
||||||
|
boost::beast::error_code ec,
|
||||||
|
std::size_t size)
|
||||||
|
{
|
||||||
|
http_session& s = *session;
|
||||||
|
|
||||||
|
if (ec == boost::beast::http::error::end_of_stream)
|
||||||
|
{
|
||||||
|
// End of stream. Shutdown and remove http session
|
||||||
|
s.stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec);
|
||||||
|
session_remove(session);
|
||||||
|
}
|
||||||
|
else if (ec)
|
||||||
|
{
|
||||||
|
// Any other kind of error, also shutdown and remove http_session
|
||||||
|
s.stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec);
|
||||||
|
session_remove(session);
|
||||||
|
}
|
||||||
|
else if (boost::beast::websocket::is_upgrade(s.parser->get()))
|
||||||
|
{
|
||||||
|
// Received a websocket upgrade. pass socket to request handler, remove session from our administration.
|
||||||
|
const auto& address = s.stream.socket().remote_endpoint().address();
|
||||||
|
m_handler.handle_request_websocket_upgrade(address, s.parser->get(), s.stream.release_socket());
|
||||||
|
session_remove(session);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Just a regular http request. Queue for handling later.
|
||||||
|
s.state = session_state::queue_responding;
|
||||||
|
session_process_queued();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void internal_server::session_finalize_response(std::shared_ptr<http_session> session)
|
||||||
|
{
|
||||||
|
if(session)
|
||||||
|
{
|
||||||
|
session->state = session_state::queue_reading;
|
||||||
|
session_process_queued();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void internal_server::session_remove(std::shared_ptr<http_session> session)
|
||||||
|
{
|
||||||
|
m_sessions.erase(std::remove(m_sessions.begin(), m_sessions.end(), session), m_sessions.end());
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
#pragma once
|
||||||
|
#include <bmrshared/response_promise.hpp>
|
||||||
|
#include <bmrshared/server.hpp>
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/ip/tcp.hpp>
|
||||||
|
#include <chrono>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace bmrshared::web {
|
||||||
|
namespace detail {
|
||||||
|
enum class session_state {
|
||||||
|
queue_reading,
|
||||||
|
reading,
|
||||||
|
queue_responding,
|
||||||
|
responding,
|
||||||
|
};
|
||||||
|
struct http_session {
|
||||||
|
explicit http_session(boost::asio::ip::tcp::socket s)
|
||||||
|
: stream(std::move(s))
|
||||||
|
{}
|
||||||
|
boost::beast::tcp_stream stream;
|
||||||
|
boost::beast::flat_buffer buffer;
|
||||||
|
boost::optional<boost::beast::http::request_parser<boost::beast::http::string_body>> parser;
|
||||||
|
session_state state = session_state::queue_reading;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class internal_server final : public std::enable_shared_from_this<internal_server> {
|
||||||
|
public:
|
||||||
|
using address_type = bmrshared::web::server::address_type;
|
||||||
|
using request_type = bmrshared::web::server::request_type;
|
||||||
|
|
||||||
|
public:
|
||||||
|
internal_server(boost::asio::io_context& io_context,
|
||||||
|
const boost::asio::ip::tcp::endpoint& listen_endpoint,
|
||||||
|
std::chrono::seconds incoming_request_timeout,
|
||||||
|
uint64_t request_body_limit,
|
||||||
|
unsigned int max_simultaneous_requests,
|
||||||
|
request_handler_interface& handler);
|
||||||
|
|
||||||
|
~internal_server();
|
||||||
|
|
||||||
|
void run();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void async_accept();
|
||||||
|
void do_accept(boost::system::error_code ec, boost::asio::ip::tcp::socket socket);
|
||||||
|
void session_process_queued();
|
||||||
|
void session_on_read(std::shared_ptr<http_session> session, boost::beast::error_code ec, std::size_t size);
|
||||||
|
void session_finalize_response(std::shared_ptr<http_session> session);
|
||||||
|
void session_remove(std::shared_ptr<http_session> session);
|
||||||
|
|
||||||
|
private:
|
||||||
|
boost::asio::io_context& m_io_context;
|
||||||
|
boost::asio::ip::tcp::endpoint m_endPoint;
|
||||||
|
std::chrono::seconds m_incoming_request_timeout;
|
||||||
|
unsigned int m_max_simultaneous_requests;
|
||||||
|
uint64_t m_request_body_limit;
|
||||||
|
boost::asio::ip::tcp::acceptor m_acceptor;
|
||||||
|
request_handler_interface& m_handler;
|
||||||
|
std::vector<std::shared_ptr<http_session>> m_sessions;
|
||||||
|
};
|
||||||
|
} // namespace detail
|
||||||
|
} // namespace bmrshared::web
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
// 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/response_promise.hpp>
|
||||||
|
|
||||||
|
using bmrshared::web::response_promise;
|
||||||
|
|
||||||
|
response_promise::response_promise(callback_on_response cbOnResponse)
|
||||||
|
: m_call_on_response(std::move(cbOnResponse))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
response_promise::~response_promise() = default;
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
// 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 "internal_server.h"
|
||||||
|
#include <bmrshared/server.hpp>
|
||||||
|
|
||||||
|
using bmrshared::web::server;
|
||||||
|
using bmrshared::web::detail::internal_server;
|
||||||
|
|
||||||
|
server::server(boost::asio::io_context& io_context,
|
||||||
|
const boost::asio::ip::tcp::endpoint& listen_endpoint,
|
||||||
|
std::chrono::seconds incoming_request_timeout,
|
||||||
|
uint64_t request_body_limit,
|
||||||
|
unsigned int max_simultaneous_requests,
|
||||||
|
request_handler_interface& handler)
|
||||||
|
: m_internal(std::make_shared<internal_server>(io_context, listen_endpoint, incoming_request_timeout, request_body_limit, max_simultaneous_requests, handler))
|
||||||
|
{
|
||||||
|
m_internal->run();
|
||||||
|
}
|
||||||
|
|
||||||
|
server::~server() = default;
|
||||||
Loading…
Reference in New Issue