module; #include #include #include #include #include #include #include #include #include #include module routemon:srv$impl; import std; import :gpx; import :srv; 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); auto handler::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 handler::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; } handler::handler(api::handler&& inner) : inner_{std::move(inner)} {} auto handler::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()}, }); } auto server::make_global_middleware(config::http_server const& cfg) -> http::middleware_t { return http::middleware_compose< http::base_ctx, http::trace_id_ctx, http::trace_id_ctx >(http::trace_id_middleware, http::cors_middleware>( cfg.allow_origins)); } server::server( log::logger const& l, config::http_server const& cfg, locale::selector&& lsel, api::handler&& inner) : handler_{std::move(inner)}, srv_{ l, http::router{ std::move(lsel), make_global_middleware(cfg), handler_.make_routes() } } { } auto server::spawn(net::io_context& ioc) -> void { srv_.spawn(ioc); } } // namespace routemon::srv