Malloy
Loading...
Searching...
No Matches
router.hpp
1#pragma once
2
3#include "endpoint_http.hpp"
4#include "endpoint_http_regex.hpp"
5#include "endpoint_http_files.hpp"
6#include "endpoint_websocket.hpp"
7#include "type_traits.hpp"
8#include "../http/connection.hpp"
9#include "../http/connection_plain.hpp"
10#include "../http/connection_t.hpp"
11#include "../http/preflight_config.hpp"
12#include "../../core/type_traits.hpp"
13#include "../../core/detail/version_checks.hpp"
14#include "../../core/http/generator.hpp"
15#include "../../core/http/http.hpp"
16#include "../../core/http/request.hpp"
17#include "../../core/http/response.hpp"
18#include "../../core/http/utils.hpp"
19#if MALLOY_FEATURE_TLS
20 #include "../http/connection_tls.hpp"
21#endif
22
23#include <boost/beast/core.hpp>
24#include <boost/beast/http.hpp>
25#include <boost/beast/version.hpp>
26#include <spdlog/logger.h>
27
28#include <concepts>
29#include <filesystem>
30#include <functional>
31#include <memory>
32#include <string>
33#include <string_view>
34#include <type_traits>
35#include <vector>
36
37namespace spdlog
38{
39 class logger;
40}
41
42namespace malloy::server
43{
44 class routing_context;
45
46 namespace detail
47 {
48
49 template<typename T, typename... Args>
50 concept has_write = requires(T t, Args... args)
51 {
52 t.do_write(std::forward<Args>(args)...);
53 };
54
62 {
64 using header_type = boost::beast::http::request_header<>;
65
66 void setup_body(const header_type&, typename request_type::body_type::value_type&) const {}
67 };
68 static_assert(concepts::request_filter<default_route_filter>, "Default handler must satisfy route filter");
69
78 template<typename Body>
79 void
80 send_response(const boost::beast::http::request_header<>& req, malloy::http::response<Body>&& resp, http::connection_t connection, std::string_view server_str)
81 {
82 // Add more information to the response
83 //resp.keep_alive(req.keep_alive); // TODO: Is this needed?, if so its a spanner in the works
84 resp.version(req.version());
85 if (!malloy::http::has_field(resp, malloy::http::field::server))
86 resp.set(malloy::http::field::server, server_str);
87 resp.prepare_payload();
88
89 std::visit(
90 [resp = std::move(resp)](auto& c) mutable {
91 c->do_write(std::move(resp));
92 },
93 connection
94 );
95 }
96 } // namespace detail
97
103 class router final
104 {
108 auto
109 make_endpt_writer_callback() {
110 return [this]<typename R>(const auto& req, R&& resp, const auto& conn) {
111 std::visit(
112 [&, this]<typename Re>(Re&& resp) {
113 detail::send_response(req, std::forward<Re>(resp), conn, m_server_str);
114 },
115 std::forward<R>(resp)
116 );
117 };
118 }
119
120 class abstract_req_validator
121 {
122 public:
123 virtual ~abstract_req_validator() = default;
124
125 virtual
126 bool
127 process(const boost::beast::http::request_header<>&, const http::connection_t& conn) = 0;
128 };
129
130 template<concepts::request_validator V, typename Writer>
131 class req_validator_impl :
132 public abstract_req_validator
133 {
134 public:
135 Writer writer;
136
137 req_validator_impl(V validator, Writer writer_) :
138 writer{std::move(writer_)},
139 m_validator{std::move(validator)}
140 {
141 }
142
143 bool
144 process(const boost::beast::http::request_header<>& h, const http::connection_t& conn) override
145 {
146 auto maybe_resp = std::invoke(m_validator, h);
147 if (!maybe_resp)
148 return false;
149 else {
150 writer(h, std::move(*maybe_resp), conn);
151 return true;
152 }
153 }
154
155 private:
156 V m_validator;
157 };
158
159 class policy_store
160 {
161 public:
162 policy_store(std::string reg, std::unique_ptr<abstract_req_validator> validator) :
163 m_validator{std::move(validator)},
164 m_raw_reg{std::move(reg)}
165 {
166 }
167
168 [[nodiscard]]
169 bool
170 process(const boost::beast::http::request_header<>& h, const http::connection_t& conn) const
171 {
172 if (!matches(h.target()))
173 return false;
174 else
175 return m_validator->process(h, conn);
176 }
177
178 private:
179 bool
180 matches(std::string_view url) const
181 {
182 if (!m_compiled_reg)
183 compile_match_expr();
184 std::string surl{url.begin(), url.end()}; // Must be null terminated
185 return std::regex_match(surl, *m_compiled_reg);
186 }
187
188 void
189 compile_match_expr() const
190 {
191 m_compiled_reg = std::regex{m_raw_reg};
192 }
193
194 std::unique_ptr<abstract_req_validator> m_validator;
195 mutable std::optional<std::regex> m_compiled_reg;
196 std::string m_raw_reg;
197 };
198
199 public:
200 template<typename Derived>
201 using req_generator = std::shared_ptr<typename http::connection<Derived>::request_generator>;
202
203 using request_header = boost::beast::http::request_header<>;
204
209
214
219
223 router() = default;
224
230 explicit
231 router(std::shared_ptr<spdlog::logger> logger);
232
236 router(const router& other) = delete;
237
241 router(router&& other) noexcept = default;
242
246 ~router() = default;
247
254 router&
255 operator=(const router& rhs) = delete;
256
263 router&
264 operator=(router&& rhs) noexcept = default;
265
271 void
272 set_logger(std::shared_ptr<spdlog::logger> logger);
273
281 bool
282 add_subrouter(std::string resource, std::unique_ptr<router> sub_router);
283
287 bool
288 add_subrouter(std::string resource, router&& sub_router);
289
302 template<
303 concepts::request_filter ExtraInfo,
305 bool
306 add(const method_type method, const std::string_view target, Func&& handler, ExtraInfo&& extra)
307 {
308 using func_t = std::decay_t<Func>;
309
310 constexpr bool uses_captures = std::invocable<func_t, const request_type&, const std::vector<std::string>&>;
311
312 if constexpr (uses_captures) {
313 return add_regex_endpoint<
314 uses_captures,
315 std::invoke_result_t<func_t, const request_type&, const std::vector<std::string>&>
316 >(
317 method, target, std::forward<Func>(handler), std::forward<ExtraInfo>(extra)
318 );
319 }
320 else {
321 return add_regex_endpoint<
322 uses_captures,
323 std::invoke_result_t<func_t, const request_type&>
324 >(
325 method, target, std::forward<Func>(handler), std::forward<ExtraInfo>(extra)
326 );
327 }
328 }
329
330 template<concepts::route_handler<typename detail::default_route_filter::request_type> Func>
331 auto
332 add(const method_type method, const std::string_view target, Func&& handler)
333 {
334 return add(method, target, std::forward<Func>(handler), detail::default_route_filter{});
335 }
336
337 bool
338 add_preflight(std::string_view target, http::preflight_config cfg);
339
349 template<malloy::concepts::callable_string CacheControl>
350 bool
351 add_file_serving(std::string resource, std::filesystem::path storage_base_path, const CacheControl& cc)
352 {
353 // Log
354 if (m_logger)
355 m_logger->trace("adding file serving location: {} -> {}", resource, storage_base_path.string());
356
357 // Create endpoint
358 auto ep = std::make_unique<endpoint_http_files>();
359 ep->resource_base = resource;
360 ep->base_path = std::move(storage_base_path);
361 ep->cache_control = cc();
362 ep->writer = make_endpt_writer_callback();
363
364 // Add
365 return add_http_endpoint(std::move(ep));
366 }
367
375 bool
376 add_file_serving(std::string resource, std::filesystem::path storage_base_path)
377 {
378 return add_file_serving(
379 std::move(resource),
380 std::move(storage_base_path),
381 []() -> std::string { return ""; }
382 );
383 }
384
393 bool
394 add_redirect(malloy::http::status status, std::string&& resource_old, std::string&& resource_new);
395
403 bool
404 add_websocket(std::string&& resource, typename websocket::connection::handler_t&& handler);
405
414 template<concepts::request_validator Policy>
415 void
416 add_policy(const std::string& resource, Policy&& policy)
417 {
418 if (m_logger)
419 m_logger->trace("adding policy: {}", resource);
420
421 using policy_t = std::decay_t<Policy>;
422 auto writer = [this](const auto& header, auto&& resp, auto&& conn) { detail::send_response(header, std::forward<decltype(resp)>(resp), std::forward<decltype(conn)>(conn), m_server_str); };
423
424 m_policies.emplace_back(resource, std::make_unique<req_validator_impl<policy_t, decltype(writer)>>(std::forward<Policy>(policy), std::move(writer)));
425 }
426
443 template<
444 bool isWebsocket = false,
445 typename Derived,
446 typename Connection>
448 const std::filesystem::path& doc_root,
449 const req_generator<Derived>& req,
450 Connection&& connection
451 )
452 {
453 // Handle policy
454 if constexpr (!isWebsocket) {
455 if (is_handled_by_policies<Derived>(req, connection))
456 return;
457 }
458
459 // Check sub-routers
460 for (const auto& [resource_base, router] : m_sub_routers) {
461 // Check match
462 const auto res_str = malloy::http::resource_string(req->header());
463 if (!res_str.starts_with(resource_base))
464 continue;
465
466 // Chop request resource path
467 malloy::http::chop_resource(req->header(), resource_base);
468
469 // Let the sub-router handle things from here...
470 router->template handle_request<isWebsocket, Derived>(doc_root, std::move(req), connection);
471
472 // We're done handling this request
473 return;
474 }
475
476 //
477 // At this point we know that this particular router is going to handle the request.
478 //
479
480 // Forward to appropriate handler.
481 if constexpr (isWebsocket)
482 handle_ws_request<Derived>(std::move(req), connection);
483 else
484 handle_http_request<Derived>(doc_root, std::move(req), connection);
485 }
486
487 constexpr
488 std::string_view
489 server_string() const
490 {
491 return m_server_str;
492 }
493
494 private:
495 std::shared_ptr<spdlog::logger> m_logger{nullptr};
496 std::unordered_map<std::string, std::unique_ptr<router>> m_sub_routers;
497 std::vector<std::unique_ptr<endpoint_http>> m_endpoints_http;
498 std::vector<std::unique_ptr<endpoint_websocket>> m_endpoints_websocket;
499 std::vector<policy_store> m_policies; // Access policies for resources
500 std::string_view m_server_str;
501
502 friend class routing_context;
503
504 router(std::shared_ptr<spdlog::logger> logger, std::string_view m_server_str);
505
506 void
507 set_server_string(std::string_view str);
508
509 template<typename Derived>
510 [[nodiscard]]
511 bool
512 is_handled_by_policies(const req_generator<Derived>& req, const http::connection_t& connection)
513 {
514 return std::any_of(std::cbegin(m_policies), std::cend(m_policies), [&](const policy_store& policy) {
515 return policy.process(req->header(), connection);
516 });
517 }
518
527 template<typename Derived>
528 void handle_http_request(
529 const std::filesystem::path&,
530 const req_generator<Derived>& req,
531 const http::connection_t& connection
532 )
533 {
534 // Log
535 if (m_logger) {
536 m_logger->trace("handling HTTP request: {} {}",
537 req->header().method_string(),
538 req->header().target());
539 }
540
541 const auto& header = req->header();
542
543 // Check routes
544 for (const auto& ep : m_endpoints_http) {
545 // Check match
546 if (!ep->matches(header))
547 continue;
548
549 // Generate the response for the request
550 auto resp = ep->handle(req, connection);
551 if (resp) {
552 // Send the response
553 detail::send_response(req->header(), std::move(*resp), connection, m_server_str);
554 }
555
556 // We're done handling this request
557 return;
558 }
559
560 // If we end up where we have no meaningful way of handling this request
561 detail::send_response(req->header(), malloy::http::generator::bad_request("unknown request"), connection, m_server_str);
562 }
563
571 template<typename Derived>
572 void handle_ws_request(
573 const req_generator<Derived>& gen,
574 const std::shared_ptr<websocket::connection>& connection
575 )
576 {
577 const auto res_string = malloy::http::resource_string(gen->header());
578 m_logger->trace("handling WS request: {} {}",
579 gen->header().method_string(),
580 res_string);
581
582 // Check routes
583 for (const auto& ep : m_endpoints_websocket) {
584 // Check match
585 if (ep->resource != res_string)
586 continue;
587
588 // Validate route handler
589 if (!ep->handler) {
590 m_logger->warn("websocket route with resource path \"{}\" has no valid handler assigned.");
591 continue;
592 }
593
595 req.base() = gen->header();
596 ep->handler(std::move(req), connection);
597
598 // We're done handling this request. The route handler will handle everything from hereon.
599 return;
600 }
601 }
602
603 template<
604 bool UsesCaptures,
605 typename Body,
606 concepts::request_filter ExtraInfo,
607 typename Func>
608 bool
609 add_regex_endpoint(method_type method, std::string_view target, Func&& handler, ExtraInfo&& extra)
610 {
611 // Log
612 if (m_logger)
613 m_logger->trace("adding route: {}", target);
614
615 // Build regex
616 std::regex regex;
617 try {
618 regex = std::regex{target.cbegin(), target.cend()};
619 }
620 catch (const std::regex_error& e) {
621 if (m_logger)
622 m_logger->error("invalid route target supplied \"{}\": {}", target, e.what());
623 return false;
624 }
625
626 constexpr bool wrapped = malloy::concepts::is_variant<Body>;
627 using bodies_t = std::conditional_t<wrapped, Body, std::variant<Body>>;
628
629 // Build endpoint
630 auto ep = std::make_unique<endpoint_http_regex<bodies_t, std::decay_t<ExtraInfo>, UsesCaptures>>();
631 ep->resource_base = std::move(regex);
632 ep->method = method;
633 ep->filter = std::forward<ExtraInfo>(extra);
634 if constexpr (wrapped) {
635 ep->handler = std::move(handler);
636 }
637 else {
638 ep->handler =
639 [w = std::forward<Func>(handler)](auto&&... args) {
640 return std::variant<Body>{w(std::forward<decltype(args)>(args)...)};
641 };
642 }
643
644 // Check handler
645 if (!ep->handler) {
646 if (m_logger)
647 m_logger->warn("route has invalid handler. ignoring.");
648 return false;
649 }
650
651 ep->writer = make_endpt_writer_callback();
652
653 // Add route
654 return add_http_endpoint(std::move(ep));
655 }
656
665 bool
666 add_http_endpoint(std::unique_ptr<endpoint_http>&& ep);
667
676 bool
677 add_websocket_endpoint(std::unique_ptr<endpoint_websocket>&& ep);
678
690 template<typename FormatString, typename... Args>
691 bool
692 log_or_throw(
693 const std::exception& exception,
694 const spdlog::level::level_enum level,
695 const FormatString& fmt,
696 Args&&... args
697 )
698 {
699 if (m_logger) {
700 m_logger->log(level,
701#if MALLOY_DETAIL_HAS_FMT_8
702 fmt::runtime(fmt)
703#else
704 fmt
705#endif
706 ,
707 std::forward<Args>(args)...
708 );
709 return false;
710 }
711
712 else
713 throw exception;
714 }
715 };
716
717} // namespace malloy::server
static response bad_request(std::string_view reason)
Definition: generator.cpp:27
Definition: request.hpp:19
Definition: response.hpp:22
Definition: router.hpp:104
bool add_websocket(std::string &&resource, typename websocket::connection::handler_t &&handler)
Definition: router.cpp:189
router & operator=(router &&rhs) noexcept=default
bool add_redirect(malloy::http::status status, std::string &&resource_old, std::string &&resource_new)
Definition: router.cpp:157
router & operator=(const router &rhs)=delete
bool add_subrouter(std::string resource, std::unique_ptr< router > sub_router)
Definition: router.cpp:59
bool add_file_serving(std::string resource, std::filesystem::path storage_base_path, const CacheControl &cc)
Definition: router.hpp:351
void set_logger(std::shared_ptr< spdlog::logger > logger)
Definition: router.cpp:21
bool add(const method_type method, const std::string_view target, Func &&handler, ExtraInfo &&extra)
Definition: router.hpp:306
malloy::http::method method_type
Definition: router.hpp:208
void add_policy(const std::string &resource, Policy &&policy)
Definition: router.hpp:416
router(const router &other)=delete
router(router &&other) noexcept=default
bool add_file_serving(std::string resource, std::filesystem::path storage_base_path)
Definition: router.hpp:376
void handle_request(const std::filesystem::path &doc_root, const req_generator< Derived > &req, Connection &&connection)
Definition: router.hpp:447
Definition: type_traits.hpp:104
Definition: type_traits.hpp:34
Definition: type_traits.hpp:26
Definition: router.hpp:50
boost::beast::http::verb method
Definition: types.hpp:18
boost::beast::http::status status
Definition: types.hpp:23