From a5d95afb96eb9a3b65d82f5f84bf8e4a4fb4c8ac Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Fri, 28 Aug 2026 21:12:55 +0200 Subject: clang-format C++ sources --- server/src/http_server.cppm | 1301 +++++++++++++++++++++++++------------------ 1 file changed, 749 insertions(+), 552 deletions(-) (limited to 'server/src/http_server.cppm') diff --git a/server/src/http_server.cppm b/server/src/http_server.cppm index 0770d97..0f7fda7 100644 --- a/server/src/http_server.cppm +++ b/server/src/http_server.cppm @@ -1,12 +1,12 @@ module; -#include #include #include #include #include #include #include +#include #include #include @@ -23,639 +23,836 @@ using tcp = net::ip::tcp; namespace routemon::http { - struct readable_request { - util::not_null*> p; - util::not_null strm; - util::not_null buf; +struct readable_request +{ + util::not_null*> p; + util::not_null strm; + util::not_null buf; +}; + +class presponse +{ +public: + using const_buffers_type = beast::span; + +private: + struct impl_base + { + virtual ~impl_base() = default; + virtual auto header() -> bhttp::response_header& = 0; + virtual auto header() const + -> bhttp::response_header const& = 0; + virtual auto is_done() const -> bool = 0; + virtual auto prepare(beast::error_code&) -> const_buffers_type = 0; + virtual auto consume(std::size_t n) -> void = 0; + virtual auto keep_alive() const -> bool = 0; }; - - class presponse { - public: - using const_buffers_type = beast::span; - - private: - struct impl_base { - virtual ~impl_base() = default; - virtual auto header() -> bhttp::response_header& = 0; - virtual auto header() const -> bhttp::response_header const& = 0; - virtual auto is_done() const -> bool = 0; - virtual auto prepare(beast::error_code&) -> const_buffers_type = 0; - virtual auto consume(std::size_t n) -> void = 0; - virtual auto keep_alive() const -> bool = 0; - }; - std::unique_ptr impl_; - - template - class impl : public impl_base { - // Initializes in the response state. - // At the first call to prepare, we switch to the message generator state. - // After that point, header may not be called anymore (it will throw). - std::variant, bhttp::message_generator> state_; - - auto ensure_message_generator() -> bhttp::message_generator& { - if (auto prsp = std::get_if>(&state_)) { - auto rsp = bhttp::response{std::move(*prsp)}; - state_.template emplace(std::move(rsp)); - } - return std::get(state_); + std::unique_ptr impl_; + + template + class impl : public impl_base + { + // Initializes in the response state. + // At the first call to prepare, we switch to the message generator + // state. After that point, header may not be called anymore (it will + // throw). + std::variant, bhttp::message_generator> state_; + + auto ensure_message_generator() -> bhttp::message_generator& + { + if (auto prsp = std::get_if>(&state_)) + { + auto rsp = bhttp::response{std::move(*prsp)}; + state_.template emplace(std::move(rsp)); } + return std::get(state_); + } - public: - explicit impl(bhttp::response&& rsp) : state_{std::move(rsp)} {} + public: + explicit impl(bhttp::response&& rsp) : state_{std::move(rsp)} {} - auto header() -> bhttp::response_header& override { - if (auto prsp = std::get_if>(&state_)) { - return prsp->base(); - } else { - // TODO: define custom exception type presponse::bad_header_access - throw std::logic_error{"header() may not be called after prepare()"}; - } + auto header() -> bhttp::response_header& override + { + if (auto prsp = std::get_if>(&state_)) + { + return prsp->base(); } - auto header() const -> bhttp::response_header const& override { - if (auto prsp = std::get_if>(&state_)) { - return prsp->base(); - } else { - throw std::logic_error{"header() may not be called after prepare()"}; - } + else + { + // TODO: define custom exception type + // presponse::bad_header_access + throw std::logic_error{"header() may not be called after prepare()"}; } - - auto is_done() const -> bool override { - if (auto pgen = std::get_if(&state_)) { - return pgen->is_done(); - } else /* still in the response state */ { - return false; - } + } + auto header() const -> bhttp::response_header const& override + { + if (auto prsp = std::get_if>(&state_)) + { + return prsp->base(); } - - auto prepare(beast::error_code& ec) -> const_buffers_type override { - return ensure_message_generator().prepare(ec); + else + { + throw std::logic_error{"header() may not be called after prepare()"}; } + } - auto consume(std::size_t n) -> void override { - ensure_message_generator().consume(n); + auto is_done() const -> bool override + { + if (auto pgen = std::get_if(&state_)) + { + return pgen->is_done(); } - - auto keep_alive() const noexcept -> bool override { - return state_.visit(util::overloaded{ - [](bhttp::response const& rsp) -> bool { - return rsp.keep_alive(); - }, - [](bhttp::message_generator const& gen) -> bool { - return gen.keep_alive(); - }, - }); + else /* still in the response state */ + { + return false; } - }; - - public: - template - explicit presponse(bhttp::response&& rsp) - : impl_{new impl{std::move(rsp)}} - {} - - 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 prepare(beast::error_code& ec) -> const_buffers_type override + { + return ensure_message_generator().prepare(ec); } - auto consume(std::size_t n) -> void { - return impl_->consume(n); + auto consume(std::size_t n) -> void override + { + ensure_message_generator().consume(n); } - auto keep_alive() const noexcept -> bool { - return impl_->keep_alive(); + auto keep_alive() const noexcept -> bool override + { + return state_.visit( + util::overloaded{ + [](bhttp::response const& rsp) -> bool + { return rsp.keep_alive(); }, + [](bhttp::message_generator const& gen) -> bool + { return gen.keep_alive(); }, + }); } }; - static_assert(beast::concepts::buffers_generator); - - template - using next_handler_t = std::function net::awaitable>; - - template - using middleware_t = std::function&, next_handler_t) -> net::awaitable>; - - template - auto lax_cors_middleware(Ctx ctx, bhttp::request_header& req_hdr, next_handler_t next) -> net::awaitable { - std::ignore = req_hdr; - auto prersp = co_await next(ctx); - prersp.header().set(bhttp::field::access_control_allow_origin, "*"); - co_return std::move(prersp); - } - - template - struct trace_id_ctx : InnerCtx { - trace::id trace_id = {}; - }; - template - auto trace_id_middleware(OuterCtx ctx0, bhttp::request_header& req_hdr, next_handler_t> next) -> net::awaitable { - std::ignore = req_hdr; - auto ctx = trace_id_ctx{std::move(ctx0)}; - auto prersp = co_await next(std::move(ctx)); - prersp.header().set("X-Routemon-Trace-Id", std::string_view{ctx.trace_id.as_string()}); - prersp.header().insert(bhttp::field::access_control_expose_headers, "X-Routemon-Trace-Id"); - co_return std::move(prersp); +public: + template + explicit presponse(bhttp::response&& rsp) + : impl_{new impl{std::move(rsp)}} + { } - struct base_ctx { - std::locale locale; - }; - - template - using basic_route_handler_fn_t = std::function const& matches) -> net::awaitable>; - - struct keep_alive { - bool value; - - explicit keep_alive(bool value) : value{value} {} - }; - - template - auto make_rsp(bhttp::status status, keep_alive ka) -> bhttp::response { - auto rsp = bhttp::response{}; // HTTP version gets set later - rsp.result(status); - rsp.keep_alive(ka.value); - return rsp; + auto header() -> bhttp::response_header& + { + return impl_->header(); } - - 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)}; + auto header() const -> bhttp::response_header const& + { + return impl_->header(); } - 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; - } + auto is_done() const -> bool { return impl_->is_done(); } - // Using base_ctx instead of a template here since that saves you - // typing on invocation (and we do not care about the context type - // anyway, but all context types should derive from base_ctx). - template - auto read_request(base_ctx const& ctx, readable_request&& r) -> net::awaitable, presponse>> { - std::ignore = ctx; - auto p = bhttp::request_parser{std::move(*r.p)}; - co_await bhttp::async_read(*r.strm, *r.buf, p); - co_return std::move(p.release()); + auto prepare(beast::error_code& ec) -> const_buffers_type + { + return impl_->prepare(ec); } - template<> - auto read_request(base_ctx const& ctx, readable_request&& r) -> net::awaitable, presponse>> { - auto [ec, _] = co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple); - if (ec == bhttp::error::unexpected_body) { - auto tpl = problem::tpl{ - .status = bhttp::status::bad_request, - .title = translate("No body expected for this request"), + auto consume(std::size_t n) -> void { return impl_->consume(n); } + + auto keep_alive() const noexcept -> bool { return impl_->keep_alive(); } +}; +static_assert(beast::concepts::buffers_generator); + +template +using next_handler_t = std::functionnet::awaitable>; + +template +using middleware_t = + std::function&, + next_handler_t) + ->net::awaitable>; + +template +auto lax_cors_middleware( + Ctx ctx, bhttp::request_header& req_hdr, + next_handler_t next) -> net::awaitable +{ + std::ignore = req_hdr; + auto prersp = co_await next(ctx); + prersp.header().set(bhttp::field::access_control_allow_origin, "*"); + co_return std::move(prersp); +} + +template +struct trace_id_ctx : InnerCtx +{ + trace::id trace_id = {}; +}; + +template +auto trace_id_middleware( + OuterCtx ctx0, bhttp::request_header& req_hdr, + next_handler_t> next) -> net::awaitable +{ + std::ignore = req_hdr; + auto ctx = trace_id_ctx{std::move(ctx0)}; + auto prersp = co_await next(std::move(ctx)); + prersp.header().set( + "X-Routemon-Trace-Id", std::string_view{ctx.trace_id.as_string()}); + prersp.header().insert( + bhttp::field::access_control_expose_headers, "X-Routemon-Trace-Id"); + co_return std::move(prersp); +} + +struct base_ctx +{ + std::locale locale; +}; + +template +using basic_route_handler_fn_t = std::function< + auto(Ctx, readable_request, std::vector const& matches) + ->net::awaitable>; + +struct keep_alive +{ + bool value; + + explicit keep_alive(bool value) : value{value} {} +}; + +template +auto make_rsp(bhttp::status status, keep_alive ka) -> bhttp::response +{ + auto rsp = bhttp::response{}; // HTTP version gets set later + rsp.result(status); + rsp.keep_alive(ka.value); + return rsp; +} + +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)}; +} + +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; +} + +// Using base_ctx instead of a template here since that saves you +// typing on invocation (and we do not care about the context type +// anyway, but all context types should derive from base_ctx). +template +auto read_request(base_ctx const& ctx, readable_request&& r) + -> net::awaitable, presponse>> +{ + std::ignore = ctx; + auto p = bhttp::request_parser{std::move(*r.p)}; + co_await bhttp::async_read(*r.strm, *r.buf, p); + co_return std::move(p.release()); +} + +template <> +auto read_request(base_ctx const& ctx, readable_request&& r) + -> net::awaitable< + std::expected, presponse>> +{ + auto [ec, _] = + co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple); + if (ec == bhttp::error::unexpected_body) + { + auto tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("No body expected for this request"), .type_uri = "https://routemon.fautchen.eu/problems/unexpected-body", - }; - co_return std::unexpected{problem_rsp(ctx, tpl.instantiate(), keep_alive{false})}; - } else if (ec) { - throw boost::system::system_error{ec}; - } - co_return r.p->release(); + }; + co_return std::unexpected{problem_rsp( + ctx, tpl.instantiate(), keep_alive{false})}; } + else if (ec) + { + throw boost::system::system_error{ec}; + } + co_return r.p->release(); +} - template - struct routed_ctx : InnerCtx { - verb_set route_methods; - }; +template +struct routed_ctx : InnerCtx +{ + verb_set route_methods; +}; - template +template requires requires(Ctx ctx) { // Ctx must be derived from an instantiation of routed_ctx [](routed_ctx const&) {}(ctx); } - auto default_options_handler(Ctx const& ctx, readable_request r, std::vector const&) -> net::awaitable { - auto mreq = co_await read_request(ctx, std::move(r)); - if (!mreq) - co_return std::move(mreq.error()); - - if (mreq->find(bhttp::field::access_control_request_method) != mreq->end()) { - // CORS preflight request - co_return make_preflight_rsp(preflight_response{ - // TODO: should access-control-allow-methods contain OPTIONS? - .allow_methods = ctx.route_methods, - .allow_headers = {bhttp::field::content_type}, - }, keep_alive{mreq->keep_alive()}); - } else { - // Normal OPTIONS request - auto rsp = make_rsp(bhttp::status::no_content, keep_alive{mreq->keep_alive()}); - rsp.set(bhttp::field::allow, ctx.route_methods.to_string()); - rsp.prepare_payload(); - co_return std::move(rsp); - } +auto default_options_handler( + Ctx const& ctx, readable_request r, std::vector const&) + -> net::awaitable +{ + auto mreq = co_await read_request(ctx, std::move(r)); + if (!mreq) + co_return std::move(mreq.error()); + + if (mreq->find(bhttp::field::access_control_request_method) != mreq->end()) + { + // CORS preflight request + co_return make_preflight_rsp( + preflight_response{ + // TODO: should access-control-allow-methods contain OPTIONS? + .allow_methods = ctx.route_methods, + .allow_headers = {bhttp::field::content_type}, + }, + keep_alive{mreq->keep_alive()}); } - - 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()}); + else + { + // Normal OPTIONS request + auto rsp = make_rsp( + bhttp::status::no_content, keep_alive{mreq->keep_alive()}); + rsp.set(bhttp::field::allow, ctx.route_methods.to_string()); rsp.prepare_payload(); co_return std::move(rsp); } +} + +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); +} + +template +auto id_middleware( + Ctx ctx, bhttp::request_header&, next_handler_t next) + -> net::awaitable +{ + co_return co_await next(std::move(ctx)); +} + +template +auto middleware_compose(middleware_t ab, middleware_t bc) + -> middleware_t +{ + return [ab = std::move(ab), bc = std::move(bc)]( + A a, bhttp::request_header& header, + next_handler_t next) -> net::awaitable + { + co_return co_await ab( + std::move(a), header, [&](B b) -> net::awaitable + { co_return co_await bc(std::move(b), header, next); }); + }; +} + +template +auto middleware_wrap_fn(middleware_t ab, basic_route_handler_fn_t fn) + -> basic_route_handler_fn_t +{ + return + [ab = std::move(ab), fn = std::move(fn)]( + A a_ctx, readable_request r, + std::vector const& matches) -> net::awaitable + { + co_return co_await ab( + std::move(a_ctx), r.p->get().base(), + [&](B b_ctx) -> net::awaitable + { co_return co_await fn(std::move(b_ctx), r, matches); }); + }; +} - template - auto id_middleware(Ctx ctx, bhttp::request_header&, next_handler_t next) -> net::awaitable { - co_return co_await next(std::move(ctx)); - } - - template - auto middleware_compose(middleware_t ab, middleware_t bc) -> middleware_t { - return [ab = std::move(ab), bc = std::move(bc)](A a, bhttp::request_header& header, next_handler_t next) -> net::awaitable { - co_return co_await ab(std::move(a), header, [&](B b) -> net::awaitable { - co_return co_await bc(std::move(b), header, next); - }); - }; - } - - template - auto middleware_wrap_fn(middleware_t ab, basic_route_handler_fn_t fn) -> basic_route_handler_fn_t { - return [ab = std::move(ab), fn = std::move(fn)](A a_ctx, readable_request r, std::vector const& matches) -> net::awaitable { - co_return co_await ab(std::move(a_ctx), r.p->get().base(), [&](B b_ctx) -> net::awaitable { - co_return co_await fn(std::move(b_ctx), r, matches); - }); - }; - } - - template +template requires requires(V v) { { static_cast(v) }; } - struct handler_map { - V options = {}; - V delete_ = {}; - V get = {}; - V head = {}; - V post = {}; - V put = {}; - - template - auto lookup(this Self&& self, supported_verb v) -> auto&& { - switch (v.value) { - case supported_verb::options: return std::forward(self).options; - case supported_verb::delete_: return std::forward(self).delete_; - case supported_verb::get: return std::forward(self).get; - case supported_verb::head: return std::forward(self).head; - case supported_verb::post: return std::forward(self).post; - case supported_verb::put: return std::forward(self).put; - } +struct handler_map +{ + V options = {}; + V delete_ = {}; + V get = {}; + V head = {}; + V post = {}; + V put = {}; + + template + auto lookup(this Self&& self, supported_verb v) -> auto&& + { + switch (v.value) + { + case supported_verb::options: + return std::forward(self).options; + case supported_verb::delete_: + return std::forward(self).delete_; + case supported_verb::get: + return std::forward(self).get; + case supported_verb::head: + return std::forward(self).head; + case supported_verb::post: + return std::forward(self).post; + case supported_verb::put: + return std::forward(self).put; } + } - 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; - } + 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; + } - auto empty() const -> bool { - return verbs().empty(); - } + auto empty() const -> bool { return verbs().empty(); } - template - auto map(std::invocable auto f) const -> handler_map - requires std::assignable_from> - { - return { + template + auto map(std::invocable auto f) const -> handler_map + requires std::assignable_from< + U&, std::invoke_result_t> + { + return { .options = static_cast(options) ? f(options) : U{}, .delete_ = static_cast(delete_) ? f(delete_) : U{}, - .get = static_cast(get) ? f(get) : U{}, - .head = static_cast(head) ? f(head) : U{}, - .post = static_cast(post) ? f(post) : U{}, - .put = static_cast(put) ? f(put) : U{}, - }; - } - }; - - template - struct route_tree { - using leaves = handler_map>; - using named_subtrees = std::unordered_map; - using wildcard_subtree = std::indirect; - - leaves here; - // TODO: consider making the first alternative a radix tree - // Note: the map is the first variant here; the variant will be - // default-constructed with the default-constructed first - // alternative. The empty map denotes a lack of subtrees. - std::variant sub; - }; - - template - auto middleware_wrap_tree(middleware_t mw, route_tree const& tree) -> route_tree { - auto new_leaves = tree.here.template map>(std::bind_front(middleware_wrap_fn, mw)); - auto new_sub = tree.sub.visit(util::overloaded{ - [&mw](route_tree::named_subtrees const& subtrees) -> decltype(route_tree::sub) { - auto new_subtrees = typename route_tree::named_subtrees{}; - for (auto [seg, subtree] : subtrees) - new_subtrees[seg] = middleware_wrap_tree(mw, subtree); - return new_subtrees; - }, - [&mw](route_tree::wildcard_subtree const& subtree) -> decltype(route_tree::sub) { - return typename route_tree::wildcard_subtree{middleware_wrap_tree(mw, *subtree)}; - }, + .get = static_cast(get) ? f(get) : U{}, + .head = static_cast(head) ? f(head) : U{}, + .post = static_cast(post) ? f(post) : U{}, + .put = static_cast(put) ? f(put) : U{}, + }; + } +}; + +template +struct route_tree +{ + using leaves = handler_map>; + using named_subtrees = std::unordered_map; + using wildcard_subtree = std::indirect; + + leaves here; + // TODO: consider making the first alternative a radix tree + // Note: the map is the first variant here; the variant will be + // default-constructed with the default-constructed first + // alternative. The empty map denotes a lack of subtrees. + std::variant sub; +}; + +template +auto middleware_wrap_tree( + middleware_t mw, route_tree const& tree) + -> route_tree +{ + auto new_leaves = tree.here.template map>( + std::bind_front(middleware_wrap_fn, mw)); + auto new_sub = tree.sub.visit( + util::overloaded{ + [&mw](route_tree::named_subtrees const& subtrees) + -> decltype(route_tree::sub) + { + auto new_subtrees = typename route_tree::named_subtrees{}; + for (auto [seg, subtree] : subtrees) + new_subtrees[seg] = middleware_wrap_tree(mw, subtree); + return new_subtrees; + }, + [&mw](route_tree::wildcard_subtree const& subtree) + -> decltype(route_tree::sub) + { + return typename route_tree::wildcard_subtree{ + middleware_wrap_tree(mw, *subtree) + }; + }, }); - return {.here = new_leaves, .sub = new_sub}; + return {.here = new_leaves, .sub = new_sub}; +} + +template +concept match_arg = std::constructible_from; + +template +using route_handler_fn_t = std::function< + auto(Ctx, readable_request, MatchArgs...)->net::awaitable>; + +template +auto degen_route_handler(route_handler_fn_t fn) + -> basic_route_handler_fn_t +{ + return + [fn = std::move(fn)]( + Ctx ctx, readable_request r, + std::vector const& matches) -> net::awaitable + { + if (sizeof...(MatchArgs) != matches.size()) + throw std::runtime_error{"got unexpected amount of matches"}; + auto it = matches.begin(); + co_return co_await fn( + std::move(ctx), r, + MatchArgs{static_cast(*it++)}...); + }; +} + +template +struct ctree : route_tree +{ + template + auto wrap(middleware_t mw) const + -> ctree + { + return {middleware_wrap_tree(std::move(mw), *this)}; + } +}; + +template +struct dtree : handler_map> +{ + [[nodiscard]] auto to_leaves() const -> typename route_tree::leaves + { + auto here = this->template map>( + degen_route_handler); + if (!here.verbs().empty() && !static_cast(this->options)) + here.options = default_options_handler; + return here; } - template - concept match_arg = std::constructible_from; - - template - using route_handler_fn_t = std::function net::awaitable>; - - template - auto degen_route_handler(route_handler_fn_t fn) -> basic_route_handler_fn_t { - return [fn = std::move(fn)](Ctx ctx, readable_request r, std::vector const& matches) -> net::awaitable { - if (sizeof...(MatchArgs) != matches.size()) - throw std::runtime_error{"got unexpected amount of matches"}; - auto it = matches.begin(); - co_return co_await fn(std::move(ctx), r, MatchArgs{static_cast(*it++)}...); + [[nodiscard]] auto named_subtrees( + std::initializer_list>> + subtrees) const -> ctree + { + auto sub = typename route_tree::named_subtrees{ + std::from_range, subtrees + | std::views::transform( + [](auto const& p) + { + return std::make_pair( + p.first, + static_cast>(p.second)); + }) }; + return {route_tree{.here = to_leaves(), .sub = sub}}; } - template - struct ctree : route_tree { - template - auto wrap(middleware_t mw) const -> ctree { - return {middleware_wrap_tree(std::move(mw), *this)}; - } - }; - - template - struct dtree : handler_map> { - [[nodiscard]] auto to_leaves() const -> typename route_tree::leaves { - auto here = this->template map>(degen_route_handler); - if (!here.verbs().empty() && !static_cast(this->options)) - here.options = default_options_handler; - return here; - } + template + [[nodiscard]] auto + wildcard_subtree(ctree subtree) + -> ctree + { + return {route_tree{ + .here = to_leaves(), + .sub = typename route_tree::wildcard_subtree{ + static_cast>(subtree) + } + }}; + } - [[nodiscard]] auto named_subtrees(std::initializer_list>> subtrees) const -> ctree { - auto sub = typename route_tree::named_subtrees{ - std::from_range, - subtrees | std::views::transform([](auto const& p) { - return std::make_pair(p.first, static_cast>(p.second)); - }) - }; - return {route_tree{.here = to_leaves(), .sub = sub}}; - } + [[nodiscard]] auto no_subtrees() const -> ctree + { + return {route_tree{.here = to_leaves(), .sub = {}}}; + } +}; + +template PreRouteCtx> +class server +{ + 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)} + { + } - template - [[nodiscard]] auto wildcard_subtree(ctree subtree) -> ctree { - return {route_tree{.here = to_leaves(), .sub = typename route_tree::wildcard_subtree{static_cast>(subtree)}}}; - } + struct match_result + { + util::not_null< + handler_map>> const*> + route_handlers; + std::vector wildcard_matches; - [[nodiscard]] auto no_subtrees() const -> ctree { - return {route_tree{.here = to_leaves(), .sub = {}}}; - } + auto allowed_methods() const -> verb_set { return route_handlers->verbs(); } }; - template PreRouteCtx> - class server { - 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 { - util::not_null>> const*> route_handlers; - std::vector wildcard_matches; - - auto allowed_methods() const -> verb_set { - return route_handlers->verbs(); - } - }; - - auto match(boost::urls::segments_view segments) const -> std::optional { - auto const* tree = &routes_; - auto wildcard_matches = std::vector{}; - for (auto const& seg : segments) { - 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; - }, + auto match(boost::urls::segments_view segments) const + -> std::optional + { + auto const* tree = &routes_; + auto wildcard_matches = std::vector{}; + for (auto const& seg : segments) + { + 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, - }; + 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(); + auto route_request(PreRouteCtx ctx, readable_request r) const + -> net::awaitable + { + auto req_base = r.p->get().base(); - auto const bad_request_tpl = problem::tpl{ - .status = bhttp::status::bad_request, - .title = translate("Bad request"), + 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", - }; - - if (req_base.target() == "*") { - // 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 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}); - } + }; - co_return co_await global_options_handler(ctx, r); - } else if (auto mreq_url0 = boost::urls::parse_origin_form(req_base.target())) { - // 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}); - } + if (req_base.target() == "*") + { + // 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 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}); + } - 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}); - } + co_return co_await global_options_handler(ctx, r); + } + else if (auto mreq_url0 = boost::urls::parse_origin_form(req_base.target())) + { + // 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 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}); - } + 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}); + } - 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 { - // We do not accept any other request-target forms. + 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}); + } - 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}); + 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 + { + // We do not accept any other request-target forms. - auto handle_request(readable_request r) const -> net::awaitable { - 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 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 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 handle_request(readable_request r) const -> net::awaitable + { + auto header = r.p->get().base(); + auto locale = lsel_.select(header[bhttp::field::accept_language]); + auto ctx0 = base_ctx{.locale = locale}; - 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; - } - } + 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)); }); + } - strm.socket().shutdown(tcp::socket::shutdown_send); - } + auto do_session(beast::tcp_stream strm) -> net::awaitable + { + auto buf = beast::flat_buffer{}; - auto do_listen(tcp::endpoint endpoint) -> net::awaitable { - auto executor = co_await net::this_coro::executor; - auto acceptor = tcp::acceptor{executor, endpoint}; - - 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()); - } - } - }); + 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; } } - 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}; - - // 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()); - } - } - }); + strm.socket().shutdown(tcp::socket::shutdown_send); + } + + auto do_listen(tcp::endpoint endpoint) -> net::awaitable + { + auto executor = co_await net::this_coro::executor; + auto acceptor = tcp::acceptor{executor, endpoint}; + + 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 spawn(net::io_context& ioc) -> void + { + auto const addr = net::ip::make_address("0.0.0.0"); + auto const endpoint = tcp::endpoint{addr, 8284}; + + // 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()); + } + } + }); + } +}; } // namespace routemon::http -- cgit v1.3