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 chrono = std::chrono; 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 { chrono::steady_clock::duration init_parse_dur_; chrono::steady_clock::duration put_parse_dur_; chrono::steady_clock::duration finish_parse_dur_; 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 init_parse_duration(chrono::steady_clock::duration dur) noexcept { init_parse_dur_ = dur; } [[nodiscard]] auto init_parse_duration() const noexcept -> chrono::steady_clock::duration { return init_parse_dur_; } auto put_parse_duration(chrono::steady_clock::duration dur) noexcept { put_parse_dur_ = dur; } [[nodiscard]] auto put_parse_duration() const noexcept -> chrono::steady_clock::duration { return put_parse_dur_; } auto finish_parse_duration(chrono::steady_clock::duration dur) noexcept { finish_parse_dur_ = dur; } [[nodiscard]] auto finish_parse_duration() const noexcept -> chrono::steady_clock::duration { return finish_parse_dur_; } 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_; std::inplace_vector buf_; public: template explicit reader(bhttp::header&, value_type& v) : res_{&v} { res_->init_parse_duration({}); res_->put_parse_duration({}); res_->finish_parse_duration({}); } // 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 { auto const init_start = chrono::steady_clock::now(); try { r_.init(); ec = {}; } catch (std::exception& ex) { res_->set_exception(std::current_exception()); ec = gpx_parse_error(); } res_->init_parse_duration(chrono::steady_clock::now() - init_start); } auto put(beast::concepts::const_buffer_sequence auto b, beast::error_code& ec) noexcept -> std::size_t { auto const put_start = chrono::steady_clock::now(); auto total = 0uz; try { for (auto it = net::buffer_sequence_begin(b); it != net::buffer_sequence_end(b); it++) { auto cur_in_buf = net::const_buffer{*it}; while (cur_in_buf.size() > 0) { auto to_read = std::min(cur_in_buf.size(), buf_.max_size() - buf_.size()); buf_.append_range( std::span{ static_cast(cur_in_buf.data()), to_read }); cur_in_buf += to_read; total += to_read; if (buf_.size() == buf_.max_size()) { r_.put(std::string_view{buf_}); buf_.clear(); } } } ec = {}; } catch (std::exception& ex) { res_->set_exception(std::current_exception()); ec = gpx_parse_error(); } res_->put_parse_duration( res_->put_parse_duration() + (chrono::steady_clock::now() - put_start)); return total; } auto finish(beast::error_code& ec) noexcept { auto const finish_start = chrono::steady_clock::now(); try { if (buf_.size() > 0) { r_.put(std::string_view{buf_}); buf_.clear(); } res_->set_gpx_file(r_.finish()); ec = {}; } catch (std::exception& ex) { res_->set_exception(std::current_exception()); ec = gpx_parse_error(); } res_->finish_parse_duration(chrono::steady_clock::now() - finish_start); } }; }; 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 l = l_.sub("handle_process_gpx").with("trace_id", ctx.trace_id.as_string()); auto gpx_file = gpx::file{}; try { l.debug("Reading GPX request body"); auto const before_read_gpx = chrono::steady_clock::now(); auto req = co_await http::read_request(ctx, std::move(r)); l.debug( "Read GPX request body in {}; init took {}, actual parsing {}, " "finishing {}", chrono::duration{ chrono::steady_clock::now() - before_read_gpx }, chrono::duration{req->body().init_parse_duration()}, chrono::duration{req->body().put_parse_duration()}, chrono::duration{ req->body().finish_parse_duration() }); 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(ctx.trace_id, 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(log::logger const& l, api::handler&& inner) : l_{l.sub("handler")}, 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::expose_content_language_middleware, middleware_compose( http::expose_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_{l, std::move(inner)}, srv_{ l, http::router{ l, std::move(lsel), make_global_middleware(cfg), handler_.make_routes() } } { } auto server::spawn(net::io_context& ioc) -> void { srv_.spawn(ioc); } } // namespace routemon::srv