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