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; +}; +}