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..599dc04 --- /dev/null +++ b/applications/http-mandelbrot/CMakeLists.txt @@ -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 +) diff --git a/applications/http-mandelbrot/src/main.cpp b/applications/http-mandelbrot/src/main.cpp new file mode 100644 index 0000000..10b37e1 --- /dev/null +++ b/applications/http-mandelbrot/src/main.cpp @@ -0,0 +1,260 @@ +// 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 +#include +#include +#include +#include + +namespace +{ +const std::regex regex_renderpath(R"REGEX(\/render_(\d+)_(\d+)_(\d+))REGEX"); + + + +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; +}; + +// Convert a pixel coordinate to the complex domain +std::complex scale(const window& scr, const window& fr, const std::complex& c) +{ + return std::complex(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& c, + int iter_max, + const std::function(std::complex, std::complex)>& func) +{ + std::complex z(0); + int iter = 0; + + while (abs(z) < 2.0 && iter < iter_max) { + z = func(z, c); + iter++; + } + return iter; +} + +void fractal( + const window& scr, + const window&fract, + int max_iterations, + const std::function(std::complex, std::complex)>& fractal_fn, + const std::function& 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(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 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 z, std::complex c) -> std::complex + { + return z * z + c; + }; + + auto zoomfactor = 1.0/std::pow(2, z); + + fractal(window{0,pixel_width, 0,pixel_height}, + window{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(95)); + + boost::beast::http::response 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 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( + ioc, + listen_endpoint, + config_request_timeout, + config_request_body_limit, + config_max_simultanious_requests, + request_handler); + }); + + std::vector threads; + while(threads.size() < 20) + { + threads.emplace_back([&ioc]{ioc.run();}); + } + + ioc.run(); + + threads.clear(); +} 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..f297d79 --- /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(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 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..27f3f95 --- /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" + +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 diff --git a/bmrshared-web/include/bmrshared/response_promise.hpp b/bmrshared-web/include/bmrshared/response_promise.hpp new file mode 100644 index 0000000..079e119 --- /dev/null +++ b/bmrshared-web/include/bmrshared/response_promise.hpp @@ -0,0 +1,40 @@ +// 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 + +namespace bmrshared::web +{ + +class response_promise final +{ + public: + using response_sender = std::function; + using callback_on_response = std::function; + + response_promise() = delete; + explicit response_promise(callback_on_response cbOnResponse); + ~response_promise(); + + template + void SendResponse(boost::beast::http::response 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 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..fef4f2a --- /dev/null +++ b/bmrshared-web/lib/CMakeLists.txt @@ -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 +) + diff --git a/bmrshared-web/lib/directory_request_handler.cpp b/bmrshared-web/lib/directory_request_handler.cpp new file mode 100644 index 0000000..ba4889c --- /dev/null +++ b/bmrshared-web/lib/directory_request_handler.cpp @@ -0,0 +1,165 @@ +// 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 +#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(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 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 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 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 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::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 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 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 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. +} diff --git a/bmrshared-web/lib/internal_server.cpp b/bmrshared-web/lib/internal_server.cpp new file mode 100644 index 0000000..6ba4f24 --- /dev/null +++ b/bmrshared-web/lib/internal_server.cpp @@ -0,0 +1,190 @@ +// 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 + +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(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& 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 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 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 session) +{ + if(session) + { + session->state = session_state::queue_reading; + session_process_queued(); + } +} + +void internal_server::session_remove(std::shared_ptr session) +{ + m_sessions.erase(std::remove(m_sessions.begin(), m_sessions.end(), session), 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..7ff55f4 --- /dev/null +++ b/bmrshared-web/lib/internal_server.h @@ -0,0 +1,73 @@ +// 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 + +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> parser; + session_state state = session_state::queue_reading; + }; + + + + + 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::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/response_promise.cpp b/bmrshared-web/lib/response_promise.cpp new file mode 100644 index 0000000..87dd9a2 --- /dev/null +++ b/bmrshared-web/lib/response_promise.cpp @@ -0,0 +1,17 @@ +// 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 + +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; 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;