summaryrefslogtreecommitdiffstats
path: root/server/src/srv.cppm
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/srv.cppm')
-rw-r--r--server/src/srv.cppm221
1 files changed, 221 insertions, 0 deletions
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 @@
1module;
2
3#include <boost/config.hpp>
4#include <boost/asio/ip/tcp.hpp>
5#include <boost/asio/as_tuple.hpp>
6#include <boost/asio/awaitable.hpp>
7#include <boost/asio/co_spawn.hpp>
8#include <boost/beast/core.hpp>
9#include <boost/beast/http.hpp>
10#include <boost/json.hpp>
11#include <boost/locale/generator.hpp>
12
13#include <expat.h>
14
15export module routemon:srv;
16
17import std;
18import :api;
19import :config;
20import :gpx;
21import :http.server;
22import :locale;
23import :log;
24import :problem;
25import :req_ctx;
26import :util;
27
28namespace beast = boost::beast;
29namespace json = boost::json;
30namespace net = boost::asio;
31using tcp = boost::asio::ip::tcp;
32
33namespace routemon::srv {
34
35 class gpx_parse_error_category_impl : public std::error_category {
36 public:
37 char const* name() const noexcept override { return "gpx_parse"; }
38 auto message(int condition) const noexcept -> std::string override {
39 std::ignore = condition;
40 return "failed to parse GPX file";
41 }
42 };
43 auto gpx_parse_error_category() noexcept -> gpx_parse_error_category_impl const& {
44 static auto const inst = gpx_parse_error_category_impl{};
45 return inst;
46 }
47 auto gpx_parse_error() noexcept -> std::error_code {
48 return std::error_code{1, gpx_parse_error_category()};
49 }
50
51 class gpx_parse_result {
52 std::variant<std::exception_ptr, gpx::file> res_;
53
54 public:
55 auto set_exception(std::exception_ptr ex) noexcept {
56 res_ = ex;
57 }
58 auto set_gpx_file(gpx::file&& f) noexcept {
59 res_ = std::move(f);
60 }
61
62 auto unwrap() -> gpx::file&& {
63 return std::visit(util::overloaded{
64 [](std::exception_ptr ex) -> gpx::file&& {
65 if (ex) std::rethrow_exception(ex);
66 else throw std::runtime_error{"no GPX file parse result available"};
67 },
68 [](gpx::file&& f) -> gpx::file&& { return std::move(f); },
69 }, std::move(res_));
70 }
71 };
72
73 struct readable_gpx_body {
74 using value_type = gpx_parse_result;
75
76 class reader {
77 gpx::reader r_;
78 util::not_null<value_type*> res_;
79
80 public:
81 template<bool isRequest, bhttp::concepts::fields Fields>
82 explicit reader(bhttp::header<isRequest, Fields>&, value_type& v)
83 : res_{&v}
84 {}
85
86 // The following methods (which are called by Beast) are marked
87 // noexcept, since Beast does not ensure that exceptions thrown
88 // here are appropriately directed to the caller of
89 // (async_)read(_some), so throwing here might cause the program
90 // to crash.
91
92 auto init(boost::optional<std::uint64_t> /* n */, beast::error_code& ec) noexcept -> void {
93 try {
94 r_.init();
95 ec = {};
96 } catch (std::exception& ex) {
97 res_->set_exception(std::current_exception());
98 ec = gpx_parse_error();
99 }
100 }
101
102 auto put(beast::concepts::const_buffer_sequence auto b, beast::error_code& ec) noexcept -> std::size_t {
103 auto total = 0uz;
104 try {
105 for (auto it = net::buffer_sequence_begin(b); it != net::buffer_sequence_end(b); it++) {
106 r_.put(std::string_view{static_cast<char const*>(it->data()), it->size()});
107 total += it->size();
108 }
109 ec = {};
110 } catch (std::exception& ex) {
111 res_->set_exception(std::current_exception());
112 ec = gpx_parse_error();
113 }
114 return total;
115 }
116
117 auto finish(beast::error_code& ec) noexcept {
118 try {
119 res_->set_gpx_file(r_.finish());
120 ec = {};
121 } catch (std::exception& ex) {
122 res_->set_exception(std::current_exception());
123 ec = gpx_parse_error();
124 }
125 }
126 };
127 };
128 static_assert(bhttp::concepts::body<readable_gpx_body>);
129 static_assert(bhttp::concepts::body_reader<readable_gpx_body>);
130
131 class handler {
132 api::handler inner_;
133
134 public:
135 using outer_ctx = http::trace_id_ctx<http::base_ctx>;
136 using l0_ctx = http::routed_ctx<outer_ctx>;
137
138 private:
139 auto handle_process_gpx(l0_ctx ctx, http::readable_request r) -> net::awaitable<http::presponse> {
140 auto gpx_file = gpx::file{};
141 try {
142 auto req = co_await http::read_request<readable_gpx_body>(ctx, std::move(r));
143 gpx_file = std::move(req->body().unwrap());
144 } catch (std::exception& ex) {
145 // TODO: more detailed problem reporting
146 auto tpl = problem::tpl{
147 .status = bhttp::status::bad_request,
148 .title = translate("Failed to parse GPX file"),
149 .type_uri = "https://routemon.fautchen.eu/problems/gpx-parse-failed",
150 };
151 co_return http::problem_rsp(ctx, tpl.instantiate(), http::keep_alive{false});
152 }
153
154 // TODO: catch handler exceptions and return 500 when raised?
155 // (keep-alive depends on whether whole request was read)
156 auto mres = inner_.process_gpx(std::move(gpx_file));
157 if (!mres) {
158 auto tpl = problem::tpl{
159 .status = bhttp::status::internal_server_error,
160 .title = translate("Internal server error"),
161 .type_uri = "https://routemon.fautchen.eu/problems/internal-server-error",
162 };
163 co_return http::problem_rsp(ctx, tpl.instantiate(), http::keep_alive{true});
164 }
165
166 auto rsp = http::make_rsp<bhttp::string_body>(bhttp::status::ok, http::keep_alive{true});
167 rsp.set(bhttp::field::content_type, "application/json");
168 rsp.body() = json::serialize(json::value_from(*mres));
169 rsp.prepare_payload();
170 co_return rsp;
171 }
172
173 auto handle_sysinfo(l0_ctx ctx, http::readable_request r) -> net::awaitable<http::presponse> {
174 auto req = co_await http::read_request<bhttp::empty_body>(ctx, std::move(r));
175 auto info = inner_.sysinfo();
176
177 auto rsp = http::make_rsp<bhttp::string_body>(bhttp::status::ok, http::keep_alive{true});
178 rsp.set(bhttp::field::content_type, "application/json");
179 rsp.body() = json::serialize(json::value_from(info));
180 rsp.prepare_payload();
181 co_return rsp;
182 }
183
184 public:
185 handler(api::handler&& inner) : inner_{std::move(inner)} {}
186
187 auto make_routes() -> http::route_tree<http::routed_ctx<outer_ctx>> {
188 auto handler = [this]<class MemFn>(MemFn member) {
189 return std::bind_front(member, this);
190 };
191
192 return http::dtree<http::routed_ctx<outer_ctx>>{}.named_subtrees({
193 {"gpx", http::dtree<l0_ctx>{{
194 .post = handler(&handler::handle_process_gpx),
195 }}.no_subtrees()},
196 {"sysinfo", http::dtree<l0_ctx>{{
197 .get = handler(&handler::handle_sysinfo),
198 }}.no_subtrees()},
199 });
200 }
201 };
202
203 export class server {
204 handler handler_;
205 http::server<handler::outer_ctx> srv_;
206
207 static auto make_global_middleware() -> http::middleware_t<http::base_ctx, handler::outer_ctx> {
208 return http::middleware_compose<http::base_ctx, http::trace_id_ctx<http::base_ctx>, http::trace_id_ctx<http::base_ctx>>(http::trace_id_middleware<http::base_ctx>, http::lax_cors_middleware<http::trace_id_ctx<http::base_ctx>>);
209 }
210
211 public:
212 server(log::logger const& l, locale::selector&& lsel, api::handler&& inner)
213 : handler_{std::move(inner)}, srv_{l, std::move(lsel), make_global_middleware(), handler_.make_routes()}
214 {}
215
216 auto spawn(net::io_context& ioc) -> void {
217 srv_.spawn(ioc);
218 }
219 };
220
221} // namespace routemon::srv