From 52b3cd46c76fd4be18aeb8422a08a073484b9fad Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Sat, 29 Aug 2026 12:01:42 +0200 Subject: More module implementation partition units --- server/src/http_server.cppm | 522 ++++++++++++++++++-------------------------- 1 file changed, 214 insertions(+), 308 deletions(-) (limited to 'server/src/http_server.cppm') diff --git a/server/src/http_server.cppm b/server/src/http_server.cppm index f5b4c3e..dc8c183 100644 --- a/server/src/http_server.cppm +++ b/server/src/http_server.cppm @@ -137,25 +137,12 @@ public: { } - auto header() -> bhttp::response_header& - { - return impl_->header(); - } - auto header() const -> bhttp::response_header const& - { - return impl_->header(); - } - - auto is_done() const -> bool { return impl_->is_done(); } - - auto prepare(beast::error_code& ec) -> const_buffers_type - { - return impl_->prepare(ec); - } - - auto consume(std::size_t n) -> void { return impl_->consume(n); } - - auto keep_alive() const noexcept -> bool { return impl_->keep_alive(); } + auto header() -> bhttp::response_header&; + auto header() const -> bhttp::response_header const&; + auto is_done() const -> bool; + auto prepare(beast::error_code& ec) -> const_buffers_type; + auto consume(std::size_t n) -> void; + auto keep_alive() const noexcept -> bool; }; static_assert(beast::concepts::buffers_generator); @@ -207,15 +194,17 @@ struct base_ctx }; template -using basic_route_handler_fn_t = std::function< - auto(Ctx, readable_request, std::vector const& matches) - ->net::awaitable>; +using basic_route_handler_fn_t = + std::function const& matches) + ->net::awaitable>; struct keep_alive { bool value; - explicit keep_alive(bool value) : value{value} {} + explicit keep_alive(bool value); }; template @@ -229,37 +218,16 @@ auto make_rsp(bhttp::status status, keep_alive ka) -> bhttp::response auto problem_rsp( base_ctx const& ctx, problem::details const& problem, keep_alive ka) - -> presponse -{ - auto rsp = make_rsp(problem.status, ka); - rsp.set(bhttp::field::content_type, "application/problem+json"); - rsp.body() = json::serialize(json::value_from(problem, ctx.locale)); - rsp.prepare_payload(); - return presponse{std::move(rsp)}; -} + -> presponse; struct preflight_response { verb_set allow_methods; std::vector allow_headers; }; + auto make_preflight_rsp(preflight_response res, keep_alive ka) - -> bhttp::response -{ - auto rsp = make_rsp(bhttp::status::no_content, ka); - auto allow_headers_str = res.allow_headers - | std::views::transform( - [](auto const& field) -> std::string_view - { return bhttp::to_string(field); }) - | std::views::join_with(std::string_view{", "}) - | std::ranges::to(); - rsp.set( - bhttp::field::access_control_allow_methods, - res.allow_methods.to_string()); - rsp.set(bhttp::field::access_control_allow_headers, allow_headers_str); - rsp.prepare_payload(); - return rsp; -} + -> bhttp::response; // Using base_ctx instead of a template here since that saves you // typing on invocation (and we do not care about the context type @@ -276,8 +244,8 @@ auto read_request(base_ctx const& ctx, readable_request&& r) template <> auto read_request(base_ctx const& ctx, readable_request&& r) - -> net::awaitable< - std::expected, presponse>> + -> net:: + awaitable, presponse>> { auto [ec, _] = co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple); @@ -340,19 +308,7 @@ auto default_options_handler( } auto global_options_handler(base_ctx const& ctx, readable_request r) - -> net::awaitable -{ - // TODO: switch to "small (4KB) discarded" body type, similar to what Go - // does? Same goes for default_options_handler? Not sure. - if (auto res = co_await read_request(ctx, std::move(r)); - !res) - co_return std::move(res.error()); - auto req = r.p->release(); - auto rsp = make_rsp( - bhttp::status::no_content, keep_alive{req.keep_alive()}); - rsp.prepare_payload(); - co_return std::move(rsp); -} + -> net::awaitable; template auto id_middleware( @@ -427,28 +383,22 @@ struct handler_map auto verbs() const -> verb_set { - auto set = verb_set{}; - if (static_cast(options)) - set.enable(supported_verb::options); - if (static_cast(delete_)) - set.enable(supported_verb::delete_); - if (static_cast(get)) - set.enable(supported_verb::get); - if (static_cast(head)) - set.enable(supported_verb::head); - if (static_cast(post)) - set.enable(supported_verb::post); - if (static_cast(put)) - set.enable(supported_verb::put); - return set; + return verb_set{ + .options = static_cast(options), + .delete_ = static_cast(delete_), + .get = static_cast(get), + .head = static_cast(head), + .post = static_cast(post), + .put = static_cast(put), + }; } auto empty() const -> bool { return verbs().empty(); } template auto map(std::invocable auto f) const -> handler_map - requires std::assignable_from< - U&, std::invoke_result_t> + requires std:: + assignable_from> { return { .options = static_cast(options) ? f(options) : U{}, @@ -508,8 +458,9 @@ template concept match_arg = std::constructible_from; template -using route_handler_fn_t = std::function< - auto(Ctx, readable_request, MatchArgs...)->net::awaitable>; +using route_handler_fn_t = + std::functionnet::awaitable>; template auto degen_route_handler(route_handler_fn_t fn) @@ -588,271 +539,226 @@ struct dtree : handler_map> } }; -template PreRouteCtx> -class server +class router { - log::logger l_; - locale::selector lsel_; - middleware_t global_middleware_; - route_tree> routes_; - -public: - explicit server( - log::logger const& l, locale::selector&& lsel, - middleware_t global_middleware, - route_tree> routes) - : l_{l.sub("http_server")}, lsel_{std::move(lsel)}, - global_middleware_{std::move(global_middleware)}, - routes_{std::move(routes)} - { - } - - struct match_result + struct impl_base { - util::not_null< - handler_map>> const*> - route_handlers; - std::vector wildcard_matches; - - auto allowed_methods() const -> verb_set { return route_handlers->verbs(); } + virtual ~impl_base() = default; + virtual auto handle_request(readable_request r) const + -> net::awaitable = 0; }; - auto match(boost::urls::segments_view segments) const - -> std::optional + std::unique_ptr impl_; + + template PreRouteCtx> + class impl : public impl_base { - auto const* tree = &routes_; - auto wildcard_matches = std::vector{}; - for (auto const& seg : segments) + locale::selector lsel_; + middleware_t global_middleware_; + route_tree> routes_; + + public: + explicit impl( + locale::selector&& lsel, + middleware_t global_middleware, + route_tree> routes) + : lsel_{std::move(lsel)}, + global_middleware_{std::move(global_middleware)}, + routes_{std::move(routes)} { - tree->sub.visit( - util::overloaded{ - [&](route_tree>::named_subtrees const& - subtrees) - { - auto it = subtrees.find(seg); - tree = it == subtrees.end() ? nullptr : &it->second; - }, - [&](route_tree>::wildcard_subtree const& - wildcard_subtree) - { - wildcard_matches.push_back(seg); - tree = &*wildcard_subtree; - }, - }); - if (!tree) - return std::nullopt; } - if (tree->here.empty()) - return std::nullopt; - return match_result{ - .route_handlers = util::not_null{&tree->here}, - .wildcard_matches = wildcard_matches, - }; - } - auto route_request(PreRouteCtx ctx, readable_request r) const - -> net::awaitable - { - auto req_base = r.p->get().base(); + struct match_result + { + util::not_null< + handler_map>> const* + > + route_handlers; + std::vector wildcard_matches; - auto const bad_request_tpl = problem::tpl{ - .status = bhttp::status::bad_request, - .title = translate("Bad request"), - .type_uri = "https://routemon.fautchen.eu/problems/bad-request", + auto allowed_methods() const -> verb_set + { + return route_handlers->verbs(); + } }; - if (req_base.target() == "*") + auto match(boost::urls::segments_view segments) const + -> std::optional { - // request-target is in asterisk-form (RFC 9112, § 3.2.4), - // so the request must be a server-wide OPTIONS request. - - if (req_base.method() != bhttp::verb::options) + auto const* tree = &routes_; + auto wildcard_matches = std::vector{}; + for (auto const& seg : segments) { - auto tpl = problem::tpl{ - .status = bhttp::status::method_not_allowed, - .title = translate("Method not allowed"), - .type_uri = "https://routemon.fautchen.eu/problems/" - "method-not-allowed", - }; - co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + tree->sub.visit( + util::overloaded{ + [&](route_tree>::named_subtrees const& + subtrees) + { + auto it = subtrees.find(seg); + tree = it == subtrees.end() ? nullptr : &it->second; + }, + [&](route_tree>::wildcard_subtree const& + wildcard_subtree) + { + wildcard_matches.push_back(seg); + tree = &*wildcard_subtree; + }, + }); + if (!tree) + return std::nullopt; } - - co_return co_await global_options_handler(ctx, r); + if (tree->here.empty()) + return std::nullopt; + return match_result{ + .route_handlers = util::not_null{&tree->here}, + .wildcard_matches = wildcard_matches, + }; } - else if (auto mreq_url0 = boost::urls::parse_origin_form(req_base.target())) + + auto route_request(PreRouteCtx ctx, readable_request r) const + -> net::awaitable { - // request-target is in origin-form (RFC 9112, § 3.2.1), - // so it must be a normal request (not a CONNECT or - // server-wide OPTIONS request). + auto req_base = r.p->get().base(); - auto req_url = boost::urls::url{*mreq_url0}; - req_url.normalize(); - if (!req_url.is_path_absolute()) - { - auto problem = bad_request_tpl.instantiate().set_detail(translate( - "Path of normalized (RFC 3986, § 6) " - "origin-form request-target (RFC " - "9112, § 3.2.1) should be " - "absolute")); - co_return problem_rsp(ctx, problem, keep_alive{false}); - } + auto const bad_request_tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("Bad request"), + .type_uri = "https://routemon.fautchen.eu/problems/bad-request", + }; - auto mres = match(req_url.segments()); - if (!mres) + if (req_base.target() == "*") { - auto tpl = problem::tpl{ - .status = bhttp::status::not_found, - .title = translate("Not found"), - .type_uri = "https://routemon.fautchen.eu/problems/not-found", - }; - co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); - } + // request-target is in asterisk-form (RFC 9112, § 3.2.4), + // so the request must be a server-wide OPTIONS request. - auto mverb = supported_verb::from(req_base.method()); - if (!mverb) - { - // Method not implemented. - auto tpl = problem::tpl{ - .status = bhttp::status::not_implemented, - .title = translate("Method not implemented"), - .type_uri = "https://routemon.fautchen.eu/problems/" - "method-not-implemented", - }; - co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); - } + if (req_base.method() != bhttp::verb::options) + { + auto tpl = problem::tpl{ + .status = bhttp::status::method_not_allowed, + .title = translate("Method not allowed"), + .type_uri = "https://routemon.fautchen.eu/problems/" + "method-not-allowed", + }; + co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + } - if (auto mhdl = mres->route_handlers->lookup(*mverb)) + co_return co_await global_options_handler(ctx, r); + } + else if (auto mreq_url0 = + boost::urls::parse_origin_form(req_base.target())) { - auto new_ctx = - routed_ctx{std::move(ctx), mres->allowed_methods()}; - co_return co_await mhdl(std::move(new_ctx), r, mres->wildcard_matches); + // request-target is in origin-form (RFC 9112, § 3.2.1), + // so it must be a normal request (not a CONNECT or + // server-wide OPTIONS request). + + auto req_url = boost::urls::url{*mreq_url0}; + req_url.normalize(); + if (!req_url.is_path_absolute()) + { + auto problem = bad_request_tpl.instantiate().set_detail(translate( + "Path of normalized (RFC 3986, § 6) " + "origin-form request-target (RFC " + "9112, § 3.2.1) should be " + "absolute")); + co_return problem_rsp(ctx, problem, keep_alive{false}); + } + + auto mres = match(req_url.segments()); + if (!mres) + { + auto tpl = problem::tpl{ + .status = bhttp::status::not_found, + .title = translate("Not found"), + .type_uri = "https://routemon.fautchen.eu/problems/not-found", + }; + co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + } + + auto mverb = supported_verb::from(req_base.method()); + if (!mverb) + { + // Method not implemented. + auto tpl = problem::tpl{ + .status = bhttp::status::not_implemented, + .title = translate("Method not implemented"), + .type_uri = "https://routemon.fautchen.eu/problems/" + "method-not-implemented", + }; + co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + } + + if (auto mhdl = mres->route_handlers->lookup(*mverb)) + { + auto new_ctx = + routed_ctx{std::move(ctx), mres->allowed_methods()}; + co_return co_await mhdl( + std::move(new_ctx), r, mres->wildcard_matches); + } + else + { + // Path recognized, but method not allowed. + auto tpl = problem::tpl{ + .status = bhttp::status::method_not_allowed, + .title = translate("Method not allowed"), + .type_uri = "https://routemon.fautchen.eu/problems/" + "method-not-allowed", + }; + auto rsp = problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + rsp.header().set( + bhttp::field::allow, mres->allowed_methods().to_string()); + co_return std::move(rsp); + } } else { - // Path recognized, but method not allowed. - auto tpl = problem::tpl{ - .status = bhttp::status::method_not_allowed, - .title = translate("Method not allowed"), - .type_uri = "https://routemon.fautchen.eu/problems/" - "method-not-allowed", - }; - auto rsp = problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); - rsp.header().set( - bhttp::field::allow, mres->allowed_methods().to_string()); - co_return std::move(rsp); + // We do not accept any other request-target forms. + + auto problem = bad_request_tpl.instantiate().set_detail(translate( + "Invalid request-target, expected " + "asterisk-form or origin-form " + "(see RFC 9112, § 3.2)")); + co_return problem_rsp(ctx, problem, keep_alive{false}); } } - else - { - // We do not accept any other request-target forms. - auto problem = bad_request_tpl.instantiate().set_detail(translate( - "Invalid request-target, expected " - "asterisk-form or origin-form " - "(see RFC 9112, § 3.2)")); - co_return problem_rsp(ctx, problem, keep_alive{false}); + auto handle_request(readable_request r) const + -> net::awaitable override + { + auto header = r.p->get().base(); + auto locale = lsel_.select(header[bhttp::field::accept_language]); + co_return co_await global_middleware_( + base_ctx{.locale = locale}, header, + [&](PreRouteCtx ctx) -> net::awaitable + { co_return co_await route_request(std::move(ctx), std::move(r)); }); } - } + }; - auto handle_request(readable_request r) const -> net::awaitable +public: + template PreRouteCtx> + explicit router( + locale::selector&& lsel, + middleware_t global_middleware, + route_tree> routes) + : impl_{std::make_unique>( + std::move(lsel), std::move(global_middleware), std::move(routes))} { - auto header = r.p->get().base(); - auto locale = lsel_.select(header[bhttp::field::accept_language]); - auto ctx0 = base_ctx{.locale = locale}; - - co_return co_await global_middleware_( - std::move(ctx0), header, - [&](PreRouteCtx ctx) -> net::awaitable - { co_return co_await route_request(std::move(ctx), std::move(r)); }); } - auto do_session(beast::tcp_stream strm) -> net::awaitable - { - auto buf = beast::flat_buffer{}; - - while (true) - { - auto p0 = bhttp::request_parser{}; - p0.body_limit(boost::none); - auto [ec, _] = - co_await bhttp::async_read_header(strm, buf, p0, net::as_tuple); - if (ec == bhttp::error::end_of_stream) - break; - else if (ec) - throw boost::system::system_error{ec}; - - auto http_version = p0.get().version(); - auto&& rsp = co_await handle_request( - readable_request{ - .p = util::not_null{&p0}, - .strm = util::not_null{&strm}, - .buf = util::not_null{&buf}, - }); - rsp.header().version(http_version); - bool keep_alive = rsp.keep_alive(); - co_await beast::async_write(strm, std::move(rsp)); - if (!keep_alive) - { - break; - } - } - - strm.socket().shutdown(tcp::socket::shutdown_send); - } + auto handle_request(readable_request r) const -> net::awaitable; +}; - auto do_listen(tcp::endpoint endpoint) -> net::awaitable - { - auto executor = co_await net::this_coro::executor; - auto acceptor = tcp::acceptor{executor, endpoint}; +class server +{ + log::logger l_; + router r_; - l_.with("endpoint", endpoint.address().to_string()) - .with("port", std::to_string(endpoint.port())) - .info("Serving"); - while (true) - { - net::co_spawn( - executor, - do_session(beast::tcp_stream{co_await acceptor.async_accept()}), - [this](std::exception_ptr e) - { - if (e) - { - try - { - std::rethrow_exception(e); - } - catch (std::exception const& e) - { - l_.error("Error in session: {}", e.what()); - } - } - }); - } - } + auto do_session(beast::tcp_stream strm) -> net::awaitable; + auto do_listen(tcp::endpoint endpoint) -> net::awaitable; - auto spawn(net::io_context& ioc) -> void - { - auto const addr = net::ip::make_address("0.0.0.0"); - auto const endpoint = tcp::endpoint{addr, 8284}; +public: + explicit server(log::logger const& l, router&& r); - // TODO: make exception handling as nice as in srv.cpp - net::co_spawn( - ioc, do_listen(endpoint), - [this](std::exception_ptr e) - { - if (e) - { - try - { - std::rethrow_exception(e); - } - catch (std::exception const& e) - { - l_.error("Error: {}", e.what()); - } - } - }); - } + auto spawn(net::io_context& ioc) -> void; }; } // namespace routemon::http -- cgit v1.3