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/utils.hpp"
13#include "../core/tcp/stream.hpp"
14#if MALLOY_FEATURE_TLS
15 #include "http/connection_tls.hpp"
16
17 #include <boost/beast/ssl.hpp>
18#endif
19
20#include <boost/asio/strand.hpp>
21#include <spdlog/logger.h>
22
23#include <filesystem>
24
25namespace boost::asio::ssl
26{
27 class context;
28}
29
30namespace malloy::client
31{
32 namespace websocket
33 {
34 class connection_plain;
35 }
36
37 namespace detail
38 {
39
47 {
49 using header_type = boost::beast::http::response_header<>;
50 using value_type = std::string;
51
52 [[nodiscard]]
53 std::variant<boost::beast::http::string_body>
54 body_for(const header_type&) const
55 {
56 return {};
57 }
58
59 void
60 setup_body(const header_type&, std::string&) const
61 {
62 }
63 };
64
65 static_assert(malloy::client::concepts::response_filter<default_resp_filter>, "default_resp_filter must satisfy response_filter");
66
67 } // namespace detail
68
73 {
74 public:
79
83 struct config :
85 {
90 std::string user_agent{"malloy"};
91
95 std::uint64_t body_limit = 100'000'000;
96 };
97
103 explicit
104 controller(config cfg);
105
109 ~controller() = default;
110
111#if MALLOY_FEATURE_TLS
117 [[nodiscard("init might fail")]]
118 bool
119 init_tls();
120#endif
121
135 template<
138 >
140 [[nodiscard]]
141 std::future<malloy::error_code>
144 Callback&& done,
145 Filter filter = {}
146 )
147 {
148 return make_http_connection<false>(std::move(req), std::forward<Callback>(done), std::move(filter));
149 }
150
151#if MALLOY_FEATURE_TLS
157 template<
159 typename Callback,
160 concepts::response_filter Filter = detail::default_resp_filter
161 >
162 requires concepts::http_callback<Callback, Filter>
163 [[nodiscard]]
164 std::future<malloy::error_code>
167 Callback&& done,
168 Filter filter = {}
169 )
170 {
171 return make_http_connection<true>(std::move(req), std::forward<Callback>(done), std::move(filter));
172 }
173
180 void
182 const std::string& host,
183 std::uint16_t port,
184 const std::string& resource,
185 std::invocable<malloy::error_code, std::shared_ptr<websocket::connection>> auto&& handler
186 )
187 {
188 check_tls();
189
190 // Create connection
191 make_ws_connection<true>(host, port, resource, std::forward<decltype(handler)>(handler));
192 }
193
201 void
202 add_ca_file(const std::filesystem::path& file);
203
211 void
212 add_ca(const std::string& contents);
213#endif
214
231 void
233 const std::string& host,
234 std::uint16_t port,
235 const std::string& resource,
236 std::invocable<malloy::error_code, std::shared_ptr<websocket::connection>> auto&& handler
237 )
238 {
239 // Create connection
240 make_ws_connection<false>(host, port, resource, std::forward<decltype(handler)>(handler));
241 }
242
243 private:
244 std::shared_ptr<boost::asio::ssl::context> m_tls_ctx{nullptr};
245 std::unique_ptr<boost::asio::io_context> m_ioc_sm{std::make_unique<boost::asio::io_context>()};
246 boost::asio::io_context* m_ioc{m_ioc_sm.get()};
247 config m_cfg;
248
249 [[nodiscard]]
250 friend
251 session
252 start(controller& ctrl)
253 {
254 return session{ctrl.m_cfg, ctrl.m_tls_ctx, std::move(ctrl.m_ioc_sm)};
255 }
256
261 void
262 check_tls() const
263 {
264 // Check whether TLS context was initialized
265 if (!m_tls_ctx)
266 throw std::logic_error("TLS context not initialized.");
267 }
268
269 template<
270 bool isHttps,
272 typename Callback,
273 typename Filter
274 >
275 std::future<malloy::error_code>
276 make_http_connection(malloy::http::request<Body>&& req, Callback&& cb, Filter&& filter)
277 {
278 std::promise<malloy::error_code> prom;
279 auto err_channel = prom.get_future();
280 [req, this](auto&& cb) { // ToDo: Don't capture req by value
281#if MALLOY_FEATURE_TLS
282 if constexpr (isHttps) {
283 // Create connection
284 auto conn = std::make_shared<http::connection_tls<Body, Filter, std::decay_t<Callback>>>(
285 m_cfg.logger->clone(m_cfg.logger->name() + " | HTTPS connection"),
286 *m_ioc,
287 *m_tls_ctx,
288 m_cfg.body_limit
289 );
290
291 // Set SNI hostname (many hosts need this to handshake successfully)
292 // Note: We copy the returned std::string_view into an std::string as the underlying OpenSSL API expects C-strings.
293 const std::string hostname{ req.base()[malloy::http::field::host] };
294 if (!SSL_set_tlsext_host_name(conn->stream().native_handle(), hostname.c_str())) {
295 // ToDo: Improve error handling
296 m_cfg.logger->error("could not set SNI hostname.");
297 boost::system::error_code ec{static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()};
298 throw boost::system::system_error{ec};
299 }
300
301 // Run
302 cb(std::move(conn));
303
304 return;
305 }
306#endif
307 cb(std::make_shared<http::connection_plain<Body, Filter, std::decay_t<Callback>>>(
308 m_cfg.logger->clone(m_cfg.logger->name() + " | HTTP connection"),
309 *m_ioc,
310 m_cfg.body_limit)
311 );
312 }
313
314 ([this, prom = std::move(prom), req = std::move(req), filter = std::forward<Filter>(filter), cb = std::forward<Callback>(cb)](auto&& conn) mutable {
315 if (!malloy::http::has_field(req, malloy::http::field::user_agent))
316 req.set(malloy::http::field::user_agent, m_cfg.user_agent);
317
318 // Run
319 conn->run(
320 req,
321 std::move(prom),
322 std::forward<Callback>(cb),
323 std::forward<Filter>(filter)
324 );
325 });
326
327 return err_channel;
328
329 }
330
331 template<bool isSecure>
332 void
333 make_ws_connection(
334 const std::string& host,
335 std::uint16_t port,
336 const std::string& resource,
337 std::invocable<malloy::error_code, std::shared_ptr<websocket::connection>> auto&& handler
338 )
339 {
340 // Create connection
341 auto resolver = std::make_shared<boost::asio::ip::tcp::resolver>(boost::asio::make_strand(*m_ioc));
342 resolver->async_resolve(
343 host,
344 std::to_string(port),
345 [this, resolver, done = std::forward<decltype(handler)>(handler), resource](auto ec, auto results) mutable {
346 if (ec)
347 std::invoke(std::forward<decltype(done)>(done), ec, std::shared_ptr<websocket::connection>{nullptr});
348 else {
349 auto conn = websocket::connection::make(m_cfg.logger->clone("connection"), [this]() -> malloy::websocket::stream {
350#if MALLOY_FEATURE_TLS
351 if constexpr (isSecure) {
352 return malloy::websocket::stream{boost::beast::ssl_stream<malloy::tcp::stream<>>{
353 malloy::tcp::stream<>{boost::asio::make_strand(*m_ioc)}, *m_tls_ctx}};
354 }
355 else
356#endif
357 return malloy::websocket::stream{malloy::tcp::stream<>{boost::asio::make_strand(*m_ioc)}};
358 }(), m_cfg.user_agent);
359
360 conn->connect(results, resource, [conn, done = std::forward<decltype(done)>(done)](auto ec) mutable {
361 if (ec)
362 std::invoke(std::forward<decltype(handler)>(done), ec, std::shared_ptr<websocket::connection>{nullptr});
363 else
364 std::invoke(std::forward<decltype(handler)>(done), ec, conn);
365 });
366 }
367 }
368 );
369 }
370 };
371
372 auto start(controller&) -> controller::session;
373
374} // namespace malloy::client
Definition: controller.hpp:73
std::future< malloy::error_code > http_request(malloy::http::request< ReqBody > req, Callback &&done, Filter filter={})
Definition: controller.hpp:142
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:232
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:181
std::future< malloy::error_code > https_request(malloy::http::request< ReqBody > req, Callback &&done, Filter filter={})
Definition: controller.hpp:165
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:78
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
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::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:85
std::string user_agent
Agent string used for connections.
Definition: controller.hpp:90
std::uint64_t body_limit
Definition: controller.hpp:95
Default filter provided to ease use of interface.
Definition: controller.hpp:47
Definition: controller_run_result.hpp:19