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 {
111 return [this]<typename R>(const auto& req, R&& resp, const auto& conn) {
112 std::visit(
113 [&, this]<typename Re>(Re&& resp) {
114 detail::send_response(req, std::forward<Re>(resp), conn, m_server_str);
115 },
116 std::forward<R>(resp)
117 );
118 };
119 }
120
121 class abstract_req_validator
122 {
123 public:
124 virtual
125 ~abstract_req_validator() = default;
126
127 virtual
128 bool
129 process(const boost::beast::http::request_header<>&, const http::connection_t& conn) = 0;
130 };
131
132 template<concepts::request_validator V, typename Writer>
133 class req_validator_impl :
134 public abstract_req_validator
135 {
136 public:
137 Writer writer;
138
139 req_validator_impl(V validator, Writer writer_) :
140 writer{std::move(writer_)},
141 m_validator{std::move(validator)}
142 {
143 }
144
145 bool
146 process(const boost::beast::http::request_header<>& h, const http::connection_t& conn) override
147 {
148 auto maybe_resp = std::invoke(m_validator, h);
149 if (!maybe_resp)
150 return false;
151 else {
152 writer(h, std::move(*maybe_resp), conn);
153 return true;
154 }
155 }
156
157 private:
158 V m_validator;
159 };
160
161 class policy_store
162 {
163 public:
164 policy_store(std::string reg, std::unique_ptr<abstract_req_validator> validator) :
165 m_validator{std::move(validator)},
166 m_raw_reg{std::move(reg)}
167 {
168 }
169
170 [[nodiscard]]
171 bool
172 process(const boost::beast::http::request_header<>& h, const http::connection_t& conn) const
173 {
174 if (!matches(h.target()))
175 return false;
176 else
177 return m_validator->process(h, conn);
178 }
179
180 private:
181 bool
182 matches(std::string_view url) const
183 {
184 if (!m_compiled_reg)
185 compile_match_expr();
186 std::string surl{url.begin(), url.end()}; // Must be null terminated
187 return std::regex_match(surl, *m_compiled_reg);
188 }
189
190 void
191 compile_match_expr() const
192 {
193 m_compiled_reg = std::regex{m_raw_reg};
194 }
195
196 std::unique_ptr<abstract_req_validator> m_validator;
197 mutable std::optional<std::regex> m_compiled_reg;
198 std::string m_raw_reg;
199 };
200
201 public:
202 template<typename Derived>
203 using req_generator = std::shared_ptr<typename http::connection<Derived>::request_generator>;
204
205 using request_header = boost::beast::http::request_header<>;
206
211
216
221
225 router() = default;
226
232 explicit
233 router(std::shared_ptr<spdlog::logger> logger);
234
238 router(const router& other) = delete;
239
243 router(router&& other) noexcept = default;
244
248 ~router() = default;
249
256 router&
257 operator=(const router& rhs) = delete;
258
265 router&
266 operator=(router&& rhs) noexcept = default;
267
273 void
274 set_logger(std::shared_ptr<spdlog::logger> logger);
275
283 bool
284 add_subrouter(std::string resource, std::unique_ptr<router> sub_router);
285
289 bool
290 add_subrouter(std::string resource, router&& sub_router);
291
304 template<
305 concepts::request_filter ExtraInfo,
307 bool
308 add(const method_type method, const std::string_view target, Func&& handler, ExtraInfo&& extra)
309 {
310 using func_t = std::decay_t<Func>;
311
312 constexpr bool uses_captures = std::invocable<func_t, const request_type&, const std::vector<std::string>&>;
313
314 if constexpr (uses_captures) {
315 return add_regex_endpoint<
316 uses_captures,
317 std::invoke_result_t<func_t, const request_type&, const std::vector<std::string>&>
318 >(
319 method, target, std::forward<Func>(handler), std::forward<ExtraInfo>(extra)
320 );
321 }
322 else {
323 return add_regex_endpoint<
324 uses_captures,
325 std::invoke_result_t<func_t, const request_type&>
326 >(
327 method, target, std::forward<Func>(handler), std::forward<ExtraInfo>(extra)
328 );
329 }
330 }
331
332 template<concepts::route_handler<typename detail::default_route_filter::request_type> Func>
333 auto
334 add(const method_type method, const std::string_view target, Func&& handler)
335 {
336 return add(method, target, std::forward<Func>(handler), detail::default_route_filter{});
337 }
338
339 bool
340 add_preflight(std::string_view target, http::preflight_config cfg);
341
351 template<malloy::concepts::callable_string CacheControl>
352 bool
353 add_file_serving(std::string resource, std::filesystem::path storage_base_path, const CacheControl& cc)
354 {
355 // Log
356 if (m_logger)
357 m_logger->trace("adding file serving location: {} -> {}", resource, storage_base_path.string());
358
359 // Create endpoint
360 auto ep = std::make_unique<endpoint_http_files>();
361 ep->resource_base = resource;
362 ep->base_path = std::move(storage_base_path);
363 ep->cache_control = cc();
364 ep->writer = make_endpt_writer_callback();
365
366 // Add
367 return add_http_endpoint(std::move(ep));
368 }
369
377 bool
378 add_file_serving(std::string resource, std::filesystem::path storage_base_path)
379 {
380 return add_file_serving(
381 std::move(resource),
382 std::move(storage_base_path),
383 []() -> std::string { return ""; }
384 );
385 }
386
395 bool
396 add_redirect(malloy::http::status status, std::string&& resource_old, std::string&& resource_new);
397
405 bool
406 add_websocket(std::string&& resource, typename websocket::connection::handler_t&& handler);
407
416 template<concepts::request_validator Policy>
417 void
418 add_policy(const std::string& resource, Policy&& policy)
419 {
420 if (m_logger)
421 m_logger->trace("adding policy: {}", resource);
422
423 using policy_t = std::decay_t<Policy>;
424 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); };
425
426 m_policies.emplace_back(resource, std::make_unique<req_validator_impl<policy_t, decltype(writer)>>(std::forward<Policy>(policy), std::move(writer)));
427 }
428
445 template<
446 bool isWebsocket = false,
447 typename Derived,
448 typename Connection>
449 void
451 const std::filesystem::path& doc_root,
452 const req_generator<Derived>& req,
453 Connection&& connection
454 )
455 {
456 // Handle policy
457 if constexpr (!isWebsocket) {
458 if (is_handled_by_policies<Derived>(req, connection))
459 return;
460 }
461
462 // Check sub-routers
463 for (const auto& [resource_base, router] : m_sub_routers) {
464 // Check match
465 const auto res_str = malloy::http::resource_string(req->header());
466 if (!res_str.starts_with(resource_base))
467 continue;
468
469 // Chop request resource path
470 malloy::http::chop_resource(req->header(), resource_base);
471
472 // Let the sub-router handle things from here...
473 router->template handle_request<isWebsocket, Derived>(doc_root, std::move(req), connection);
474
475 // We're done handling this request
476 return;
477 }
478
479 //
480 // At this point we know that this particular router is going to handle the request.
481 //
482
483 // Forward to appropriate handler.
484 if constexpr (isWebsocket)
485 handle_ws_request<Derived>(std::move(req), connection);
486 else
487 handle_http_request<Derived>(doc_root, std::move(req), connection);
488 }
489
490 constexpr
491 std::string_view
492 server_string() const
493 {
494 return m_server_str;
495 }
496
497 private:
498 std::shared_ptr<spdlog::logger> m_logger{nullptr};
499 std::unordered_map<std::string, std::unique_ptr<router>> m_sub_routers;
500 std::vector<std::unique_ptr<endpoint_http>> m_endpoints_http;
501 std::vector<std::unique_ptr<endpoint_websocket>> m_endpoints_websocket;
502 std::vector<policy_store> m_policies; // Access policies for resources
503 std::string_view m_server_str;
504
505 friend class routing_context;
506
507 router(std::shared_ptr<spdlog::logger> logger, std::string_view m_server_str);
508
509 void
510 set_server_string(std::string_view str);
511
512 template<typename Derived>
513 [[nodiscard]]
514 bool
515 is_handled_by_policies(const req_generator<Derived>& req, const http::connection_t& connection)
516 {
517 return std::any_of(
518 std::cbegin(m_policies),
519 std::cend(m_policies),
520 [&](const policy_store& policy) {
521 return policy.process(req->header(), connection);
522 });
523 }
524
533 template<typename Derived>
534 void
535 handle_http_request(
536 const std::filesystem::path&,
537 const req_generator<Derived>& req,
538 const http::connection_t& connection
539 )
540 {
541 // Log
542 if (m_logger) {
543 m_logger->trace("handling HTTP request: {} {}",
544 std::string_view{req->header().method_string()},
545 std::string_view{req->header().target()}
546 );
547 }
548
549 const auto& header = req->header();
550
551 // Check routes
552 for (const auto& ep : m_endpoints_http) {
553 // Check match
554 if (!ep->matches(header))
555 continue;
556
557 // Generate the response for the request
558 auto resp = ep->handle(req, connection);
559 if (resp) {
560 // Send the response
561 detail::send_response(req->header(), std::move(*resp), connection, m_server_str);
562 }
563
564 // We're done handling this request
565 return;
566 }
567
568 // If we end up where we have no meaningful way of handling this request
569 detail::send_response(req->header(), malloy::http::generator::bad_request("unknown request"), connection, m_server_str);
570 }
571
579 template<typename Derived>
580 void
581 handle_ws_request(
582 const req_generator<Derived>& gen,
583 const std::shared_ptr<websocket::connection>& connection
584 )
585 {
586 const auto res_string = malloy::http::resource_string(gen->header());
587 m_logger->trace("handling WS request: {} {}",
588 std::string_view{gen->header().method_string()},
589 res_string
590 );
591
592 // Check routes
593 for (const auto& ep : m_endpoints_websocket) {
594 // Check match
595 if (ep->resource != res_string)
596 continue;
597
598 // Validate route handler
599 if (!ep->handler) {
600 m_logger->warn("websocket route with resource path \"{}\" has no valid handler assigned.");
601 continue;
602 }
603
605 req.base() = gen->header();
606 ep->handler(std::move(req), connection);
607
608 // We're done handling this request. The route handler will handle everything from hereon.
609 return;
610 }
611 }
612
613 template<
614 bool UsesCaptures,
615 typename Body,
616 concepts::request_filter ExtraInfo,
617 typename Func>
618 bool
619 add_regex_endpoint(method_type method, std::string_view target, Func&& handler, ExtraInfo&& extra)
620 {
621 // Log
622 if (m_logger)
623 m_logger->trace("adding route: {}", target);
624
625 // Build regex
626 std::regex regex;
627 try {
628 regex = std::regex{target.cbegin(), target.cend()};
629 }
630 catch (const std::regex_error& e) {
631 if (m_logger)
632 m_logger->error("invalid route target supplied \"{}\": {}", target, e.what());
633 return false;
634 }
635
636 constexpr bool wrapped = malloy::concepts::is_variant<Body>;
637 using bodies_t = std::conditional_t<wrapped, Body, std::variant<Body>>;
638
639 // Build endpoint
640 auto ep = std::make_unique<endpoint_http_regex<bodies_t, std::decay_t<ExtraInfo>, UsesCaptures>>();
641 ep->resource_base = std::move(regex);
642 ep->method = method;
643 ep->filter = std::forward<ExtraInfo>(extra);
644 if constexpr (wrapped) {
645 ep->handler = std::move(handler);
646 }
647 else {
648 ep->handler =
649 [w = std::forward<Func>(handler)](auto&&... args) {
650 return std::variant<Body>{w(std::forward<decltype(args)>(args)...)};
651 };
652 }
653
654 // Check handler
655 if (!ep->handler) {
656 if (m_logger)
657 m_logger->warn("route has invalid handler. ignoring.");
658 return false;
659 }
660
661 ep->writer = make_endpt_writer_callback();
662
663 // Add route
664 return add_http_endpoint(std::move(ep));
665 }
666
675 bool
676 add_http_endpoint(std::unique_ptr<endpoint_http>&& ep);
677
686 bool
687 add_websocket_endpoint(std::unique_ptr<endpoint_websocket>&& ep);
688
700 template<typename FormatString, typename... Args>
701 bool
702 log_or_throw(
703 const std::exception& exception,
704 const spdlog::level::level_enum level,
705 const FormatString& fmt,
706 Args&&... args
707 )
708 {
709 if (m_logger) {
710 m_logger->log(level,
711#if MALLOY_DETAIL_HAS_FMT_8
712 fmt::runtime(fmt)
713#else
714 fmt
715#endif
716 ,
717 std::forward<Args>(args)...
718 );
719 return false;
720 }
721
722 else
723 throw exception;
724 }
725 };
726
727} // 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:353
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:308
malloy::http::method method_type
Definition: router.hpp:210
void add_policy(const std::string &resource, Policy &&policy)
Definition: router.hpp:418
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:378
void handle_request(const std::filesystem::path &doc_root, const req_generator< Derived > &req, Connection &&connection)
Definition: router.hpp:450
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