summaryrefslogtreecommitdiffstats
path: root/server/src/http_server.cppm
diff options
context:
space:
mode:
authorRutger Broekhoff2026-08-28 18:03:05 +0200
committerRutger Broekhoff2026-08-28 18:03:05 +0200
commit973aec43ea54bbf95b64fbcb636403401d1ca60e (patch)
tree41b7911c420766a9b463245b9296f44c5bf35258 /server/src/http_server.cppm
downloadroutemon-973aec43ea54bbf95b64fbcb636403401d1ca60e.tar.gz
routemon-973aec43ea54bbf95b64fbcb636403401d1ca60e.zip
Import from e4b104792206ee7ea64bf39c6b7d2c0c230f9d14
Diffstat (limited to 'server/src/http_server.cppm')
-rw-r--r--server/src/http_server.cppm661
1 files changed, 661 insertions, 0 deletions
diff --git a/server/src/http_server.cppm b/server/src/http_server.cppm
new file mode 100644
index 0000000..0770d97
--- /dev/null
+++ b/server/src/http_server.cppm
@@ -0,0 +1,661 @@
1module;
2
3#include <boost/config.hpp>
4#include <boost/asio/as_tuple.hpp>
5#include <boost/asio/awaitable.hpp>
6#include <boost/asio/co_spawn.hpp>
7#include <boost/asio/ip/tcp.hpp>
8#include <boost/beast/core.hpp>
9#include <boost/beast/http.hpp>
10#include <boost/json/serialize.hpp>
11#include <boost/url.hpp>
12
13export module routemon:http.server;
14
15import std;
16import :config;
17import :trace;
18export import :http.common;
19import :problem;
20
21namespace net = boost::asio;
22using tcp = net::ip::tcp;
23
24namespace routemon::http {
25
26 struct readable_request {
27 util::not_null<bhttp::request_parser<bhttp::empty_body>*> p;
28 util::not_null<beast::tcp_stream*> strm;
29 util::not_null<beast::flat_buffer*> buf;
30 };
31
32 class presponse {
33 public:
34 using const_buffers_type = beast::span<net::const_buffer>;
35
36 private:
37 struct impl_base {
38 virtual ~impl_base() = default;
39 virtual auto header() -> bhttp::response_header<bhttp::fields>& = 0;
40 virtual auto header() const -> bhttp::response_header<bhttp::fields> const& = 0;
41 virtual auto is_done() const -> bool = 0;
42 virtual auto prepare(beast::error_code&) -> const_buffers_type = 0;
43 virtual auto consume(std::size_t n) -> void = 0;
44 virtual auto keep_alive() const -> bool = 0;
45 };
46 std::unique_ptr<impl_base> impl_;
47
48 template<bhttp::concepts::body Body>
49 class impl : public impl_base {
50 // Initializes in the response state.
51 // At the first call to prepare, we switch to the message generator state.
52 // After that point, header may not be called anymore (it will throw).
53 std::variant<bhttp::response<Body>, bhttp::message_generator> state_;
54
55 auto ensure_message_generator() -> bhttp::message_generator& {
56 if (auto prsp = std::get_if<bhttp::response<Body>>(&state_)) {
57 auto rsp = bhttp::response<Body>{std::move(*prsp)};
58 state_.template emplace<bhttp::message_generator>(std::move(rsp));
59 }
60 return std::get<bhttp::message_generator>(state_);
61 }
62
63 public:
64 explicit impl(bhttp::response<Body>&& rsp) : state_{std::move(rsp)} {}
65
66 auto header() -> bhttp::response_header<bhttp::fields>& override {
67 if (auto prsp = std::get_if<bhttp::response<Body>>(&state_)) {
68 return prsp->base();
69 } else {
70 // TODO: define custom exception type presponse::bad_header_access
71 throw std::logic_error{"header() may not be called after prepare()"};
72 }
73 }
74 auto header() const -> bhttp::response_header<bhttp::fields> const& override {
75 if (auto prsp = std::get_if<bhttp::response<Body>>(&state_)) {
76 return prsp->base();
77 } else {
78 throw std::logic_error{"header() may not be called after prepare()"};
79 }
80 }
81
82 auto is_done() const -> bool override {
83 if (auto pgen = std::get_if<bhttp::message_generator>(&state_)) {
84 return pgen->is_done();
85 } else /* still in the response state */ {
86 return false;
87 }
88 }
89
90 auto prepare(beast::error_code& ec) -> const_buffers_type override {
91 return ensure_message_generator().prepare(ec);
92 }
93
94 auto consume(std::size_t n) -> void override {
95 ensure_message_generator().consume(n);
96 }
97
98 auto keep_alive() const noexcept -> bool override {
99 return state_.visit(util::overloaded{
100 [](bhttp::response<Body> const& rsp) -> bool {
101 return rsp.keep_alive();
102 },
103 [](bhttp::message_generator const& gen) -> bool {
104 return gen.keep_alive();
105 },
106 });
107 }
108 };
109
110 public:
111 template<bhttp::concepts::body Body>
112 explicit presponse(bhttp::response<Body>&& rsp)
113 : impl_{new impl{std::move(rsp)}}
114 {}
115
116 auto header() -> bhttp::response_header<bhttp::fields>& {
117 return impl_->header();
118 }
119 auto header() const -> bhttp::response_header<bhttp::fields> const& {
120 return impl_->header();
121 }
122
123 auto is_done() const -> bool {
124 return impl_->is_done();
125 }
126
127 auto prepare(beast::error_code& ec) -> const_buffers_type {
128 return impl_->prepare(ec);
129 }
130
131 auto consume(std::size_t n) -> void {
132 return impl_->consume(n);
133 }
134
135 auto keep_alive() const noexcept -> bool {
136 return impl_->keep_alive();
137 }
138 };
139 static_assert(beast::concepts::buffers_generator<presponse>);
140
141 template<class Ctx>
142 using next_handler_t = std::function<auto(Ctx) -> net::awaitable<presponse>>;
143
144 template<class OuterCtx, class InnerCtx>
145 using middleware_t = std::function<auto(OuterCtx, bhttp::request_header<bhttp::fields>&, next_handler_t<InnerCtx>) -> net::awaitable<presponse>>;
146
147 template<class Ctx>
148 auto lax_cors_middleware(Ctx ctx, bhttp::request_header<bhttp::fields>& req_hdr, next_handler_t<Ctx> next) -> net::awaitable<presponse> {
149 std::ignore = req_hdr;
150 auto prersp = co_await next(ctx);
151 prersp.header().set(bhttp::field::access_control_allow_origin, "*");
152 co_return std::move(prersp);
153 }
154
155 template<class InnerCtx>
156 struct trace_id_ctx : InnerCtx {
157 trace::id trace_id = {};
158 };
159
160 template<class OuterCtx>
161 auto trace_id_middleware(OuterCtx ctx0, bhttp::request_header<bhttp::fields>& req_hdr, next_handler_t<trace_id_ctx<OuterCtx>> next) -> net::awaitable<presponse> {
162 std::ignore = req_hdr;
163 auto ctx = trace_id_ctx{std::move(ctx0)};
164 auto prersp = co_await next(std::move(ctx));
165 prersp.header().set("X-Routemon-Trace-Id", std::string_view{ctx.trace_id.as_string()});
166 prersp.header().insert(bhttp::field::access_control_expose_headers, "X-Routemon-Trace-Id");
167 co_return std::move(prersp);
168 }
169
170 struct base_ctx {
171 std::locale locale;
172 };
173
174 template<class Ctx>
175 using basic_route_handler_fn_t = std::function<auto(Ctx, readable_request, std::vector<std::string> const& matches) -> net::awaitable<presponse>>;
176
177 struct keep_alive {
178 bool value;
179
180 explicit keep_alive(bool value) : value{value} {}
181 };
182
183 template<bhttp::concepts::body Body>
184 auto make_rsp(bhttp::status status, keep_alive ka) -> bhttp::response<Body> {
185 auto rsp = bhttp::response<Body>{}; // HTTP version gets set later
186 rsp.result(status);
187 rsp.keep_alive(ka.value);
188 return rsp;
189 }
190
191 auto problem_rsp(base_ctx const& ctx, problem::details const& problem, keep_alive ka) -> presponse {
192 auto rsp = make_rsp<bhttp::string_body>(problem.status, ka);
193 rsp.set(bhttp::field::content_type, "application/problem+json");
194 rsp.body() = json::serialize(json::value_from(problem, ctx.locale));
195 rsp.prepare_payload();
196 return presponse{std::move(rsp)};
197 }
198
199 struct preflight_response {
200 verb_set allow_methods;
201 std::vector<bhttp::field> allow_headers;
202 };
203 auto make_preflight_rsp(preflight_response res, keep_alive ka) -> bhttp::response<bhttp::empty_body> {
204 auto rsp = make_rsp<bhttp::empty_body>(bhttp::status::no_content, ka);
205 auto allow_headers_str = res.allow_headers
206 | std::views::transform([](auto const& field) -> std::string_view { return bhttp::to_string(field); })
207 | std::views::join_with(std::string_view{", "})
208 | std::ranges::to<std::string>();
209 rsp.set(bhttp::field::access_control_allow_methods, res.allow_methods.to_string());
210 rsp.set(bhttp::field::access_control_allow_headers, allow_headers_str);
211 rsp.prepare_payload();
212 return rsp;
213 }
214
215 // Using base_ctx instead of a template here since that saves you
216 // typing on invocation (and we do not care about the context type
217 // anyway, but all context types should derive from base_ctx).
218 template<bhttp::concepts::body_reader Body>
219 auto read_request(base_ctx const& ctx, readable_request&& r) -> net::awaitable<std::expected<bhttp::request<Body>, presponse>> {
220 std::ignore = ctx;
221 auto p = bhttp::request_parser<Body>{std::move(*r.p)};
222 co_await bhttp::async_read(*r.strm, *r.buf, p);
223 co_return std::move(p.release());
224 }
225
226 template<>
227 auto read_request<bhttp::empty_body>(base_ctx const& ctx, readable_request&& r) -> net::awaitable<std::expected<bhttp::request<bhttp::empty_body>, presponse>> {
228 auto [ec, _] = co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple);
229 if (ec == bhttp::error::unexpected_body) {
230 auto tpl = problem::tpl{
231 .status = bhttp::status::bad_request,
232 .title = translate("No body expected for this request"),
233 .type_uri = "https://routemon.fautchen.eu/problems/unexpected-body",
234 };
235 co_return std::unexpected{problem_rsp(ctx, tpl.instantiate(), keep_alive{false})};
236 } else if (ec) {
237 throw boost::system::system_error{ec};
238 }
239 co_return r.p->release();
240 }
241
242 template<class InnerCtx>
243 struct routed_ctx : InnerCtx {
244 verb_set route_methods;
245 };
246
247 template<class Ctx>
248 requires requires(Ctx ctx) {
249 // Ctx must be derived from an instantiation of routed_ctx
250 []<class InnerCtx>(routed_ctx<InnerCtx> const&) {}(ctx);
251 }
252 auto default_options_handler(Ctx const& ctx, readable_request r, std::vector<std::string> const&) -> net::awaitable<presponse> {
253 auto mreq = co_await read_request<bhttp::empty_body>(ctx, std::move(r));
254 if (!mreq)
255 co_return std::move(mreq.error());
256
257 if (mreq->find(bhttp::field::access_control_request_method) != mreq->end()) {
258 // CORS preflight request
259 co_return make_preflight_rsp(preflight_response{
260 // TODO: should access-control-allow-methods contain OPTIONS?
261 .allow_methods = ctx.route_methods,
262 .allow_headers = {bhttp::field::content_type},
263 }, keep_alive{mreq->keep_alive()});
264 } else {
265 // Normal OPTIONS request
266 auto rsp = make_rsp<bhttp::empty_body>(bhttp::status::no_content, keep_alive{mreq->keep_alive()});
267 rsp.set(bhttp::field::allow, ctx.route_methods.to_string());
268 rsp.prepare_payload();
269 co_return std::move(rsp);
270 }
271 }
272
273 auto global_options_handler(base_ctx const& ctx, readable_request r) -> net::awaitable<presponse> {
274 // TODO: switch to "small (4KB) discarded" body type, similar to what Go does?
275 // Same goes for default_options_handler? Not sure.
276 if (auto res = co_await read_request<bhttp::empty_body>(ctx, std::move(r)); !res)
277 co_return std::move(res.error());
278 auto req = r.p->release();
279 auto rsp = make_rsp<bhttp::empty_body>(bhttp::status::no_content, keep_alive{req.keep_alive()});
280 rsp.prepare_payload();
281 co_return std::move(rsp);
282 }
283
284 template<class Ctx>
285 auto id_middleware(Ctx ctx, bhttp::request_header<bhttp::fields>&, next_handler_t<Ctx> next) -> net::awaitable<presponse> {
286 co_return co_await next(std::move(ctx));
287 }
288
289 template<class A, class B, class C>
290 auto middleware_compose(middleware_t<A, B> ab, middleware_t<B, C> bc) -> middleware_t<A, C> {
291 return [ab = std::move(ab), bc = std::move(bc)](A a, bhttp::request_header<bhttp::fields>& header, next_handler_t<C> next) -> net::awaitable<presponse> {
292 co_return co_await ab(std::move(a), header, [&](B b) -> net::awaitable<presponse> {
293 co_return co_await bc(std::move(b), header, next);
294 });
295 };
296 }
297
298 template<class A, class B>
299 auto middleware_wrap_fn(middleware_t<A, B> ab, basic_route_handler_fn_t<B> fn) -> basic_route_handler_fn_t<A> {
300 return [ab = std::move(ab), fn = std::move(fn)](A a_ctx, readable_request r, std::vector<std::string> const& matches) -> net::awaitable<presponse> {
301 co_return co_await ab(std::move(a_ctx), r.p->get().base(), [&](B b_ctx) -> net::awaitable<presponse> {
302 co_return co_await fn(std::move(b_ctx), r, matches);
303 });
304 };
305 }
306
307 template<std::default_initializable V>
308 requires requires(V v) {
309 { static_cast<bool>(v) };
310 }
311 struct handler_map {
312 V options = {};
313 V delete_ = {};
314 V get = {};
315 V head = {};
316 V post = {};
317 V put = {};
318
319 template<class Self>
320 auto lookup(this Self&& self, supported_verb v) -> auto&& {
321 switch (v.value) {
322 case supported_verb::options: return std::forward<Self>(self).options;
323 case supported_verb::delete_: return std::forward<Self>(self).delete_;
324 case supported_verb::get: return std::forward<Self>(self).get;
325 case supported_verb::head: return std::forward<Self>(self).head;
326 case supported_verb::post: return std::forward<Self>(self).post;
327 case supported_verb::put: return std::forward<Self>(self).put;
328 }
329 }
330
331 auto verbs() const -> verb_set {
332 auto set = verb_set{};
333 if (static_cast<bool>(options))
334 set.enable(supported_verb::options);
335 if (static_cast<bool>(delete_))
336 set.enable(supported_verb::delete_);
337 if (static_cast<bool>(get))
338 set.enable(supported_verb::get);
339 if (static_cast<bool>(head))
340 set.enable(supported_verb::head);
341 if (static_cast<bool>(post))
342 set.enable(supported_verb::post);
343 if (static_cast<bool>(put))
344 set.enable(supported_verb::put);
345 return set;
346 }
347
348 auto empty() const -> bool {
349 return verbs().empty();
350 }
351
352 template<std::default_initializable U>
353 auto map(std::invocable<V const&> auto f) const -> handler_map<U>
354 requires std::assignable_from<U&, std::invoke_result_t<decltype(f), V const&>>
355 {
356 return {
357 .options = static_cast<bool>(options) ? f(options) : U{},
358 .delete_ = static_cast<bool>(delete_) ? f(delete_) : U{},
359 .get = static_cast<bool>(get) ? f(get) : U{},
360 .head = static_cast<bool>(head) ? f(head) : U{},
361 .post = static_cast<bool>(post) ? f(post) : U{},
362 .put = static_cast<bool>(put) ? f(put) : U{},
363 };
364 }
365 };
366
367 template<class Ctx>
368 struct route_tree {
369 using leaves = handler_map<basic_route_handler_fn_t<Ctx>>;
370 using named_subtrees = std::unordered_map<std::string, route_tree>;
371 using wildcard_subtree = std::indirect<route_tree>;
372
373 leaves here;
374 // TODO: consider making the first alternative a radix tree
375 // Note: the map is the first variant here; the variant will be
376 // default-constructed with the default-constructed first
377 // alternative. The empty map denotes a lack of subtrees.
378 std::variant<named_subtrees, wildcard_subtree> sub;
379 };
380
381 template<class OuterCtx, class InnerCtx>
382 auto middleware_wrap_tree(middleware_t<OuterCtx, InnerCtx> mw, route_tree<InnerCtx> const& tree) -> route_tree<OuterCtx> {
383 auto new_leaves = tree.here.template map<basic_route_handler_fn_t<OuterCtx>>(std::bind_front(middleware_wrap_fn<OuterCtx, InnerCtx>, mw));
384 auto new_sub = tree.sub.visit(util::overloaded{
385 [&mw](route_tree<InnerCtx>::named_subtrees const& subtrees) -> decltype(route_tree<OuterCtx>::sub) {
386 auto new_subtrees = typename route_tree<OuterCtx>::named_subtrees{};
387 for (auto [seg, subtree] : subtrees)
388 new_subtrees[seg] = middleware_wrap_tree(mw, subtree);
389 return new_subtrees;
390 },
391 [&mw](route_tree<InnerCtx>::wildcard_subtree const& subtree) -> decltype(route_tree<OuterCtx>::sub) {
392 return typename route_tree<OuterCtx>::wildcard_subtree{middleware_wrap_tree(mw, *subtree)};
393 },
394 });
395 return {.here = new_leaves, .sub = new_sub};
396 }
397
398 template<class T>
399 concept match_arg = std::constructible_from<T, std::string const&>;
400
401 template<class Ctx, match_arg... MatchArgs>
402 using route_handler_fn_t = std::function<auto(Ctx, readable_request, MatchArgs...) -> net::awaitable<presponse>>;
403
404 template<class Ctx, match_arg... MatchArgs>
405 auto degen_route_handler(route_handler_fn_t<Ctx, MatchArgs...> fn) -> basic_route_handler_fn_t<Ctx> {
406 return [fn = std::move(fn)](Ctx ctx, readable_request r, std::vector<std::string> const& matches) -> net::awaitable<presponse> {
407 if (sizeof...(MatchArgs) != matches.size())
408 throw std::runtime_error{"got unexpected amount of matches"};
409 auto it = matches.begin();
410 co_return co_await fn(std::move(ctx), r, MatchArgs{static_cast<std::string const&>(*it++)}...);
411 };
412 }
413
414 template<class Ctx, match_arg... MatchArgs>
415 struct ctree : route_tree<Ctx> {
416 template<class OuterCtx>
417 auto wrap(middleware_t<OuterCtx, Ctx> mw) const -> ctree<OuterCtx, MatchArgs...> {
418 return {middleware_wrap_tree(std::move(mw), *this)};
419 }
420 };
421
422 template<class Ctx, match_arg... MatchArgs>
423 struct dtree : handler_map<route_handler_fn_t<Ctx, MatchArgs...>> {
424 [[nodiscard]] auto to_leaves() const -> typename route_tree<Ctx>::leaves {
425 auto here = this->template map<basic_route_handler_fn_t<Ctx>>(degen_route_handler<Ctx, MatchArgs...>);
426 if (!here.verbs().empty() && !static_cast<bool>(this->options))
427 here.options = default_options_handler<Ctx>;
428 return here;
429 }
430
431 [[nodiscard]] auto named_subtrees(std::initializer_list<std::pair<std::string, ctree<Ctx, MatchArgs...>>> subtrees) const -> ctree<Ctx, MatchArgs...> {
432 auto sub = typename route_tree<Ctx>::named_subtrees{
433 std::from_range,
434 subtrees | std::views::transform([](auto const& p) {
435 return std::make_pair(p.first, static_cast<route_tree<Ctx>>(p.second));
436 })
437 };
438 return {route_tree<Ctx>{.here = to_leaves(), .sub = sub}};
439 }
440
441 template<match_arg MatchArg>
442 [[nodiscard]] auto wildcard_subtree(ctree<Ctx, MatchArgs..., MatchArg> subtree) -> ctree<Ctx, MatchArgs...> {
443 return {route_tree<Ctx>{.here = to_leaves(), .sub = typename route_tree<Ctx>::wildcard_subtree{static_cast<route_tree<Ctx>>(subtree)}}};
444 }
445
446 [[nodiscard]] auto no_subtrees() const -> ctree<Ctx, MatchArgs...> {
447 return {route_tree<Ctx>{.here = to_leaves(), .sub = {}}};
448 }
449 };
450
451 template<std::derived_from<base_ctx> PreRouteCtx>
452 class server {
453 log::logger l_;
454 locale::selector lsel_;
455 middleware_t<base_ctx, PreRouteCtx> global_middleware_;
456 route_tree<routed_ctx<PreRouteCtx>> routes_;
457
458 public:
459 explicit server(log::logger const& l, locale::selector&& lsel, middleware_t<base_ctx, PreRouteCtx> global_middleware, route_tree<routed_ctx<PreRouteCtx>> routes)
460 : l_{l.sub("http_server")}, lsel_{std::move(lsel)}, global_middleware_{std::move(global_middleware)}, routes_{std::move(routes)}
461 {}
462
463 struct match_result {
464 util::not_null<handler_map<basic_route_handler_fn_t<routed_ctx<PreRouteCtx>>> const*> route_handlers;
465 std::vector<std::string> wildcard_matches;
466
467 auto allowed_methods() const -> verb_set {
468 return route_handlers->verbs();
469 }
470 };
471
472 auto match(boost::urls::segments_view segments) const -> std::optional<match_result> {
473 auto const* tree = &routes_;
474 auto wildcard_matches = std::vector<std::string>{};
475 for (auto const& seg : segments) {
476 tree->sub.visit(util::overloaded{
477 [&](route_tree<routed_ctx<PreRouteCtx>>::named_subtrees const& subtrees) {
478 auto it = subtrees.find(seg);
479 tree = it == subtrees.end() ? nullptr : &it->second;
480 },
481 [&](route_tree<routed_ctx<PreRouteCtx>>::wildcard_subtree const& wildcard_subtree) {
482 wildcard_matches.push_back(seg);
483 tree = &*wildcard_subtree;
484 },
485 });
486 if (!tree) return std::nullopt;
487 }
488 if (tree->here.empty()) return std::nullopt;
489 return match_result{
490 .route_handlers = util::not_null{&tree->here},
491 .wildcard_matches = wildcard_matches,
492 };
493 }
494
495 auto route_request(PreRouteCtx ctx, readable_request r) const -> net::awaitable<presponse> {
496 auto req_base = r.p->get().base();
497
498 auto const bad_request_tpl = problem::tpl{
499 .status = bhttp::status::bad_request,
500 .title = translate("Bad request"),
501 .type_uri = "https://routemon.fautchen.eu/problems/bad-request",
502 };
503
504 if (req_base.target() == "*") {
505 // request-target is in asterisk-form (RFC 9112, § 3.2.4),
506 // so the request must be a server-wide OPTIONS request.
507
508 if (req_base.method() != bhttp::verb::options) {
509 auto tpl = problem::tpl{
510 .status = bhttp::status::method_not_allowed,
511 .title = translate("Method not allowed"),
512 .type_uri = "https://routemon.fautchen.eu/problems/method-not-allowed",
513 };
514 co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false});
515 }
516
517 co_return co_await global_options_handler(ctx, r);
518 } else if (auto mreq_url0 = boost::urls::parse_origin_form(req_base.target())) {
519 // request-target is in origin-form (RFC 9112, § 3.2.1),
520 // so it must be a normal request (not a CONNECT or
521 // server-wide OPTIONS request).
522
523 auto req_url = boost::urls::url{*mreq_url0};
524 req_url.normalize();
525 if (!req_url.is_path_absolute()) {
526 auto problem = bad_request_tpl.instantiate().
527 set_detail(translate("Path of normalized (RFC 3986, § 6) "
528 "origin-form request-target (RFC "
529 "9112, § 3.2.1) should be "
530 "absolute"));
531 co_return problem_rsp(ctx, problem, keep_alive{false});
532 }
533
534 auto mres = match(req_url.segments());
535 if (!mres) {
536 auto tpl = problem::tpl{
537 .status = bhttp::status::not_found,
538 .title = translate("Not found"),
539 .type_uri = "https://routemon.fautchen.eu/problems/not-found",
540 };
541 co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false});
542 }
543
544 auto mverb = supported_verb::from(req_base.method());
545 if (!mverb) {
546 // Method not implemented.
547 auto tpl = problem::tpl{
548 .status = bhttp::status::not_implemented,
549 .title = translate("Method not implemented"),
550 .type_uri = "https://routemon.fautchen.eu/problems/method-not-implemented",
551 };
552 co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false});
553 }
554
555 if (auto mhdl = mres->route_handlers->lookup(*mverb)) {
556 auto new_ctx = routed_ctx<PreRouteCtx>{std::move(ctx), mres->allowed_methods()};
557 co_return co_await mhdl(std::move(new_ctx), r, mres->wildcard_matches);
558 } else {
559 // Path recognized, but method not allowed.
560 auto tpl = problem::tpl{
561 .status = bhttp::status::method_not_allowed,
562 .title = translate("Method not allowed"),
563 .type_uri = "https://routemon.fautchen.eu/problems/method-not-allowed",
564 };
565 auto rsp = problem_rsp(ctx, tpl.instantiate(), keep_alive{false});
566 rsp.header().set(bhttp::field::allow, mres->allowed_methods().to_string());
567 co_return std::move(rsp);
568 }
569 } else {
570 // We do not accept any other request-target forms.
571
572 auto problem = bad_request_tpl.instantiate().
573 set_detail(translate("Invalid request-target, expected "
574 "asterisk-form or origin-form "
575 "(see RFC 9112, § 3.2)"));
576 co_return problem_rsp(ctx, problem, keep_alive{false});
577 }
578 }
579
580 auto handle_request(readable_request r) const -> net::awaitable<presponse> {
581 auto header = r.p->get().base();
582 auto locale = lsel_.select(header[bhttp::field::accept_language]);
583 auto ctx0 = base_ctx{.locale = locale};
584
585 co_return co_await global_middleware_(std::move(ctx0), header, [&](PreRouteCtx ctx) -> net::awaitable<presponse> {
586 co_return co_await route_request(std::move(ctx), std::move(r));
587 });
588 }
589
590 auto do_session(beast::tcp_stream strm) -> net::awaitable<void> {
591 auto buf = beast::flat_buffer{};
592
593 while (true) {
594 auto p0 = bhttp::request_parser<bhttp::empty_body>{};
595 p0.body_limit(boost::none);
596 auto [ec, _] = co_await bhttp::async_read_header(strm, buf, p0, net::as_tuple);
597 if (ec == bhttp::error::end_of_stream) {
598 break;
599 } else if (ec) {
600 throw boost::system::system_error{ec};
601 }
602
603 auto http_version = p0.get().version();
604 auto&& rsp = co_await handle_request(readable_request{
605 .p = util::not_null{&p0},
606 .strm = util::not_null{&strm},
607 .buf = util::not_null{&buf},
608 });
609 rsp.header().version(http_version);
610 bool keep_alive = rsp.keep_alive();
611 co_await beast::async_write(strm, std::move(rsp));
612 if (!keep_alive) {
613 break;
614 }
615 }
616
617 strm.socket().shutdown(tcp::socket::shutdown_send);
618 }
619
620 auto do_listen(tcp::endpoint endpoint) -> net::awaitable<void> {
621 auto executor = co_await net::this_coro::executor;
622 auto acceptor = tcp::acceptor{executor, endpoint};
623
624 l_.with("endpoint", endpoint.address().to_string()).
625 with("port", std::to_string(endpoint.port())).
626 info("Serving");
627 while (true) {
628 net::co_spawn(executor,
629 do_session(beast::tcp_stream{co_await acceptor.async_accept()}),
630 [this](std::exception_ptr e) {
631 if (e) {
632 try {
633 std::rethrow_exception(e);
634 } catch (std::exception const& e) {
635 l_.error("Error in session: {}", e.what());
636 }
637 }
638 });
639 }
640 }
641
642 auto spawn(net::io_context& ioc) -> void {
643 auto const addr = net::ip::make_address("0.0.0.0");
644 auto const endpoint = tcp::endpoint{addr, 8284};
645
646 // TODO: make exception handling as nice as in srv.cpp
647 net::co_spawn(ioc,
648 do_listen(endpoint),
649 [this](std::exception_ptr e) {
650 if (e) {
651 try {
652 std::rethrow_exception(e);
653 } catch (std::exception const& e) {
654 l_.error("Error: {}", e.what());
655 }
656 }
657 });
658 }
659 };
660
661} // namespace routemon::http