From 973aec43ea54bbf95b64fbcb636403401d1ca60e Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Fri, 28 Aug 2026 18:03:05 +0200 Subject: Import from e4b104792206ee7ea64bf39c6b7d2c0c230f9d14 --- server/src/srv.cppm | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 server/src/srv.cppm (limited to 'server/src/srv.cppm') diff --git a/server/src/srv.cppm b/server/src/srv.cppm new file mode 100644 index 0000000..2ca48c6 --- /dev/null +++ b/server/src/srv.cppm @@ -0,0 +1,221 @@ +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 -- cgit v1.3