diff --git a/CMakeLists.txt b/CMakeLists.txt
index ba60876..69056b2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -7,3 +7,4 @@ add_subdirectory(applications)
add_subdirectory(bmrshared)
add_subdirectory(bmrshared-freetype)
add_subdirectory(bmrshared-magic)
+add_subdirectory(bmrshared-web)
diff --git a/applications/CMakeLists.txt b/applications/CMakeLists.txt
index c0bfeb5..81dca02 100644
--- a/applications/CMakeLists.txt
+++ b/applications/CMakeLists.txt
@@ -1 +1,2 @@
+add_subdirectory(http-mandelbrot)
add_subdirectory(text2image)
\ No newline at end of file
diff --git a/applications/http-mandelbrot/CMakeLists.txt b/applications/http-mandelbrot/CMakeLists.txt
new file mode 100644
index 0000000..82466f7
--- /dev/null
+++ b/applications/http-mandelbrot/CMakeLists.txt
@@ -0,0 +1,28 @@
+find_package(Boost 1.90.0 REQUIRED COMPONENTS headers CONFIG)
+find_package(JPEG REQUIRED)
+
+project(http-mandelbrot)
+
+add_executable(
+ ${PROJECT_NAME}
+ ./src/main.cpp
+ ./src/mandelbrot.cpp
+)
+
+set_property(
+ TARGET ${PROJECT_NAME}
+ PROPERTY CXX_STANDARD 23
+)
+
+target_link_libraries(
+ ${PROJECT_NAME}
+ PRIVATE
+ bmrshared-web
+ Boost::headers
+ JPEG::JPEG
+)
+
+install(
+ TARGETS ${PROJECT_NAME}
+ RUNTIME DESTINATION bin
+)
diff --git a/applications/http-mandelbrot/html/index.html b/applications/http-mandelbrot/html/index.html
new file mode 100644
index 0000000..cbf08a9
--- /dev/null
+++ b/applications/http-mandelbrot/html/index.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+ Quick Start - Leaflet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/applications/http-mandelbrot/src/main.cpp b/applications/http-mandelbrot/src/main.cpp
new file mode 100644
index 0000000..6850023
--- /dev/null
+++ b/applications/http-mandelbrot/src/main.cpp
@@ -0,0 +1,191 @@
+// GNU Lesser General Public License v3.0
+// Copyright (c) 2026 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
+#include
+#include
+#include
+#include
+#include "mandelbrot.hpp"
+
+namespace
+{
+ const std::regex regex_renderpath(R"REGEX(\/render_z(\d+)x(-?\d+)y(-?\d+)w(\d+)h(\d+)ar(-?\d+\.\d+)ai(-?\d+\.\d+)maxiter(\d+).jpg)REGEX");
+
+ constexpr unsigned int config_max_simultanious_requests = 10;
+ constexpr uint64_t config_request_body_limit = (10 * 1024);
+ constexpr std::chrono::seconds config_request_timeout(3);
+
+ 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(bmrshared::web::request_response rs) override
+ {
+ auto& req = rs.get_request();
+ std::string target = req.target();
+ if (req.method() != boost::beast::http::verb::get)
+ {
+ // Other methods are not supported for directory acces
+ //
+ auto& bad_request = rs.create_response>(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());
+ return;
+ }
+
+ std::cout << "GET: " << target << std::endl;
+ std::smatch base_match;
+ std::string tmp(target);
+ if (std::regex_match(tmp, base_match, regex_renderpath) && base_match.size() == 9)
+ {
+ int z = std::stoi(base_match[1]);
+ int offset_y = std::stoi(base_match[2]);
+ int offset_x = std::stoi(base_match[3]);
+ int width = std::stoi(base_match[4]);
+ int height = std::stoi(base_match[5]);
+ double a_real = std::stof(base_match[6]);
+ double a_img = std::stof(base_match[7]);
+ int max_iter = std::stoi(base_match[8]);
+ std::complex factor_a(a_real, a_img);
+
+ auto renderfn = [offset_y, offset_x, z, width, height, factor_a, max_iter, req, target, rs, tmp]() mutable
+ {
+ int pixel_width = width;
+ int pixel_height = height;
+ int max_iterations = max_iter;
+
+
+ boost::gil::rgb8_image_t image(pixel_width,pixel_height);
+ auto view = boost::gil::view(image);
+ boost::gil::fill_pixels(view, boost::gil::rgb8_pixel_t(0));
+
+ auto painter = [&](int x, int y, int num_iter) -> void
+ {
+ if(x >= 0 && y >= 0 && x < pixel_width && y < pixel_height)
+ {
+ bmrshared::color c({.h=static_cast((num_iter*2)%360),.s=1.0, .v=1.0});
+ auto crgb = c.rgb();
+ uint8_t r = crgb.r * 255;
+ uint8_t g = crgb.g * 255;
+ uint8_t b = crgb.b * 255;
+
+
+ const auto iter = view.at({x, y});
+ boost::gil::color_convert(boost::gil::rgb8_pixel_t(r,g,b), *iter);
+ }
+ };
+
+
+
+ const float zoomfactor = 1.0/std::pow(2, z);
+ window src_window{offset_x * zoomfactor, (offset_x + 1)*zoomfactor, offset_y * zoomfactor ,(offset_y + 1)*zoomfactor};
+ window dst_window{0.0, static_cast(pixel_width), 0.0, static_cast(pixel_height)};
+
+ mandelbrot fractal(src_window, dst_window, max_iterations, factor_a);
+ fractal.apply(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(90));
+
+ auto& ok = rs.create_response>(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();
+ };
+
+ boost::asio::post(m_ioc, renderfn);
+ }
+ else
+ {
+ m_dir.handle_request_http(rs);
+ }
+ }
+
+ 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 httpserver;
+
+ signals.async_wait([&ioc](const boost::system::error_code& error, int signal_number)
+ {
+ ioc.stop();
+ });
+
+
+ boost::asio::post(ioc, [&ioc, &httpserver, &request_handler]{
+ httpserver = std::make_shared(
+ ioc,
+ listen_endpoint,
+ config_request_timeout,
+ config_request_body_limit,
+ config_max_simultanious_requests,
+ request_handler);
+ });
+
+
+
+ std::vector threads;
+ while(threads.size() < 4)
+ {
+ threads.emplace_back([&ioc]{ioc.run();});
+ }
+
+ ioc.run();
+
+ threads.clear();
+
+}
+
+
diff --git a/applications/http-mandelbrot/src/mandelbrot.cpp b/applications/http-mandelbrot/src/mandelbrot.cpp
new file mode 100644
index 0000000..b892588
--- /dev/null
+++ b/applications/http-mandelbrot/src/mandelbrot.cpp
@@ -0,0 +1,8 @@
+// GNU Lesser General Public License v3.0
+// Copyright (c) 2026 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 "mandelbrot.hpp"
\ No newline at end of file
diff --git a/applications/http-mandelbrot/src/mandelbrot.hpp b/applications/http-mandelbrot/src/mandelbrot.hpp
new file mode 100644
index 0000000..bb41a89
--- /dev/null
+++ b/applications/http-mandelbrot/src/mandelbrot.hpp
@@ -0,0 +1,71 @@
+// GNU Lesser General Public License v3.0
+// Copyright (c) 2026 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 "window.hpp"
+
+template
+class mandelbrot
+{
+public:
+ using painter_fn = std::function;
+ using escape_fn = std::function(std::complex, std::complex)>;
+
+ mandelbrot(
+ window source_window,
+ window destination_window,
+ int max_iterations,
+ std::complex factor_a)
+ : m_source_window(source_window)
+ , m_destination_window(destination_window)
+ , m_max_iterations(max_iterations)
+ , m_factor_a(factor_a)
+ {}
+
+ void apply(const painter_fn& painter) const
+ {
+ auto fract_fn = [factor = m_factor_a](std::complex z, std::complex c) -> std::complex
+ {
+ return std::pow(z, factor) + c;
+ };
+
+ const TFloat dst_factor_x = 1.0 / m_destination_window.width() * m_source_window.width();
+ const TFloat dst_factor_y = 1.0 / m_destination_window.height() * m_source_window.height();
+
+ for (int x = 0; x < m_destination_window.width(); ++x)
+ {
+ for (int y = 0; y < m_destination_window.height(); ++y)
+ {
+ const auto _x = x * dst_factor_x + m_source_window.x_min();
+ const auto _y = y * dst_factor_y + m_source_window.y_min();
+ const int iterations = escape(std::complex(_y, _x), fract_fn);
+
+ painter(y,x,iterations);
+ }
+ }
+ }
+
+ int escape(const std::complex& c,
+ const escape_fn& func) const
+{
+ std::complex z(0);
+ int iter = 0;
+
+ while (abs(z) < 2.0 && iter < m_max_iterations) {
+ z = func(z, c);
+ iter++;
+ }
+ return iter;
+}
+
+private:
+ window m_source_window;
+ window m_destination_window;
+ int m_max_iterations;
+ std::complex m_factor_a;
+};
diff --git a/applications/http-mandelbrot/src/window.hpp b/applications/http-mandelbrot/src/window.hpp
new file mode 100644
index 0000000..86afbae
--- /dev/null
+++ b/applications/http-mandelbrot/src/window.hpp
@@ -0,0 +1,41 @@
+// GNU Lesser General Public License v3.0
+// Copyright (c) 2026 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.
+//
+
+#pragma once
+
+template
+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;
+};
\ No newline at end of file
diff --git a/applications/text2image/CMakeLists.txt b/applications/text2image/CMakeLists.txt
index 8a7a26b..678a8ee 100644
--- a/applications/text2image/CMakeLists.txt
+++ b/applications/text2image/CMakeLists.txt
@@ -1,5 +1,4 @@
-cmake_minimum_required(VERSION 3.20)
-find_package(Boost 1.90.0 REQUIRED COMPONENTS program_options headers CONFIG)
+find_package(Boost 1.89.0 REQUIRED COMPONENTS program_options headers CONFIG)
find_package(JPEG REQUIRED)
project(text2image)
diff --git a/bmrshared-web/CMakeLists.txt b/bmrshared-web/CMakeLists.txt
new file mode 100644
index 0000000..8570eeb
--- /dev/null
+++ b/bmrshared-web/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(lib)
\ No newline at end of file
diff --git a/bmrshared-web/include/bmrshared/directory_request_handler.hpp b/bmrshared-web/include/bmrshared/directory_request_handler.hpp
new file mode 100644
index 0000000..5769fbc
--- /dev/null
+++ b/bmrshared-web/include/bmrshared/directory_request_handler.hpp
@@ -0,0 +1,29 @@
+// 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.
+//
+#pragma once
+#include "request_handler_interface.hpp"
+#include
+
+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(request_response rs) 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
diff --git a/bmrshared-web/include/bmrshared/request_handler_interface.hpp b/bmrshared-web/include/bmrshared/request_handler_interface.hpp
new file mode 100644
index 0000000..e65cecf
--- /dev/null
+++ b/bmrshared-web/include/bmrshared/request_handler_interface.hpp
@@ -0,0 +1,26 @@
+// 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.
+//
+#pragma once
+#include "server.hpp"
+#include
+
+namespace bmrshared::web
+{
+
+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(request_response rs) = 0;
+ virtual void handle_request_websocket_upgrade(const address_type&, const request_type&, boost::asio::ip::tcp::socket) = 0;
+};
+} // namespace bmrshared::web
diff --git a/bmrshared-web/include/bmrshared/request_response.hpp b/bmrshared-web/include/bmrshared/request_response.hpp
new file mode 100644
index 0000000..364e3de
--- /dev/null
+++ b/bmrshared-web/include/bmrshared/request_response.hpp
@@ -0,0 +1,104 @@
+// 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.
+//
+#pragma once
+
+#include "server.hpp"
+#include
+#include
+#include
+
+namespace bmrshared::web
+{
+class request_response final
+{
+public:
+ using response_sender = std::function;
+ using request_type = bmrshared::web::server::request_type;
+ using callback_on_finalize = std::function;
+
+private:
+ class response_writer_interface
+ {
+ public:
+ virtual ~response_writer_interface();
+ virtual void write_response(boost::beast::tcp_stream&) = 0;
+ };
+
+ template
+ class response_writer final : public response_writer_interface
+ {
+ public:
+ template
+ response_writer(TArgs&&... args)
+ : m_response(std::forward(args)...)
+ {}
+
+ ~response_writer() override = default;
+
+ void write_response(boost::beast::tcp_stream& stream) override
+ {
+ boost::beast::http::write(stream, m_response);
+ }
+
+ response_type& response()
+ {
+ return m_response;
+ }
+
+ private:
+ response_type m_response;
+ };
+
+ class internal_request_response final
+ {
+ public:
+ // Disabling copying and moving.
+ internal_request_response() = delete;
+ internal_request_response(const internal_request_response&) = delete;
+ internal_request_response(internal_request_response&&) = delete;
+ internal_request_response& operator=(const internal_request_response&) = delete;
+ internal_request_response& operator=(internal_request_response&&) = delete;
+
+
+ internal_request_response(request_type request, callback_on_finalize cb_finalize);
+ ~internal_request_response();
+
+ const request_type& get_request() const;
+ void set_response_sender(std::unique_ptr response_writer);
+
+ private:
+ request_type m_request;
+ callback_on_finalize m_cb_finalize;
+ std::unique_ptr m_response_writer;
+ };
+
+public:
+ request_response() = delete;
+ request_response(const request_response&) = default;
+ request_response(request_response&&) = default;
+ request_response& operator=(const request_response&) = default;
+ request_response& operator=(request_response&&) = default;
+
+ request_response(request_type request, callback_on_finalize cb_finalize);
+ ~request_response();
+
+ template
+ response_type& create_response(arg_types&&... args)
+ {
+ auto created = std::make_unique>(std::forward(args)...);
+ auto& resp = created->response();
+ m_internal->set_response_sender(std::move(created));
+ return resp;
+ }
+
+ const request_type& get_request() const;
+
+private:
+ std::shared_ptr m_internal;
+};
+}
\ No newline at end of file
diff --git a/bmrshared-web/include/bmrshared/server.hpp b/bmrshared-web/include/bmrshared/server.hpp
new file mode 100644
index 0000000..3042ec8
--- /dev/null
+++ b/bmrshared-web/include/bmrshared/server.hpp
@@ -0,0 +1,45 @@
+// 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.
+//
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+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;
+
+ 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 m_internal;
+};
+} // namespace bmrshared::web
diff --git a/bmrshared-web/lib/CMakeLists.txt b/bmrshared-web/lib/CMakeLists.txt
new file mode 100644
index 0000000..9e158e0
--- /dev/null
+++ b/bmrshared-web/lib/CMakeLists.txt
@@ -0,0 +1,29 @@
+find_package(Boost 1.90.0 REQUIRED COMPONENTS headers CONFIG)
+project(bmrshared-web)
+
+add_library(
+ ${PROJECT_NAME}
+ directory_request_handler.cpp
+ internal_server.cpp
+ request_response.cpp
+ server.cpp
+)
+
+set_property(
+ TARGET ${PROJECT_NAME}
+ PROPERTY CXX_STANDARD 23
+)
+
+target_include_directories(
+ ${PROJECT_NAME}
+ PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../include
+)
+
+target_link_libraries(
+ ${PROJECT_NAME}
+ PUBLIC
+ bmrshared
+ bmrshared-magic
+ Boost::headers
+)
+
diff --git a/bmrshared-web/lib/directory_request_handler.cpp b/bmrshared-web/lib/directory_request_handler.cpp
new file mode 100644
index 0000000..b92aac4
--- /dev/null
+++ b/bmrshared-web/lib/directory_request_handler.cpp
@@ -0,0 +1,159 @@
+// 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
+
+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::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 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::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::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::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::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::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 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>(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.
+}
diff --git a/bmrshared-web/lib/internal_server.cpp b/bmrshared-web/lib/internal_server.cpp
new file mode 100644
index 0000000..fc14752
--- /dev/null
+++ b/bmrshared-web/lib/internal_server.cpp
@@ -0,0 +1,226 @@
+// 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 "internal_server.h"
+#include
+#include
+#include
+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_strand(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)
+ {
+ boost::asio::post(shared_server->m_strand, [weak_server, ec, s = std::make_shared(std::move(socket))]
+ {
+ auto shared_server = weak_server.lock();
+ if (shared_server)
+ {
+ shared_server->do_accept(ec, std::move(*s));
+ }
+ });
+ }
+ });
+}
+
+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
+ {
+ auto sessionptr = std::make_shared(std::move(socket));
+ m_sessions.push_back(sessionptr);
+ }
+ async_accept();
+ session_process_queued();
+}
+
+void internal_server::session_process_queued()
+{
+ // Determine how many outstanding requests we have.
+ std::size_t count_responding = 0;
+ for(const auto& session : m_sessions)
+ {
+ if (session->m_state == session_state::wait_response)
+ {
+ count_responding++;
+ }
+ }
+
+ // Send out all processed responses.
+ for (auto& session : m_sessions)
+ {
+ if (session)
+ {
+ http_session& s = *session;
+ auto& our = s.m_outstanding_request;
+
+ if (our && our->m_response_sender)
+ {
+ (*our->m_response_sender)(s.stream);
+ our.reset();
+ s.m_state = session_state::queue_reading;
+ }
+ }
+ }
+
+ // Allow for more outstanding requests unless we are already over the maximum.
+ if (count_responding < m_max_simultaneous_requests)
+ {
+ for(auto& session : m_sessions)
+ {
+ if (!session)
+ {
+ // do nothing.
+ }
+ else if (session->m_state == session_state::queue_reading)
+ {
+ http_session& s = *session;
+ s.m_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)
+ {
+ boost::asio::post(shared_server->m_strand, [weak_server, session, ec, 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 session,
+ boost::beast::error_code ec,
+ std::size_t size)
+{
+ http_session& s = *session;
+
+ 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
+ {
+ std::shared_ptr req = std::make_shared();
+ s.m_outstanding_request = req;
+
+ // A regular HTTP request.
+ auto finalize_response = [session, weak_server = weak_from_this(), req](const request_response::response_sender& rs)
+ {
+ // We have a response, add it to our spot in the queue.
+ auto shared_server = weak_server.lock();
+ if (shared_server)
+ {
+ // Queue processing of the queue so we can continue.
+ boost::asio::post(shared_server->m_strand,
+ [shared_server, req, rs]
+ {
+ req->m_response_sender = rs;
+ shared_server->session_process_queued();
+ });
+ }
+ };
+
+ const auto& address = session->stream.socket().remote_endpoint().address();
+ const auto& request = session->parser->get();
+
+ auto rs = request_response(request, finalize_response);
+ auto& internal = rs.create_response>(boost::beast::http::status::internal_server_error, request.version());
+ internal.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING);
+ internal.set(boost::beast::http::field::content_type, "text/plain");
+ internal.prepare_payload();
+ internal.body() = "Internal server error.";
+ internal.keep_alive(request.keep_alive());
+
+ m_handler.handle_request_http(rs);
+
+ s.m_state = session_state::wait_response;
+ }
+ session_process_queued();
+}
+
+void internal_server::session_finalize_response(std::shared_ptr session)
+{
+ if(session)
+ {
+ session->m_state = session_state::queue_reading;
+ session_process_queued();
+ }
+}
+
+void internal_server::session_remove(std::shared_ptr session)
+{
+ m_sessions.erase(std::remove_if(m_sessions.begin(),
+ m_sessions.end(),
+ [session](const std::shared_ptr& s)
+ {
+ return session.get() == s.get();
+ }),
+ m_sessions.end());
+}
diff --git a/bmrshared-web/lib/internal_server.h b/bmrshared-web/lib/internal_server.h
new file mode 100644
index 0000000..b5d6a85
--- /dev/null
+++ b/bmrshared-web/lib/internal_server.h
@@ -0,0 +1,85 @@
+// 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.
+//
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace bmrshared::web {
+namespace detail {
+ enum class session_state {
+ queue_reading,
+ reading,
+ wait_response
+ };
+
+ struct http_request
+ {
+ std::optional m_response_sender;
+ };
+
+ struct http_session {
+ explicit http_session(boost::asio::ip::tcp::socket s)
+ : stream(std::move(s))
+ {
+ }
+
+ ~http_session()
+ {
+ }
+ boost::beast::tcp_stream stream;
+ boost::beast::flat_buffer buffer;
+ boost::optional> parser;
+ session_state m_state = session_state::queue_reading;
+ std::shared_ptr m_outstanding_request;
+ };
+
+ class internal_server final : public std::enable_shared_from_this {
+ 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 session, boost::beast::error_code ec, std::size_t size);
+ void session_finalize_response(std::shared_ptr session);
+ void session_remove(std::shared_ptr session);
+
+ private:
+ boost::asio::io_context& m_io_context;
+ boost::asio::io_context::strand m_strand;
+ 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> m_sessions;
+ };
+} // namespace detail
+} // namespace bmrshared::web
diff --git a/bmrshared-web/lib/request_response.cpp b/bmrshared-web/lib/request_response.cpp
new file mode 100644
index 0000000..2fbc7bb
--- /dev/null
+++ b/bmrshared-web/lib/request_response.cpp
@@ -0,0 +1,56 @@
+// 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
+
+using bmrshared::web::request_response;
+
+request_response::response_writer_interface::~response_writer_interface() = default;
+
+request_response::internal_request_response::internal_request_response(request_type request, callback_on_finalize cb_finalize)
+ : m_request(std::move(request))
+ , m_cb_finalize(std::move(cb_finalize))
+ , m_response_writer()
+{
+}
+
+request_response::internal_request_response::~internal_request_response()
+{
+ if (m_response_writer)
+ {
+ std::shared_ptr shared_writer(std::move(m_response_writer));
+ m_cb_finalize(
+ [shared_writer](boost::beast::tcp_stream& stream)
+ {
+ shared_writer->write_response(stream);
+ });
+ }
+}
+
+const request_response::request_type& request_response::internal_request_response::get_request() const
+{
+ return m_request;
+}
+
+void request_response::internal_request_response::set_response_sender(std::unique_ptr response_writer)
+{
+ m_response_writer = std::move(response_writer);
+}
+
+request_response::request_response(request_type request, callback_on_finalize cb_finalize)
+ : m_internal(std::make_unique(std::move(request), std::move(cb_finalize)))
+{
+}
+
+request_response::~request_response() = default;
+
+const request_response::request_type& request_response::get_request() const
+{
+ return m_internal->get_request();
+}
+
diff --git a/bmrshared-web/lib/server.cpp b/bmrshared-web/lib/server.cpp
new file mode 100644
index 0000000..1cd5025
--- /dev/null
+++ b/bmrshared-web/lib/server.cpp
@@ -0,0 +1,25 @@
+// 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 "internal_server.h"
+#include
+
+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(io_context, listen_endpoint, incoming_request_timeout, request_body_limit, max_simultaneous_requests, handler))
+{
+ m_internal->run();
+}
+
+server::~server() = default;
diff --git a/bmrshared/include/bmrshared/color.hpp b/bmrshared/include/bmrshared/color.hpp
new file mode 100644
index 0000000..88536ef
--- /dev/null
+++ b/bmrshared/include/bmrshared/color.hpp
@@ -0,0 +1,138 @@
+// GNU Lesser General Public License v3.0
+// Copyright (c) 2026 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.
+//
+#pragma once
+#include
+#include
+#include
+
+
+namespace bmrshared
+{
+class color
+{
+public:
+ struct color_hsv
+ {
+ double h;
+ double s;
+ double v;
+ };
+
+ struct color_rgb
+ {
+ double r;
+ double g;
+ double b;
+ };
+
+ color() = delete;
+ ~color() = default;
+ constexpr color(const color&) = default;
+ constexpr color(color&&) = default;
+ constexpr color& operator=(const color&) = default;
+ constexpr color& operator=(color&&) = default;
+
+ constexpr color(const color_hsv& c)
+ : m_rgb()
+ , m_hsv(c)
+ {}
+
+ constexpr color(const color_rgb& c)
+ : m_rgb(c)
+ , m_hsv()
+ {}
+
+ constexpr const color_rgb& rgb() const
+ {
+ if (!m_rgb)
+ {
+ m_rgb = calculate_rgb(*m_hsv);
+ }
+ return *m_rgb;
+ }
+
+ constexpr const color_hsv& hsv() const
+ {
+ if (!m_hsv)
+ {
+ m_hsv = calculate_hsv(*m_rgb);
+ }
+ return *m_hsv;
+ }
+
+private:
+ static constexpr color_hsv calculate_hsv(const color_rgb& c)
+ {
+ color_hsv r{.h = 0.0, .s = 0.0, .v = 0.0};
+ double min = std::min(c.r, std::min(c.g,c.b));
+ r.v = std::max(c.r, std::max(c.g,c.b));
+ double delta = r.v - min;
+ r.s = (r.v <= 0.0) ? 0.0 : (delta / r.v);
+ if (r.s <= 0.0)
+ {
+ r.h = 0.0;
+ }
+ else
+ {
+ if (c.r == r.v)
+ {
+ r.h = (c.g - c.b) / delta;
+ }
+ else if (c.g == r.v)
+ {
+ r.h = 2.0 + (c.b - c.r) / delta;
+ }
+ else if (c.b == r.v)
+ {
+ r.h = 4.0 + (c.r - c.g) / delta;
+ }
+ r.h*=60.0;
+ if (r.h < 0.0)
+ {
+ r.h +=360.0;
+ }
+ }
+ return r;
+ }
+
+ static constexpr color_rgb calculate_rgb(const color_hsv& in)
+ {
+ double C = in.s * in.v;
+ double X = C*(1-std::fabs(std::fmod(in.h/60.0, 2)-1));
+ double m = in.v-C;
+
+ color_rgb result{.r = 0.0, .g = 0.0, .b = 0.0};
+ if(in.h >= 0.0 && in.h < 60.0){
+ result = {.r = C, .g = X, .b = 0.0};
+ }
+ else if(in.h >= 60.0 && in.h < 120.0){
+ result = {.r = X, .g = C, . b = 0.0};
+ }
+ else if(in.h >= 120.0 && in.h < 180.0){
+ result = {.r = 0.0, .g = C, .b = X};
+ }
+ else if(in.h >= 180.0 && in.h < 240.0){
+ result = {.r = 0.0, .g = X, .b = C};
+ }
+ else if(in.h >= 240.0 && in.h < 300.0){
+ result = {.r = X, .g = 0.0, .b = C};
+ }
+ else{
+ result = {.r = C, .g = 0.0, .b = X};
+ }
+ result.r+=m;
+ result.g+=m;
+ result.b+=m;
+
+ return result;
+ }
+
+ mutable std::optional m_rgb;
+ mutable std::optional m_hsv;
+};
+}