module; #include #include #include #include #include #include #include #include #include export module routemon:http.server; import std; import :config; import :trace; import :http.common; import :problem; namespace net = boost::asio; using tcp = net::ip::tcp; namespace routemon::http { 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; }; 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)} {} 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() 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()"}; } } 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 prepare(beast::error_code& ec) -> const_buffers_type override { return ensure_message_generator().prepare(ec); } auto consume(std::size_t n) -> void override { ensure_message_generator().consume(n); } 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(); }, }); } }; public: template explicit presponse(bhttp::response&& rsp) : impl_{new impl{std::move(rsp)}} { } 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); template using next_handler_t = std::functionnet::awaitable>; template using middleware_t = std::function&, next_handler_t) ->net::awaitable>; template auto id_middleware( Ctx ctx, bhttp::request_header&, next_handler_t next) -> net::awaitable { co_return co_await next(std::move(ctx)); } struct base_ctx { std::locale locale; trace::id trace_id; }; template using basic_route_handler_fn_t = std::function const& matches) ->net::awaitable>; template auto cors_middleware(std::vector const& allow_origins) -> middleware_t { if (allow_origins.empty()) return id_middleware; auto header_str = allow_origins | std::views::join_with(std::string_view{", "}) | std::ranges::to(); return [header_str = std::move(header_str)]( 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, header_str); co_return std::move(prersp); }; } template auto expose_trace_id_middleware( Ctx ctx, bhttp::request_header& req_hdr, next_handler_t next) -> net::awaitable { std::ignore = req_hdr; auto prersp = co_await next(std::move(ctx)); prersp.header().set( "X-Routemon-Trace-Id", std::string_view{static_cast(ctx).trace_id.as_string()}); prersp.header().insert( bhttp::field::access_control_expose_headers, "X-Routemon-Trace-Id"); co_return std::move(prersp); } template auto expose_content_language_middleware( Ctx ctx, bhttp::request_header& req_hdr, next_handler_t next) -> net::awaitable { std::ignore = req_hdr; auto prersp = co_await next(std::move(ctx)); if (prersp.header().find(bhttp::field::content_language) == prersp.header().end() && std::has_facet(ctx.locale)) { prersp.header().set( bhttp::field::content_language, std::use_facet(ctx.locale).tag()); } co_return std::move(prersp); } struct keep_alive { bool value; explicit keep_alive(bool 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; struct preflight_response { verb_set allow_methods; std::vector allow_headers; }; auto make_preflight_rsp(preflight_response res, keep_alive ka) -> 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 // 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, 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(); } template struct routed_ctx : InnerCtx { verb_set route_methods; }; 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 global_options_handler(base_ctx const& ctx, readable_request r) -> net::awaitable; 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 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; } } auto verbs() const -> verb_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> { 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) }; }, }); return {.here = new_leaves, .sub = new_sub}; } template concept match_arg = std::constructible_from; template using route_handler_fn_t = std::functionnet::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; } [[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 [[nodiscard]] auto wildcard_subtree(ctree subtree) -> ctree { return {route_tree{ .here = to_leaves(), .sub = typename route_tree::wildcard_subtree{ static_cast>(subtree) } }}; } [[nodiscard]] auto no_subtrees() const -> ctree { return {route_tree{.here = to_leaves(), .sub = {}}}; } }; class router { struct impl_base { virtual ~impl_base() = default; virtual auto handle_request(trace::id trace_id, readable_request r) const -> net::awaitable = 0; }; std::unique_ptr impl_; template PreRouteCtx> class impl : public impl_base { log::logger l_; locale::selector lsel_; middleware_t global_middleware_; route_tree> routes_; public: explicit impl( log::logger const& l, locale::selector&& lsel, middleware_t global_middleware, route_tree> routes) : l_{l.sub("router")}, lsel_{std::move(lsel)}, global_middleware_{std::move(global_middleware)}, routes_{std::move(routes)} { } struct match_result { util::not_null< handler_map>> 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; }, }); 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 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}); } l_.with( "trace_id", static_cast(ctx).trace_id.as_string()) .debug( "Request targets {} {}", req_base.method_string(), req_url.path()); 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 { // 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(trace::id trace_id, 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, .trace_id = trace_id}, header, [&](PreRouteCtx ctx) -> net::awaitable { co_return co_await route_request(std::move(ctx), std::move(r)); }); } }; public: template PreRouteCtx> explicit router( log::logger const& l, locale::selector&& lsel, middleware_t global_middleware, route_tree> routes) : impl_{std::make_unique>( l, std::move(lsel), std::move(global_middleware), std::move(routes))} { } auto handle_request(trace::id trace_id, readable_request r) const -> net::awaitable; }; class server { log::logger l_; router r_; auto do_session(beast::tcp_stream strm) -> net::awaitable; auto do_listen(tcp::endpoint endpoint) -> net::awaitable; public: explicit server(log::logger const& l, router&& r); auto spawn(net::io_context& ioc) -> void; }; } // namespace routemon::http