module; #include #include #include #include #include #include #include #include #include #include export module routemon:srv; import std; import :api; import :config; import :gpx; import :http.server; import :locale; import :log; import :problem; import :req_ctx; import :util; namespace beast = boost::beast; namespace json = boost::json; namespace net = boost::asio; using tcp = boost::asio::ip::tcp; namespace routemon::srv { class gpx_parse_error_category_impl : public std::error_category { public: char const* name() const noexcept override { return "gpx_parse"; } auto message(int condition) const noexcept -> std::string override { std::ignore = condition; return "failed to parse GPX file"; } }; auto gpx_parse_error_category() noexcept -> gpx_parse_error_category_impl const& { static auto const inst = gpx_parse_error_category_impl{}; return inst; } auto gpx_parse_error() noexcept -> std::error_code { return std::error_code{1, gpx_parse_error_category()}; } class gpx_parse_result { std::variant res_; public: auto set_exception(std::exception_ptr ex) noexcept { res_ = ex; } auto set_gpx_file(gpx::file&& f) noexcept { res_ = std::move(f); } auto unwrap() -> gpx::file&& { return std::visit(util::overloaded{ [](std::exception_ptr ex) -> gpx::file&& { if (ex) std::rethrow_exception(ex); else throw std::runtime_error{"no GPX file parse result available"}; }, [](gpx::file&& f) -> gpx::file&& { return std::move(f); }, }, std::move(res_)); } }; struct readable_gpx_body { using value_type = gpx_parse_result; class reader { gpx::reader r_; util::not_null res_; public: template explicit reader(bhttp::header&, value_type& v) : res_{&v} {} // The following methods (which are called by Beast) are marked // noexcept, since Beast does not ensure that exceptions thrown // here are appropriately directed to the caller of // (async_)read(_some), so throwing here might cause the program // to crash. auto init(boost::optional /* n */, beast::error_code& ec) noexcept -> void { try { r_.init(); ec = {}; } catch (std::exception& ex) { res_->set_exception(std::current_exception()); ec = gpx_parse_error(); } } auto put(beast::concepts::const_buffer_sequence auto b, beast::error_code& ec) noexcept -> std::size_t { auto total = 0uz; try { for (auto it = net::buffer_sequence_begin(b); it != net::buffer_sequence_end(b); it++) { r_.put(std::string_view{static_cast(it->data()), it->size()}); total += it->size(); } ec = {}; } catch (std::exception& ex) { res_->set_exception(std::current_exception()); ec = gpx_parse_error(); } return total; } auto finish(beast::error_code& ec) noexcept { try { res_->set_gpx_file(r_.finish()); ec = {}; } catch (std::exception& ex) { res_->set_exception(std::current_exception()); ec = gpx_parse_error(); } } }; }; static_assert(bhttp::concepts::body); static_assert(bhttp::concepts::body_reader); class handler { api::handler inner_; public: using outer_ctx = http::trace_id_ctx; using l0_ctx = http::routed_ctx; private: auto handle_process_gpx(l0_ctx ctx, http::readable_request r) -> net::awaitable { auto gpx_file = gpx::file{}; try { auto req = co_await http::read_request(ctx, std::move(r)); gpx_file = std::move(req->body().unwrap()); } catch (std::exception& ex) { // TODO: more detailed problem reporting auto tpl = problem::tpl{ .status = bhttp::status::bad_request, .title = translate("Failed to parse GPX file"), .type_uri = "https://routemon.fautchen.eu/problems/gpx-parse-failed", }; co_return http::problem_rsp(ctx, tpl.instantiate(), http::keep_alive{false}); } // TODO: catch handler exceptions and return 500 when raised? // (keep-alive depends on whether whole request was read) auto mres = inner_.process_gpx(std::move(gpx_file)); if (!mres) { auto tpl = problem::tpl{ .status = bhttp::status::internal_server_error, .title = translate("Internal server error"), .type_uri = "https://routemon.fautchen.eu/problems/internal-server-error", }; co_return http::problem_rsp(ctx, tpl.instantiate(), http::keep_alive{true}); } auto rsp = http::make_rsp(bhttp::status::ok, http::keep_alive{true}); rsp.set(bhttp::field::content_type, "application/json"); rsp.body() = json::serialize(json::value_from(*mres)); rsp.prepare_payload(); co_return rsp; } auto handle_sysinfo(l0_ctx ctx, http::readable_request r) -> net::awaitable { auto req = co_await http::read_request(ctx, std::move(r)); auto info = inner_.sysinfo(); auto rsp = http::make_rsp(bhttp::status::ok, http::keep_alive{true}); rsp.set(bhttp::field::content_type, "application/json"); rsp.body() = json::serialize(json::value_from(info)); rsp.prepare_payload(); co_return rsp; } public: handler(api::handler&& inner) : inner_{std::move(inner)} {} auto make_routes() -> http::route_tree> { auto handler = [this](MemFn member) { return std::bind_front(member, this); }; return http::dtree>{}.named_subtrees({ {"gpx", http::dtree{{ .post = handler(&handler::handle_process_gpx), }}.no_subtrees()}, {"sysinfo", http::dtree{{ .get = handler(&handler::handle_sysinfo), }}.no_subtrees()}, }); } }; export class server { handler handler_; http::server srv_; static auto make_global_middleware() -> http::middleware_t { return http::middleware_compose, http::trace_id_ctx>(http::trace_id_middleware, http::lax_cors_middleware>); } public: server(log::logger const& l, locale::selector&& lsel, api::handler&& inner) : handler_{std::move(inner)}, srv_{l, std::move(lsel), make_global_middleware(), handler_.make_routes()} {} auto spawn(net::io_context& ioc) -> void { srv_.spawn(ioc); } }; } // namespace routemon::srv