Malloy
Loading...
Searching...
No Matches
controller.hpp
1#pragma once
2
3#include "type_traits.hpp"
4#include "http/connection_plain.hpp"
5#include "websocket/connection.hpp"
6#include "../core/controller.hpp"
7#include "../core/error.hpp"
8#include "../core/detail/controller_run_result.hpp"
9#include "../core/http/request.hpp"
10#include "../core/http/response.hpp"
11#include "../core/http/type_traits.hpp"
12#include "../core/http/url.hpp"
13#include "../core/http/utils.hpp"
14#include "../core/tcp/stream.hpp"
15#if MALLOY_FEATURE_TLS
16 #include "http/connection_tls.hpp"
17
18 #include <boost/beast/ssl.hpp>
19#endif
20
21#include <boost/asio/strand.hpp>
22#include <spdlog/logger.h>
23
24#include <filesystem>
25
26namespace boost::asio::ssl
27{
28 class context;
29}
30
31namespace malloy::client
32{
33 namespace websocket
34 {
35 class connection_plain;
36 }
37
38 namespace detail
39 {
40
48 {
50 using header_type = boost::beast::http::response_header<>;
51 using value_type = std::string;
52
53 [[nodiscard]]
54 std::variant<boost::beast::http::string_body>
55 body_for(const header_type&) const
56 {
57 return {};
58 }
59
60 void
61 setup_body(const header_type&, std::string&) const
62 {
63 }
64 };
65
66 static_assert(malloy::client::concepts::response_filter<default_resp_filter>, "default_resp_filter must satisfy response_filter");
67
68 } // namespace detail
69
74 {
75 public:
80
84 struct config :
86 {
91 std::string user_agent{"malloy"};
92
96 std::uint64_t body_limit = 100'000'000;
97 };
98
104 explicit
105 controller(config cfg);
106
110 ~controller() = default;
111
112#if MALLOY_FEATURE_TLS
118 [[nodiscard("init might fail")]]
119 bool
120 init_tls();
121#endif
122
136 template<
139 >
141 [[nodiscard]]
142 std::future<malloy::error_code>
145 Callback&& done,
146 Filter filter = {}
147 )
148 {
149 return make_http_connection<false>(std::move(req), std::forward<Callback>(done), std::move(filter));
150 }
151
169 template<
170 malloy::http::concepts::body ReqBody = boost::beast::http::string_body,
171 typename Callback,
172 concepts::response_filter Filter = detail::default_resp_filter
173 >
174 requires concepts::http_callback<Callback, Filter>
175 [[nodiscard]]
176 std::future<malloy::error_code>
178 const malloy::http::method method_,
179 const std::string_view url,
180 Callback&& done,
181 Filter filter = {}
182 ){
183 // Build request
184 auto req = malloy::http::build_request<ReqBody>(method_, url);
185 if (!req) {
186 // ToDo: Here, we'd want to assign a proper error code indicating the actual failure.
187
189 ec.assign(0, boost::beast::generic_category());
190 std::promise<malloy::error_code> p;
191 p.set_value(std::forward<malloy::error_code>(ec));
192 return p.get_future();
193 }
194
195 // Make request
196#if MALLOY_FEATURE_TLS
197 if (req->use_tls())
198 return make_http_connection<true>(std::move(*req), std::forward<Callback>(done), std::move(filter));
199 else
200#endif
201 return make_http_connection<false>(std::move(*req), std::forward<Callback>(done), std::move(filter));
202 }
203
204#if MALLOY_FEATURE_TLS
210 template<
212 typename Callback,
213 concepts::response_filter Filter = detail::default_resp_filter
214 >
215 requires concepts::http_callback<Callback, Filter>
216 [[nodiscard]]
217 std::future<malloy::error_code>
220 Callback&& done,
221 Filter filter = {}
222 )
223 {
224 return make_http_connection<true>(std::move(req), std::forward<Callback>(done), std::move(filter));
225 }
226
233 void
235 const std::string& host,
236 std::uint16_t port,
237 const std::string& resource,
238 std::invocable<malloy::error_code, std::shared_ptr<websocket::connection>> auto&& handler
239 )
240 {
241 check_tls();
242
243 // Create connection
244 make_ws_connection<true>(host, port, resource, std::forward<decltype(handler)>(handler));
245 }
246
254 void
255 add_ca_file(const std::filesystem::path& file);
256
264 void
265 add_ca(const std::string& contents);
266#endif
267
284 void
286 const std::string& host,
287 std::uint16_t port,
288 const std::string& resource,
289 std::invocable<malloy::error_code, std::shared_ptr<websocket::connection>> auto&& handler
290 )
291 {
292 // Create connection
293 make_ws_connection<false>(host, port, resource, std::forward<decltype(handler)>(handler));
294 }
295
296 private:
297 std::shared_ptr<boost::asio::ssl::context> m_tls_ctx{nullptr};
298 std::unique_ptr<boost::asio::io_context> m_ioc_sm{std::make_unique<boost::asio::io_context>()};
299 boost::asio::io_context* m_ioc{m_ioc_sm.get()};
300 config m_cfg;
301
302 [[nodiscard]]
303 friend
304 session
305 start(controller& ctrl)
306 {
307 return session{ctrl.m_cfg, ctrl.m_tls_ctx, std::move(ctrl.m_ioc_sm)};
308 }
309
314 void
315 check_tls() const
316 {
317 // Check whether TLS context was initialized
318 if (!m_tls_ctx)
319 throw std::logic_error("TLS context not initialized.");
320 }
321
322 // ToDo: Change this so it returns an awaitable
323 template<
324 bool isHttps,
326 typename Callback,
327 typename Filter
328 >
329 std::future<malloy::error_code>
330 make_http_connection(malloy::http::request<Body>&& req, Callback&& cb, Filter&& filter)
331 {
332 std::promise<malloy::error_code> prom;
333 auto err_channel = prom.get_future();
334
335 // Set User-Agent header if not already set
336 if (!malloy::http::has_field(req, malloy::http::field::user_agent))
337 req.set(malloy::http::field::user_agent, m_cfg.user_agent);
338
339#if MALLOY_FEATURE_TLS
340 if constexpr (isHttps) {
341 // Create connection
342 auto conn = std::make_shared<http::connection_tls<Body, Filter>>(
343 m_cfg.logger->clone(m_cfg.logger->name() + " | HTTPS connection"),
344 *m_ioc,
345 *m_tls_ctx,
346 m_cfg.body_limit
347 );
348
349 // Set SNI hostname (many hosts need this to handshake successfully)
350 // Note: We copy the returned std::string_view into an std::string as the underlying OpenSSL API expects C-strings.
351 // ToDo: Can we move this to connection_tls.hpp?
352 const std::string hostname{ req.base()[malloy::http::field::host] };
353 if (!SSL_set_tlsext_host_name(conn->stream().native_handle(), hostname.c_str())) {
354 // ToDo: Improve error handling
355 m_cfg.logger->error("could not set SNI hostname.");
356 boost::system::error_code ec{static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()};
357 throw boost::system::system_error{ec};
358 }
359
360 // Run
361 boost::asio::co_spawn(
362 *m_ioc,
363 conn->run(std::move(req), std::forward<Filter>(filter)),
364 [conn, prom = std::move(prom), cb = std::move(cb)](std::exception_ptr e, http::request_result<Filter> req_result) mutable { // ToDo: Do we need to capture conn to keep it alive here?!
365 // Note: The order here is important. We need to invoke the callback before we set the promise. Otherwise, a consumer calling get() on the
366 // promise will get the promise before the consumer callback gets executed. This leads to synchronization problems. The consumer
367 // expects that the callback gets executed before the error code promise is set (if there is no error).
368
369 // Handle exceptions
370 if (e)
371 std::rethrow_exception(e);
372
373 // Invoke callback
374 // Note: Do this BEFORE we set the error code promise (if there's no error)
375 if (!req_result.error_code)
376 cb(std::move(req_result.response));
377
378 // Set error_code promise
379 prom.set_value(req_result.error_code);
380 }
381 );
382 }
383 else {
384#endif
385 auto conn = std::make_shared<http::connection_plain<Body, Filter>>(
386 m_cfg.logger->clone(m_cfg.logger->name() + " | HTTP connection"),
387 *m_ioc,
388 m_cfg.body_limit
389 );
390
391 // Run
392 boost::asio::co_spawn(
393 *m_ioc,
394 conn->run(std::move(req), std::forward<Filter>(filter)),
395 [conn, prom = std::move(prom), cb = std::move(cb)](std::exception_ptr e, http::request_result<Filter> req_result) mutable { // ToDo: Do we need to capture conn to keep it alive here?!
396 // Note: The order here is important. We need to invoke the callback before we set the promise. Otherwise, a consumer calling get() on the
397 // promise will get the promise before the consumer callback gets executed. This leads to synchronization problems. The consumer
398 // expects that the callback gets executed before the error code promise is set (if there is no error).
399
400 // Handle exceptions
401 if (e)
402 std::rethrow_exception(e);
403
404 // Invoke callback
405 // Note: Do this BEFORE we set the error code promise (if there's no error)
406 if (!req_result.error_code)
407 cb(std::move(req_result.response));
408
409 // Set error_code promise
410 prom.set_value(req_result.error_code);
411 }
412 );
413#if MALLOY_FEATURE_TLS
414 }
415#endif
416
417 return err_channel;
418 }
419
420 template<bool isSecure>
421 void
422 make_ws_connection(
423 const std::string& host,
424 std::uint16_t port,
425 const std::string& resource,
426 std::invocable<malloy::error_code, std::shared_ptr<websocket::connection>> auto&& handler
427 )
428 {
429 // Create connection
430 auto resolver = std::make_shared<boost::asio::ip::tcp::resolver>(boost::asio::make_strand(*m_ioc));
431 resolver->async_resolve(
432 host,
433 std::to_string(port),
434 [this, resolver, done = std::forward<decltype(handler)>(handler), resource](auto ec, auto results) mutable {
435 if (ec)
436 std::invoke(std::forward<decltype(done)>(done), ec, std::shared_ptr<websocket::connection>{nullptr});
437 else {
438 auto conn = websocket::connection::make(m_cfg.logger->clone("connection"), [this]() -> malloy::websocket::stream {
439#if MALLOY_FEATURE_TLS
440 if constexpr (isSecure) {
441 return malloy::websocket::stream{boost::beast::ssl_stream<malloy::tcp::stream<>>{
442 malloy::tcp::stream<>{boost::asio::make_strand(*m_ioc)}, *m_tls_ctx}};
443 }
444 else
445#endif
446 return malloy::websocket::stream{malloy::tcp::stream<>{boost::asio::make_strand(*m_ioc)}};
447 }(), m_cfg.user_agent);
448
449 conn->connect(results, resource, [conn, done = std::forward<decltype(done)>(done)](auto ec) mutable {
450 if (ec)
451 std::invoke(std::forward<decltype(handler)>(done), ec, std::shared_ptr<websocket::connection>{nullptr});
452 else
453 std::invoke(std::forward<decltype(handler)>(done), ec, conn);
454 });
455 }
456 }
457 );
458 }
459 };
460
461 auto start(controller&) -> controller::session;
462
463} // namespace malloy::client
Definition: controller.hpp:74
std::future< malloy::error_code > http_request(malloy::http::request< ReqBody > req, Callback &&done, Filter filter={})
Definition: controller.hpp:143
bool init_tls()
Definition: controller.cpp:19
void ws_connect(const std::string &host, std::uint16_t port, const std::string &resource, std::invocable< malloy::error_code, std::shared_ptr< websocket::connection > > auto &&handler)
Definition: controller.hpp:285
void wss_connect(const std::string &host, std::uint16_t port, const std::string &resource, std::invocable< malloy::error_code, std::shared_ptr< websocket::connection > > auto &&handler)
Same as ws_connect() but uses TLS.
Definition: controller.hpp:234
std::future< malloy::error_code > https_request(malloy::http::request< ReqBody > req, Callback &&done, Filter filter={})
Definition: controller.hpp:218
void add_ca(const std::string &contents)
Like add_ca_file(std::filesystem::path) but loads from an in-memory string.
Definition: controller.cpp:40
malloy::detail::controller_run_result< std::shared_ptr< boost::asio::ssl::context > > session
Definition: controller.hpp:79
std::future< malloy::error_code > http_request(const malloy::http::method method_, const std::string_view url, Callback &&done, Filter filter={})
Definition: controller.hpp:177
void add_ca_file(const std::filesystem::path &file)
Load a certificate authority for use with TLS validation.
Definition: controller.cpp:30
Definition: controller_run_result.hpp:43
Definition: request.hpp:19
constexpr void use_tls(const bool enabled) noexcept
Definition: request.hpp:117
Definition: response.hpp:22
static std::shared_ptr< connection > make(const std::shared_ptr< spdlog::logger > logger, stream &&ws, const std::string &agent_string)
Construct a new connection object.
Definition: connection.hpp:106
Websocket stream. May use TLS.
Definition: stream.hpp:50
Definition: type_traits.hpp:68
Definition: type_traits.hpp:56
Definition: type_traits.hpp:9
boost::beast::http::verb method
Definition: types.hpp:18
boost::beast::error_code error_code
Error code used to signify errors without throwing. Truthy means it holds an error.
Definition: error.hpp:9
Definition: controller.hpp:86
std::string user_agent
Agent string used for connections.
Definition: controller.hpp:91
std::uint64_t body_limit
Definition: controller.hpp:96
Default filter provided to ease use of interface.
Definition: controller.hpp:48
Definition: controller_run_result.hpp:19